Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f5cc69ed53 | |||
| 57cbb739d2 |
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
.nogit/
|
.nogit/
|
||||||
|
.playwright-mcp/
|
||||||
|
|
||||||
# artifacts
|
# artifacts
|
||||||
coverage/
|
coverage/
|
||||||
@@ -17,4 +18,4 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
dist_*/
|
dist_*/
|
||||||
|
|
||||||
# custom
|
# custom
|
||||||
|
|||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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)
|
## 2025-12-18 - 1.2.0 - feat(icons)
|
||||||
migrate icon usage to the new dees-icon API and integrate collaboration sidebar into the editor
|
migrate icon usage to the new dees-icon API and integrate collaboration sidebar into the editor
|
||||||
|
|
||||||
|
|||||||
@@ -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,33 +0,0 @@
|
|||||||
{
|
|
||||||
"gitzone": {
|
|
||||||
"projectType": "wcc",
|
|
||||||
"module": {
|
|
||||||
"githost": "gitlab.com",
|
|
||||||
"gitscope": "signature.digital_private",
|
|
||||||
"gitrepo": "catalog",
|
|
||||||
"description": "A comprehensive catalog of customizable web components designed for building and managing e-signature applications.",
|
|
||||||
"npmPackagename": "@signature.digital_private/catalog",
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"npmci": {
|
|
||||||
"npmGlobalTools": [],
|
|
||||||
"npmAccessLevel": "private"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+18
-23
@@ -1,33 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "@signature.digital/catalog",
|
"name": "@signature.digital/catalog",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A comprehensive catalog of customizable web components designed for building and managing e-signature applications.",
|
"description": "A comprehensive catalog of customizable web components designed for building and managing e-signature applications.",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist_ts_web/index.ts"
|
".": "./dist_ts_web/index.js"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run build",
|
"test": "pnpm run build",
|
||||||
"build": "tsbuild element && tsbundle element --production",
|
"build": "tsbuild tsfolders --allowimplicitany && tsbundle",
|
||||||
"watch": "tswatch element"
|
"watch": "tswatch"
|
||||||
},
|
},
|
||||||
"author": "Task Venture Capital GmbH",
|
"author": "Task Venture Capital GmbH",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@design.estate/dees-catalog": "^3.3.1",
|
"@design.estate/dees-catalog": "3.81.0",
|
||||||
"@design.estate/dees-domtools": "^2.3.6",
|
"@design.estate/dees-domtools": "^2.5.6",
|
||||||
"@design.estate/dees-element": "^2.1.3",
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
"@design.estate/dees-wcctools": "^2.0.1",
|
"@design.estate/dees-wcctools": "^3.9.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.3",
|
||||||
"@signature.digital/tools": "^1.1.0",
|
|
||||||
"signature_pad": "^5.1.3"
|
"signature_pad": "^5.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.0.2",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsbundle": "^2.6.3",
|
"@git.zone/tsbundle": "2.10.1",
|
||||||
"@git.zone/tswatch": "^2.3.13",
|
"@git.zone/tswatch": "^3.3.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.2"
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
|
"@types/node": "^25.6.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"ts/**/*",
|
"ts/**/*",
|
||||||
@@ -38,7 +38,8 @@
|
|||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
".smartconfig.json",
|
||||||
|
"license",
|
||||||
"readme.md"
|
"readme.md"
|
||||||
],
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
@@ -58,11 +59,5 @@
|
|||||||
"custom elements",
|
"custom elements",
|
||||||
"electronic signing",
|
"electronic signing",
|
||||||
"npm package"
|
"npm package"
|
||||||
],
|
]
|
||||||
"pnpm": {
|
|
||||||
"overrides": {
|
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
|
||||||
"agentkeepalive": "^4.6.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2814
-3440
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
|||||||
# @signature.digital/catalog
|
# @signature.digital/catalog
|
||||||
|
|
||||||
A comprehensive catalog of customizable web components designed for building and managing e-signature applications. Built with modern web technologies using LitElement and TypeScript.
|
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`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
@@ -9,154 +11,148 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm install @signature.digital/catalog
|
pnpm add @signature.digital/catalog
|
||||||
# or
|
|
||||||
pnpm install @signature.digital/catalog
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎯 Overview
|
## Quick Start
|
||||||
|
|
||||||
This package provides three main components for e-signature workflows:
|
Register all elements once in your browser bundle:
|
||||||
|
|
||||||
| Component | Tag | Description |
|
|
||||||
|-----------|-----|-------------|
|
|
||||||
| **SignPad** | `<sdig-signpad>` | Canvas-based signature capture pad |
|
|
||||||
| **SignBox** | `<sdig-signbox>` | Complete signing interface with controls |
|
|
||||||
| **ContractEditor** | `<sdig-contracteditor>` | Contract document management component |
|
|
||||||
|
|
||||||
## 📦 Usage
|
|
||||||
|
|
||||||
### Basic Import
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import '@signature.digital/catalog';
|
import '@signature.digital/catalog';
|
||||||
```
|
```
|
||||||
|
|
||||||
This registers all custom elements and makes them available for use in your HTML.
|
Render the product workspace:
|
||||||
|
|
||||||
### SignPad Component
|
|
||||||
|
|
||||||
The `<sdig-signpad>` is a canvas-based signature capture component that allows users to draw their signatures directly in the browser.
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<sdig-signpad></sdig-signpad>
|
<sdig-workspace accent="#3b82f6" density="comfortable" theme="dark"></sdig-workspace>
|
||||||
```
|
```
|
||||||
|
|
||||||
**API Methods:**
|
Render a standalone signature box:
|
||||||
|
|
||||||
```typescript
|
|
||||||
const signpad = document.querySelector('sdig-signpad');
|
|
||||||
|
|
||||||
// Get signature data as point arrays
|
|
||||||
const data = await signpad.toData();
|
|
||||||
|
|
||||||
// Load signature from data
|
|
||||||
await signpad.fromData(data);
|
|
||||||
|
|
||||||
// Export signature as SVG string
|
|
||||||
const svg = await signpad.toSVG();
|
|
||||||
|
|
||||||
// Undo last stroke
|
|
||||||
await signpad.undo();
|
|
||||||
|
|
||||||
// Clear the signature pad
|
|
||||||
await signpad.clear();
|
|
||||||
```
|
|
||||||
|
|
||||||
### SignBox Component
|
|
||||||
|
|
||||||
The `<sdig-signbox>` wraps `SignPad` with a complete UI including Clear, Undo, and Submit buttons.
|
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<sdig-signbox></sdig-signbox>
|
<sdig-signbox></sdig-signbox>
|
||||||
```
|
```
|
||||||
|
|
||||||
**Events:**
|
## 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
|
```typescript
|
||||||
const signbox = document.querySelector('sdig-signbox');
|
const signbox = document.querySelector('sdig-signbox');
|
||||||
|
|
||||||
signbox.addEventListener('signature', (event) => {
|
signbox?.addEventListener('signature', (event) => {
|
||||||
const signatureData = event.detail.signature;
|
const { signature } = (event as CustomEvent<{ signature: unknown[] }>).detail;
|
||||||
console.log('Signature captured:', signatureData);
|
console.log(signature);
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
### ContractEditor Component
|
The `signature` event detail contains the `signature_pad` stroke data returned by `sdig-signpad.toData()`.
|
||||||
|
|
||||||
The `<sdig-contracteditor>` provides contract viewing and editing capabilities using the `IPortableContract` interface from `@signature.digital/tools`.
|
## Signature Pad
|
||||||
|
|
||||||
|
Use `sdig-signpad` directly when you need lower-level control over the canvas:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<sdig-signpad></sdig-signpad>
|
||||||
|
```
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import '@signature.digital/catalog';
|
const signpad = document.querySelector('sdig-signpad');
|
||||||
import { IPortableContract } from '@signature.digital/tools/interfaces';
|
|
||||||
import { demoContract } from '@signature.digital/tools/demodata';
|
|
||||||
|
|
||||||
const editor = document.querySelector('sdig-contracteditor');
|
const strokes = await signpad?.toData();
|
||||||
editor.contract = demoContract;
|
const svg = await signpad?.toSVG();
|
||||||
|
|
||||||
|
await signpad?.undo();
|
||||||
|
await signpad?.clear();
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🔧 Integration Example
|
| 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 |
|
||||||
|
|
||||||
Here's a complete example showing all components working together:
|
## Demo Data Boundary
|
||||||
|
|
||||||
```typescript
|
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.
|
||||||
import '@signature.digital/catalog';
|
|
||||||
import { LitElement, html, css } from 'lit';
|
|
||||||
import { customElement } from 'lit/decorators.js';
|
|
||||||
|
|
||||||
@customElement('my-signature-app')
|
## Development
|
||||||
class MySignatureApp extends LitElement {
|
|
||||||
static styles = css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
render() {
|
```shell
|
||||||
return html`
|
pnpm install
|
||||||
<h2>Please sign below</h2>
|
pnpm run build
|
||||||
<sdig-signbox @signature=${this.handleSignature}></sdig-signbox>
|
pnpm test
|
||||||
`;
|
pnpm run watch
|
||||||
}
|
|
||||||
|
|
||||||
private handleSignature(e: CustomEvent) {
|
|
||||||
console.log('Signature submitted:', e.detail.signature);
|
|
||||||
// Process or store the signature
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🎨 Theming
|
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.
|
||||||
|
|
||||||
Components support automatic light/dark theme detection and can be customized using CSS custom properties:
|
|
||||||
|
|
||||||
```css
|
|
||||||
sdig-signpad, sdig-signbox {
|
|
||||||
--main-background-color: #ffffff;
|
|
||||||
--line-color: #000000;
|
|
||||||
--button-color: #007bff;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The components use `cssManager.bdTheme()` for automatic theme switching based on system preferences.
|
|
||||||
|
|
||||||
## 📋 Requirements
|
|
||||||
|
|
||||||
- Modern browser with Custom Elements V1 support
|
|
||||||
- ECMAScript Modules (ESM) compatible environment
|
|
||||||
- TypeScript 5.0+ (for development)
|
|
||||||
|
|
||||||
## 🔗 Dependencies
|
|
||||||
|
|
||||||
- `@design.estate/dees-element` - LitElement-based component framework
|
|
||||||
- `@signature.digital/tools` - Contract interfaces and demo data
|
|
||||||
- `signature_pad` - Canvas signature capture library
|
|
||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
@@ -168,7 +164,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH
|
||||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
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.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@signature.digital/catalog',
|
name: '@signature.digital/catalog',
|
||||||
version: '1.2.0',
|
version: '1.3.0',
|
||||||
description: 'A comprehensive catalog of customizable web components designed for building and managing e-signature applications.'
|
description: 'A comprehensive catalog of customizable web components designed for building and managing e-signature applications.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,6 @@
|
|||||||
// Contract Editor (main module)
|
|
||||||
export * from './sdig-contracteditor/index.js';
|
|
||||||
|
|
||||||
// Contract sub-components
|
|
||||||
export * from './sdig-contract-header/index.js';
|
|
||||||
export * from './sdig-contract-metadata/index.js';
|
|
||||||
export * from './sdig-contract-parties/index.js';
|
|
||||||
export * from './sdig-contract-content/index.js';
|
|
||||||
export * from './sdig-contract-terms/index.js';
|
|
||||||
export * from './sdig-contract-signatures/index.js';
|
|
||||||
export * from './sdig-contract-attachments/index.js';
|
|
||||||
export * from './sdig-contract-collaboration/index.js';
|
|
||||||
export * from './sdig-contract-audit/index.js';
|
|
||||||
export * from './sdig-collaboration-sidebar/index.js';
|
|
||||||
|
|
||||||
// Signature components
|
// Signature components
|
||||||
export * from './sdig-signbox/index.js';
|
export * from './sdig-signbox/index.js';
|
||||||
export * from './sdig-signpad/index.js';
|
export * from './sdig-signpad/index.js';
|
||||||
|
|
||||||
|
// Product workspace component
|
||||||
|
export * from './sdig-workspace/index.js';
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-collaboration-sidebar.js';
|
|
||||||
@@ -1,846 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-collaboration-sidebar.ts
|
|
||||||
* @description Compact collaboration sidebar for the contract editor
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-collaboration-sidebar': SdigCollaborationSidebar;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comment interface
|
|
||||||
interface IComment {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
userColor: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt?: number;
|
|
||||||
anchorPath?: string;
|
|
||||||
anchorText?: string;
|
|
||||||
resolved: boolean;
|
|
||||||
replies: IComment[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suggestion interface
|
|
||||||
interface ISuggestion {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
userColor: string;
|
|
||||||
originalText: string;
|
|
||||||
suggestedText: string;
|
|
||||||
path: string;
|
|
||||||
status: 'pending' | 'accepted' | 'rejected';
|
|
||||||
createdAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presence interface
|
|
||||||
interface IPresence {
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
userColor: string;
|
|
||||||
currentSection: string;
|
|
||||||
cursorPosition?: { path: string; offset: number };
|
|
||||||
lastActive: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('sdig-collaboration-sidebar')
|
|
||||||
export class SdigCollaborationSidebar extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<div style="width: 320px; height: 600px; border: 1px solid #ccc;">
|
|
||||||
<sdig-collaboration-sidebar
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-collaboration-sidebar>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Presence bar */
|
|
||||||
.presence-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatars {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatar {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
margin-left: -6px;
|
|
||||||
border: 2px solid ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatar:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatar .status-dot {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -1px;
|
|
||||||
right: -1px;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #10b981;
|
|
||||||
border: 2px solid ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatar .status-dot.away {
|
|
||||||
background: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-count {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-left: -6px;
|
|
||||||
border: 2px solid ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollable content */
|
|
||||||
.sidebar-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapsible sections */
|
|
||||||
.collapsible-section {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
transition: background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title dees-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-badge {
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-chevron {
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-chevron.expanded {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-body {
|
|
||||||
padding: 0;
|
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: max-height 0.2s ease, padding 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-body.expanded {
|
|
||||||
padding: 12px;
|
|
||||||
max-height: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Compact comment cards */
|
|
||||||
.comment-card {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-card:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-card:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-card.resolved {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-avatar {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-body {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-author {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-time {
|
|
||||||
font-size: 10px;
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-preview {
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-replies {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-replies dees-icon {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Suggestion cards */
|
|
||||||
.suggestion-card {
|
|
||||||
padding: 10px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-radius: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-card:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-card:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-user {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-status dees-icon {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-status.pending {
|
|
||||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
|
||||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-status.accepted {
|
|
||||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
|
||||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-status.rejected {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-diff {
|
|
||||||
font-family: 'Roboto Mono', monospace;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-removed {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
|
||||||
text-decoration: line-through;
|
|
||||||
padding: 1px 3px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-added {
|
|
||||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
|
||||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
|
||||||
padding: 1px 3px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quick add comment */
|
|
||||||
.quick-add {
|
|
||||||
padding: 12px;
|
|
||||||
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 6px;
|
|
||||||
outline: none;
|
|
||||||
resize: none;
|
|
||||||
min-height: 60px;
|
|
||||||
font-family: inherit;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-input:focus {
|
|
||||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-input::placeholder {
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-add-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 24px 16px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state dees-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor readonly: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor commentsExpanded: boolean = true;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor suggestionsExpanded: boolean = true;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor newCommentText: string = '';
|
|
||||||
|
|
||||||
// Demo presence data
|
|
||||||
@state()
|
|
||||||
private accessor presenceList: IPresence[] = [
|
|
||||||
{ userId: '1', userName: 'Alice Smith', userColor: '#3b82f6', currentSection: 'content', lastActive: Date.now() },
|
|
||||||
{ userId: '2', userName: 'Bob Johnson', userColor: '#10b981', currentSection: 'parties', lastActive: Date.now() - 60000 },
|
|
||||||
{ userId: '3', userName: 'Carol Davis', userColor: '#f59e0b', currentSection: 'terms', lastActive: Date.now() - 300000 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Demo comments data
|
|
||||||
@state()
|
|
||||||
private accessor comments: IComment[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
userId: '1',
|
|
||||||
userName: 'Alice Smith',
|
|
||||||
userColor: '#3b82f6',
|
|
||||||
content: 'Can we clarify the payment terms in paragraph 3? The current wording seems ambiguous.',
|
|
||||||
createdAt: Date.now() - 3600000,
|
|
||||||
anchorPath: 'paragraphs.2',
|
|
||||||
anchorText: 'Compensation',
|
|
||||||
resolved: false,
|
|
||||||
replies: [
|
|
||||||
{
|
|
||||||
id: '1-1',
|
|
||||||
userId: '2',
|
|
||||||
userName: 'Bob Johnson',
|
|
||||||
userColor: '#10b981',
|
|
||||||
content: 'Good point. I\'ll update the wording to be more specific.',
|
|
||||||
createdAt: Date.now() - 1800000,
|
|
||||||
resolved: false,
|
|
||||||
replies: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
userId: '3',
|
|
||||||
userName: 'Carol Davis',
|
|
||||||
userColor: '#f59e0b',
|
|
||||||
content: 'The termination clause needs to comply with the latest regulations.',
|
|
||||||
createdAt: Date.now() - 86400000,
|
|
||||||
resolved: true,
|
|
||||||
replies: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
userId: '2',
|
|
||||||
userName: 'Bob Johnson',
|
|
||||||
userColor: '#10b981',
|
|
||||||
content: 'Should we add an automatic renewal clause?',
|
|
||||||
createdAt: Date.now() - 7200000,
|
|
||||||
resolved: false,
|
|
||||||
replies: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Demo suggestions data
|
|
||||||
@state()
|
|
||||||
private accessor suggestions: ISuggestion[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
userId: '1',
|
|
||||||
userName: 'Alice Smith',
|
|
||||||
userColor: '#3b82f6',
|
|
||||||
originalText: 'monthly salary',
|
|
||||||
suggestedText: 'monthly gross salary',
|
|
||||||
path: 'paragraphs.2.content',
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: Date.now() - 7200000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
userId: '3',
|
|
||||||
userName: 'Carol Davis',
|
|
||||||
userColor: '#f59e0b',
|
|
||||||
originalText: '30 days',
|
|
||||||
suggestedText: '60 days',
|
|
||||||
path: 'paragraphs.5.content',
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: Date.now() - 3600000,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleCommentClick(comment: IComment) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('comment-click', {
|
|
||||||
detail: { comment },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSuggestionClick(suggestion: ISuggestion) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('suggestion-click', {
|
|
||||||
detail: { suggestion },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddComment() {
|
|
||||||
if (!this.newCommentText.trim()) return;
|
|
||||||
|
|
||||||
const newComment: IComment = {
|
|
||||||
id: `comment-${Date.now()}`,
|
|
||||||
userId: 'current-user',
|
|
||||||
userName: 'You',
|
|
||||||
userColor: '#6366f1',
|
|
||||||
content: this.newCommentText,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
resolved: false,
|
|
||||||
replies: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.comments = [newComment, ...this.comments];
|
|
||||||
this.newCommentText = '';
|
|
||||||
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('add-comment', {
|
|
||||||
detail: { comment: newComment },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private formatTimeAgo(timestamp: number): string {
|
|
||||||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
||||||
if (seconds < 60) return 'now';
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60) return `${minutes}m`;
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return `${hours}h`;
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
return `${days}d`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getActivePresence(): IPresence[] {
|
|
||||||
const fiveMinutesAgo = Date.now() - 300000;
|
|
||||||
return this.presenceList.filter((p) => p.lastActive > fiveMinutesAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getOpenComments(): IComment[] {
|
|
||||||
return this.comments.filter((c) => !c.resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPendingSuggestions(): ISuggestion[] {
|
|
||||||
return this.suggestions.filter((s) => s.status === 'pending');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
const activePresence = this.getActivePresence();
|
|
||||||
const openComments = this.getOpenComments();
|
|
||||||
const pendingSuggestions = this.getPendingSuggestions();
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="sidebar-container">
|
|
||||||
<!-- Presence Bar -->
|
|
||||||
<div class="presence-bar">
|
|
||||||
<div class="presence-avatars">
|
|
||||||
${activePresence.slice(0, 3).map(
|
|
||||||
(p) => html`
|
|
||||||
<div
|
|
||||||
class="presence-avatar"
|
|
||||||
style="background: ${p.userColor}"
|
|
||||||
title="${p.userName} - ${p.currentSection}"
|
|
||||||
>
|
|
||||||
${p.userName.charAt(0)}
|
|
||||||
<span class="status-dot ${Date.now() - p.lastActive > 60000 ? 'away' : ''}"></span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${activePresence.length > 3
|
|
||||||
? html`<div class="presence-count">+${activePresence.length - 3}</div>`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
<span class="presence-label">${activePresence.length} active</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scrollable Content -->
|
|
||||||
<div class="sidebar-content">
|
|
||||||
<!-- Comments Section -->
|
|
||||||
<div class="collapsible-section">
|
|
||||||
<div
|
|
||||||
class="section-header"
|
|
||||||
@click=${() => (this.commentsExpanded = !this.commentsExpanded)}
|
|
||||||
>
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:MessageCircle'}></dees-icon>
|
|
||||||
Comments
|
|
||||||
${openComments.length > 0
|
|
||||||
? html`<span class="section-badge">${openComments.length}</span>`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
<dees-icon
|
|
||||||
class="section-chevron ${this.commentsExpanded ? 'expanded' : ''}"
|
|
||||||
.icon=${'lucide:ChevronDown'}
|
|
||||||
></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="section-body ${this.commentsExpanded ? 'expanded' : ''}">
|
|
||||||
${this.comments.length > 0
|
|
||||||
? this.comments.slice(0, 5).map((comment) => this.renderCommentCard(comment))
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:MessageSquare'}></dees-icon>
|
|
||||||
<p>No comments yet</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Suggestions Section -->
|
|
||||||
<div class="collapsible-section">
|
|
||||||
<div
|
|
||||||
class="section-header"
|
|
||||||
@click=${() => (this.suggestionsExpanded = !this.suggestionsExpanded)}
|
|
||||||
>
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:GitPullRequest'}></dees-icon>
|
|
||||||
Suggestions
|
|
||||||
${pendingSuggestions.length > 0
|
|
||||||
? html`<span class="section-badge">${pendingSuggestions.length}</span>`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
<dees-icon
|
|
||||||
class="section-chevron ${this.suggestionsExpanded ? 'expanded' : ''}"
|
|
||||||
.icon=${'lucide:ChevronDown'}
|
|
||||||
></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="section-body ${this.suggestionsExpanded ? 'expanded' : ''}">
|
|
||||||
${this.suggestions.length > 0
|
|
||||||
? this.suggestions.slice(0, 5).map((suggestion) => this.renderSuggestionCard(suggestion))
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:Edit3'}></dees-icon>
|
|
||||||
<p>No suggestions yet</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick Add Comment -->
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<div class="quick-add">
|
|
||||||
<textarea
|
|
||||||
class="quick-add-input"
|
|
||||||
placeholder="Add a comment..."
|
|
||||||
.value=${this.newCommentText}
|
|
||||||
@input=${(e: Event) => (this.newCommentText = (e.target as HTMLTextAreaElement).value)}
|
|
||||||
></textarea>
|
|
||||||
<div class="quick-add-actions">
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
@click=${this.handleAddComment}
|
|
||||||
?disabled=${!this.newCommentText.trim()}
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:Send'}></dees-icon>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderCommentCard(comment: IComment): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="comment-card ${comment.resolved ? 'resolved' : ''}"
|
|
||||||
@click=${() => this.handleCommentClick(comment)}
|
|
||||||
>
|
|
||||||
<div class="comment-avatar" style="background: ${comment.userColor}">
|
|
||||||
${comment.userName.charAt(0)}
|
|
||||||
</div>
|
|
||||||
<div class="comment-body">
|
|
||||||
<div class="comment-meta">
|
|
||||||
<span class="comment-author">${comment.userName}</span>
|
|
||||||
<span class="comment-time">${this.formatTimeAgo(comment.createdAt)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="comment-preview">${comment.content}</div>
|
|
||||||
${comment.replies.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="comment-replies">
|
|
||||||
<dees-icon .icon=${'lucide:MessageSquare'}></dees-icon>
|
|
||||||
${comment.replies.length} ${comment.replies.length === 1 ? 'reply' : 'replies'}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSuggestionCard(suggestion: ISuggestion): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div class="suggestion-card" @click=${() => this.handleSuggestionClick(suggestion)}>
|
|
||||||
<div class="suggestion-header">
|
|
||||||
<div class="suggestion-user">
|
|
||||||
<div class="comment-avatar" style="background: ${suggestion.userColor}; width: 20px; height: 20px; font-size: 9px;">
|
|
||||||
${suggestion.userName.charAt(0)}
|
|
||||||
</div>
|
|
||||||
<span class="comment-author">${suggestion.userName}</span>
|
|
||||||
</div>
|
|
||||||
<div class="suggestion-status ${suggestion.status}">
|
|
||||||
<dees-icon .icon=${suggestion.status === 'pending' ? 'lucide:Clock' : suggestion.status === 'accepted' ? 'lucide:Check' : 'lucide:X'}></dees-icon>
|
|
||||||
${suggestion.status}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="suggestion-diff">
|
|
||||||
<span class="diff-removed">${suggestion.originalText}</span>
|
|
||||||
<span> → </span>
|
|
||||||
<span class="diff-added">${suggestion.suggestedText}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-contract-attachments.js';
|
|
||||||
@@ -1,806 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contract-attachments.ts
|
|
||||||
* @description Contract attachments and prior contracts manager
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contract-attachments': SdigContractAttachments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attachment interface
|
|
||||||
interface IAttachment {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: 'document' | 'image' | 'spreadsheet' | 'pdf' | 'other';
|
|
||||||
mimeType: string;
|
|
||||||
size: number;
|
|
||||||
uploadedAt: number;
|
|
||||||
uploadedBy: string;
|
|
||||||
description?: string;
|
|
||||||
url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// File type configuration
|
|
||||||
const FILE_TYPES = {
|
|
||||||
document: { icon: 'lucide:FileText', color: '#3b82f6', label: 'Document' },
|
|
||||||
image: { icon: 'lucide:Image', color: '#10b981', label: 'Image' },
|
|
||||||
spreadsheet: { icon: 'lucide:Sheet', color: '#22c55e', label: 'Spreadsheet' },
|
|
||||||
pdf: { icon: 'lucide:FileType', color: '#ef4444', label: 'PDF' },
|
|
||||||
other: { icon: 'lucide:File', color: '#6b7280', label: 'File' },
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement('sdig-contract-attachments')
|
|
||||||
export class SdigContractAttachments extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contract-attachments
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contract-attachments>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachments-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section card */
|
|
||||||
.section-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title dees-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-count {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Upload zone */
|
|
||||||
.upload-zone {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
border: 2px dashed ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 12px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-zone:hover {
|
|
||||||
border-color: ${cssManager.bdTheme('#9ca3af', '#52525b')};
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-zone.dragging {
|
|
||||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-zone-icon {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
border-radius: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 28px;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-zone-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-zone-subtitle {
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-zone-hint {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Attachments list */
|
|
||||||
.attachments-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 14px 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-item:hover {
|
|
||||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-icon {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
margin-bottom: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-meta-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Prior contracts */
|
|
||||||
.prior-contracts-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prior-contract-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prior-contract-item:hover {
|
|
||||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.prior-contract-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 22px;
|
|
||||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prior-contract-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prior-contract-title {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prior-contract-context {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prior-contract-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state dees-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h4 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Storage summary */
|
|
||||||
.storage-summary {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storage-info {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storage-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storage-bar {
|
|
||||||
height: 6px;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storage-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.storage-text {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Type badge */
|
|
||||||
.type-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor readonly: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor isDragging: boolean = false;
|
|
||||||
|
|
||||||
// Demo attachments data
|
|
||||||
@state()
|
|
||||||
private accessor attachments: IAttachment[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'Employment_Terms_v2.pdf',
|
|
||||||
type: 'pdf',
|
|
||||||
mimeType: 'application/pdf',
|
|
||||||
size: 245760,
|
|
||||||
uploadedAt: Date.now() - 86400000 * 3,
|
|
||||||
uploadedBy: 'employer',
|
|
||||||
description: 'Original employment terms document',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
name: 'ID_Verification.png',
|
|
||||||
type: 'image',
|
|
||||||
mimeType: 'image/png',
|
|
||||||
size: 1024000,
|
|
||||||
uploadedAt: Date.now() - 86400000,
|
|
||||||
uploadedBy: 'employee',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
name: 'Tax_Information.xlsx',
|
|
||||||
type: 'spreadsheet',
|
|
||||||
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
||||||
size: 52480,
|
|
||||||
uploadedAt: Date.now() - 86400000 * 2,
|
|
||||||
uploadedBy: 'employer',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleFieldChange(path: string, value: unknown) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('field-change', {
|
|
||||||
detail: { path, value },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDragEnter(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.isDragging = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDragLeave(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.isDragging = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDragOver(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDrop(e: DragEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.isDragging = false;
|
|
||||||
|
|
||||||
const files = e.dataTransfer?.files;
|
|
||||||
if (files && files.length > 0) {
|
|
||||||
this.handleFiles(files);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleFileSelect() {
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.multiple = true;
|
|
||||||
input.onchange = () => {
|
|
||||||
if (input.files && input.files.length > 0) {
|
|
||||||
this.handleFiles(input.files);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleFiles(files: FileList) {
|
|
||||||
// Demo: just add to list
|
|
||||||
Array.from(files).forEach((file) => {
|
|
||||||
const newAttachment: IAttachment = {
|
|
||||||
id: `att-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
name: file.name,
|
|
||||||
type: this.getFileType(file.type),
|
|
||||||
mimeType: file.type,
|
|
||||||
size: file.size,
|
|
||||||
uploadedAt: Date.now(),
|
|
||||||
uploadedBy: 'user',
|
|
||||||
};
|
|
||||||
this.attachments = [...this.attachments, newAttachment];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDeleteAttachment(attachmentId: string) {
|
|
||||||
this.attachments = this.attachments.filter((a) => a.id !== attachmentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddPriorContract() {
|
|
||||||
// TODO: Open prior contract picker modal
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRemovePriorContract(index: number) {
|
|
||||||
if (!this.contract) return;
|
|
||||||
const updatedPriorContracts = [...this.contract.priorContracts];
|
|
||||||
updatedPriorContracts.splice(index, 1);
|
|
||||||
this.handleFieldChange('priorContracts', updatedPriorContracts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private getFileType(mimeType: string): IAttachment['type'] {
|
|
||||||
if (mimeType.includes('pdf')) return 'pdf';
|
|
||||||
if (mimeType.includes('image')) return 'image';
|
|
||||||
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) return 'spreadsheet';
|
|
||||||
if (mimeType.includes('document') || mimeType.includes('word')) return 'document';
|
|
||||||
return 'other';
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFileTypeConfig(type: IAttachment['type']) {
|
|
||||||
return FILE_TYPES[type] || FILE_TYPES.other;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatFileSize(bytes: number): string {
|
|
||||||
if (bytes === 0) return '0 B';
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatDate(timestamp: number): string {
|
|
||||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTotalSize(): number {
|
|
||||||
return this.attachments.reduce((sum, a) => sum + a.size, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPartyName(roleId: string): string {
|
|
||||||
const role = this.contract?.availableRoles.find((r) => r.id === roleId);
|
|
||||||
return role?.name || roleId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
if (!this.contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalSize = this.getTotalSize();
|
|
||||||
const maxSize = 50 * 1024 * 1024; // 50MB demo limit
|
|
||||||
const usagePercent = Math.min((totalSize / maxSize) * 100, 100);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="attachments-container">
|
|
||||||
<!-- Attachments Section -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:Paperclip'}></dees-icon>
|
|
||||||
Attachments
|
|
||||||
</div>
|
|
||||||
<span class="section-count">${this.attachments.length} files</span>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
<!-- Storage summary -->
|
|
||||||
<div class="storage-summary">
|
|
||||||
<div class="storage-info">
|
|
||||||
<div class="storage-label">Storage used</div>
|
|
||||||
<div class="storage-bar">
|
|
||||||
<div class="storage-fill" style="width: ${usagePercent}%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="storage-text">
|
|
||||||
${this.formatFileSize(totalSize)} / ${this.formatFileSize(maxSize)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Upload zone -->
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<div
|
|
||||||
class="upload-zone ${this.isDragging ? 'dragging' : ''}"
|
|
||||||
@dragenter=${this.handleDragEnter}
|
|
||||||
@dragleave=${this.handleDragLeave}
|
|
||||||
@dragover=${this.handleDragOver}
|
|
||||||
@drop=${this.handleDrop}
|
|
||||||
@click=${this.handleFileSelect}
|
|
||||||
>
|
|
||||||
<div class="upload-zone-icon">
|
|
||||||
<dees-icon .icon=${'lucide:UploadCloud'}></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="upload-zone-title">Drop files here or click to upload</div>
|
|
||||||
<div class="upload-zone-subtitle">Add supporting documents, images, or spreadsheets</div>
|
|
||||||
<div class="upload-zone-hint">PDF, DOCX, XLSX, PNG, JPG up to 10MB each</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
|
|
||||||
<!-- Attachments list -->
|
|
||||||
${this.attachments.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="attachments-list" style="margin-top: 20px;">
|
|
||||||
${this.attachments.map((attachment) => this.renderAttachmentItem(attachment))}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state" style="margin-top: 20px;">
|
|
||||||
<dees-icon .icon=${'lucide:FileX'}></dees-icon>
|
|
||||||
<h4>No Attachments</h4>
|
|
||||||
<p>Upload files to attach them to this contract</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Prior Contracts Section -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:Files'}></dees-icon>
|
|
||||||
Prior Contracts
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-secondary" @click=${this.handleAddPriorContract}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Link Contract
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
${this.contract.priorContracts.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="prior-contracts-list">
|
|
||||||
${this.contract.priorContracts.map((priorContract, index) =>
|
|
||||||
this.renderPriorContractItem(priorContract, index)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:Link'}></dees-icon>
|
|
||||||
<h4>No Prior Contracts</h4>
|
|
||||||
<p>Link related or predecessor contracts here</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderAttachmentItem(attachment: IAttachment): TemplateResult {
|
|
||||||
const typeConfig = this.getFileTypeConfig(attachment.type);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="attachment-item">
|
|
||||||
<div class="attachment-icon" style="background: ${typeConfig.color}20; color: ${typeConfig.color}">
|
|
||||||
<dees-icon .icon=${typeConfig.icon}></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="attachment-info">
|
|
||||||
<div class="attachment-name">${attachment.name}</div>
|
|
||||||
<div class="attachment-meta">
|
|
||||||
<span class="type-badge">${typeConfig.label}</span>
|
|
||||||
<span class="attachment-meta-item">
|
|
||||||
${this.formatFileSize(attachment.size)}
|
|
||||||
</span>
|
|
||||||
<span class="attachment-meta-item">
|
|
||||||
<dees-icon .icon=${'lucide:Calendar'}></dees-icon>
|
|
||||||
${this.formatDate(attachment.uploadedAt)}
|
|
||||||
</span>
|
|
||||||
<span class="attachment-meta-item">
|
|
||||||
<dees-icon .icon=${'lucide:User'}></dees-icon>
|
|
||||||
${this.getPartyName(attachment.uploadedBy)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="attachment-actions">
|
|
||||||
<button class="btn btn-ghost" title="Download">
|
|
||||||
<dees-icon .icon=${'lucide:Download'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost" title="Preview">
|
|
||||||
<dees-icon .icon=${'lucide:Eye'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-danger"
|
|
||||||
title="Delete"
|
|
||||||
@click=${() => this.handleDeleteAttachment(attachment.id)}
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:Trash2'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderPriorContractItem(priorContract: plugins.sdInterfaces.IPortableContract, index: number): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div class="prior-contract-item">
|
|
||||||
<div class="prior-contract-icon">
|
|
||||||
<dees-icon .icon=${'lucide:FileText'}></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="prior-contract-info">
|
|
||||||
<div class="prior-contract-title">${priorContract.title}</div>
|
|
||||||
<div class="prior-contract-context">${priorContract.context || 'No description'}</div>
|
|
||||||
</div>
|
|
||||||
<div class="prior-contract-actions">
|
|
||||||
<button class="btn btn-secondary btn-sm">
|
|
||||||
<dees-icon .icon=${'lucide:ExternalLink'}></dees-icon>
|
|
||||||
View
|
|
||||||
</button>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-danger"
|
|
||||||
@click=${() => this.handleRemovePriorContract(index)}
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:Unlink'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-contract-audit.js';
|
|
||||||
@@ -1,772 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contract-audit.ts
|
|
||||||
* @description Contract audit log and lifecycle history component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contract-audit': SdigContractAudit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audit event interface
|
|
||||||
interface IAuditEvent {
|
|
||||||
id: string;
|
|
||||||
timestamp: number;
|
|
||||||
type: 'created' | 'updated' | 'status_change' | 'signature' | 'comment' | 'attachment' | 'viewed' | 'shared';
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
userColor: string;
|
|
||||||
description: string;
|
|
||||||
details?: {
|
|
||||||
field?: string;
|
|
||||||
oldValue?: string;
|
|
||||||
newValue?: string;
|
|
||||||
attachmentName?: string;
|
|
||||||
signatureStatus?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status workflow configuration
|
|
||||||
const STATUS_WORKFLOW = [
|
|
||||||
{ id: 'draft', label: 'Draft', icon: 'lucide:FileEdit', color: '#f59e0b' },
|
|
||||||
{ id: 'review', label: 'Review', icon: 'lucide:Eye', color: '#3b82f6' },
|
|
||||||
{ id: 'pending', label: 'Pending Signatures', icon: 'lucide:PenTool', color: '#8b5cf6' },
|
|
||||||
{ id: 'signed', label: 'Signed', icon: 'lucide:CheckCircle', color: '#10b981' },
|
|
||||||
{ id: 'executed', label: 'Executed', icon: 'lucide:ShieldCheck', color: '#059669' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Event type configuration
|
|
||||||
const EVENT_TYPES = {
|
|
||||||
created: { icon: 'lucide:PlusCircle', color: '#10b981', label: 'Created' },
|
|
||||||
updated: { icon: 'lucide:Pencil', color: '#3b82f6', label: 'Updated' },
|
|
||||||
status_change: { icon: 'lucide:ArrowRightCircle', color: '#8b5cf6', label: 'Status Changed' },
|
|
||||||
signature: { icon: 'lucide:PenTool', color: '#10b981', label: 'Signature' },
|
|
||||||
comment: { icon: 'lucide:MessageCircle', color: '#f59e0b', label: 'Comment' },
|
|
||||||
attachment: { icon: 'lucide:Paperclip', color: '#6366f1', label: 'Attachment' },
|
|
||||||
viewed: { icon: 'lucide:Eye', color: '#6b7280', label: 'Viewed' },
|
|
||||||
shared: { icon: 'lucide:Share2', color: '#ec4899', label: 'Shared' },
|
|
||||||
};
|
|
||||||
|
|
||||||
@customElement('sdig-contract-audit')
|
|
||||||
export class SdigContractAudit extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contract-audit
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contract-audit>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audit-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Lifecycle status */
|
|
||||||
.lifecycle-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lifecycle-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-workflow {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
overflow-x: auto;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-step {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-icon {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 22px;
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-step.completed .status-icon {
|
|
||||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
|
||||||
color: ${cssManager.bdTheme('#059669', '#34d399')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-step.current .status-icon {
|
|
||||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
box-shadow: 0 0 0 4px ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(96, 165, 250, 0.2)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-step.completed .status-label,
|
|
||||||
.status-step.current .status-label {
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-connector {
|
|
||||||
flex: 1;
|
|
||||||
height: 2px;
|
|
||||||
background: ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-connector.completed {
|
|
||||||
background: ${cssManager.bdTheme('#10b981', '#34d399')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section card */
|
|
||||||
.section-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title dees-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter controls */
|
|
||||||
.filter-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-select {
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 6px;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 6px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input:focus {
|
|
||||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Timeline */
|
|
||||||
.timeline {
|
|
||||||
position: relative;
|
|
||||||
padding-left: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 11px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 2px;
|
|
||||||
background: ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item {
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item:last-child {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-dot {
|
|
||||||
position: absolute;
|
|
||||||
left: -32px;
|
|
||||||
top: 0;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 12px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 2px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-content {
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-user {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-avatar {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-username {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-description {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-details {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-family: 'Roboto Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-row:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-value {
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-old {
|
|
||||||
text-decoration: line-through;
|
|
||||||
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-new {
|
|
||||||
color: ${cssManager.bdTheme('#10b981', '#34d399')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Event type badge */
|
|
||||||
.event-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stats row */
|
|
||||||
.stats-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-card {
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state dees-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h4 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor filterType: string = 'all';
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor searchQuery: string = '';
|
|
||||||
|
|
||||||
// Demo audit events
|
|
||||||
@state()
|
|
||||||
private accessor auditEvents: IAuditEvent[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
timestamp: Date.now() - 3600000,
|
|
||||||
type: 'signature',
|
|
||||||
userId: '1',
|
|
||||||
userName: 'Alice Smith',
|
|
||||||
userColor: '#3b82f6',
|
|
||||||
description: 'Signed the contract',
|
|
||||||
details: { signatureStatus: 'completed' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
timestamp: Date.now() - 7200000,
|
|
||||||
type: 'status_change',
|
|
||||||
userId: '2',
|
|
||||||
userName: 'Bob Johnson',
|
|
||||||
userColor: '#10b981',
|
|
||||||
description: 'Changed status from Review to Pending Signatures',
|
|
||||||
details: { field: 'status', oldValue: 'review', newValue: 'pending' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
timestamp: Date.now() - 86400000,
|
|
||||||
type: 'updated',
|
|
||||||
userId: '2',
|
|
||||||
userName: 'Bob Johnson',
|
|
||||||
userColor: '#10b981',
|
|
||||||
description: 'Updated compensation amount',
|
|
||||||
details: { field: 'paragraphs.2.content', oldValue: '[Salary Amount]', newValue: '€520/month' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
timestamp: Date.now() - 86400000 * 2,
|
|
||||||
type: 'comment',
|
|
||||||
userId: '1',
|
|
||||||
userName: 'Alice Smith',
|
|
||||||
userColor: '#3b82f6',
|
|
||||||
description: 'Added a comment on Compensation section',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '5',
|
|
||||||
timestamp: Date.now() - 86400000 * 3,
|
|
||||||
type: 'attachment',
|
|
||||||
userId: '3',
|
|
||||||
userName: 'Carol Davis',
|
|
||||||
userColor: '#f59e0b',
|
|
||||||
description: 'Uploaded ID verification document',
|
|
||||||
details: { attachmentName: 'ID_Verification.pdf' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '6',
|
|
||||||
timestamp: Date.now() - 86400000 * 5,
|
|
||||||
type: 'created',
|
|
||||||
userId: '2',
|
|
||||||
userName: 'Bob Johnson',
|
|
||||||
userColor: '#10b981',
|
|
||||||
description: 'Created the contract',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private getEventConfig(type: IAuditEvent['type']) {
|
|
||||||
return EVENT_TYPES[type] || EVENT_TYPES.updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFilteredEvents(): IAuditEvent[] {
|
|
||||||
let events = this.auditEvents;
|
|
||||||
|
|
||||||
if (this.filterType !== 'all') {
|
|
||||||
events = events.filter((e) => e.type === this.filterType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.searchQuery) {
|
|
||||||
const query = this.searchQuery.toLowerCase();
|
|
||||||
events = events.filter(
|
|
||||||
(e) =>
|
|
||||||
e.description.toLowerCase().includes(query) ||
|
|
||||||
e.userName.toLowerCase().includes(query)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatDate(timestamp: number): string {
|
|
||||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatTimeAgo(timestamp: number): string {
|
|
||||||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
||||||
if (seconds < 60) return 'just now';
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60) return `${minutes}m ago`;
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return `${hours}h ago`;
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
return `${days}d ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCurrentStatusIndex(): number {
|
|
||||||
// Demo: Return a fixed position
|
|
||||||
return 2; // Pending Signatures
|
|
||||||
}
|
|
||||||
|
|
||||||
private getEventStats() {
|
|
||||||
const total = this.auditEvents.length;
|
|
||||||
const updates = this.auditEvents.filter((e) => e.type === 'updated').length;
|
|
||||||
const signatures = this.auditEvents.filter((e) => e.type === 'signature').length;
|
|
||||||
const comments = this.auditEvents.filter((e) => e.type === 'comment').length;
|
|
||||||
return { total, updates, signatures, comments };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
if (!this.contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentStatusIndex = this.getCurrentStatusIndex();
|
|
||||||
const filteredEvents = this.getFilteredEvents();
|
|
||||||
const stats = this.getEventStats();
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="audit-container">
|
|
||||||
<!-- Lifecycle Status -->
|
|
||||||
<div class="lifecycle-card">
|
|
||||||
<div class="lifecycle-title">Contract Lifecycle</div>
|
|
||||||
<div class="status-workflow">
|
|
||||||
${STATUS_WORKFLOW.map((status, index) => html`
|
|
||||||
<div class="status-step ${index < currentStatusIndex ? 'completed' : ''} ${index === currentStatusIndex ? 'current' : ''}">
|
|
||||||
<div class="status-icon">
|
|
||||||
<dees-icon .icon=${status.icon}></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="status-label">${status.label}</div>
|
|
||||||
</div>
|
|
||||||
${index < STATUS_WORKFLOW.length - 1
|
|
||||||
? html`<div class="status-connector ${index < currentStatusIndex ? 'completed' : ''}"></div>`
|
|
||||||
: ''}
|
|
||||||
`)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Stats -->
|
|
||||||
<div class="stats-row">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-value">${stats.total}</div>
|
|
||||||
<div class="stat-label">Total Events</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-value">${stats.updates}</div>
|
|
||||||
<div class="stat-label">Updates</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-value">${stats.signatures}</div>
|
|
||||||
<div class="stat-label">Signatures</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-value">${stats.comments}</div>
|
|
||||||
<div class="stat-label">Comments</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Audit Log -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:History'}></dees-icon>
|
|
||||||
Activity Log
|
|
||||||
</div>
|
|
||||||
<button class="btn btn-secondary">
|
|
||||||
<dees-icon .icon=${'lucide:Download'}></dees-icon>
|
|
||||||
Export
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="filter-row">
|
|
||||||
<select
|
|
||||||
class="filter-select"
|
|
||||||
.value=${this.filterType}
|
|
||||||
@change=${(e: Event) => (this.filterType = (e.target as HTMLSelectElement).value)}
|
|
||||||
>
|
|
||||||
<option value="all">All Events</option>
|
|
||||||
${Object.entries(EVENT_TYPES).map(
|
|
||||||
([key, config]) => html`<option value=${key}>${config.label}</option>`
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="search-input"
|
|
||||||
placeholder="Search events..."
|
|
||||||
.value=${this.searchQuery}
|
|
||||||
@input=${(e: Event) => (this.searchQuery = (e.target as HTMLInputElement).value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Timeline -->
|
|
||||||
${filteredEvents.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="timeline">
|
|
||||||
${filteredEvents.map((event) => this.renderTimelineItem(event))}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:Clock'}></dees-icon>
|
|
||||||
<h4>No Events Found</h4>
|
|
||||||
<p>No activity matches your current filters</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderTimelineItem(event: IAuditEvent): TemplateResult {
|
|
||||||
const config = this.getEventConfig(event.type);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="timeline-item">
|
|
||||||
<div class="timeline-dot" style="border-color: ${config.color}; color: ${config.color}">
|
|
||||||
<dees-icon .icon=${config.icon}></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-content">
|
|
||||||
<div class="timeline-header">
|
|
||||||
<div class="timeline-title">
|
|
||||||
<span class="event-badge" style="background: ${config.color}20; color: ${config.color}">
|
|
||||||
${config.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span class="timeline-time" title="${this.formatDate(event.timestamp)}">
|
|
||||||
${this.formatTimeAgo(event.timestamp)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-user">
|
|
||||||
<div class="timeline-avatar" style="background: ${event.userColor}">
|
|
||||||
${event.userName.charAt(0)}
|
|
||||||
</div>
|
|
||||||
<span class="timeline-username">${event.userName}</span>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-description">${event.description}</div>
|
|
||||||
${event.details
|
|
||||||
? html`
|
|
||||||
<div class="timeline-details">
|
|
||||||
${event.details.field
|
|
||||||
? html`
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">Field:</span>
|
|
||||||
<span class="detail-value">${event.details.field}</span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
${event.details.oldValue && event.details.newValue
|
|
||||||
? html`
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-old">${event.details.oldValue}</span>
|
|
||||||
<span>→</span>
|
|
||||||
<span class="detail-new">${event.details.newValue}</span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
${event.details.attachmentName
|
|
||||||
? html`
|
|
||||||
<div class="detail-row">
|
|
||||||
<span class="detail-label">File:</span>
|
|
||||||
<span class="detail-value">${event.details.attachmentName}</span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-contract-collaboration.js';
|
|
||||||
@@ -1,972 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contract-collaboration.ts
|
|
||||||
* @description Contract collaboration - comments, suggestions, and presence
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contract-collaboration': SdigContractCollaboration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comment interface
|
|
||||||
interface IComment {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
userColor: string;
|
|
||||||
content: string;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt?: number;
|
|
||||||
anchorPath?: string;
|
|
||||||
anchorText?: string;
|
|
||||||
resolved: boolean;
|
|
||||||
replies: IComment[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Suggestion interface
|
|
||||||
interface ISuggestion {
|
|
||||||
id: string;
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
userColor: string;
|
|
||||||
originalText: string;
|
|
||||||
suggestedText: string;
|
|
||||||
path: string;
|
|
||||||
status: 'pending' | 'accepted' | 'rejected';
|
|
||||||
createdAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presence interface
|
|
||||||
interface IPresence {
|
|
||||||
userId: string;
|
|
||||||
userName: string;
|
|
||||||
userColor: string;
|
|
||||||
currentSection: string;
|
|
||||||
cursorPosition?: { path: string; offset: number };
|
|
||||||
lastActive: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('sdig-contract-collaboration')
|
|
||||||
export class SdigContractCollaboration extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contract-collaboration
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contract-collaboration>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collaboration-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Presence bar */
|
|
||||||
.presence-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-label {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatars {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatar {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
margin-left: -8px;
|
|
||||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatar:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatar .status-dot {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #10b981;
|
|
||||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-avatar .status-dot.away {
|
|
||||||
background: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.presence-count {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-left: -8px;
|
|
||||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-btn:hover {
|
|
||||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section card */
|
|
||||||
.section-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title dees-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-badge {
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Comments list */
|
|
||||||
.comments-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-thread {
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-thread.resolved {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-avatar {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-meta {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-author {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-time {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-anchor {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
|
||||||
margin-bottom: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-anchor:hover {
|
|
||||||
background: ${cssManager.bdTheme('#fde68a', '#713f12')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-content {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.comment-replies {
|
|
||||||
margin-top: 16px;
|
|
||||||
padding-left: 16px;
|
|
||||||
border-left: 2px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-item {
|
|
||||||
padding: 12px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reply-item:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Suggestions list */
|
|
||||||
.suggestions-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-card {
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-user {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-status.pending {
|
|
||||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
|
||||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-status.accepted {
|
|
||||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
|
||||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-status.rejected {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-diff {
|
|
||||||
padding: 12px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
font-family: 'Roboto Mono', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-removed {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
|
||||||
text-decoration: line-through;
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diff-added {
|
|
||||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
|
||||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
|
||||||
padding: 2px 4px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.suggestion-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* New comment input */
|
|
||||||
.new-comment {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-comment-input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 8px;
|
|
||||||
outline: none;
|
|
||||||
resize: none;
|
|
||||||
min-height: 80px;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-comment-input:focus {
|
|
||||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-comment-input::placeholder {
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state dees-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h4 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Filter tabs */
|
|
||||||
.filter-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-tab {
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-tab:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-tab.active {
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background: ${cssManager.bdTheme('#10b981', '#059669')};
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success:hover {
|
|
||||||
background: ${cssManager.bdTheme('#059669', '#047857')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: ${cssManager.bdTheme('#fecaca', '#7f1d1d')};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor readonly: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor activeTab: 'comments' | 'suggestions' = 'comments';
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor commentFilter: 'all' | 'open' | 'resolved' = 'all';
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor newCommentText: string = '';
|
|
||||||
|
|
||||||
// Demo presence data
|
|
||||||
@state()
|
|
||||||
private accessor presenceList: IPresence[] = [
|
|
||||||
{ userId: '1', userName: 'Alice Smith', userColor: '#3b82f6', currentSection: 'content', lastActive: Date.now() },
|
|
||||||
{ userId: '2', userName: 'Bob Johnson', userColor: '#10b981', currentSection: 'parties', lastActive: Date.now() - 60000 },
|
|
||||||
{ userId: '3', userName: 'Carol Davis', userColor: '#f59e0b', currentSection: 'terms', lastActive: Date.now() - 300000 },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Demo comments data
|
|
||||||
@state()
|
|
||||||
private accessor comments: IComment[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
userId: '1',
|
|
||||||
userName: 'Alice Smith',
|
|
||||||
userColor: '#3b82f6',
|
|
||||||
content: 'Can we clarify the payment terms in paragraph 3? The current wording seems ambiguous.',
|
|
||||||
createdAt: Date.now() - 3600000,
|
|
||||||
anchorPath: 'paragraphs.2',
|
|
||||||
anchorText: 'Compensation',
|
|
||||||
resolved: false,
|
|
||||||
replies: [
|
|
||||||
{
|
|
||||||
id: '1-1',
|
|
||||||
userId: '2',
|
|
||||||
userName: 'Bob Johnson',
|
|
||||||
userColor: '#10b981',
|
|
||||||
content: 'Good point. I\'ll update the wording to be more specific.',
|
|
||||||
createdAt: Date.now() - 1800000,
|
|
||||||
resolved: false,
|
|
||||||
replies: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
userId: '3',
|
|
||||||
userName: 'Carol Davis',
|
|
||||||
userColor: '#f59e0b',
|
|
||||||
content: 'The termination clause needs to comply with the latest regulations.',
|
|
||||||
createdAt: Date.now() - 86400000,
|
|
||||||
resolved: true,
|
|
||||||
replies: [],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Demo suggestions data
|
|
||||||
@state()
|
|
||||||
private accessor suggestions: ISuggestion[] = [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
userId: '1',
|
|
||||||
userName: 'Alice Smith',
|
|
||||||
userColor: '#3b82f6',
|
|
||||||
originalText: 'monthly salary',
|
|
||||||
suggestedText: 'monthly gross salary',
|
|
||||||
path: 'paragraphs.2.content',
|
|
||||||
status: 'pending',
|
|
||||||
createdAt: Date.now() - 7200000,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleFieldChange(path: string, value: unknown) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('field-change', {
|
|
||||||
detail: { path, value },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddComment() {
|
|
||||||
if (!this.newCommentText.trim()) return;
|
|
||||||
|
|
||||||
const newComment: IComment = {
|
|
||||||
id: `comment-${Date.now()}`,
|
|
||||||
userId: 'current-user',
|
|
||||||
userName: 'You',
|
|
||||||
userColor: '#6366f1',
|
|
||||||
content: this.newCommentText,
|
|
||||||
createdAt: Date.now(),
|
|
||||||
resolved: false,
|
|
||||||
replies: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.comments = [newComment, ...this.comments];
|
|
||||||
this.newCommentText = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleResolveComment(commentId: string) {
|
|
||||||
this.comments = this.comments.map((c) =>
|
|
||||||
c.id === commentId ? { ...c, resolved: !c.resolved } : c
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAcceptSuggestion(suggestionId: string) {
|
|
||||||
this.suggestions = this.suggestions.map((s) =>
|
|
||||||
s.id === suggestionId ? { ...s, status: 'accepted' as const } : s
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRejectSuggestion(suggestionId: string) {
|
|
||||||
this.suggestions = this.suggestions.map((s) =>
|
|
||||||
s.id === suggestionId ? { ...s, status: 'rejected' as const } : s
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private getFilteredComments(): IComment[] {
|
|
||||||
if (this.commentFilter === 'all') return this.comments;
|
|
||||||
if (this.commentFilter === 'open') return this.comments.filter((c) => !c.resolved);
|
|
||||||
return this.comments.filter((c) => c.resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatTimeAgo(timestamp: number): string {
|
|
||||||
const seconds = Math.floor((Date.now() - timestamp) / 1000);
|
|
||||||
if (seconds < 60) return 'just now';
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
if (minutes < 60) return `${minutes}m ago`;
|
|
||||||
const hours = Math.floor(minutes / 60);
|
|
||||||
if (hours < 24) return `${hours}h ago`;
|
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
return `${days}d ago`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getActivePresence(): IPresence[] {
|
|
||||||
const fiveMinutesAgo = Date.now() - 300000;
|
|
||||||
return this.presenceList.filter((p) => p.lastActive > fiveMinutesAgo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
if (!this.contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const activePresence = this.getActivePresence();
|
|
||||||
const openComments = this.comments.filter((c) => !c.resolved).length;
|
|
||||||
const pendingSuggestions = this.suggestions.filter((s) => s.status === 'pending').length;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="collaboration-container">
|
|
||||||
<!-- Presence Bar -->
|
|
||||||
<div class="presence-bar">
|
|
||||||
<div class="presence-info">
|
|
||||||
<span class="presence-label">Currently viewing:</span>
|
|
||||||
<div class="presence-avatars">
|
|
||||||
${activePresence.slice(0, 4).map(
|
|
||||||
(p) => html`
|
|
||||||
<div
|
|
||||||
class="presence-avatar"
|
|
||||||
style="background: ${p.userColor}"
|
|
||||||
title="${p.userName} - ${p.currentSection}"
|
|
||||||
>
|
|
||||||
${p.userName.charAt(0)}
|
|
||||||
<span class="status-dot ${Date.now() - p.lastActive > 60000 ? 'away' : ''}"></span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${activePresence.length > 4
|
|
||||||
? html`<div class="presence-count">+${activePresence.length - 4}</div>`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="share-btn">
|
|
||||||
<dees-icon .icon=${'lucide:Share2'}></dees-icon>
|
|
||||||
Share
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Comments Section -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:MessageCircle'}></dees-icon>
|
|
||||||
Comments
|
|
||||||
${openComments > 0 ? html`<span class="section-badge">${openComments} open</span>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
<!-- Filter tabs -->
|
|
||||||
<div class="filter-tabs">
|
|
||||||
<button
|
|
||||||
class="filter-tab ${this.commentFilter === 'all' ? 'active' : ''}"
|
|
||||||
@click=${() => (this.commentFilter = 'all')}
|
|
||||||
>
|
|
||||||
All (${this.comments.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="filter-tab ${this.commentFilter === 'open' ? 'active' : ''}"
|
|
||||||
@click=${() => (this.commentFilter = 'open')}
|
|
||||||
>
|
|
||||||
Open (${openComments})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="filter-tab ${this.commentFilter === 'resolved' ? 'active' : ''}"
|
|
||||||
@click=${() => (this.commentFilter = 'resolved')}
|
|
||||||
>
|
|
||||||
Resolved (${this.comments.length - openComments})
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- New comment input -->
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<div class="new-comment">
|
|
||||||
<textarea
|
|
||||||
class="new-comment-input"
|
|
||||||
placeholder="Add a comment..."
|
|
||||||
.value=${this.newCommentText}
|
|
||||||
@input=${(e: Event) => (this.newCommentText = (e.target as HTMLTextAreaElement).value)}
|
|
||||||
></textarea>
|
|
||||||
<button class="btn btn-primary" @click=${this.handleAddComment}>
|
|
||||||
<dees-icon .icon=${'lucide:Send'}></dees-icon>
|
|
||||||
Comment
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
|
|
||||||
<!-- Comments list -->
|
|
||||||
${this.getFilteredComments().length > 0
|
|
||||||
? html`
|
|
||||||
<div class="comments-list" style="margin-top: 16px;">
|
|
||||||
${this.getFilteredComments().map((comment) => this.renderComment(comment))}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state" style="margin-top: 16px;">
|
|
||||||
<dees-icon .icon=${'lucide:MessageSquare'}></dees-icon>
|
|
||||||
<h4>No Comments</h4>
|
|
||||||
<p>Start a discussion by adding a comment</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Suggestions Section -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:GitPullRequest'}></dees-icon>
|
|
||||||
Suggestions
|
|
||||||
${pendingSuggestions > 0 ? html`<span class="section-badge">${pendingSuggestions} pending</span>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
${this.suggestions.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="suggestions-list">
|
|
||||||
${this.suggestions.map((suggestion) => this.renderSuggestion(suggestion))}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:Edit3'}></dees-icon>
|
|
||||||
<h4>No Suggestions</h4>
|
|
||||||
<p>Suggested changes will appear here</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderComment(comment: IComment): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div class="comment-thread ${comment.resolved ? 'resolved' : ''}">
|
|
||||||
<div class="comment-header">
|
|
||||||
<div class="comment-avatar" style="background: ${comment.userColor}">
|
|
||||||
${comment.userName.charAt(0)}
|
|
||||||
</div>
|
|
||||||
<div class="comment-meta">
|
|
||||||
<div class="comment-author">${comment.userName}</div>
|
|
||||||
<div class="comment-time">${this.formatTimeAgo(comment.createdAt)}</div>
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-sm"
|
|
||||||
@click=${() => this.handleResolveComment(comment.id)}
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${comment.resolved ? 'lucide:RotateCcw' : 'lucide:Check'}></dees-icon>
|
|
||||||
${comment.resolved ? 'Reopen' : 'Resolve'}
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${comment.anchorText
|
|
||||||
? html`
|
|
||||||
<div class="comment-anchor">
|
|
||||||
<dees-icon .icon=${'lucide:Link'}></dees-icon>
|
|
||||||
${comment.anchorText}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
|
|
||||||
<div class="comment-content">${comment.content}</div>
|
|
||||||
|
|
||||||
${comment.replies.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="comment-replies">
|
|
||||||
${comment.replies.map(
|
|
||||||
(reply) => html`
|
|
||||||
<div class="reply-item">
|
|
||||||
<div class="comment-header">
|
|
||||||
<div class="comment-avatar" style="background: ${reply.userColor}; width: 28px; height: 28px; font-size: 11px;">
|
|
||||||
${reply.userName.charAt(0)}
|
|
||||||
</div>
|
|
||||||
<div class="comment-meta">
|
|
||||||
<div class="comment-author" style="font-size: 13px;">${reply.userName}</div>
|
|
||||||
<div class="comment-time">${this.formatTimeAgo(reply.createdAt)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="comment-content" style="font-size: 13px; margin-bottom: 0;">${reply.content}</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSuggestion(suggestion: ISuggestion): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div class="suggestion-card">
|
|
||||||
<div class="suggestion-header">
|
|
||||||
<div class="suggestion-user">
|
|
||||||
<div class="comment-avatar" style="background: ${suggestion.userColor}; width: 28px; height: 28px; font-size: 11px;">
|
|
||||||
${suggestion.userName.charAt(0)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="comment-author" style="font-size: 13px;">${suggestion.userName}</div>
|
|
||||||
<div class="comment-time">${this.formatTimeAgo(suggestion.createdAt)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="suggestion-status ${suggestion.status}">
|
|
||||||
<dees-icon .icon=${suggestion.status === 'pending' ? 'lucide:Clock' : suggestion.status === 'accepted' ? 'lucide:Check' : 'lucide:X'}></dees-icon>
|
|
||||||
${suggestion.status.charAt(0).toUpperCase() + suggestion.status.slice(1)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="suggestion-diff">
|
|
||||||
<span class="diff-removed">${suggestion.originalText}</span>
|
|
||||||
<span> → </span>
|
|
||||||
<span class="diff-added">${suggestion.suggestedText}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${suggestion.status === 'pending' && !this.readonly
|
|
||||||
? html`
|
|
||||||
<div class="suggestion-actions">
|
|
||||||
<button class="btn btn-success btn-sm" @click=${() => this.handleAcceptSuggestion(suggestion.id)}>
|
|
||||||
<dees-icon .icon=${'lucide:Check'}></dees-icon>
|
|
||||||
Accept
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger btn-sm" @click=${() => this.handleRejectSuggestion(suggestion.id)}>
|
|
||||||
<dees-icon .icon=${'lucide:X'}></dees-icon>
|
|
||||||
Reject
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-contract-content.js';
|
|
||||||
@@ -1,920 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contract-content.ts
|
|
||||||
* @description Contract content/paragraphs editor component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contract-content': SdigContractContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paragraph type configuration
|
|
||||||
const PARAGRAPH_TYPES = [
|
|
||||||
{ value: 'section', label: 'Section', icon: 'lucide:Heading' },
|
|
||||||
{ value: 'clause', label: 'Clause', icon: 'lucide:FileText' },
|
|
||||||
{ value: 'definition', label: 'Definition', icon: 'lucide:BookOpen' },
|
|
||||||
{ value: 'obligation', label: 'Obligation', icon: 'lucide:CheckSquare' },
|
|
||||||
{ value: 'condition', label: 'Condition', icon: 'lucide:GitBranch' },
|
|
||||||
{ value: 'schedule', label: 'Schedule', icon: 'lucide:Calendar' },
|
|
||||||
];
|
|
||||||
|
|
||||||
@customElement('sdig-contract-content')
|
|
||||||
export class SdigContractContent extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contract-content
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contract-content>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toolbar */
|
|
||||||
.content-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box input {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
outline: none;
|
|
||||||
width: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box input::placeholder {
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-box dees-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section card */
|
|
||||||
.section-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title dees-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-count {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-content {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Paragraph list */
|
|
||||||
.paragraphs-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 20px;
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-item:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-item.selected {
|
|
||||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
|
||||||
border-color: ${cssManager.bdTheme('#bfdbfe', '#1e40af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-item.editing {
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-drag-handle {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
cursor: grab;
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-drag-handle:hover {
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-number {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-title-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-title-input {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-title-input:focus {
|
|
||||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-type-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-body {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: ${cssManager.bdTheme('#4b5563', '#9ca3af')};
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-body.expanded {
|
|
||||||
-webkit-line-clamp: unset;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-body-textarea {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 150px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 12px;
|
|
||||||
outline: none;
|
|
||||||
resize: vertical;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-body-textarea:focus {
|
|
||||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-meta-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paragraph-edit-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Variable highlighting */
|
|
||||||
.variable {
|
|
||||||
display: inline;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'Roboto Mono', monospace;
|
|
||||||
font-size: 13px;
|
|
||||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
|
||||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Child paragraphs */
|
|
||||||
.child-paragraphs {
|
|
||||||
margin-left: 48px;
|
|
||||||
border-left: 2px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
padding-left: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.child-paragraphs .paragraph-item {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.child-paragraphs .paragraph-number {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add paragraph button */
|
|
||||||
.add-paragraph-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-top: 1px dashed ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-paragraph-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: transparent;
|
|
||||||
border: 2px dashed ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 8px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-paragraph-btn:hover {
|
|
||||||
border-color: ${cssManager.bdTheme('#9ca3af', '#52525b')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 60px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state dees-icon {
|
|
||||||
font-size: 64px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h4 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0 0 24px;
|
|
||||||
font-size: 14px;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: ${cssManager.bdTheme('#fef2f2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#dc2626', '#fca5a5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#7f1d1d')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* View mode toggle */
|
|
||||||
.view-toggle {
|
|
||||||
display: flex;
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-toggle-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 6px 10px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-toggle-btn.active {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#3f3f46')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-toggle-btn dees-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor readonly: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor selectedParagraphId: string | null = null;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor editingParagraphId: string | null = null;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor searchQuery: string = '';
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor viewMode: 'list' | 'outline' = 'list';
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor expandedParagraphs: Set<string> = new Set();
|
|
||||||
|
|
||||||
// Editing state
|
|
||||||
@state()
|
|
||||||
private accessor editTitle: string = '';
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor editContent: string = '';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleFieldChange(path: string, value: unknown) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('field-change', {
|
|
||||||
detail: { path, value },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSelectParagraph(paragraphId: string) {
|
|
||||||
this.selectedParagraphId = this.selectedParagraphId === paragraphId ? null : paragraphId;
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('paragraph-select', {
|
|
||||||
detail: { paragraphId: this.selectedParagraphId },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleEditParagraph(paragraph: plugins.sdInterfaces.IParagraph) {
|
|
||||||
this.editingParagraphId = paragraph.uniqueId;
|
|
||||||
this.editTitle = paragraph.title;
|
|
||||||
this.editContent = paragraph.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSaveEdit() {
|
|
||||||
if (!this.contract || !this.editingParagraphId) return;
|
|
||||||
|
|
||||||
const updatedParagraphs = this.contract.paragraphs.map((p) => {
|
|
||||||
if (p.uniqueId === this.editingParagraphId) {
|
|
||||||
return { ...p, title: this.editTitle, content: this.editContent };
|
|
||||||
}
|
|
||||||
return p;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.handleFieldChange('paragraphs', updatedParagraphs);
|
|
||||||
this.editingParagraphId = null;
|
|
||||||
this.editTitle = '';
|
|
||||||
this.editContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCancelEdit() {
|
|
||||||
this.editingParagraphId = null;
|
|
||||||
this.editTitle = '';
|
|
||||||
this.editContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddParagraph(parentId: string | null = null) {
|
|
||||||
if (!this.contract) return;
|
|
||||||
|
|
||||||
const newParagraph: plugins.sdInterfaces.IParagraph = {
|
|
||||||
uniqueId: `p-${Date.now()}`,
|
|
||||||
parent: parentId ? this.contract.paragraphs.find((p) => p.uniqueId === parentId) || null : null,
|
|
||||||
title: 'New Paragraph',
|
|
||||||
content: 'Enter paragraph content here...',
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedParagraphs = [...this.contract.paragraphs, newParagraph];
|
|
||||||
this.handleFieldChange('paragraphs', updatedParagraphs);
|
|
||||||
|
|
||||||
// Start editing the new paragraph
|
|
||||||
this.handleEditParagraph(newParagraph);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDeleteParagraph(paragraphId: string) {
|
|
||||||
if (!this.contract) return;
|
|
||||||
|
|
||||||
// Remove the paragraph and any children
|
|
||||||
const idsToRemove = new Set<string>([paragraphId]);
|
|
||||||
|
|
||||||
// Find child paragraphs recursively
|
|
||||||
const findChildren = (parentId: string) => {
|
|
||||||
this.contract!.paragraphs.forEach((p) => {
|
|
||||||
if (p.parent?.uniqueId === parentId) {
|
|
||||||
idsToRemove.add(p.uniqueId);
|
|
||||||
findChildren(p.uniqueId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
findChildren(paragraphId);
|
|
||||||
|
|
||||||
const updatedParagraphs = this.contract.paragraphs.filter((p) => !idsToRemove.has(p.uniqueId));
|
|
||||||
this.handleFieldChange('paragraphs', updatedParagraphs);
|
|
||||||
|
|
||||||
if (this.selectedParagraphId === paragraphId) {
|
|
||||||
this.selectedParagraphId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleMoveParagraph(paragraphId: string, direction: 'up' | 'down') {
|
|
||||||
if (!this.contract) return;
|
|
||||||
|
|
||||||
const paragraphs = [...this.contract.paragraphs];
|
|
||||||
const index = paragraphs.findIndex((p) => p.uniqueId === paragraphId);
|
|
||||||
|
|
||||||
if (index === -1) return;
|
|
||||||
if (direction === 'up' && index === 0) return;
|
|
||||||
if (direction === 'down' && index === paragraphs.length - 1) return;
|
|
||||||
|
|
||||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
|
||||||
[paragraphs[index], paragraphs[newIndex]] = [paragraphs[newIndex], paragraphs[index]];
|
|
||||||
|
|
||||||
this.handleFieldChange('paragraphs', paragraphs);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSearchChange(e: Event) {
|
|
||||||
const input = e.target as HTMLInputElement;
|
|
||||||
this.searchQuery = input.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleExpanded(paragraphId: string) {
|
|
||||||
const expanded = new Set(this.expandedParagraphs);
|
|
||||||
if (expanded.has(paragraphId)) {
|
|
||||||
expanded.delete(paragraphId);
|
|
||||||
} else {
|
|
||||||
expanded.add(paragraphId);
|
|
||||||
}
|
|
||||||
this.expandedParagraphs = expanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private getRootParagraphs(): plugins.sdInterfaces.IParagraph[] {
|
|
||||||
if (!this.contract) return [];
|
|
||||||
return this.contract.paragraphs.filter((p) => !p.parent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getChildParagraphs(parentId: string): plugins.sdInterfaces.IParagraph[] {
|
|
||||||
if (!this.contract) return [];
|
|
||||||
return this.contract.paragraphs.filter((p) => p.parent?.uniqueId === parentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private filterParagraphs(paragraphs: plugins.sdInterfaces.IParagraph[]): plugins.sdInterfaces.IParagraph[] {
|
|
||||||
if (!this.searchQuery) return paragraphs;
|
|
||||||
const query = this.searchQuery.toLowerCase();
|
|
||||||
return paragraphs.filter(
|
|
||||||
(p) =>
|
|
||||||
p.title.toLowerCase().includes(query) ||
|
|
||||||
p.content.toLowerCase().includes(query)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private highlightVariables(content: string): TemplateResult {
|
|
||||||
// Match {{variableName}} patterns
|
|
||||||
const parts = content.split(/(\{\{[^}]+\}\})/g);
|
|
||||||
return html`${parts.map((part) =>
|
|
||||||
part.startsWith('{{') && part.endsWith('}}')
|
|
||||||
? html`<span class="variable">${part}</span>`
|
|
||||||
: part
|
|
||||||
)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getParagraphNumber(paragraph: plugins.sdInterfaces.IParagraph, index: number): string {
|
|
||||||
// Simple numbering - can be enhanced for hierarchical numbering
|
|
||||||
return String(index + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
if (!this.contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootParagraphs = this.getRootParagraphs();
|
|
||||||
const filteredParagraphs = this.filterParagraphs(rootParagraphs);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="content-container">
|
|
||||||
<!-- Toolbar -->
|
|
||||||
<div class="content-toolbar">
|
|
||||||
<div class="toolbar-left">
|
|
||||||
<div class="search-box">
|
|
||||||
<dees-icon .icon=${'lucide:Search'}></dees-icon>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Search paragraphs..."
|
|
||||||
.value=${this.searchQuery}
|
|
||||||
@input=${this.handleSearchChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="toolbar-right">
|
|
||||||
<div class="view-toggle">
|
|
||||||
<button
|
|
||||||
class="view-toggle-btn ${this.viewMode === 'list' ? 'active' : ''}"
|
|
||||||
@click=${() => (this.viewMode = 'list')}
|
|
||||||
title="List view"
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:List'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="view-toggle-btn ${this.viewMode === 'outline' ? 'active' : ''}"
|
|
||||||
@click=${() => (this.viewMode = 'outline')}
|
|
||||||
title="Outline view"
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:LayoutList'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-primary" @click=${() => this.handleAddParagraph()}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add Paragraph
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Content Section -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:FileText'}></dees-icon>
|
|
||||||
Contract Content
|
|
||||||
</div>
|
|
||||||
<span class="paragraph-count">${this.contract.paragraphs.length} paragraphs</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section-content">
|
|
||||||
${filteredParagraphs.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="paragraphs-list">
|
|
||||||
${filteredParagraphs.map((paragraph, index) =>
|
|
||||||
this.renderParagraph(paragraph, index)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<div class="add-paragraph-row">
|
|
||||||
<button class="add-paragraph-btn" @click=${() => this.handleAddParagraph()}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add paragraph
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:FilePlus'}></dees-icon>
|
|
||||||
<h4>No Paragraphs Yet</h4>
|
|
||||||
<p>Start building your contract by adding paragraphs. Each paragraph can contain clauses, definitions, or obligations.</p>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-primary" @click=${() => this.handleAddParagraph()}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add First Paragraph
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderParagraph(paragraph: plugins.sdInterfaces.IParagraph, index: number): TemplateResult {
|
|
||||||
const isSelected = this.selectedParagraphId === paragraph.uniqueId;
|
|
||||||
const isEditing = this.editingParagraphId === paragraph.uniqueId;
|
|
||||||
const isExpanded = this.expandedParagraphs.has(paragraph.uniqueId);
|
|
||||||
const childParagraphs = this.getChildParagraphs(paragraph.uniqueId);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="paragraph-item ${isSelected ? 'selected' : ''} ${isEditing ? 'editing' : ''}"
|
|
||||||
@click=${() => !isEditing && this.handleSelectParagraph(paragraph.uniqueId)}
|
|
||||||
>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<div class="paragraph-drag-handle">
|
|
||||||
<dees-icon .icon=${'lucide:GripVertical'}></dees-icon>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
|
|
||||||
<div class="paragraph-number">${this.getParagraphNumber(paragraph, index)}</div>
|
|
||||||
|
|
||||||
<div class="paragraph-content">
|
|
||||||
${isEditing
|
|
||||||
? html`
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="paragraph-title-input"
|
|
||||||
.value=${this.editTitle}
|
|
||||||
@input=${(e: Event) => (this.editTitle = (e.target as HTMLInputElement).value)}
|
|
||||||
@click=${(e: Event) => e.stopPropagation()}
|
|
||||||
placeholder="Paragraph title"
|
|
||||||
/>
|
|
||||||
<textarea
|
|
||||||
class="paragraph-body-textarea"
|
|
||||||
.value=${this.editContent}
|
|
||||||
@input=${(e: Event) => (this.editContent = (e.target as HTMLTextAreaElement).value)}
|
|
||||||
@click=${(e: Event) => e.stopPropagation()}
|
|
||||||
placeholder="Paragraph content..."
|
|
||||||
></textarea>
|
|
||||||
<div class="paragraph-edit-actions">
|
|
||||||
<button class="btn btn-primary" @click=${(e: Event) => { e.stopPropagation(); this.handleSaveEdit(); }}>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-secondary" @click=${(e: Event) => { e.stopPropagation(); this.handleCancelEdit(); }}>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="paragraph-title-row">
|
|
||||||
<span class="paragraph-title">${paragraph.title}</span>
|
|
||||||
</div>
|
|
||||||
<div class="paragraph-body ${isExpanded ? 'expanded' : ''}">${this.highlightVariables(paragraph.content)}</div>
|
|
||||||
${paragraph.content.length > 200
|
|
||||||
? html`
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-sm"
|
|
||||||
@click=${(e: Event) => { e.stopPropagation(); this.toggleExpanded(paragraph.uniqueId); }}
|
|
||||||
style="margin-top: 8px;"
|
|
||||||
>
|
|
||||||
${isExpanded ? 'Show less' : 'Show more'}
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${!this.readonly && !isEditing
|
|
||||||
? html`
|
|
||||||
<div class="paragraph-actions">
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost"
|
|
||||||
@click=${(e: Event) => { e.stopPropagation(); this.handleEditParagraph(paragraph); }}
|
|
||||||
title="Edit"
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:Pencil'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost"
|
|
||||||
@click=${(e: Event) => { e.stopPropagation(); this.handleMoveParagraph(paragraph.uniqueId, 'up'); }}
|
|
||||||
title="Move up"
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:ChevronUp'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost"
|
|
||||||
@click=${(e: Event) => { e.stopPropagation(); this.handleMoveParagraph(paragraph.uniqueId, 'down'); }}
|
|
||||||
title="Move down"
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:ChevronDown'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost btn-danger"
|
|
||||||
@click=${(e: Event) => { e.stopPropagation(); this.handleDeleteParagraph(paragraph.uniqueId); }}
|
|
||||||
title="Delete"
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:Trash2'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${childParagraphs.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="child-paragraphs">
|
|
||||||
${childParagraphs.map((child, childIndex) => this.renderParagraph(child, childIndex))}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-contract-header.js';
|
|
||||||
@@ -1,558 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contract-header.ts
|
|
||||||
* @description Contract header component with title, status, and quick actions
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contract-header': SdigContractHeader;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('sdig-contract-header')
|
|
||||||
export class SdigContractHeader extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contract-header
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contract-header>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-top {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-section {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contract-number {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-input-wrapper {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-input {
|
|
||||||
width: 100%;
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
outline: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
transition: border-color 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-input:focus {
|
|
||||||
border-bottom-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-input::placeholder {
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* shadcn-style badge */
|
|
||||||
.status-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 2px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge:hover:not(:disabled) {
|
|
||||||
filter: brightness(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge:disabled {
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.draft {
|
|
||||||
background: ${cssManager.bdTheme('hsl(48 96% 89%)', 'hsl(48 96% 15%)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(25 95% 30%)', 'hsl(48 96% 70%)')};
|
|
||||||
border-color: ${cssManager.bdTheme('hsl(48 96% 76%)', 'hsl(48 96% 25%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.review {
|
|
||||||
background: ${cssManager.bdTheme('hsl(214 95% 93%)', 'hsl(214 95% 15%)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(214 95% 35%)', 'hsl(214 95% 70%)')};
|
|
||||||
border-color: ${cssManager.bdTheme('hsl(214 95% 80%)', 'hsl(214 95% 25%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.pending {
|
|
||||||
background: ${cssManager.bdTheme('hsl(38 92% 90%)', 'hsl(38 92% 15%)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(25 95% 35%)', 'hsl(38 92% 65%)')};
|
|
||||||
border-color: ${cssManager.bdTheme('hsl(38 92% 75%)', 'hsl(38 92% 25%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.signed,
|
|
||||||
.status-badge.active {
|
|
||||||
background: ${cssManager.bdTheme('hsl(142 76% 90%)', 'hsl(142 76% 15%)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(142 76% 28%)', 'hsl(142 76% 65%)')};
|
|
||||||
border-color: ${cssManager.bdTheme('hsl(142 76% 75%)', 'hsl(142 76% 25%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.terminated {
|
|
||||||
background: ${cssManager.bdTheme('hsl(0 84% 92%)', 'hsl(0 84% 15%)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(0 84% 35%)', 'hsl(0 84% 65%)')};
|
|
||||||
border-color: ${cssManager.bdTheme('hsl(0 84% 80%)', 'hsl(0 84% 25%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: currentColor;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Meta info row */
|
|
||||||
.meta-row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 24px;
|
|
||||||
padding-top: 20px;
|
|
||||||
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-value {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-value.clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-value.clickable:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tags */
|
|
||||||
.tags-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Quick actions */
|
|
||||||
.quick-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn dees-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status dropdown */
|
|
||||||
.status-dropdown {
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
margin-top: 8px;
|
|
||||||
min-width: 200px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
||||||
z-index: 100;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.1s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-option:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-option.selected {
|
|
||||||
background: ${cssManager.bdTheme('#eff6ff', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor readonly: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor showStatusDropdown: boolean = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor editingTitle: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATUS CONFIGURATION
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private statusOptions: Array<{
|
|
||||||
value: plugins.sdInterfaces.TContractStatus;
|
|
||||||
label: string;
|
|
||||||
category: string;
|
|
||||||
}> = [
|
|
||||||
{ value: 'draft', label: 'Draft', category: 'draft' },
|
|
||||||
{ value: 'internal_review', label: 'Internal Review', category: 'review' },
|
|
||||||
{ value: 'legal_review', label: 'Legal Review', category: 'review' },
|
|
||||||
{ value: 'negotiation', label: 'Negotiation', category: 'review' },
|
|
||||||
{ value: 'pending_approval', label: 'Pending Approval', category: 'pending' },
|
|
||||||
{ value: 'pending_signature', label: 'Pending Signature', category: 'pending' },
|
|
||||||
{ value: 'partially_signed', label: 'Partially Signed', category: 'pending' },
|
|
||||||
{ value: 'signed', label: 'Signed', category: 'signed' },
|
|
||||||
{ value: 'executed', label: 'Executed', category: 'signed' },
|
|
||||||
{ value: 'active', label: 'Active', category: 'active' },
|
|
||||||
{ value: 'expired', label: 'Expired', category: 'terminated' },
|
|
||||||
{ value: 'terminated', label: 'Terminated', category: 'terminated' },
|
|
||||||
{ value: 'cancelled', label: 'Cancelled', category: 'terminated' },
|
|
||||||
{ value: 'voided', label: 'Voided', category: 'terminated' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleTitleChange(e: Event) {
|
|
||||||
const input = e.target as HTMLInputElement;
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('field-change', {
|
|
||||||
detail: { path: 'title', value: input.value },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleStatusChange(status: plugins.sdInterfaces.TContractStatus) {
|
|
||||||
this.showStatusDropdown = false;
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('field-change', {
|
|
||||||
detail: { path: 'lifecycle.currentStatus', value: status },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleStatusDropdown() {
|
|
||||||
if (!this.readonly) {
|
|
||||||
this.showStatusDropdown = !this.showStatusDropdown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleExport() {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('action', {
|
|
||||||
detail: { action: 'export' },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDuplicate() {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('action', {
|
|
||||||
detail: { action: 'duplicate' },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleShare() {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('action', {
|
|
||||||
detail: { action: 'share' },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private getStatusCategory(status: string): string {
|
|
||||||
const option = this.statusOptions.find((o) => o.value === status);
|
|
||||||
return option?.category || 'draft';
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatStatus(status: string): string {
|
|
||||||
const option = this.statusOptions.find((o) => o.value === status);
|
|
||||||
return option?.label || status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatDate(timestamp: number): string {
|
|
||||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatContractType(type: string): string {
|
|
||||||
return type.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
if (!this.contract) {
|
|
||||||
return html`<div class="header-card">No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = this.contract.lifecycle?.currentStatus || 'draft';
|
|
||||||
const statusCategory = this.getStatusCategory(status);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="header-card">
|
|
||||||
<div class="header-top">
|
|
||||||
<div class="title-section">
|
|
||||||
${this.contract.metadata?.contractNumber
|
|
||||||
? html`<div class="contract-number">#${this.contract.metadata.contractNumber}</div>`
|
|
||||||
: ''}
|
|
||||||
<div class="title-input-wrapper">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="title-input"
|
|
||||||
.value=${this.contract.title}
|
|
||||||
placeholder="Contract Title"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${this.handleTitleChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status-section">
|
|
||||||
<div style="position: relative;">
|
|
||||||
<button
|
|
||||||
class="status-badge ${statusCategory}"
|
|
||||||
@click=${this.toggleStatusDropdown}
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
>
|
|
||||||
<span class="status-dot"></span>
|
|
||||||
${this.formatStatus(status)}
|
|
||||||
${!this.readonly
|
|
||||||
? html`<dees-icon .icon=${'lucide:ChevronDown'} style="font-size: 14px;"></dees-icon>`
|
|
||||||
: ''}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
${this.showStatusDropdown
|
|
||||||
? html`
|
|
||||||
<div class="status-dropdown">
|
|
||||||
${this.statusOptions.map(
|
|
||||||
(option) => html`
|
|
||||||
<div
|
|
||||||
class="status-option ${status === option.value ? 'selected' : ''}"
|
|
||||||
@click=${() => this.handleStatusChange(option.value)}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="status-dot"
|
|
||||||
style="background: ${option.category === 'draft'
|
|
||||||
? '#f59e0b'
|
|
||||||
: option.category === 'review'
|
|
||||||
? '#3b82f6'
|
|
||||||
: option.category === 'pending'
|
|
||||||
? '#f59e0b'
|
|
||||||
: option.category === 'signed' || option.category === 'active'
|
|
||||||
? '#10b981'
|
|
||||||
: '#ef4444'}"
|
|
||||||
></span>
|
|
||||||
${option.label}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="quick-actions">
|
|
||||||
<button class="action-btn" @click=${this.handleExport} title="Export">
|
|
||||||
<dees-icon .icon=${'lucide:Download'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button class="action-btn" @click=${this.handleDuplicate} title="Duplicate">
|
|
||||||
<dees-icon .icon=${'lucide:Copy'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button class="action-btn" @click=${this.handleShare} title="Share">
|
|
||||||
<dees-icon .icon=${'lucide:Share2'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.contract.metadata ? html`
|
|
||||||
<div class="meta-row">
|
|
||||||
<div class="meta-item">
|
|
||||||
<span class="meta-label">Type</span>
|
|
||||||
<span class="meta-value">${this.formatContractType(this.contract.metadata.contractType)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="meta-item">
|
|
||||||
<span class="meta-label">Category</span>
|
|
||||||
<span class="meta-value">${this.formatContractType(this.contract.metadata.category)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="meta-item">
|
|
||||||
<span class="meta-label">Language</span>
|
|
||||||
<span class="meta-value">${this.contract.metadata.language?.toUpperCase() || 'N/A'}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="meta-item">
|
|
||||||
<span class="meta-label">Jurisdiction</span>
|
|
||||||
<span class="meta-value">
|
|
||||||
${this.contract.metadata.governingLaw?.country || 'Not specified'}
|
|
||||||
${this.contract.metadata.governingLaw?.state
|
|
||||||
? `, ${this.contract.metadata.governingLaw.state}`
|
|
||||||
: ''}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="meta-item">
|
|
||||||
<span class="meta-label">Created</span>
|
|
||||||
<span class="meta-value">${this.formatDate(this.contract.createdAt)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="meta-item">
|
|
||||||
<span class="meta-label">Parties</span>
|
|
||||||
<span class="meta-value clickable">${this.contract.involvedParties?.length || 0} parties</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.contract.metadata.tags?.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="meta-item">
|
|
||||||
<span class="meta-label">Tags</span>
|
|
||||||
<div class="tags-container">
|
|
||||||
${this.contract.metadata.tags.map((tag) => html`<span class="tag">${tag}</span>`)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-contract-metadata.js';
|
|
||||||
@@ -1,820 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contract-metadata.ts
|
|
||||||
* @description Contract metadata editor component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contract-metadata': SdigContractMetadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type-safe options arrays
|
|
||||||
const CONTRACT_CATEGORIES: Array<{ value: plugins.sdInterfaces.TContractCategory; label: string }> = [
|
|
||||||
{ value: 'employment', label: 'Employment' },
|
|
||||||
{ value: 'service', label: 'Service Agreement' },
|
|
||||||
{ value: 'sales', label: 'Sales' },
|
|
||||||
{ value: 'lease', label: 'Lease / Rental' },
|
|
||||||
{ value: 'license', label: 'License' },
|
|
||||||
{ value: 'partnership', label: 'Partnership' },
|
|
||||||
{ value: 'confidentiality', label: 'Confidentiality / NDA' },
|
|
||||||
{ value: 'financial', label: 'Financial' },
|
|
||||||
{ value: 'real_estate', label: 'Real Estate' },
|
|
||||||
{ value: 'intellectual_property', label: 'Intellectual Property' },
|
|
||||||
{ value: 'government', label: 'Government' },
|
|
||||||
{ value: 'construction', label: 'Construction' },
|
|
||||||
{ value: 'healthcare', label: 'Healthcare' },
|
|
||||||
{ value: 'insurance', label: 'Insurance' },
|
|
||||||
{ value: 'other', label: 'Other' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const CONFIDENTIALITY_LEVELS: Array<{ value: plugins.sdInterfaces.TConfidentialityLevel; label: string }> = [
|
|
||||||
{ value: 'public', label: 'Public' },
|
|
||||||
{ value: 'internal', label: 'Internal' },
|
|
||||||
{ value: 'confidential', label: 'Confidential' },
|
|
||||||
{ value: 'restricted', label: 'Restricted' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const DISPUTE_RESOLUTIONS: Array<{ value: plugins.sdInterfaces.TDisputeResolution; label: string }> = [
|
|
||||||
{ value: 'litigation', label: 'Litigation' },
|
|
||||||
{ value: 'arbitration', label: 'Arbitration' },
|
|
||||||
{ value: 'mediation', label: 'Mediation' },
|
|
||||||
{ value: 'negotiation', label: 'Negotiation' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const COMMON_LANGUAGES = [
|
|
||||||
{ value: 'en', label: 'English' },
|
|
||||||
{ value: 'de', label: 'German' },
|
|
||||||
{ value: 'fr', label: 'French' },
|
|
||||||
{ value: 'es', label: 'Spanish' },
|
|
||||||
{ value: 'it', label: 'Italian' },
|
|
||||||
{ value: 'pt', label: 'Portuguese' },
|
|
||||||
{ value: 'nl', label: 'Dutch' },
|
|
||||||
{ value: 'pl', label: 'Polish' },
|
|
||||||
{ value: 'sv', label: 'Swedish' },
|
|
||||||
{ value: 'da', label: 'Danish' },
|
|
||||||
{ value: 'fi', label: 'Finnish' },
|
|
||||||
{ value: 'no', label: 'Norwegian' },
|
|
||||||
{ value: 'zh', label: 'Chinese' },
|
|
||||||
{ value: 'ja', label: 'Japanese' },
|
|
||||||
{ value: 'ko', label: 'Korean' },
|
|
||||||
{ value: 'ar', label: 'Arabic' },
|
|
||||||
{ value: 'ru', label: 'Russian' },
|
|
||||||
];
|
|
||||||
|
|
||||||
@customElement('sdig-contract-metadata')
|
|
||||||
export class SdigContractMetadata extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contract-metadata
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contract-metadata>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.metadata-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section cards */
|
|
||||||
.section-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title dees-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form grid */
|
|
||||||
.form-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group.full-width {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label .required {
|
|
||||||
color: #ef4444;
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-description {
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input styles */
|
|
||||||
.form-input,
|
|
||||||
.form-select,
|
|
||||||
.form-textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 8px;
|
|
||||||
outline: none;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input:focus,
|
|
||||||
.form-select:focus,
|
|
||||||
.form-textarea:focus {
|
|
||||||
border-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0,0,0,0.05)', 'rgba(255,255,255,0.05)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input::placeholder,
|
|
||||||
.form-textarea::placeholder {
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input:disabled,
|
|
||||||
.form-select:disabled,
|
|
||||||
.form-textarea:disabled {
|
|
||||||
opacity: 0.6;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-select {
|
|
||||||
appearance: none;
|
|
||||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
|
||||||
background-position: right 10px center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 16px;
|
|
||||||
padding-right: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-textarea {
|
|
||||||
min-height: 80px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Radio group */
|
|
||||||
.radio-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-option input[type="radio"] {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
accent-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-option span {
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tags input */
|
|
||||||
.tags-input-container {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
min-height: 44px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-input-container:focus-within {
|
|
||||||
border-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0,0,0,0.05)', 'rgba(255,255,255,0.05)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-item {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-remove {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-remove:hover {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-input {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 120px;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
padding: 4px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags-input::placeholder {
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Divider */
|
|
||||||
.section-divider {
|
|
||||||
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapsible sections */
|
|
||||||
.collapsible-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 0;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible-header:hover .collapse-icon {
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapse-icon.expanded {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible-content {
|
|
||||||
overflow: hidden;
|
|
||||||
max-height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collapsible-content.expanded {
|
|
||||||
max-height: 1000px;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor readonly: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor showArbitrationFields: boolean = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor newTag: string = '';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LIFECYCLE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public updated(changedProperties: Map<string, unknown>) {
|
|
||||||
if (changedProperties.has('contract') && this.contract?.metadata?.governingLaw) {
|
|
||||||
this.showArbitrationFields = this.contract.metadata.governingLaw.disputeResolution === 'arbitration';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleFieldChange(path: string, value: unknown) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('field-change', {
|
|
||||||
detail: { path, value },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleInputChange(path: string, e: Event) {
|
|
||||||
const input = e.target as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
||||||
this.handleFieldChange(path, input.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRadioChange(path: string, value: string) {
|
|
||||||
this.handleFieldChange(path, value);
|
|
||||||
|
|
||||||
// Special handling for dispute resolution
|
|
||||||
if (path === 'metadata.governingLaw.disputeResolution') {
|
|
||||||
this.showArbitrationFields = value === 'arbitration';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleTagAdd(e: KeyboardEvent) {
|
|
||||||
if (e.key === 'Enter' && this.newTag.trim()) {
|
|
||||||
e.preventDefault();
|
|
||||||
const currentTags = this.contract?.metadata.tags || [];
|
|
||||||
if (!currentTags.includes(this.newTag.trim())) {
|
|
||||||
this.handleFieldChange('metadata.tags', [...currentTags, this.newTag.trim()]);
|
|
||||||
}
|
|
||||||
this.newTag = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleTagRemove(tag: string) {
|
|
||||||
const currentTags = this.contract?.metadata.tags || [];
|
|
||||||
this.handleFieldChange(
|
|
||||||
'metadata.tags',
|
|
||||||
currentTags.filter((t) => t !== tag)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
if (!this.contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.contract.metadata) {
|
|
||||||
return html`<div class="metadata-container"><div class="section-card">Contract metadata not available</div></div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadata = this.contract.metadata;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="metadata-container">
|
|
||||||
<!-- Basic Information -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:Info'}></dees-icon>
|
|
||||||
Basic Information
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Contract Number</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.contractNumber || ''}
|
|
||||||
placeholder="e.g., CNT-2024-001"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.contractNumber', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Category <span class="required">*</span></label>
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
.value=${metadata.category}
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@change=${(e: Event) => this.handleInputChange('metadata.category', e)}
|
|
||||||
>
|
|
||||||
${CONTRACT_CATEGORIES.map(
|
|
||||||
(cat) => html`
|
|
||||||
<option value=${cat.value} ?selected=${metadata.category === cat.value}>
|
|
||||||
${cat.label}
|
|
||||||
</option>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Contract Type <span class="required">*</span></label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.contractType}
|
|
||||||
placeholder="e.g., employment_minijob"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.contractType', e)}
|
|
||||||
/>
|
|
||||||
<p class="form-description">Specific contract type identifier</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Confidentiality Level</label>
|
|
||||||
<div class="radio-group">
|
|
||||||
${CONFIDENTIALITY_LEVELS.map(
|
|
||||||
(level) => html`
|
|
||||||
<label class="radio-option">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="confidentiality"
|
|
||||||
.value=${level.value}
|
|
||||||
?checked=${metadata.confidentialityLevel === level.value}
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@change=${() => this.handleRadioChange('metadata.confidentialityLevel', level.value)}
|
|
||||||
/>
|
|
||||||
<span>${level.label}</span>
|
|
||||||
</label>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group full-width">
|
|
||||||
<label class="form-label">Tags</label>
|
|
||||||
<div class="tags-input-container">
|
|
||||||
${metadata.tags.map(
|
|
||||||
(tag) => html`
|
|
||||||
<span class="tag-item">
|
|
||||||
${tag}
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="tag-remove" @click=${() => this.handleTagRemove(tag)}>×</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="tags-input"
|
|
||||||
.value=${this.newTag}
|
|
||||||
placeholder="Add tag and press Enter"
|
|
||||||
@input=${(e: Event) => (this.newTag = (e.target as HTMLInputElement).value)}
|
|
||||||
@keydown=${this.handleTagAdd}
|
|
||||||
/>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Language Settings -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:Globe'}></dees-icon>
|
|
||||||
Language Settings
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Primary Language <span class="required">*</span></label>
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
.value=${metadata.language}
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@change=${(e: Event) => this.handleInputChange('metadata.language', e)}
|
|
||||||
>
|
|
||||||
${COMMON_LANGUAGES.map(
|
|
||||||
(lang) => html`
|
|
||||||
<option value=${lang.value} ?selected=${metadata.language === lang.value}>
|
|
||||||
${lang.label}
|
|
||||||
</option>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Binding Language</label>
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
.value=${metadata.bindingLanguage || ''}
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@change=${(e: Event) => this.handleInputChange('metadata.bindingLanguage', e)}
|
|
||||||
>
|
|
||||||
<option value="">Same as primary</option>
|
|
||||||
${COMMON_LANGUAGES.map(
|
|
||||||
(lang) => html`
|
|
||||||
<option value=${lang.value} ?selected=${metadata.bindingLanguage === lang.value}>
|
|
||||||
${lang.label}
|
|
||||||
</option>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
<p class="form-description">Language that takes precedence in case of conflicts</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Governing Law -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:Scale'}></dees-icon>
|
|
||||||
Governing Law & Jurisdiction
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Country <span class="required">*</span></label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.governingLaw.country}
|
|
||||||
placeholder="e.g., Germany"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.country', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">State / Province</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.governingLaw.state || ''}
|
|
||||||
placeholder="e.g., Bavaria"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.state', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Dispute Jurisdiction</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.governingLaw.disputeJurisdiction || ''}
|
|
||||||
placeholder="e.g., Munich Courts"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.disputeJurisdiction', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group full-width">
|
|
||||||
<label class="form-label">Dispute Resolution Method</label>
|
|
||||||
<div class="radio-group">
|
|
||||||
${DISPUTE_RESOLUTIONS.map(
|
|
||||||
(res) => html`
|
|
||||||
<label class="radio-option">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="disputeResolution"
|
|
||||||
.value=${res.value}
|
|
||||||
?checked=${metadata.governingLaw.disputeResolution === res.value}
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@change=${() => this.handleRadioChange('metadata.governingLaw.disputeResolution', res.value)}
|
|
||||||
/>
|
|
||||||
<span>${res.label}</span>
|
|
||||||
</label>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.showArbitrationFields
|
|
||||||
? html`
|
|
||||||
<div class="section-divider full-width"></div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Arbitration Institution</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.governingLaw.arbitrationInstitution || ''}
|
|
||||||
placeholder="e.g., ICC, LCIA, AAA"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.arbitrationInstitution', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Arbitration Rules</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.governingLaw.arbitrationRules || ''}
|
|
||||||
placeholder="e.g., ICC Rules of Arbitration"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.arbitrationRules', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Seat of Arbitration</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.governingLaw.arbitrationSeat || ''}
|
|
||||||
placeholder="e.g., Paris, London"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.governingLaw.arbitrationSeat', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Number of Arbitrators</label>
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
.value=${String(metadata.governingLaw.numberOfArbitrators || 1)}
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@change=${(e: Event) =>
|
|
||||||
this.handleFieldChange(
|
|
||||||
'metadata.governingLaw.numberOfArbitrators',
|
|
||||||
parseInt((e.target as HTMLSelectElement).value, 10)
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<option value="1">1 Arbitrator</option>
|
|
||||||
<option value="3">3 Arbitrators</option>
|
|
||||||
<option value="5">5 Arbitrators</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Proceedings Language</label>
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
.value=${metadata.governingLaw.proceedingsLanguage || ''}
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@change=${(e: Event) => this.handleInputChange('metadata.governingLaw.proceedingsLanguage', e)}
|
|
||||||
>
|
|
||||||
<option value="">Select language</option>
|
|
||||||
${COMMON_LANGUAGES.map(
|
|
||||||
(lang) => html`
|
|
||||||
<option
|
|
||||||
value=${lang.value}
|
|
||||||
?selected=${metadata.governingLaw.proceedingsLanguage === lang.value}
|
|
||||||
>
|
|
||||||
${lang.label}
|
|
||||||
</option>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- References -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:Link'}></dees-icon>
|
|
||||||
References & Integration
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="form-grid">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Internal Reference</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.internalReference || ''}
|
|
||||||
placeholder="Internal tracking reference"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.internalReference', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">External Reference</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.externalReference || ''}
|
|
||||||
placeholder="External system reference"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.externalReference', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Template ID</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.templateId || ''}
|
|
||||||
placeholder="Source template ID"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.templateId', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Parent Contract ID</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-input"
|
|
||||||
.value=${metadata.parentContractId || ''}
|
|
||||||
placeholder="Parent/master contract"
|
|
||||||
?disabled=${this.readonly}
|
|
||||||
@input=${(e: Event) => this.handleInputChange('metadata.parentContractId', e)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-contract-parties.js';
|
|
||||||
@@ -1,736 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contract-parties.ts
|
|
||||||
* @description Contract parties and roles management component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contract-parties': SdigContractParties;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Party role display configuration
|
|
||||||
const PARTY_ROLES: Array<{ value: plugins.sdInterfaces.TPartyRole; label: string; icon: string }> = [
|
|
||||||
{ value: 'signer', label: 'Signer', icon: 'lucide:PenTool' },
|
|
||||||
{ value: 'witness', label: 'Witness', icon: 'lucide:Eye' },
|
|
||||||
{ value: 'notary', label: 'Notary', icon: 'lucide:Stamp' },
|
|
||||||
{ value: 'cc', label: 'CC (Copy)', icon: 'lucide:Mail' },
|
|
||||||
{ value: 'approver', label: 'Approver', icon: 'lucide:CheckCircle' },
|
|
||||||
{ value: 'guarantor', label: 'Guarantor', icon: 'lucide:Shield' },
|
|
||||||
{ value: 'beneficiary', label: 'Beneficiary', icon: 'lucide:UserCheck' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const SIGNING_DEPENDENCIES: Array<{ value: plugins.sdInterfaces.TSigningDependency; label: string }> = [
|
|
||||||
{ value: 'none', label: 'No dependency' },
|
|
||||||
{ value: 'sequential', label: 'Sequential (in order)' },
|
|
||||||
{ value: 'parallel', label: 'Parallel (any order)' },
|
|
||||||
{ value: 'after_specific', label: 'After specific parties' },
|
|
||||||
];
|
|
||||||
|
|
||||||
@customElement('sdig-contract-parties')
|
|
||||||
export class SdigContractParties extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contract-parties
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contract-parties>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parties-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section cards */
|
|
||||||
.section-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title dees-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Roles list */
|
|
||||||
.roles-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-card:hover {
|
|
||||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-name {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-name dees-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
padding: 6px;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-badge {
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-description {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-meta-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Parties list */
|
|
||||||
.parties-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-card:hover {
|
|
||||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-card.selected {
|
|
||||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-avatar {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-name {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-role-tag {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-details {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-detail {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-detail dees-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-status {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-status {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-status.pending {
|
|
||||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
|
||||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-status.signed {
|
|
||||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
|
||||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-status.declined {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.signing-order {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-number {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add button */
|
|
||||||
.add-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 20px;
|
|
||||||
background: transparent;
|
|
||||||
border: 2px dashed ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 10px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-button:hover {
|
|
||||||
border-color: ${cssManager.bdTheme('#9ca3af', '#52525b')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state dees-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h4 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Action buttons */
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor readonly: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor selectedPartyId: string | null = null;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor showRoleEditor: boolean = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor showPartyEditor: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleFieldChange(path: string, value: unknown) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('field-change', {
|
|
||||||
detail: { path, value },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSelectParty(partyId: string) {
|
|
||||||
this.selectedPartyId = this.selectedPartyId === partyId ? null : partyId;
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('party-select', {
|
|
||||||
detail: { partyId: this.selectedPartyId },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddRole() {
|
|
||||||
this.showRoleEditor = true;
|
|
||||||
// TODO: Open role editor modal
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddParty() {
|
|
||||||
this.showPartyEditor = true;
|
|
||||||
// TODO: Open party editor modal
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRemoveParty(partyId: string) {
|
|
||||||
if (!this.contract) return;
|
|
||||||
const updatedParties = this.contract.involvedParties.filter((p) => p.partyId !== partyId);
|
|
||||||
this.handleFieldChange('involvedParties', updatedParties);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private getPartyDisplayName(party: plugins.sdInterfaces.IInvolvedParty): string {
|
|
||||||
if (!party) return 'Unknown Party';
|
|
||||||
const contact = party.contact;
|
|
||||||
if (!contact) return party.deliveryEmail || 'Unknown Party';
|
|
||||||
if ('name' in contact && contact.name) {
|
|
||||||
return contact.name as string;
|
|
||||||
}
|
|
||||||
if ('firstName' in contact && 'lastName' in contact) {
|
|
||||||
return `${contact.firstName || ''} ${contact.lastName || ''}`.trim() || party.deliveryEmail || 'Unknown Party';
|
|
||||||
}
|
|
||||||
return party.deliveryEmail || 'Unknown Party';
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPartyInitials(party: plugins.sdInterfaces.IInvolvedParty): string {
|
|
||||||
const name = this.getPartyDisplayName(party);
|
|
||||||
if (!name || name.length === 0) return '??';
|
|
||||||
const parts = name.split(' ');
|
|
||||||
if (parts.length >= 2 && parts[0].length > 0 && parts[parts.length - 1].length > 0) {
|
|
||||||
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
||||||
}
|
|
||||||
return name.substring(0, Math.min(2, name.length)).toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPartyColor(party: plugins.sdInterfaces.IInvolvedParty): string {
|
|
||||||
// Generate a consistent color based on party ID
|
|
||||||
const colors = [
|
|
||||||
'#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6',
|
|
||||||
'#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1',
|
|
||||||
];
|
|
||||||
const idStr = party?.partyId || 'default';
|
|
||||||
const hash = idStr.split('').reduce((a, b) => a + b.charCodeAt(0), 0);
|
|
||||||
return colors[hash % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRoleName(roleId: string): string {
|
|
||||||
if (!roleId) return 'Unknown Role';
|
|
||||||
const role = this.contract?.availableRoles.find((r) => r.id === roleId);
|
|
||||||
return role?.name || roleId.charAt(0).toUpperCase() + roleId.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSignatureStatusClass(status: string): string {
|
|
||||||
if (status === 'signed') return 'signed';
|
|
||||||
if (status === 'declined') return 'declined';
|
|
||||||
return 'pending';
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatSignatureStatus(status: string): string {
|
|
||||||
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
if (!this.contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roles = this.contract.availableRoles;
|
|
||||||
const parties = this.contract.involvedParties;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="parties-container">
|
|
||||||
<!-- Roles Section -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:Users2'}></dees-icon>
|
|
||||||
Available Roles
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-secondary btn-sm" @click=${this.handleAddRole}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add Role
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
${roles.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="roles-grid">
|
|
||||||
${roles.map((role) => this.renderRoleCard(role))}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:Users'}></dees-icon>
|
|
||||||
<h4>No Roles Defined</h4>
|
|
||||||
<p>Add roles to define the types of parties in this contract</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Parties Section -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:UserPlus'}></dees-icon>
|
|
||||||
Involved Parties (${parties.length})
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-primary btn-sm" @click=${this.handleAddParty}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add Party
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
${parties.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="parties-list">
|
|
||||||
${parties.map((party) => this.renderPartyCard(party))}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:UserPlus'}></dees-icon>
|
|
||||||
<h4>No Parties Added</h4>
|
|
||||||
<p>Add parties who will be involved in this contract</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderRoleCard(role: plugins.sdInterfaces.IRole): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div class="role-card">
|
|
||||||
<div class="role-header">
|
|
||||||
<div class="role-name">
|
|
||||||
<dees-icon
|
|
||||||
.icon=${role.icon || 'lucide:User'}
|
|
||||||
style="color: ${role.displayColor || 'inherit'}"
|
|
||||||
></dees-icon>
|
|
||||||
${role.name}
|
|
||||||
</div>
|
|
||||||
<span class="role-badge">${role.category}</span>
|
|
||||||
</div>
|
|
||||||
<div class="role-description">${role.description || 'No description'}</div>
|
|
||||||
<div class="role-meta">
|
|
||||||
${role.signatureRequired
|
|
||||||
? html`
|
|
||||||
<span class="role-meta-item">
|
|
||||||
<dees-icon .icon=${'lucide:PenTool'}></dees-icon>
|
|
||||||
Signature required
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
${role.defaultSigningOrder > 0
|
|
||||||
? html`
|
|
||||||
<span class="role-meta-item">
|
|
||||||
<dees-icon .icon=${'lucide:ListOrdered'}></dees-icon>
|
|
||||||
Order: ${role.defaultSigningOrder}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
${role.minParties
|
|
||||||
? html`
|
|
||||||
<span class="role-meta-item">
|
|
||||||
<dees-icon .icon=${'lucide:Users'}></dees-icon>
|
|
||||||
Min: ${role.minParties}${role.maxParties ? `, Max: ${role.maxParties}` : ''}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderPartyCard(party: plugins.sdInterfaces.IInvolvedParty): TemplateResult {
|
|
||||||
// Handle both full IInvolvedParty and minimal demo data
|
|
||||||
const partyId = (party as any).partyId || (party as any).role || 'unknown';
|
|
||||||
const roleId = (party as any).roleId || (party as any).role || '';
|
|
||||||
const partyRole = (party as any).partyRole || 'signer';
|
|
||||||
const signatureStatus = (party as any).signature?.status || 'pending';
|
|
||||||
const signingOrder = (party as any).signingOrder || 0;
|
|
||||||
const deliveryEmail = (party as any).deliveryEmail;
|
|
||||||
const deliveryPhone = (party as any).deliveryPhone;
|
|
||||||
const actingAsProxy = (party as any).actingAsProxy;
|
|
||||||
|
|
||||||
const isSelected = this.selectedPartyId === partyId;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="party-card ${isSelected ? 'selected' : ''}"
|
|
||||||
@click=${() => this.handleSelectParty(partyId)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="party-avatar"
|
|
||||||
style="background: ${this.getPartyColor(party)}"
|
|
||||||
>
|
|
||||||
${this.getPartyInitials(party)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="party-info">
|
|
||||||
<div class="party-name">${this.getPartyDisplayName(party)}</div>
|
|
||||||
<div class="party-role-tag">
|
|
||||||
${this.getRoleName(roleId)} (${PARTY_ROLES.find((r) => r.value === partyRole)?.label || partyRole})
|
|
||||||
</div>
|
|
||||||
<div class="party-details">
|
|
||||||
${deliveryEmail
|
|
||||||
? html`
|
|
||||||
<div class="party-detail">
|
|
||||||
<dees-icon .icon=${'lucide:Mail'}></dees-icon>
|
|
||||||
${deliveryEmail}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
${deliveryPhone
|
|
||||||
? html`
|
|
||||||
<div class="party-detail">
|
|
||||||
<dees-icon .icon=${'lucide:Phone'}></dees-icon>
|
|
||||||
${deliveryPhone}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
${actingAsProxy
|
|
||||||
? html`
|
|
||||||
<div class="party-detail">
|
|
||||||
<dees-icon .icon=${'lucide:Users'}></dees-icon>
|
|
||||||
Acting as proxy
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="party-status">
|
|
||||||
<span class="signature-status ${this.getSignatureStatusClass(signatureStatus)}">
|
|
||||||
${this.formatSignatureStatus(signatureStatus)}
|
|
||||||
</span>
|
|
||||||
${signingOrder > 0
|
|
||||||
? html`
|
|
||||||
<div class="signing-order">
|
|
||||||
<span class="order-number">${signingOrder}</span>
|
|
||||||
<span>Signing order</span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-contract-signatures.js';
|
|
||||||
@@ -1,840 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contract-signatures.ts
|
|
||||||
* @description Contract signature fields manager component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contract-signatures': SdigContractSignatures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signature field interface (for future interface updates)
|
|
||||||
interface ISignatureField {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
assignedPartyId: string | null;
|
|
||||||
roleId: string;
|
|
||||||
type: 'signature' | 'initials' | 'date' | 'text';
|
|
||||||
required: boolean;
|
|
||||||
status: 'pending' | 'ready' | 'signed' | 'declined';
|
|
||||||
signedAt?: number;
|
|
||||||
signatureData?: any;
|
|
||||||
position: {
|
|
||||||
paragraphId?: string;
|
|
||||||
pageNumber?: number;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Signature status configuration
|
|
||||||
const SIGNATURE_STATUSES = [
|
|
||||||
{ value: 'pending', label: 'Pending', color: '#f59e0b', icon: 'lucide:Clock' },
|
|
||||||
{ value: 'ready', label: 'Ready to Sign', color: '#3b82f6', icon: 'lucide:PenTool' },
|
|
||||||
{ value: 'signed', label: 'Signed', color: '#10b981', icon: 'lucide:CheckCircle' },
|
|
||||||
{ value: 'declined', label: 'Declined', color: '#ef4444', icon: 'lucide:XCircle' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const FIELD_TYPES = [
|
|
||||||
{ value: 'signature', label: 'Full Signature', icon: 'lucide:PenTool' },
|
|
||||||
{ value: 'initials', label: 'Initials', icon: 'lucide:Type' },
|
|
||||||
{ value: 'date', label: 'Date', icon: 'lucide:Calendar' },
|
|
||||||
{ value: 'text', label: 'Text Field', icon: 'lucide:TextCursor' },
|
|
||||||
];
|
|
||||||
|
|
||||||
@customElement('sdig-contract-signatures')
|
|
||||||
export class SdigContractSignatures extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contract-signatures
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contract-signatures>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signatures-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Summary cards */
|
|
||||||
.summary-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card-icon.pending {
|
|
||||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
|
||||||
color: ${cssManager.bdTheme('#f59e0b', '#fcd34d')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card-icon.ready {
|
|
||||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card-icon.signed {
|
|
||||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
|
||||||
color: ${cssManager.bdTheme('#10b981', '#34d399')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card-icon.declined {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card-value {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-card-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section card */
|
|
||||||
.section-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 20px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title dees-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-content {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Signature fields list */
|
|
||||||
.fields-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-card:hover {
|
|
||||||
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-card.selected {
|
|
||||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-icon {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 20px;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-name {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-meta-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-meta-item dees-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-status {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-status.pending {
|
|
||||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
|
||||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-status.ready {
|
|
||||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-status.signed {
|
|
||||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
|
||||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-status.declined {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Signer progress */
|
|
||||||
.signers-section {
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signers-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signer-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signer-avatar {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signer-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signer-name {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signer-role {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signer-progress {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
flex: 1;
|
|
||||||
height: 6px;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
border-radius: 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: ${cssManager.bdTheme('#10b981', '#34d399')};
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
min-width: 36px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Signature preview */
|
|
||||||
.signature-preview {
|
|
||||||
position: relative;
|
|
||||||
padding: 24px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-preview-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-preview-image {
|
|
||||||
max-width: 200px;
|
|
||||||
max-height: 80px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-preview-placeholder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.signature-preview-placeholder dees-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 48px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state dees-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h4 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background: ${cssManager.bdTheme('#10b981', '#059669')};
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success:hover {
|
|
||||||
background: ${cssManager.bdTheme('#059669', '#047857')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Type badge */
|
|
||||||
.type-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Signing order */
|
|
||||||
.signing-order-badge {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor readonly: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor selectedFieldId: string | null = null;
|
|
||||||
|
|
||||||
// Demo signature fields data
|
|
||||||
@state()
|
|
||||||
private accessor signatureFields: ISignatureField[] = [];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LIFECYCLE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public async firstUpdated() {
|
|
||||||
// Generate demo signature fields based on contract parties
|
|
||||||
if (this.contract && this.contract.involvedParties.length > 0) {
|
|
||||||
this.signatureFields = this.contract.involvedParties.map((party, index) => ({
|
|
||||||
id: `sig-${index + 1}`,
|
|
||||||
name: `Signature - ${this.getPartyRoleName(party.role)}`,
|
|
||||||
assignedPartyId: null,
|
|
||||||
roleId: party.role,
|
|
||||||
type: 'signature' as const,
|
|
||||||
required: true,
|
|
||||||
status: index === 0 ? 'signed' : index === 1 ? 'ready' : 'pending',
|
|
||||||
signedAt: index === 0 ? Date.now() - 86400000 : undefined,
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleFieldChange(path: string, value: unknown) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('field-change', {
|
|
||||||
detail: { path, value },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSelectField(fieldId: string) {
|
|
||||||
this.selectedFieldId = this.selectedFieldId === fieldId ? null : fieldId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddField() {
|
|
||||||
const newField: ISignatureField = {
|
|
||||||
id: `sig-${Date.now()}`,
|
|
||||||
name: 'New Signature Field',
|
|
||||||
assignedPartyId: null,
|
|
||||||
roleId: '',
|
|
||||||
type: 'signature',
|
|
||||||
required: true,
|
|
||||||
status: 'pending',
|
|
||||||
position: { x: 0, y: 0 },
|
|
||||||
};
|
|
||||||
this.signatureFields = [...this.signatureFields, newField];
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDeleteField(fieldId: string) {
|
|
||||||
this.signatureFields = this.signatureFields.filter((f) => f.id !== fieldId);
|
|
||||||
if (this.selectedFieldId === fieldId) {
|
|
||||||
this.selectedFieldId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private getPartyRoleName(roleId: string): string {
|
|
||||||
const role = this.contract?.availableRoles.find((r) => r.id === roleId);
|
|
||||||
return role?.name || roleId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getStatusConfig(status: string) {
|
|
||||||
return SIGNATURE_STATUSES.find((s) => s.value === status) || SIGNATURE_STATUSES[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFieldTypeConfig(type: string) {
|
|
||||||
return FIELD_TYPES.find((t) => t.value === type) || FIELD_TYPES[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private getSignatureStats() {
|
|
||||||
const total = this.signatureFields.length;
|
|
||||||
const signed = this.signatureFields.filter((f) => f.status === 'signed').length;
|
|
||||||
const ready = this.signatureFields.filter((f) => f.status === 'ready').length;
|
|
||||||
const pending = this.signatureFields.filter((f) => f.status === 'pending').length;
|
|
||||||
const declined = this.signatureFields.filter((f) => f.status === 'declined').length;
|
|
||||||
return { total, signed, ready, pending, declined };
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPartyColor(index: number): string {
|
|
||||||
const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
|
|
||||||
return colors[index % colors.length];
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatDate(timestamp: number): string {
|
|
||||||
return new Date(timestamp).toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
if (!this.contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = this.getSignatureStats();
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="signatures-container">
|
|
||||||
<!-- Summary Cards -->
|
|
||||||
<div class="summary-row">
|
|
||||||
<div class="summary-card">
|
|
||||||
<div class="summary-card-icon pending">
|
|
||||||
<dees-icon .icon=${'lucide:Clock'}></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="summary-card-value">${stats.pending}</div>
|
|
||||||
<div class="summary-card-label">Pending</div>
|
|
||||||
</div>
|
|
||||||
<div class="summary-card">
|
|
||||||
<div class="summary-card-icon ready">
|
|
||||||
<dees-icon .icon=${'lucide:PenTool'}></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="summary-card-value">${stats.ready}</div>
|
|
||||||
<div class="summary-card-label">Ready to Sign</div>
|
|
||||||
</div>
|
|
||||||
<div class="summary-card">
|
|
||||||
<div class="summary-card-icon signed">
|
|
||||||
<dees-icon .icon=${'lucide:CheckCircle'}></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="summary-card-value">${stats.signed}</div>
|
|
||||||
<div class="summary-card-label">Signed</div>
|
|
||||||
</div>
|
|
||||||
<div class="summary-card">
|
|
||||||
<div class="summary-card-icon declined">
|
|
||||||
<dees-icon .icon=${'lucide:XCircle'}></dees-icon>
|
|
||||||
</div>
|
|
||||||
<div class="summary-card-value">${stats.declined}</div>
|
|
||||||
<div class="summary-card-label">Declined</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Signature Fields Section -->
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:PenTool'}></dees-icon>
|
|
||||||
Signature Fields
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-primary" @click=${this.handleAddField}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add Field
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
${this.signatureFields.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="fields-list">
|
|
||||||
${this.signatureFields.map((field, index) => this.renderSignatureField(field, index))}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:PenTool'}></dees-icon>
|
|
||||||
<h4>No Signature Fields</h4>
|
|
||||||
<p>Add signature fields to define where parties should sign the contract</p>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-primary" @click=${this.handleAddField}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add Signature Field
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Signers Progress Section -->
|
|
||||||
${this.contract.involvedParties.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="section-card">
|
|
||||||
<div class="section-header">
|
|
||||||
<div class="section-title">
|
|
||||||
<dees-icon .icon=${'lucide:Users'}></dees-icon>
|
|
||||||
Signers Progress
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="section-content">
|
|
||||||
<div class="signers-grid">
|
|
||||||
${this.contract.involvedParties.map((party, index) => this.renderSignerCard(party, index))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSignatureField(field: ISignatureField, index: number): TemplateResult {
|
|
||||||
const isSelected = this.selectedFieldId === field.id;
|
|
||||||
const statusConfig = this.getStatusConfig(field.status);
|
|
||||||
const typeConfig = this.getFieldTypeConfig(field.type);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div
|
|
||||||
class="field-card ${isSelected ? 'selected' : ''}"
|
|
||||||
@click=${() => this.handleSelectField(field.id)}
|
|
||||||
>
|
|
||||||
<div class="signing-order-badge">${index + 1}</div>
|
|
||||||
|
|
||||||
<div class="field-icon">
|
|
||||||
<dees-icon .icon=${typeConfig.icon}></dees-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-info">
|
|
||||||
<div class="field-name">${field.name}</div>
|
|
||||||
<div class="field-meta">
|
|
||||||
<span class="field-meta-item">
|
|
||||||
<dees-icon .icon=${'lucide:User'}></dees-icon>
|
|
||||||
${this.getPartyRoleName(field.roleId)}
|
|
||||||
</span>
|
|
||||||
<span class="type-badge">
|
|
||||||
<dees-icon .icon=${typeConfig.icon}></dees-icon>
|
|
||||||
${typeConfig.label}
|
|
||||||
</span>
|
|
||||||
${field.required
|
|
||||||
? html`
|
|
||||||
<span class="field-meta-item">
|
|
||||||
<dees-icon .icon=${'lucide:Asterisk'}></dees-icon>
|
|
||||||
Required
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
${field.signedAt
|
|
||||||
? html`
|
|
||||||
<span class="field-meta-item">
|
|
||||||
<dees-icon .icon=${'lucide:Calendar'}></dees-icon>
|
|
||||||
${this.formatDate(field.signedAt)}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-status ${field.status}">
|
|
||||||
<dees-icon .icon=${statusConfig.icon}></dees-icon>
|
|
||||||
${statusConfig.label}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<div class="field-actions">
|
|
||||||
<button class="btn btn-ghost" @click=${(e: Event) => { e.stopPropagation(); }} title="Edit">
|
|
||||||
<dees-icon .icon=${'lucide:Pencil'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost"
|
|
||||||
@click=${(e: Event) => { e.stopPropagation(); this.handleDeleteField(field.id); }}
|
|
||||||
title="Delete"
|
|
||||||
style="color: #ef4444;"
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:Trash2'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSignerCard(party: plugins.sdInterfaces.IInvolvedParty, index: number): TemplateResult {
|
|
||||||
const partyFields = this.signatureFields.filter((f) => f.roleId === party.role);
|
|
||||||
const signedFields = partyFields.filter((f) => f.status === 'signed').length;
|
|
||||||
const totalFields = partyFields.length;
|
|
||||||
const progress = totalFields > 0 ? Math.round((signedFields / totalFields) * 100) : 0;
|
|
||||||
const roleName = this.getPartyRoleName(party.role);
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="signer-card">
|
|
||||||
<div class="signer-avatar" style="background: ${this.getPartyColor(index)}">
|
|
||||||
${roleName.charAt(0).toUpperCase()}
|
|
||||||
</div>
|
|
||||||
<div class="signer-info">
|
|
||||||
<div class="signer-name">${roleName}</div>
|
|
||||||
<div class="signer-role">${signedFields} of ${totalFields} signatures</div>
|
|
||||||
<div class="signer-progress">
|
|
||||||
<div class="progress-bar">
|
|
||||||
<div class="progress-fill" style="width: ${progress}%"></div>
|
|
||||||
</div>
|
|
||||||
<span class="progress-text">${progress}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './sdig-contract-terms.js';
|
|
||||||
@@ -1,873 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contract-terms.ts
|
|
||||||
* @description Contract terms editor - tabbed container for financial, time, and obligation terms
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
state,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contract-terms': SdigContractTerms;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Term types
|
|
||||||
type TTermTab = 'financial' | 'time' | 'obligations';
|
|
||||||
|
|
||||||
interface ITermTabConfig {
|
|
||||||
id: TTermTab;
|
|
||||||
label: string;
|
|
||||||
icon: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TERM_TABS: ITermTabConfig[] = [
|
|
||||||
{ id: 'financial', label: 'Financial Terms', icon: 'lucide:Banknote', description: 'Payment schedules, rates, and penalties' },
|
|
||||||
{ id: 'time', label: 'Time Terms', icon: 'lucide:Calendar', description: 'Milestones, deadlines, and renewal' },
|
|
||||||
{ id: 'obligations', label: 'Obligations', icon: 'lucide:CheckSquare', description: 'Deliverables, SLAs, and warranties' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Extended contract terms interfaces (for future interface updates)
|
|
||||||
interface IPaymentScheduleItem {
|
|
||||||
id: string;
|
|
||||||
description: string;
|
|
||||||
amount: number;
|
|
||||||
currency: string;
|
|
||||||
dueDate: string;
|
|
||||||
status: 'pending' | 'paid' | 'overdue';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IMilestone {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
dueDate: string;
|
|
||||||
status: 'pending' | 'in_progress' | 'completed' | 'delayed';
|
|
||||||
dependencies: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IObligation {
|
|
||||||
id: string;
|
|
||||||
description: string;
|
|
||||||
responsibleParty: string;
|
|
||||||
deadline: string;
|
|
||||||
status: 'pending' | 'completed' | 'waived';
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('sdig-contract-terms')
|
|
||||||
export class SdigContractTerms extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contract-terms
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contract-terms>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terms-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section card */
|
|
||||||
.section-card {
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tab navigation */
|
|
||||||
.tabs-nav {
|
|
||||||
display: flex;
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px 24px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn:hover {
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn.active {
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
border-bottom-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-btn dees-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tab content */
|
|
||||||
.tab-content {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sub-sections */
|
|
||||||
.sub-section {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-section:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-section-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-section-description {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form groups */
|
|
||||||
.form-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input {
|
|
||||||
padding: 10px 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
|
|
||||||
border-radius: 6px;
|
|
||||||
outline: none;
|
|
||||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input:focus {
|
|
||||||
border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-input::placeholder {
|
|
||||||
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
|
||||||
}
|
|
||||||
|
|
||||||
select.form-input {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Data table */
|
|
||||||
.data-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table th {
|
|
||||||
text-align: left;
|
|
||||||
padding: 12px 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 12px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table td {
|
|
||||||
padding: 12px 16px;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table tr:hover td {
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Status badges */
|
|
||||||
.status-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.pending {
|
|
||||||
background: ${cssManager.bdTheme('#fef3c7', '#422006')};
|
|
||||||
color: ${cssManager.bdTheme('#92400e', '#fcd34d')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.paid,
|
|
||||||
.status-badge.completed {
|
|
||||||
background: ${cssManager.bdTheme('#d1fae5', '#064e3b')};
|
|
||||||
color: ${cssManager.bdTheme('#065f46', '#6ee7b7')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.overdue,
|
|
||||||
.status-badge.delayed {
|
|
||||||
background: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
|
||||||
color: ${cssManager.bdTheme('#991b1b', '#fca5a5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.in_progress {
|
|
||||||
background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')};
|
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Amount display */
|
|
||||||
.amount {
|
|
||||||
font-weight: 600;
|
|
||||||
font-family: 'Roboto Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount.positive {
|
|
||||||
color: ${cssManager.bdTheme('#059669', '#34d399')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount.negative {
|
|
||||||
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Summary card */
|
|
||||||
.summary-card {
|
|
||||||
display: flex;
|
|
||||||
gap: 32px;
|
|
||||||
padding: 20px;
|
|
||||||
background: ${cssManager.bdTheme('#f9fafb', '#111111')};
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-label {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-value {
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary-value.currency {
|
|
||||||
font-family: 'Roboto Mono', monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.empty-state {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 48px 20px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state dees-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state h4 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state p {
|
|
||||||
margin: 0 0 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-sm {
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add row button */
|
|
||||||
.add-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 16px;
|
|
||||||
border-top: 1px dashed ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Info banner */
|
|
||||||
.info-banner {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
background: ${cssManager.bdTheme('#eff6ff', '#172554')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#bfdbfe', '#1e40af')};
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-banner dees-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-banner-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-banner-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-banner-text {
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')};
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor readonly: boolean = false;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor activeTab: TTermTab = 'financial';
|
|
||||||
|
|
||||||
// Demo data for terms (will be replaced with actual contract data when interface is extended)
|
|
||||||
@state()
|
|
||||||
private accessor paymentSchedule: IPaymentScheduleItem[] = [
|
|
||||||
{ id: '1', description: 'Initial deposit', amount: 5000, currency: 'EUR', dueDate: '2024-02-01', status: 'paid' },
|
|
||||||
{ id: '2', description: 'Monthly payment - March', amount: 1000, currency: 'EUR', dueDate: '2024-03-01', status: 'paid' },
|
|
||||||
{ id: '3', description: 'Monthly payment - April', amount: 1000, currency: 'EUR', dueDate: '2024-04-01', status: 'pending' },
|
|
||||||
];
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor milestones: IMilestone[] = [
|
|
||||||
{ id: '1', name: 'Project Kickoff', description: 'Initial planning and setup', dueDate: '2024-02-15', status: 'completed', dependencies: [] },
|
|
||||||
{ id: '2', name: 'Phase 1 Delivery', description: 'First deliverable milestone', dueDate: '2024-03-15', status: 'in_progress', dependencies: ['1'] },
|
|
||||||
{ id: '3', name: 'Final Delivery', description: 'Complete project delivery', dueDate: '2024-05-01', status: 'pending', dependencies: ['2'] },
|
|
||||||
];
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor obligations: IObligation[] = [
|
|
||||||
{ id: '1', description: 'Provide access credentials', responsibleParty: 'employer', deadline: '2024-02-01', status: 'completed' },
|
|
||||||
{ id: '2', description: 'Submit monthly reports', responsibleParty: 'employee', deadline: '2024-03-01', status: 'pending' },
|
|
||||||
{ id: '3', description: 'Conduct quarterly review', responsibleParty: 'employer', deadline: '2024-04-01', status: 'pending' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleFieldChange(path: string, value: unknown) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('field-change', {
|
|
||||||
detail: { path, value },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleTabChange(tab: TTermTab) {
|
|
||||||
this.activeTab = tab;
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddPayment() {
|
|
||||||
const newPayment: IPaymentScheduleItem = {
|
|
||||||
id: `pay-${Date.now()}`,
|
|
||||||
description: 'New payment',
|
|
||||||
amount: 0,
|
|
||||||
currency: 'EUR',
|
|
||||||
dueDate: new Date().toISOString().split('T')[0],
|
|
||||||
status: 'pending',
|
|
||||||
};
|
|
||||||
this.paymentSchedule = [...this.paymentSchedule, newPayment];
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddMilestone() {
|
|
||||||
const newMilestone: IMilestone = {
|
|
||||||
id: `ms-${Date.now()}`,
|
|
||||||
name: 'New Milestone',
|
|
||||||
description: '',
|
|
||||||
dueDate: new Date().toISOString().split('T')[0],
|
|
||||||
status: 'pending',
|
|
||||||
dependencies: [],
|
|
||||||
};
|
|
||||||
this.milestones = [...this.milestones, newMilestone];
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleAddObligation() {
|
|
||||||
const newObligation: IObligation = {
|
|
||||||
id: `obl-${Date.now()}`,
|
|
||||||
description: 'New obligation',
|
|
||||||
responsibleParty: '',
|
|
||||||
deadline: new Date().toISOString().split('T')[0],
|
|
||||||
status: 'pending',
|
|
||||||
};
|
|
||||||
this.obligations = [...this.obligations, newObligation];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private formatCurrency(amount: number, currency: string): string {
|
|
||||||
return new Intl.NumberFormat('en-US', {
|
|
||||||
style: 'currency',
|
|
||||||
currency: currency,
|
|
||||||
}).format(amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatDate(dateStr: string): string {
|
|
||||||
return new Date(dateStr).toLocaleDateString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTotalAmount(): number {
|
|
||||||
return this.paymentSchedule.reduce((sum, p) => sum + p.amount, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPaidAmount(): number {
|
|
||||||
return this.paymentSchedule.filter((p) => p.status === 'paid').reduce((sum, p) => sum + p.amount, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPartyName(roleId: string): string {
|
|
||||||
const role = this.contract?.availableRoles.find((r) => r.id === roleId);
|
|
||||||
return role?.name || roleId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
if (!this.contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="terms-container">
|
|
||||||
<div class="section-card">
|
|
||||||
<!-- Tabs Navigation -->
|
|
||||||
<nav class="tabs-nav">
|
|
||||||
${TERM_TABS.map(
|
|
||||||
(tab) => html`
|
|
||||||
<button
|
|
||||||
class="tab-btn ${this.activeTab === tab.id ? 'active' : ''}"
|
|
||||||
@click=${() => this.handleTabChange(tab.id)}
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${tab.icon}></dees-icon>
|
|
||||||
${tab.label}
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Tab Content -->
|
|
||||||
<div class="tab-content">
|
|
||||||
${this.activeTab === 'financial'
|
|
||||||
? this.renderFinancialTerms()
|
|
||||||
: this.activeTab === 'time'
|
|
||||||
? this.renderTimeTerms()
|
|
||||||
: this.renderObligations()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderFinancialTerms(): TemplateResult {
|
|
||||||
const totalAmount = this.getTotalAmount();
|
|
||||||
const paidAmount = this.getPaidAmount();
|
|
||||||
const pendingAmount = totalAmount - paidAmount;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<!-- Summary -->
|
|
||||||
<div class="summary-card">
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Total Value</span>
|
|
||||||
<span class="summary-value currency">${this.formatCurrency(totalAmount, 'EUR')}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Paid</span>
|
|
||||||
<span class="summary-value currency" style="color: #059669;">${this.formatCurrency(paidAmount, 'EUR')}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Pending</span>
|
|
||||||
<span class="summary-value currency" style="color: #f59e0b;">${this.formatCurrency(pendingAmount, 'EUR')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Payment Schedule -->
|
|
||||||
<div class="sub-section">
|
|
||||||
<div class="sub-section-header">
|
|
||||||
<div>
|
|
||||||
<div class="sub-section-title">Payment Schedule</div>
|
|
||||||
<div class="sub-section-description">Scheduled payments and their status</div>
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-secondary" @click=${this.handleAddPayment}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add Payment
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.paymentSchedule.length > 0
|
|
||||||
? html`
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Amount</th>
|
|
||||||
<th>Due Date</th>
|
|
||||||
<th>Status</th>
|
|
||||||
${!this.readonly ? html`<th></th>` : ''}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${this.paymentSchedule.map(
|
|
||||||
(payment) => html`
|
|
||||||
<tr>
|
|
||||||
<td>${payment.description}</td>
|
|
||||||
<td><span class="amount">${this.formatCurrency(payment.amount, payment.currency)}</span></td>
|
|
||||||
<td>${this.formatDate(payment.dueDate)}</td>
|
|
||||||
<td><span class="status-badge ${payment.status}">${payment.status}</span></td>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-ghost btn-sm">
|
|
||||||
<dees-icon .icon=${'lucide:Pencil'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:Banknote'}></dees-icon>
|
|
||||||
<h4>No Payment Schedule</h4>
|
|
||||||
<p>Add payment terms to track financial obligations</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderTimeTerms(): TemplateResult {
|
|
||||||
const completedCount = this.milestones.filter((m) => m.status === 'completed').length;
|
|
||||||
const totalCount = this.milestones.length;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<!-- Summary -->
|
|
||||||
<div class="summary-card">
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Total Milestones</span>
|
|
||||||
<span class="summary-value">${totalCount}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Completed</span>
|
|
||||||
<span class="summary-value" style="color: #059669;">${completedCount}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Progress</span>
|
|
||||||
<span class="summary-value">${totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Milestones -->
|
|
||||||
<div class="sub-section">
|
|
||||||
<div class="sub-section-header">
|
|
||||||
<div>
|
|
||||||
<div class="sub-section-title">Milestones</div>
|
|
||||||
<div class="sub-section-description">Key project milestones and deadlines</div>
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-secondary" @click=${this.handleAddMilestone}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add Milestone
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.milestones.length > 0
|
|
||||||
? html`
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Milestone</th>
|
|
||||||
<th>Description</th>
|
|
||||||
<th>Due Date</th>
|
|
||||||
<th>Status</th>
|
|
||||||
${!this.readonly ? html`<th></th>` : ''}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${this.milestones.map(
|
|
||||||
(milestone) => html`
|
|
||||||
<tr>
|
|
||||||
<td><strong>${milestone.name}</strong></td>
|
|
||||||
<td>${milestone.description || '—'}</td>
|
|
||||||
<td>${this.formatDate(milestone.dueDate)}</td>
|
|
||||||
<td><span class="status-badge ${milestone.status}">${milestone.status.replace('_', ' ')}</span></td>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-ghost btn-sm">
|
|
||||||
<dees-icon .icon=${'lucide:Pencil'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:Calendar'}></dees-icon>
|
|
||||||
<h4>No Milestones</h4>
|
|
||||||
<p>Add milestones to track project progress</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderObligations(): TemplateResult {
|
|
||||||
const completedCount = this.obligations.filter((o) => o.status === 'completed').length;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<!-- Summary -->
|
|
||||||
<div class="summary-card">
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Total Obligations</span>
|
|
||||||
<span class="summary-value">${this.obligations.length}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Completed</span>
|
|
||||||
<span class="summary-value" style="color: #059669;">${completedCount}</span>
|
|
||||||
</div>
|
|
||||||
<div class="summary-item">
|
|
||||||
<span class="summary-label">Pending</span>
|
|
||||||
<span class="summary-value" style="color: #f59e0b;">${this.obligations.length - completedCount}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Info banner -->
|
|
||||||
<div class="info-banner">
|
|
||||||
<dees-icon .icon=${'lucide:Info'}></dees-icon>
|
|
||||||
<div class="info-banner-content">
|
|
||||||
<div class="info-banner-title">Contractual Obligations</div>
|
|
||||||
<div class="info-banner-text">
|
|
||||||
Track responsibilities assigned to each party. Mark obligations as completed when fulfilled.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Obligations -->
|
|
||||||
<div class="sub-section">
|
|
||||||
<div class="sub-section-header">
|
|
||||||
<div>
|
|
||||||
<div class="sub-section-title">Party Obligations</div>
|
|
||||||
<div class="sub-section-description">Responsibilities and deliverables by party</div>
|
|
||||||
</div>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-secondary" @click=${this.handleAddObligation}>
|
|
||||||
<dees-icon .icon=${'lucide:Plus'}></dees-icon>
|
|
||||||
Add Obligation
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.obligations.length > 0
|
|
||||||
? html`
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Obligation</th>
|
|
||||||
<th>Responsible Party</th>
|
|
||||||
<th>Deadline</th>
|
|
||||||
<th>Status</th>
|
|
||||||
${!this.readonly ? html`<th></th>` : ''}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
${this.obligations.map(
|
|
||||||
(obligation) => html`
|
|
||||||
<tr>
|
|
||||||
<td>${obligation.description}</td>
|
|
||||||
<td>${this.getPartyName(obligation.responsibleParty)}</td>
|
|
||||||
<td>${this.formatDate(obligation.deadline)}</td>
|
|
||||||
<td><span class="status-badge ${obligation.status}">${obligation.status}</span></td>
|
|
||||||
${!this.readonly
|
|
||||||
? html`
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-ghost btn-sm">
|
|
||||||
<dees-icon .icon=${'lucide:Pencil'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</tr>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<div class="empty-state">
|
|
||||||
<dees-icon .icon=${'lucide:CheckSquare'}></dees-icon>
|
|
||||||
<h4>No Obligations</h4>
|
|
||||||
<p>Add obligations to track party responsibilities</p>
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file index.ts
|
|
||||||
* @description Export barrel for sdig-contracteditor module
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './sdig-contracteditor.js';
|
|
||||||
export * from './types.js';
|
|
||||||
export * from './state.js';
|
|
||||||
@@ -1,891 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file sdig-contracteditor.ts
|
|
||||||
* @description Main contract editor orchestrator component
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
DeesElement,
|
|
||||||
property,
|
|
||||||
state,
|
|
||||||
html,
|
|
||||||
customElement,
|
|
||||||
type TemplateResult,
|
|
||||||
css,
|
|
||||||
cssManager,
|
|
||||||
} from '@design.estate/dees-element';
|
|
||||||
|
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
import { createEditorStore, type TEditorStore } from './state.js';
|
|
||||||
import {
|
|
||||||
type TEditorSection,
|
|
||||||
type IEditorState,
|
|
||||||
EDITOR_SECTIONS,
|
|
||||||
type IContractChangeEventDetail,
|
|
||||||
type ISectionChangeEventDetail,
|
|
||||||
} from './types.js';
|
|
||||||
|
|
||||||
// Import sub-components
|
|
||||||
import '../sdig-contract-header/sdig-contract-header.js';
|
|
||||||
import '../sdig-contract-metadata/sdig-contract-metadata.js';
|
|
||||||
import '../sdig-contract-parties/sdig-contract-parties.js';
|
|
||||||
import '../sdig-contract-content/sdig-contract-content.js';
|
|
||||||
import '../sdig-contract-terms/sdig-contract-terms.js';
|
|
||||||
import '../sdig-contract-signatures/sdig-contract-signatures.js';
|
|
||||||
import '../sdig-contract-attachments/sdig-contract-attachments.js';
|
|
||||||
import '../sdig-contract-collaboration/sdig-contract-collaboration.js';
|
|
||||||
import '../sdig-contract-audit/sdig-contract-audit.js';
|
|
||||||
import '../sdig-collaboration-sidebar/sdig-collaboration-sidebar.js';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
'sdig-contracteditor': SdigContracteditor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('sdig-contracteditor')
|
|
||||||
export class SdigContracteditor extends DeesElement {
|
|
||||||
// ============================================================================
|
|
||||||
// STATIC
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public static demo = () => html`
|
|
||||||
<sdig-contracteditor
|
|
||||||
.contract=${plugins.sdDemodata.demoContract}
|
|
||||||
></sdig-contracteditor>
|
|
||||||
`;
|
|
||||||
|
|
||||||
public static styles = [
|
|
||||||
cssManager.defaultStyles,
|
|
||||||
css`
|
|
||||||
:host {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100%;
|
|
||||||
background: ${cssManager.bdTheme('#f8f9fa', '#09090b')};
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
.editor-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 16px 24px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contract-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* shadcn-style badge */
|
|
||||||
.contract-status {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.4;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
background: ${cssManager.bdTheme('hsl(214 95% 93%)', 'hsl(214 95% 15%)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(214 95% 35%)', 'hsl(214 95% 70%)')};
|
|
||||||
border-color: ${cssManager.bdTheme('hsl(214 95% 80%)', 'hsl(214 95% 25%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.contract-status.draft {
|
|
||||||
background: ${cssManager.bdTheme('hsl(48 96% 89%)', 'hsl(48 96% 15%)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(25 95% 30%)', 'hsl(48 96% 70%)')};
|
|
||||||
border-color: ${cssManager.bdTheme('hsl(48 96% 76%)', 'hsl(48 96% 25%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.contract-status.executed {
|
|
||||||
background: ${cssManager.bdTheme('hsl(142 76% 90%)', 'hsl(142 76% 15%)')};
|
|
||||||
color: ${cssManager.bdTheme('hsl(142 76% 28%)', 'hsl(142 76% 65%)')};
|
|
||||||
border-color: ${cssManager.bdTheme('hsl(142 76% 75%)', 'hsl(142 76% 25%)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dirty-indicator {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.dirty-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collaborators {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collaborator-avatar {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: white;
|
|
||||||
margin-left: -8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.collaborator-avatar:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation Tabs */
|
|
||||||
.editor-nav {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 0 24px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tab {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tab:hover {
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tab.active {
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
border-bottom-color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-tab dees-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
padding: 0 6px;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
background: ${cssManager.bdTheme('#ef4444', '#dc2626')};
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content Area */
|
|
||||||
.editor-main {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editor-sidebar {
|
|
||||||
width: 320px;
|
|
||||||
border-left: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section placeholder */
|
|
||||||
.section-placeholder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 400px;
|
|
||||||
text-align: center;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-placeholder dees-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-placeholder h3 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-placeholder p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.editor-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 12px 24px;
|
|
||||||
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
|
|
||||||
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
color: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#333333', '#e5e5e5')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost {
|
|
||||||
background: transparent;
|
|
||||||
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost:hover {
|
|
||||||
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-ghost.active {
|
|
||||||
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
|
|
||||||
color: ${cssManager.bdTheme('#111111', '#fafafa')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:disabled {
|
|
||||||
opacity: 0.5;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading state */
|
|
||||||
.loading-overlay {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: ${cssManager.bdTheme('rgba(255,255,255,0.8)', 'rgba(0,0,0,0.8)')};
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Overview section layout */
|
|
||||||
.overview-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PROPERTIES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@property({ type: Object })
|
|
||||||
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
|
|
||||||
|
|
||||||
@property({ type: Boolean })
|
|
||||||
public accessor showSidebar: boolean = true;
|
|
||||||
|
|
||||||
@property({ type: String })
|
|
||||||
public accessor initialSection: TEditorSection = 'overview';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private accessor editorState: IEditorState | null = null;
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// INSTANCE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private store: TEditorStore | null = null;
|
|
||||||
private unsubscribe: (() => void) | null = null;
|
|
||||||
private storeReady: Promise<void>;
|
|
||||||
private resolveStoreReady!: () => void;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.storeReady = new Promise((resolve) => {
|
|
||||||
this.resolveStoreReady = resolve;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LIFECYCLE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public connectedCallback() {
|
|
||||||
super.connectedCallback();
|
|
||||||
this.initStore();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initStore() {
|
|
||||||
this.store = await createEditorStore();
|
|
||||||
this.unsubscribe = this.store.subscribe((state) => {
|
|
||||||
this.editorState = state;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set initial section
|
|
||||||
this.store.setActiveSection(this.initialSection);
|
|
||||||
this.resolveStoreReady();
|
|
||||||
|
|
||||||
// If contract was already set, apply it now
|
|
||||||
if (this.contract) {
|
|
||||||
this.store.setContract(this.contract);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnectedCallback() {
|
|
||||||
super.disconnectedCallback();
|
|
||||||
if (this.unsubscribe) {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async updated(changedProperties: Map<string, unknown>) {
|
|
||||||
if (changedProperties.has('contract') && this.contract) {
|
|
||||||
await this.storeReady;
|
|
||||||
this.store?.setContract(this.contract);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENT HANDLERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private handleSectionChange(section: TEditorSection) {
|
|
||||||
const previousSection = this.editorState?.activeSection || 'overview';
|
|
||||||
this.store?.setActiveSection(section);
|
|
||||||
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent<ISectionChangeEventDetail>('section-change', {
|
|
||||||
detail: { section, previousSection },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSave() {
|
|
||||||
if (!this.editorState?.contract) return;
|
|
||||||
|
|
||||||
this.store?.setSaving(true);
|
|
||||||
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('contract-save', {
|
|
||||||
detail: {
|
|
||||||
contract: this.editorState.contract,
|
|
||||||
isDraft: this.editorState.contract.lifecycle.currentStatus === 'draft',
|
|
||||||
},
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleDiscard() {
|
|
||||||
this.store?.discardChanges();
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('contract-discard', {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleUndo() {
|
|
||||||
this.store?.undo();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleRedo() {
|
|
||||||
this.store?.redo();
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleCommentClick(e: CustomEvent) {
|
|
||||||
// Navigate to collaboration section and highlight comment
|
|
||||||
this.store?.setActiveSection('collaboration');
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('comment-focus', {
|
|
||||||
detail: e.detail,
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSuggestionClick(e: CustomEvent) {
|
|
||||||
// Navigate to collaboration section and highlight suggestion
|
|
||||||
this.store?.setActiveSection('collaboration');
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('suggestion-focus', {
|
|
||||||
detail: e.detail,
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSidebarAddComment(e: CustomEvent) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('comment-added', {
|
|
||||||
detail: e.detail,
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// PUBLIC API
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a field in the contract
|
|
||||||
*/
|
|
||||||
public updateField(path: string, value: unknown, description?: string) {
|
|
||||||
this.store?.updateContract(path, value, description);
|
|
||||||
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent<IContractChangeEventDetail>('contract-change', {
|
|
||||||
detail: { path, value, source: 'user' },
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get current contract state
|
|
||||||
*/
|
|
||||||
public getContract(): plugins.sdInterfaces.IPortableContract | null {
|
|
||||||
return this.editorState?.contract || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark contract as saved externally
|
|
||||||
*/
|
|
||||||
public markSaved() {
|
|
||||||
this.store?.markSaved();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER HELPERS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
private getStatusClass(status: string): string {
|
|
||||||
if (status === 'draft' || status === 'internal_review') return 'draft';
|
|
||||||
if (status === 'executed' || status === 'active') return 'executed';
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatStatus(status: string): string {
|
|
||||||
return status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleFieldChange(e: CustomEvent<{ path: string; value: unknown }>) {
|
|
||||||
const { path, value } = e.detail;
|
|
||||||
this.updateField(path, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSectionContent(): TemplateResult {
|
|
||||||
const section = this.editorState?.activeSection || 'overview';
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
const sectionConfig = EDITOR_SECTIONS.find((s) => s.id === section);
|
|
||||||
|
|
||||||
// Render section based on active tab
|
|
||||||
switch (section) {
|
|
||||||
case 'overview':
|
|
||||||
return this.renderOverviewSection();
|
|
||||||
case 'parties':
|
|
||||||
return this.renderPartiesSection();
|
|
||||||
case 'content':
|
|
||||||
return this.renderContentSection();
|
|
||||||
case 'terms':
|
|
||||||
return this.renderTermsSection();
|
|
||||||
case 'signatures':
|
|
||||||
return this.renderSignaturesSection();
|
|
||||||
case 'attachments':
|
|
||||||
return this.renderAttachmentsSection();
|
|
||||||
case 'collaboration':
|
|
||||||
return this.renderCollaborationSection();
|
|
||||||
case 'audit':
|
|
||||||
return this.renderAuditSection();
|
|
||||||
default:
|
|
||||||
return this.renderPlaceholder(sectionConfig, 'This section is being implemented...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderOverviewSection(): TemplateResult {
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
if (!contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="overview-section">
|
|
||||||
<sdig-contract-header
|
|
||||||
.contract=${contract}
|
|
||||||
@field-change=${this.handleFieldChange}
|
|
||||||
></sdig-contract-header>
|
|
||||||
|
|
||||||
<sdig-contract-metadata
|
|
||||||
.contract=${contract}
|
|
||||||
@field-change=${this.handleFieldChange}
|
|
||||||
></sdig-contract-metadata>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderPartiesSection(): TemplateResult {
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
if (!contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<sdig-contract-parties
|
|
||||||
.contract=${contract}
|
|
||||||
@field-change=${this.handleFieldChange}
|
|
||||||
></sdig-contract-parties>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderContentSection(): TemplateResult {
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
if (!contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<sdig-contract-content
|
|
||||||
.contract=${contract}
|
|
||||||
@field-change=${this.handleFieldChange}
|
|
||||||
></sdig-contract-content>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderTermsSection(): TemplateResult {
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
if (!contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<sdig-contract-terms
|
|
||||||
.contract=${contract}
|
|
||||||
@field-change=${this.handleFieldChange}
|
|
||||||
></sdig-contract-terms>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderSignaturesSection(): TemplateResult {
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
if (!contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<sdig-contract-signatures
|
|
||||||
.contract=${contract}
|
|
||||||
@field-change=${this.handleFieldChange}
|
|
||||||
></sdig-contract-signatures>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderAttachmentsSection(): TemplateResult {
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
if (!contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<sdig-contract-attachments
|
|
||||||
.contract=${contract}
|
|
||||||
@field-change=${this.handleFieldChange}
|
|
||||||
></sdig-contract-attachments>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderCollaborationSection(): TemplateResult {
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
if (!contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<sdig-contract-collaboration
|
|
||||||
.contract=${contract}
|
|
||||||
@field-change=${this.handleFieldChange}
|
|
||||||
></sdig-contract-collaboration>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderAuditSection(): TemplateResult {
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
if (!contract) {
|
|
||||||
return html`<div>No contract loaded</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<sdig-contract-audit
|
|
||||||
.contract=${contract}
|
|
||||||
></sdig-contract-audit>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderPlaceholder(sectionConfig: typeof EDITOR_SECTIONS[0] | undefined, message: string): TemplateResult {
|
|
||||||
return html`
|
|
||||||
<div class="section-placeholder">
|
|
||||||
<dees-icon .icon=${sectionConfig?.icon || 'lucide:File'}></dees-icon>
|
|
||||||
<h3>${sectionConfig?.label || 'Section'}</h3>
|
|
||||||
<p>${message}</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// RENDER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
public render(): TemplateResult {
|
|
||||||
const contract = this.editorState?.contract;
|
|
||||||
const activeSection = this.editorState?.activeSection || 'overview';
|
|
||||||
const isDirty = this.editorState?.isDirty || false;
|
|
||||||
const isSaving = this.editorState?.isSaving || false;
|
|
||||||
const collaborators = this.editorState?.activeCollaborators || [];
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<div class="editor-container">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="editor-header">
|
|
||||||
<div class="header-left">
|
|
||||||
<h1 class="contract-title">${contract?.title || 'Untitled Contract'}</h1>
|
|
||||||
${contract?.lifecycle?.currentStatus
|
|
||||||
? html`
|
|
||||||
<span class="contract-status ${this.getStatusClass(contract.lifecycle.currentStatus)}">
|
|
||||||
${this.formatStatus(contract.lifecycle.currentStatus)}
|
|
||||||
</span>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
${isDirty
|
|
||||||
? html`
|
|
||||||
<div class="dirty-indicator">
|
|
||||||
<span class="dirty-dot"></span>
|
|
||||||
<span>Unsaved changes</span>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
${collaborators.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="collaborators">
|
|
||||||
${collaborators.slice(0, 3).map(
|
|
||||||
(c) => html`
|
|
||||||
<div
|
|
||||||
class="collaborator-avatar"
|
|
||||||
style="background: ${c.color}"
|
|
||||||
title="${c.displayName}"
|
|
||||||
>
|
|
||||||
${c.displayName.charAt(0).toUpperCase()}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${collaborators.length > 3
|
|
||||||
? html`
|
|
||||||
<div class="collaborator-avatar" style="background: #6b7280">
|
|
||||||
+${collaborators.length - 3}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
<button class="btn btn-ghost" @click=${this.handleUndo} ?disabled=${!this.store?.canUndo()}>
|
|
||||||
<dees-icon .icon=${'lucide:Undo2'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-ghost" @click=${this.handleRedo} ?disabled=${!this.store?.canRedo()}>
|
|
||||||
<dees-icon .icon=${'lucide:Redo2'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost ${this.showSidebar ? 'active' : ''}"
|
|
||||||
@click=${() => (this.showSidebar = !this.showSidebar)}
|
|
||||||
title="${this.showSidebar ? 'Hide sidebar' : 'Show sidebar'}"
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${'lucide:PanelRight'}></dees-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Navigation -->
|
|
||||||
<nav class="editor-nav">
|
|
||||||
${EDITOR_SECTIONS.map(
|
|
||||||
(section) => html`
|
|
||||||
<button
|
|
||||||
class="nav-tab ${activeSection === section.id ? 'active' : ''}"
|
|
||||||
@click=${() => this.handleSectionChange(section.id)}
|
|
||||||
?disabled=${section.disabled}
|
|
||||||
>
|
|
||||||
<dees-icon .icon=${section.icon}></dees-icon>
|
|
||||||
<span>${section.label}</span>
|
|
||||||
${section.badge
|
|
||||||
? html`<span class="nav-badge">${section.badge}</span>`
|
|
||||||
: ''}
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Main Content -->
|
|
||||||
<div class="editor-main">
|
|
||||||
<div class="editor-content">
|
|
||||||
${this.renderSectionContent()}
|
|
||||||
</div>
|
|
||||||
${this.showSidebar
|
|
||||||
? html`
|
|
||||||
<aside class="editor-sidebar">
|
|
||||||
<sdig-collaboration-sidebar
|
|
||||||
.contract=${contract}
|
|
||||||
@comment-click=${this.handleCommentClick}
|
|
||||||
@suggestion-click=${this.handleSuggestionClick}
|
|
||||||
@add-comment=${this.handleSidebarAddComment}
|
|
||||||
></sdig-collaboration-sidebar>
|
|
||||||
</aside>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<div class="editor-footer">
|
|
||||||
<div class="footer-left">
|
|
||||||
${contract?.updatedAt
|
|
||||||
? html`<span>Last updated: ${new Date(contract.updatedAt).toLocaleString()}</span>`
|
|
||||||
: ''}
|
|
||||||
${contract?.versionHistory?.currentVersionId
|
|
||||||
? html`<span>Version: ${contract.versionHistory.currentVersionId}</span>`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
<div class="footer-right">
|
|
||||||
${isDirty
|
|
||||||
? html`
|
|
||||||
<button class="btn btn-secondary" @click=${this.handleDiscard}>
|
|
||||||
Discard
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
@click=${this.handleSave}
|
|
||||||
?disabled=${!isDirty || isSaving}
|
|
||||||
>
|
|
||||||
${isSaving ? 'Saving...' : 'Save'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${this.editorState?.isLoading
|
|
||||||
? html`
|
|
||||||
<div class="loading-overlay">
|
|
||||||
<dees-spinner></dees-spinner>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,407 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file state.ts
|
|
||||||
* @description Smartstate store for contract editor
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { domtools } from '@design.estate/dees-element';
|
|
||||||
import type * as sdInterfaces from '@signature.digital/tools/interfaces';
|
|
||||||
import {
|
|
||||||
type IEditorState,
|
|
||||||
type TEditorSection,
|
|
||||||
type TEditorMode,
|
|
||||||
type IContractChange,
|
|
||||||
type IValidationError,
|
|
||||||
type IEditorUser,
|
|
||||||
createInitialEditorState,
|
|
||||||
} from './types.js';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// STATE STORE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new editor state store instance
|
|
||||||
*/
|
|
||||||
export async function createEditorStore() {
|
|
||||||
const smartstate = new domtools.plugins.smartstate.Smartstate<{ editor: IEditorState }>();
|
|
||||||
|
|
||||||
// Initialize with default state (getStatePart is now async)
|
|
||||||
const statePart = await smartstate.getStatePart<IEditorState>('editor', createInitialEditorState(), 'soft');
|
|
||||||
|
|
||||||
// Create actions for state modifications
|
|
||||||
const setContractAction = statePart.createAction<{ contract: sdInterfaces.IPortableContract }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
contract: structuredClone(payload.contract),
|
|
||||||
originalContract: structuredClone(payload.contract),
|
|
||||||
isDirty: false,
|
|
||||||
undoStack: [],
|
|
||||||
redoStack: [],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateContractAction = statePart.createAction<{ path: string; value: unknown; description?: string; userId?: string }>(
|
|
||||||
async (statePartArg, payload) => {
|
|
||||||
const state = statePartArg.getState();
|
|
||||||
if (!state.contract) return state;
|
|
||||||
|
|
||||||
const previousValue = getNestedValue(state.contract, payload.path);
|
|
||||||
|
|
||||||
const change: IContractChange = {
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
path: payload.path,
|
|
||||||
previousValue,
|
|
||||||
newValue: payload.value,
|
|
||||||
description: payload.description || `Updated ${payload.path}`,
|
|
||||||
userId: payload.userId,
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedContract = setNestedValue(
|
|
||||||
structuredClone(state.contract),
|
|
||||||
payload.path,
|
|
||||||
payload.value
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
contract: updatedContract,
|
|
||||||
isDirty: true,
|
|
||||||
undoStack: [...state.undoStack, change],
|
|
||||||
redoStack: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const setActiveSectionAction = statePart.createAction<{ section: TEditorSection }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
activeSection: payload.section,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const setEditorModeAction = statePart.createAction<{ mode: TEditorMode }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
editorMode: payload.mode,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectParagraphAction = statePart.createAction<{ paragraphId: string | null }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
selectedParagraphId: payload.paragraphId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectPartyAction = statePart.createAction<{ partyId: string | null }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
selectedPartyId: payload.partyId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectSignatureFieldAction = statePart.createAction<{ fieldId: string | null }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
selectedSignatureFieldId: payload.fieldId,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const undoAction = statePart.createAction<void>(
|
|
||||||
async (statePartArg) => {
|
|
||||||
const state = statePartArg.getState();
|
|
||||||
if (state.undoStack.length === 0 || !state.contract) return state;
|
|
||||||
|
|
||||||
const change = state.undoStack[state.undoStack.length - 1];
|
|
||||||
const updatedContract = setNestedValue(
|
|
||||||
structuredClone(state.contract),
|
|
||||||
change.path,
|
|
||||||
change.previousValue
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
contract: updatedContract,
|
|
||||||
undoStack: state.undoStack.slice(0, -1),
|
|
||||||
redoStack: [...state.redoStack, change],
|
|
||||||
isDirty: state.undoStack.length > 1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const redoAction = statePart.createAction<void>(
|
|
||||||
async (statePartArg) => {
|
|
||||||
const state = statePartArg.getState();
|
|
||||||
if (state.redoStack.length === 0 || !state.contract) return state;
|
|
||||||
|
|
||||||
const change = state.redoStack[state.redoStack.length - 1];
|
|
||||||
const updatedContract = setNestedValue(
|
|
||||||
structuredClone(state.contract),
|
|
||||||
change.path,
|
|
||||||
change.newValue
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
contract: updatedContract,
|
|
||||||
undoStack: [...state.undoStack, change],
|
|
||||||
redoStack: state.redoStack.slice(0, -1),
|
|
||||||
isDirty: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const setLoadingAction = statePart.createAction<{ isLoading: boolean }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
isLoading: payload.isLoading,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const setSavingAction = statePart.createAction<{ isSaving: boolean }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
isSaving: payload.isSaving,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const markSavedAction = statePart.createAction<void>(
|
|
||||||
async (statePartArg) => {
|
|
||||||
const state = statePartArg.getState();
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
originalContract: state.contract ? structuredClone(state.contract) : null,
|
|
||||||
isDirty: false,
|
|
||||||
isSaving: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const discardChangesAction = statePart.createAction<void>(
|
|
||||||
async (statePartArg) => {
|
|
||||||
const state = statePartArg.getState();
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
contract: state.originalContract ? structuredClone(state.originalContract) : null,
|
|
||||||
isDirty: false,
|
|
||||||
undoStack: [],
|
|
||||||
redoStack: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const setValidationErrorsAction = statePart.createAction<{ errors: IValidationError[] }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
validationErrors: payload.errors,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearValidationErrorsAction = statePart.createAction<void>(
|
|
||||||
async (statePartArg) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
validationErrors: [],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const setCurrentUserAction = statePart.createAction<{ user: IEditorUser }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
currentUser: payload.user,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const setActiveCollaboratorsAction = statePart.createAction<{ collaborators: sdInterfaces.IUserPresence[] }>(
|
|
||||||
async (statePartArg, payload) => ({
|
|
||||||
...statePartArg.getState(),
|
|
||||||
activeCollaborators: payload.collaborators,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const addCollaboratorAction = statePart.createAction<{ collaborator: sdInterfaces.IUserPresence }>(
|
|
||||||
async (statePartArg, payload) => {
|
|
||||||
const state = statePartArg.getState();
|
|
||||||
if (state.activeCollaborators.find(c => c.userId === payload.collaborator.userId)) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
activeCollaborators: [...state.activeCollaborators, payload.collaborator],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeCollaboratorAction = statePart.createAction<{ userId: string }>(
|
|
||||||
async (statePartArg, payload) => {
|
|
||||||
const state = statePartArg.getState();
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
activeCollaborators: state.activeCollaborators.filter(c => c.userId !== payload.userId),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateCollaboratorAction = statePart.createAction<{ userId: string; updates: Partial<sdInterfaces.IUserPresence> }>(
|
|
||||||
async (statePartArg, payload) => {
|
|
||||||
const state = statePartArg.getState();
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
activeCollaborators: state.activeCollaborators.map(c =>
|
|
||||||
c.userId === payload.userId ? { ...c, ...payload.updates } : c
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
smartstate,
|
|
||||||
statePart,
|
|
||||||
|
|
||||||
// Getters
|
|
||||||
getContract: () => statePart.getState().contract,
|
|
||||||
getActiveSection: () => statePart.getState().activeSection,
|
|
||||||
isDirty: () => statePart.getState().isDirty,
|
|
||||||
isLoading: () => statePart.getState().isLoading,
|
|
||||||
isSaving: () => statePart.getState().isSaving,
|
|
||||||
|
|
||||||
// Contract operations
|
|
||||||
setContract: (contract: sdInterfaces.IPortableContract) => setContractAction.trigger({ contract }),
|
|
||||||
|
|
||||||
updateContract: (path: string, value: unknown, description?: string) => {
|
|
||||||
const state = statePart.getState();
|
|
||||||
return updateContractAction.trigger({ path, value, description, userId: state.currentUser?.userId });
|
|
||||||
},
|
|
||||||
|
|
||||||
// Navigation
|
|
||||||
setActiveSection: (section: TEditorSection) => setActiveSectionAction.trigger({ section }),
|
|
||||||
setEditorMode: (mode: TEditorMode) => setEditorModeAction.trigger({ mode }),
|
|
||||||
|
|
||||||
// Selection
|
|
||||||
selectParagraph: (paragraphId: string | null) => selectParagraphAction.trigger({ paragraphId }),
|
|
||||||
selectParty: (partyId: string | null) => selectPartyAction.trigger({ partyId }),
|
|
||||||
selectSignatureField: (fieldId: string | null) => selectSignatureFieldAction.trigger({ fieldId }),
|
|
||||||
|
|
||||||
// Undo/Redo
|
|
||||||
undo: () => undoAction.trigger(),
|
|
||||||
redo: () => redoAction.trigger(),
|
|
||||||
canUndo: () => statePart.getState().undoStack.length > 0,
|
|
||||||
canRedo: () => statePart.getState().redoStack.length > 0,
|
|
||||||
|
|
||||||
// Loading/Saving state
|
|
||||||
setLoading: (isLoading: boolean) => setLoadingAction.trigger({ isLoading }),
|
|
||||||
setSaving: (isSaving: boolean) => setSavingAction.trigger({ isSaving }),
|
|
||||||
markSaved: () => markSavedAction.trigger(),
|
|
||||||
|
|
||||||
// Discard changes
|
|
||||||
discardChanges: () => discardChangesAction.trigger(),
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
setValidationErrors: (errors: IValidationError[]) => setValidationErrorsAction.trigger({ errors }),
|
|
||||||
clearValidationErrors: () => clearValidationErrorsAction.trigger(),
|
|
||||||
|
|
||||||
// User/Collaboration
|
|
||||||
setCurrentUser: (user: IEditorUser) => setCurrentUserAction.trigger({ user }),
|
|
||||||
setActiveCollaborators: (collaborators: sdInterfaces.IUserPresence[]) => setActiveCollaboratorsAction.trigger({ collaborators }),
|
|
||||||
addCollaborator: (collaborator: sdInterfaces.IUserPresence) => addCollaboratorAction.trigger({ collaborator }),
|
|
||||||
removeCollaborator: (userId: string) => removeCollaboratorAction.trigger({ userId }),
|
|
||||||
updateCollaborator: (userId: string, updates: Partial<sdInterfaces.IUserPresence>) => updateCollaboratorAction.trigger({ userId, updates }),
|
|
||||||
|
|
||||||
// Subscribe to state changes (using new API)
|
|
||||||
subscribe: (callback: (state: IEditorState) => void) => {
|
|
||||||
const subscription = statePart.select().subscribe(callback);
|
|
||||||
return () => subscription.unsubscribe();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// UTILITY FUNCTIONS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get nested value from object by path
|
|
||||||
*/
|
|
||||||
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
||||||
const keys = path.split('.');
|
|
||||||
let current: unknown = obj;
|
|
||||||
|
|
||||||
for (const key of keys) {
|
|
||||||
if (current === null || current === undefined) return undefined;
|
|
||||||
if (typeof current !== 'object') return undefined;
|
|
||||||
|
|
||||||
// Handle array index
|
|
||||||
const arrayMatch = key.match(/^(\w+)\[(\d+)\]$/);
|
|
||||||
if (arrayMatch) {
|
|
||||||
const [, arrayKey, index] = arrayMatch;
|
|
||||||
current = (current as Record<string, unknown>)[arrayKey];
|
|
||||||
if (Array.isArray(current)) {
|
|
||||||
current = current[parseInt(index, 10)];
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
current = (current as Record<string, unknown>)[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set nested value in object by path (immutably)
|
|
||||||
*/
|
|
||||||
function setNestedValue<T extends Record<string, unknown>>(
|
|
||||||
obj: T,
|
|
||||||
path: string,
|
|
||||||
value: unknown
|
|
||||||
): T {
|
|
||||||
const keys = path.split('.');
|
|
||||||
const result = { ...obj } as Record<string, unknown>;
|
|
||||||
let current = result;
|
|
||||||
|
|
||||||
for (let i = 0; i < keys.length - 1; i++) {
|
|
||||||
const key = keys[i];
|
|
||||||
|
|
||||||
// Handle array index
|
|
||||||
const arrayMatch = key.match(/^(\w+)\[(\d+)\]$/);
|
|
||||||
if (arrayMatch) {
|
|
||||||
const [, arrayKey, index] = arrayMatch;
|
|
||||||
const arr = [...((current[arrayKey] as unknown[]) || [])];
|
|
||||||
if (i === keys.length - 2) {
|
|
||||||
arr[parseInt(index, 10)] = value;
|
|
||||||
current[arrayKey] = arr;
|
|
||||||
return result as T;
|
|
||||||
} else {
|
|
||||||
arr[parseInt(index, 10)] = { ...(arr[parseInt(index, 10)] as Record<string, unknown> || {}) };
|
|
||||||
current[arrayKey] = arr;
|
|
||||||
current = arr[parseInt(index, 10)] as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (typeof current[key] !== 'object' || current[key] === null) {
|
|
||||||
current[key] = {};
|
|
||||||
} else {
|
|
||||||
current[key] = { ...(current[key] as Record<string, unknown>) };
|
|
||||||
}
|
|
||||||
current = current[key] as Record<string, unknown>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastKey = keys[keys.length - 1];
|
|
||||||
const arrayMatch = lastKey.match(/^(\w+)\[(\d+)\]$/);
|
|
||||||
if (arrayMatch) {
|
|
||||||
const [, arrayKey, index] = arrayMatch;
|
|
||||||
const arr = [...((current[arrayKey] as unknown[]) || [])];
|
|
||||||
arr[parseInt(index, 10)] = value;
|
|
||||||
current[arrayKey] = arr;
|
|
||||||
} else {
|
|
||||||
current[lastKey] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type for editor store
|
|
||||||
*/
|
|
||||||
export type TEditorStore = Awaited<ReturnType<typeof createEditorStore>>;
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
/**
|
|
||||||
* @file types.ts
|
|
||||||
* @description Editor-specific types and event interfaces
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type * as sdInterfaces from '@signature.digital/tools/interfaces';
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EDITOR NAVIGATION
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Available editor sections/tabs
|
|
||||||
*/
|
|
||||||
export type TEditorSection =
|
|
||||||
| 'overview'
|
|
||||||
| 'parties'
|
|
||||||
| 'content'
|
|
||||||
| 'terms'
|
|
||||||
| 'signatures'
|
|
||||||
| 'attachments'
|
|
||||||
| 'collaboration'
|
|
||||||
| 'audit';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section configuration
|
|
||||||
*/
|
|
||||||
export interface IEditorSectionConfig {
|
|
||||||
id: TEditorSection;
|
|
||||||
label: string;
|
|
||||||
icon: string;
|
|
||||||
badge?: number | string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default section configurations
|
|
||||||
*/
|
|
||||||
export const EDITOR_SECTIONS: IEditorSectionConfig[] = [
|
|
||||||
{ id: 'overview', label: 'Overview', icon: 'lucide:FileText' },
|
|
||||||
{ id: 'parties', label: 'Parties & Roles', icon: 'lucide:Users' },
|
|
||||||
{ id: 'content', label: 'Content', icon: 'lucide:FileEdit' },
|
|
||||||
{ id: 'terms', label: 'Terms', icon: 'lucide:Calculator' },
|
|
||||||
{ id: 'signatures', label: 'Signatures', icon: 'lucide:PenTool' },
|
|
||||||
{ id: 'attachments', label: 'Attachments', icon: 'lucide:Paperclip' },
|
|
||||||
{ id: 'collaboration', label: 'Collaboration', icon: 'lucide:MessageCircle' },
|
|
||||||
{ id: 'audit', label: 'Audit & History', icon: 'lucide:History' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EDITOR STATE
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current user in the editor
|
|
||||||
*/
|
|
||||||
export interface IEditorUser {
|
|
||||||
userId: string;
|
|
||||||
displayName: string;
|
|
||||||
email: string;
|
|
||||||
avatarUrl?: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Editor mode
|
|
||||||
*/
|
|
||||||
export type TEditorMode = 'edit' | 'view' | 'review' | 'sign';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Editor state interface
|
|
||||||
*/
|
|
||||||
export interface IEditorState {
|
|
||||||
// Contract data
|
|
||||||
contract: sdInterfaces.IPortableContract | null;
|
|
||||||
originalContract: sdInterfaces.IPortableContract | null;
|
|
||||||
|
|
||||||
// UI state
|
|
||||||
activeSection: TEditorSection;
|
|
||||||
editorMode: TEditorMode;
|
|
||||||
isDirty: boolean;
|
|
||||||
isSaving: boolean;
|
|
||||||
isLoading: boolean;
|
|
||||||
|
|
||||||
// Selection state
|
|
||||||
selectedParagraphId: string | null;
|
|
||||||
selectedPartyId: string | null;
|
|
||||||
selectedSignatureFieldId: string | null;
|
|
||||||
|
|
||||||
// Collaboration
|
|
||||||
currentUser: IEditorUser | null;
|
|
||||||
activeCollaborators: sdInterfaces.IUserPresence[];
|
|
||||||
|
|
||||||
// Validation
|
|
||||||
validationErrors: IValidationError[];
|
|
||||||
|
|
||||||
// History
|
|
||||||
undoStack: IContractChange[];
|
|
||||||
redoStack: IContractChange[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initial editor state factory
|
|
||||||
*/
|
|
||||||
export function createInitialEditorState(): IEditorState {
|
|
||||||
return {
|
|
||||||
contract: null,
|
|
||||||
originalContract: null,
|
|
||||||
activeSection: 'overview',
|
|
||||||
editorMode: 'edit',
|
|
||||||
isDirty: false,
|
|
||||||
isSaving: false,
|
|
||||||
isLoading: false,
|
|
||||||
selectedParagraphId: null,
|
|
||||||
selectedPartyId: null,
|
|
||||||
selectedSignatureFieldId: null,
|
|
||||||
currentUser: null,
|
|
||||||
activeCollaborators: [],
|
|
||||||
validationErrors: [],
|
|
||||||
undoStack: [],
|
|
||||||
redoStack: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// CHANGE TRACKING
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contract change for undo/redo
|
|
||||||
*/
|
|
||||||
export interface IContractChange {
|
|
||||||
id: string;
|
|
||||||
timestamp: number;
|
|
||||||
path: string;
|
|
||||||
previousValue: unknown;
|
|
||||||
newValue: unknown;
|
|
||||||
description: string;
|
|
||||||
userId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validation error
|
|
||||||
*/
|
|
||||||
export interface IValidationError {
|
|
||||||
path: string;
|
|
||||||
message: string;
|
|
||||||
severity: 'error' | 'warning' | 'info';
|
|
||||||
fieldLabel?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// EVENTS
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contract change event detail
|
|
||||||
*/
|
|
||||||
export interface IContractChangeEventDetail {
|
|
||||||
path: string;
|
|
||||||
value: unknown;
|
|
||||||
previousValue?: unknown;
|
|
||||||
source?: 'user' | 'collaboration' | 'system';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Section change event detail
|
|
||||||
*/
|
|
||||||
export interface ISectionChangeEventDetail {
|
|
||||||
section: TEditorSection;
|
|
||||||
previousSection: TEditorSection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save event detail
|
|
||||||
*/
|
|
||||||
export interface ISaveEventDetail {
|
|
||||||
contract: sdInterfaces.IPortableContract;
|
|
||||||
isDraft: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom event types
|
|
||||||
*/
|
|
||||||
export interface IEditorEvents {
|
|
||||||
'contract-change': CustomEvent<IContractChangeEventDetail>;
|
|
||||||
'section-change': CustomEvent<ISectionChangeEventDetail>;
|
|
||||||
'contract-save': CustomEvent<ISaveEventDetail>;
|
|
||||||
'contract-discard': CustomEvent<void>;
|
|
||||||
'validation-error': CustomEvent<IValidationError[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// UTILITY TYPES
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deep path type for nested object access
|
|
||||||
*/
|
|
||||||
export type TDeepPath<T, K extends keyof T = keyof T> = K extends string
|
|
||||||
? T[K] extends Record<string, unknown>
|
|
||||||
? `${K}` | `${K}.${TDeepPath<T[K]>}`
|
|
||||||
: `${K}`
|
|
||||||
: never;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contract field path
|
|
||||||
*/
|
|
||||||
export type TContractPath = string; // Simplified for runtime use
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Field metadata for UI rendering
|
|
||||||
*/
|
|
||||||
export interface IFieldMetadata {
|
|
||||||
path: TContractPath;
|
|
||||||
label: string;
|
|
||||||
description?: string;
|
|
||||||
required: boolean;
|
|
||||||
type: 'text' | 'textarea' | 'number' | 'date' | 'select' | 'multiselect' | 'checkbox' | 'custom';
|
|
||||||
options?: Array<{ value: string; label: string }>;
|
|
||||||
validation?: {
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
minLength?: number;
|
|
||||||
maxLength?: number;
|
|
||||||
pattern?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
import { DeesElement, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||||
import * as plugins from '../../plugins.js';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
@@ -12,6 +11,7 @@ export class SignBox extends DeesElement {
|
|||||||
public static demo = () => html`
|
public static demo = () => html`
|
||||||
<sdig-signbox></sdig-signbox>
|
<sdig-signbox></sdig-signbox>
|
||||||
`;
|
`;
|
||||||
|
public static demoGroups = ['Signature Digital Primitives'];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
@@ -80,17 +80,19 @@ export class SignBox extends DeesElement {
|
|||||||
<sdig-signpad></sdig-signpad>
|
<sdig-signpad></sdig-signpad>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<div class="button" @click=${async () => {
|
<div class="button" @click=${async () => {
|
||||||
await this.shadowRoot.querySelector('sdig-signpad').clear();
|
await this.shadowRoot?.querySelector('sdig-signpad')?.clear();
|
||||||
}}>
|
}}>
|
||||||
Clear
|
Clear
|
||||||
</div>
|
</div>
|
||||||
<div class="button" @click=${async () => {
|
<div class="button" @click=${async () => {
|
||||||
await this.shadowRoot.querySelector('sdig-signpad').undo();
|
await this.shadowRoot?.querySelector('sdig-signpad')?.undo();
|
||||||
}}>
|
}}>
|
||||||
Undo
|
Undo
|
||||||
</div>
|
</div>
|
||||||
<div class="button" @click=${async () => {
|
<div class="button" @click=${async () => {
|
||||||
const signature = await this.shadowRoot.querySelector('sdig-signpad').toData();
|
const signaturePad = this.shadowRoot?.querySelector('sdig-signpad');
|
||||||
|
if (!signaturePad) return;
|
||||||
|
const signature = await signaturePad.toData();
|
||||||
this.dispatchEvent(new CustomEvent('signature', {
|
this.dispatchEvent(new CustomEvent('signature', {
|
||||||
detail: {
|
detail: {
|
||||||
signature,
|
signature,
|
||||||
@@ -104,4 +106,4 @@ export class SignBox extends DeesElement {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DeesElement, property, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
import { DeesElement, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
|
||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -12,6 +12,7 @@ export class SignPad extends DeesElement {
|
|||||||
public static demo = () => html`
|
public static demo = () => html`
|
||||||
<sdig-signpad></sdig-signpad>
|
<sdig-signpad></sdig-signpad>
|
||||||
`;
|
`;
|
||||||
|
public static demoGroups = ['Signature Digital Primitives'];
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -59,11 +60,14 @@ export class SignPad extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public signaturePad: typeof plugins.signaturePad.prototype;
|
public signaturePad?: typeof plugins.signaturePad.prototype;
|
||||||
|
|
||||||
public async firstUpdated() {
|
public async firstUpdated() {
|
||||||
const domtools = await this.domtoolsPromise;
|
await this.domtoolsPromise;
|
||||||
|
const mainbox = this.shadowRoot?.querySelector('.mainbox');
|
||||||
|
if (!mainbox) return;
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
this.shadowRoot.querySelector('.mainbox').appendChild(canvas);
|
mainbox.appendChild(canvas);
|
||||||
await this.resizeCanvas();
|
await this.resizeCanvas();
|
||||||
this.signaturePad = new plugins.signaturePad(canvas, {
|
this.signaturePad = new plugins.signaturePad(canvas, {
|
||||||
|
|
||||||
@@ -72,10 +76,11 @@ export class SignPad extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async resizeCanvas() {
|
public async resizeCanvas() {
|
||||||
const mainbox = this.shadowRoot.querySelector('.mainbox');
|
const mainbox = this.shadowRoot?.querySelector('.mainbox');
|
||||||
|
const canvas = this.shadowRoot?.querySelector('canvas');
|
||||||
|
if (!mainbox || !canvas) return;
|
||||||
const mainboxWidth = mainbox.clientWidth;
|
const mainboxWidth = mainbox.clientWidth;
|
||||||
const mainboxHeight = mainbox.clientHeight;
|
const mainboxHeight = mainbox.clientHeight;
|
||||||
const canvas = this.shadowRoot.querySelector('canvas');
|
|
||||||
canvas.width = mainboxWidth;
|
canvas.width = mainboxWidth;
|
||||||
canvas.height = mainboxHeight;
|
canvas.height = mainboxHeight;
|
||||||
if (this.signaturePad) {
|
if (this.signaturePad) {
|
||||||
@@ -84,22 +89,22 @@ export class SignPad extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async clear() {
|
public async clear() {
|
||||||
this.signaturePad.clear();
|
this.signaturePad?.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toData() {
|
public async toData() {
|
||||||
const returnData = this.signaturePad.toData();
|
const returnData = this.signaturePad?.toData() || [];
|
||||||
return returnData;
|
return returnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fromData(dataArrayArg: any[]) {
|
public async fromData(dataArrayArg: any[]) {
|
||||||
this.signaturePad.fromData(dataArrayArg);
|
this.signaturePad?.fromData(dataArrayArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async toSVG() {
|
public async toSVG() {
|
||||||
return this.signaturePad.toSVG({
|
return this.signaturePad?.toSVG({
|
||||||
includeBackgroundColor: false,
|
includeBackgroundColor: false,
|
||||||
});
|
}) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public async undo() {
|
public async undo() {
|
||||||
@@ -107,4 +112,4 @@ export class SignPad extends DeesElement {
|
|||||||
data.pop();
|
data.pop();
|
||||||
await this.fromData(data);
|
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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-19
@@ -1,21 +1,3 @@
|
|||||||
// @signature.digital scope
|
|
||||||
import * as sdDemodata from '@signature.digital/tools/demodata';
|
|
||||||
import * as sdInterfaces from '@signature.digital/tools/interfaces';
|
|
||||||
import * as sdTools from '@signature.digital/tools';
|
|
||||||
|
|
||||||
export {
|
|
||||||
sdDemodata,
|
|
||||||
sdInterfaces,
|
|
||||||
sdTools,
|
|
||||||
}
|
|
||||||
|
|
||||||
// @design.estate scope
|
|
||||||
import * as deesCatalog from '@design.estate/dees-catalog';
|
|
||||||
|
|
||||||
export {
|
|
||||||
deesCatalog,
|
|
||||||
}
|
|
||||||
|
|
||||||
// third party
|
// third party
|
||||||
import signaturePadMod from 'signature_pad';
|
import signaturePadMod from 'signature_pad';
|
||||||
type signaturePadType = (typeof import('signature_pad'))['default'];
|
type signaturePadType = (typeof import('signature_pad'))['default'];
|
||||||
@@ -23,4 +5,4 @@ const signaturePad = signaturePadMod as any as signaturePadType;
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
signaturePad,
|
signaturePad,
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
"module": "NodeNext",
|
"module": "NodeNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true,
|
||||||
|
"types": ["node"]
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"dist_*/**/*.d.ts"
|
"dist_*/**/*.d.ts"
|
||||||
|
|||||||
Reference in New Issue
Block a user