feat(catalog): add ContractEditor and many editor subcomponents; implement SignPad and SignBox; update README and bump dependencies
This commit is contained in:
BIN
.playwright-mcp/contract-editor-badge.png
Normal file
BIN
.playwright-mcp/contract-editor-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
11
changelog.md
11
changelog.md
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-18 - 1.1.0 - feat(catalog)
|
||||
add ContractEditor and many editor subcomponents; implement SignPad and SignBox; update README and bump dependencies
|
||||
|
||||
- New contract editor module with many subcomponents added (header, metadata, parties, content, terms, signatures, attachments, collaboration, audit)
|
||||
- Implemented signature UI components: sdig-signpad and sdig-signbox (canvas-based signature capture, undo/clear/export APIs)
|
||||
- Reorganized exports (ts_web/elements/index.ts) to expose new submodules and barrel files
|
||||
- Added editor types and smartstate store (ts_web/elements/sdig-contracteditor/types.ts and state.ts) for editor state management, undo/redo and events
|
||||
- Large README overhaul: improved usage examples, API docs, security/issue reporting section, theming and integration examples
|
||||
- Dependencies bumped in package.json (notable bumps: @design.estate/* packages, @git.zone tooling, signature_pad) and pnpm overrides added
|
||||
- tsconfig.json compiler options modified (removed experimentalDecorators and useDefineForClassFields) — may affect local build configurations
|
||||
|
||||
## 2024-12-19 - 1.0.59 - fix(dependencies)
|
||||
Update package dependencies and project metadata
|
||||
|
||||
|
||||
26
package.json
26
package.json
@@ -15,18 +15,18 @@
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@design.estate/dees-catalog": "^1.3.3",
|
||||
"@design.estate/dees-domtools": "^2.0.65",
|
||||
"@design.estate/dees-element": "^2.0.39",
|
||||
"@design.estate/dees-wcctools": "^1.0.90",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@design.estate/dees-catalog": "^3.3.1",
|
||||
"@design.estate/dees-domtools": "^2.3.6",
|
||||
"@design.estate/dees-element": "^2.1.3",
|
||||
"@design.estate/dees-wcctools": "^2.0.1",
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@signature.digital/tools": "^1.1.0",
|
||||
"signature_pad": "^5.0.4"
|
||||
"signature_pad": "^5.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^2.2.0",
|
||||
"@git.zone/tsbundle": "^2.1.0",
|
||||
"@git.zone/tswatch": "^2.0.37",
|
||||
"@git.zone/tsbuild": "^4.0.2",
|
||||
"@git.zone/tsbundle": "^2.6.3",
|
||||
"@git.zone/tswatch": "^2.3.13",
|
||||
"@push.rocks/projectinfo": "^5.0.2"
|
||||
},
|
||||
"files": [
|
||||
@@ -58,5 +58,11 @@
|
||||
"custom elements",
|
||||
"electronic signing",
|
||||
"npm package"
|
||||
]
|
||||
],
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@push.rocks/smartrequest": "^5.0.1",
|
||||
"agentkeepalive": "^4.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3725
pnpm-lock.yaml
generated
3725
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
248
readme.md
248
readme.md
@@ -1,164 +1,176 @@
|
||||
# @signature.digital/catalog
|
||||
A catalog containing components for e-signing, built using modern web technologies for seamless integration and functionality.
|
||||
|
||||
A comprehensive catalog of customizable web components designed for building and managing e-signature applications. Built with modern web technologies using LitElement and TypeScript.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## Install
|
||||
To add `@signature.digital/catalog` to your project, run the following command in your terminal:
|
||||
|
||||
```shell
|
||||
npm install @signature.digital/catalog
|
||||
# or
|
||||
pnpm install @signature.digital/catalog
|
||||
```
|
||||
|
||||
This command will install `@signature.digital/catalog` along with its required peer dependencies. Ensure that your project is set up for ECMAScript Modules (ESM).
|
||||
## 🎯 Overview
|
||||
|
||||
## Usage
|
||||
This package provides three main components for e-signature workflows:
|
||||
|
||||
The `@signature.digital/catalog` package provides a set of web components specifically designed for building e-signature applications. These components can be used for capturing, displaying, and managing electronic signatures. The primary components offered in this package include `SignPad`, `SignBox`, and `ContractEditor`.
|
||||
| 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 |
|
||||
|
||||
### Setting Up the Environment
|
||||
## 📦 Usage
|
||||
|
||||
To make the most of `@signature.digital/catalog`, you'll want to ensure you have your environment ready to use web components. If you're using a standard web setup with a module bundler like Webpack, Rollup, or Vite, make sure your build process supports ECMAScript modules.
|
||||
|
||||
### Basic Example of `SignPad`
|
||||
|
||||
The `SignPad` component is a customizable signature pad that can be used to capture user signatures. Here's how to implement it in your HTML:
|
||||
### Basic Import
|
||||
|
||||
```typescript
|
||||
import '@signature.digital/catalog/element';
|
||||
|
||||
class MySignatureComponent extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = '<sdig-signpad></sdig-signpad>';
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-signature-component', MySignatureComponent);
|
||||
import '@signature.digital/catalog';
|
||||
```
|
||||
|
||||
In this example, a simple HTML element is constructed that includes the `sdig-signpad` custom element. This signature pad allows users to draw their signatures directly in the browser.
|
||||
This registers all custom elements and makes them available for use in your HTML.
|
||||
|
||||
### Advanced Usage with `SignBox`
|
||||
### SignPad Component
|
||||
|
||||
The `SignBox` component wraps `SignPad` with additional controls for signing actions like clear and undo. This provides a complete signing interface out of the box.
|
||||
The `<sdig-signpad>` is a canvas-based signature capture component that allows users to draw their signatures directly in the browser.
|
||||
|
||||
```typescript
|
||||
import '@signature.digital/catalog/element';
|
||||
|
||||
class MyCompleteSignatureBox extends HTMLElement {
|
||||
connectedCallback() {
|
||||
this.innerHTML = `<sdig-signbox></sdig-signbox>`;
|
||||
this.querySelector('sdig-signbox').addEventListener('signature', (e) => {
|
||||
const signatureData = e.detail.signature;
|
||||
console.log('Signature data:', signatureData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('my-complete-signature-box', MyCompleteSignatureBox);
|
||||
```html
|
||||
<sdig-signpad></sdig-signpad>
|
||||
```
|
||||
|
||||
In this setup, `SignBox` also manages the signature data and emits a custom event once the signature is submitted. You can listen for this event to gather the signature data for storage or further processing.
|
||||
|
||||
### Integrating `ContractEditor`
|
||||
|
||||
ContractEditor allows for manipulating contract details and integrating signature capabilities directly into documents. This component communicates with its state using Smartstate, an inbuilt state management system.
|
||||
**API Methods:**
|
||||
|
||||
```typescript
|
||||
import '@signature.digital/catalog/element';
|
||||
import { IPortableContract } from '@signature.digital/tools/interfaces';
|
||||
const signpad = document.querySelector('sdig-signpad');
|
||||
|
||||
class MyContractEditor extends HTMLElement {
|
||||
editContract(contract: IPortableContract) {
|
||||
const editor = this.querySelector('sdig-contracteditor');
|
||||
editor.contract = contract;
|
||||
}
|
||||
// Get signature data as point arrays
|
||||
const data = await signpad.toData();
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = `<sdig-contracteditor></sdig-contracteditor>`;
|
||||
}
|
||||
}
|
||||
// Load signature from data
|
||||
await signpad.fromData(data);
|
||||
|
||||
customElements.define('my-contract-editor', MyContractEditor);
|
||||
// Export signature as SVG string
|
||||
const svg = await signpad.toSVG();
|
||||
|
||||
// Undo last stroke
|
||||
await signpad.undo();
|
||||
|
||||
// Clear the signature pad
|
||||
await signpad.clear();
|
||||
```
|
||||
|
||||
`ContractEditor` can be dynamically updated by changing the `contract` property. This component expects contract definitions compatible with the `IPortableContract` interface, offering an adaptable and customizable editing solution.
|
||||
### SignBox Component
|
||||
|
||||
### Example Application
|
||||
The `<sdig-signbox>` wraps `SignPad` with a complete UI including Clear, Undo, and Submit buttons.
|
||||
|
||||
Below is an integrated example showing how all of these components can be put together to form a basic signature application.
|
||||
|
||||
```typescript
|
||||
import '@signature.digital/catalog/element';
|
||||
import { IPortableContract } from '@signature.digital/tools/interfaces';
|
||||
import { demoContract } from '@signature.digital/tools/demodata';
|
||||
|
||||
class CombinedSignatureApp extends HTMLElement {
|
||||
|
||||
private contract: IPortableContract = demoContract;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<my-complete-signature-box></my-complete-signature-box>
|
||||
<my-contract-editor></my-contract-editor>
|
||||
`;
|
||||
this.querySelector('my-contract-editor').editContract(this.contract);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('combined-signature-app', CombinedSignatureApp);
|
||||
```html
|
||||
<sdig-signbox></sdig-signbox>
|
||||
```
|
||||
|
||||
In a real-world scenario, `CombinedSignatureApp` could be binding multiple processes around collecting and displaying signatures, including validation logic, storage mechanisms, and user feedback systems.
|
||||
|
||||
### Handling Signatures
|
||||
|
||||
Once you have a user signature, you can convert it between different formats like `Data`, `SVG` or simply clear or undo modifications. Here is how you can handle these activities within your JavaScript:
|
||||
**Events:**
|
||||
|
||||
```typescript
|
||||
document.querySelector('sdig-signpad').addEventListener('signature', async (event) => {
|
||||
const signaturePad = event.target as SignPad;
|
||||
const signbox = document.querySelector('sdig-signbox');
|
||||
|
||||
console.log(await signaturePad.toData()); // Get signature data
|
||||
console.log(await signaturePad.toSVG()); // Convert to SVG
|
||||
|
||||
await signaturePad.undo(); // Undo last action
|
||||
await signaturePad.clear(); // Clear the signature pad
|
||||
signbox.addEventListener('signature', (event) => {
|
||||
const signatureData = event.detail.signature;
|
||||
console.log('Signature captured:', signatureData);
|
||||
});
|
||||
```
|
||||
|
||||
This versatile use of `sdig-signpad` demonstrates how diverse use case scenarios for signatures can be developed, be it collecting, transforming, or editing current user inputs.
|
||||
### ContractEditor Component
|
||||
|
||||
### Responsive Design Considerations
|
||||
|
||||
When building applications that include e-signature capabilities, you must ensure your components respond well to different screen sizes. Components in `@signature.digital/catalog` are designed with CSS variables and flexible dimensions, but specific implementations can benefit from additional CSS media queries.
|
||||
|
||||
### Custom Styling
|
||||
|
||||
Each component is styled using Light DOM scoped styles. Components like `sdig-signpad` come with default styles, but they are capable of overriding these styles for consistent design alignment within your project. For example:
|
||||
The `<sdig-contracteditor>` provides contract viewing and editing capabilities using the `IPortableContract` interface from `@signature.digital/tools`.
|
||||
|
||||
```typescript
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
sdig-signpad, sdig-signbox {
|
||||
--main-background-color: #fff;
|
||||
--line-color: #000;
|
||||
--button-color: #007bff;
|
||||
}
|
||||
`;
|
||||
import '@signature.digital/catalog';
|
||||
import { IPortableContract } from '@signature.digital/tools/interfaces';
|
||||
import { demoContract } from '@signature.digital/tools/demodata';
|
||||
|
||||
document.head.appendChild(style);
|
||||
const editor = document.querySelector('sdig-contracteditor');
|
||||
editor.contract = demoContract;
|
||||
```
|
||||
|
||||
These CSS custom properties modify component styles, from appearance to behavior, creating a cohesive look with the rest of your site's aesthetics.
|
||||
## 🔧 Integration Example
|
||||
|
||||
### Conclusion
|
||||
Here's a complete example showing all components working together:
|
||||
|
||||
With `@signature.digital/catalog`, developers have a comprehensive toolkit for integrating sophisticated e-signature functionalities into their web applications. Its wide array of customizable components, flexibility, and ease of use out of the box makes it an indispensable inclusion for building modern digital signature workflows. Whether for simple signature capture or complex contract management, this package has you covered. Explore more advanced topics and extend the capabilities to fit unique business needs in your applications.
|
||||
undefined
|
||||
```typescript
|
||||
import '@signature.digital/catalog';
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('my-signature-app')
|
||||
class MySignatureApp extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<h2>Please sign below</h2>
|
||||
<sdig-signbox @signature=${this.handleSignature}></sdig-signbox>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleSignature(e: CustomEvent) {
|
||||
console.log('Signature submitted:', e.detail.signature);
|
||||
// Process or store the signature
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Theming
|
||||
|
||||
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
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
||||
|
||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@signature.digital/catalog',
|
||||
version: '1.0.59',
|
||||
version: '1.1.0',
|
||||
description: 'A comprehensive catalog of customizable web components designed for building and managing e-signature applications.'
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
export * from './sdig-contracteditor.js';
|
||||
export * from './sdig-signbox.js';
|
||||
export * from './sdig-signpad.js';
|
||||
// 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';
|
||||
|
||||
// Signature components
|
||||
export * from './sdig-signbox/index.js';
|
||||
export * from './sdig-signpad/index.js';
|
||||
|
||||
1
ts_web/elements/sdig-contract-attachments/index.ts
Normal file
1
ts_web/elements/sdig-contract-attachments/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-attachments.js';
|
||||
@@ -0,0 +1,806 @@
|
||||
/**
|
||||
* @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:file-text', color: '#3b82f6', label: 'Document' },
|
||||
image: { icon: 'lucide:image', color: '#10b981', label: 'Image' },
|
||||
spreadsheet: { icon: 'lucide:sheet', color: '#22c55e', label: 'Spreadsheet' },
|
||||
pdf: { icon: 'lucide:file-type', 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 .iconFA=${'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 .iconFA=${'lucide:upload-cloud'}></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 .iconFA=${'lucide:file-x'}></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 .iconFA=${'lucide:files'}></dees-icon>
|
||||
Prior Contracts
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-secondary" @click=${this.handleAddPriorContract}>
|
||||
<dees-icon .iconFA=${'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 .iconFA=${'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 .iconFA=${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 .iconFA=${'lucide:calendar'}></dees-icon>
|
||||
${this.formatDate(attachment.uploadedAt)}
|
||||
</span>
|
||||
<span class="attachment-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:user'}></dees-icon>
|
||||
${this.getPartyName(attachment.uploadedBy)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachment-actions">
|
||||
<button class="btn btn-ghost" title="Download">
|
||||
<dees-icon .iconFA=${'lucide:download'}></dees-icon>
|
||||
</button>
|
||||
<button class="btn btn-ghost" title="Preview">
|
||||
<dees-icon .iconFA=${'lucide:eye'}></dees-icon>
|
||||
</button>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button
|
||||
class="btn btn-ghost btn-danger"
|
||||
title="Delete"
|
||||
@click=${() => this.handleDeleteAttachment(attachment.id)}
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:trash-2'}></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 .iconFA=${'lucide:file-text'}></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 .iconFA=${'lucide:external-link'}></dees-icon>
|
||||
View
|
||||
</button>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button
|
||||
class="btn btn-ghost btn-danger"
|
||||
@click=${() => this.handleRemovePriorContract(index)}
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:unlink'}></dees-icon>
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-audit/index.ts
Normal file
1
ts_web/elements/sdig-contract-audit/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-audit.js';
|
||||
772
ts_web/elements/sdig-contract-audit/sdig-contract-audit.ts
Normal file
772
ts_web/elements/sdig-contract-audit/sdig-contract-audit.ts
Normal file
@@ -0,0 +1,772 @@
|
||||
/**
|
||||
* @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:file-edit', color: '#f59e0b' },
|
||||
{ id: 'review', label: 'Review', icon: 'lucide:eye', color: '#3b82f6' },
|
||||
{ id: 'pending', label: 'Pending Signatures', icon: 'lucide:pen-tool', color: '#8b5cf6' },
|
||||
{ id: 'signed', label: 'Signed', icon: 'lucide:check-circle', color: '#10b981' },
|
||||
{ id: 'executed', label: 'Executed', icon: 'lucide:shield-check', color: '#059669' },
|
||||
];
|
||||
|
||||
// Event type configuration
|
||||
const EVENT_TYPES = {
|
||||
created: { icon: 'lucide:plus-circle', color: '#10b981', label: 'Created' },
|
||||
updated: { icon: 'lucide:pencil', color: '#3b82f6', label: 'Updated' },
|
||||
status_change: { icon: 'lucide:arrow-right-circle', color: '#8b5cf6', label: 'Status Changed' },
|
||||
signature: { icon: 'lucide:pen-tool', color: '#10b981', label: 'Signature' },
|
||||
comment: { icon: 'lucide:message-circle', color: '#f59e0b', label: 'Comment' },
|
||||
attachment: { icon: 'lucide:paperclip', color: '#6366f1', label: 'Attachment' },
|
||||
viewed: { icon: 'lucide:eye', color: '#6b7280', label: 'Viewed' },
|
||||
shared: { icon: 'lucide:share-2', 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 .iconFA=${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 .iconFA=${'lucide:history'}></dees-icon>
|
||||
Activity Log
|
||||
</div>
|
||||
<button class="btn btn-secondary">
|
||||
<dees-icon .iconFA=${'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 .iconFA=${'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 .iconFA=${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
ts_web/elements/sdig-contract-collaboration/index.ts
Normal file
1
ts_web/elements/sdig-contract-collaboration/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-collaboration.js';
|
||||
@@ -0,0 +1,972 @@
|
||||
/**
|
||||
* @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 .iconFA=${'lucide:share-2'}></dees-icon>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div class="section-card">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<dees-icon .iconFA=${'lucide:message-circle'}></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 .iconFA=${'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 .iconFA=${'lucide:message-square'}></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 .iconFA=${'lucide:git-pull-request'}></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 .iconFA=${'lucide:edit-3'}></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 .iconFA=${comment.resolved ? 'lucide:rotate-ccw' : 'lucide:check'}></dees-icon>
|
||||
${comment.resolved ? 'Reopen' : 'Resolve'}
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
${comment.anchorText
|
||||
? html`
|
||||
<div class="comment-anchor">
|
||||
<dees-icon .iconFA=${'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 .iconFA=${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 .iconFA=${'lucide:check'}></dees-icon>
|
||||
Accept
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm" @click=${() => this.handleRejectSuggestion(suggestion.id)}>
|
||||
<dees-icon .iconFA=${'lucide:x'}></dees-icon>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-content/index.ts
Normal file
1
ts_web/elements/sdig-contract-content/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-content.js';
|
||||
920
ts_web/elements/sdig-contract-content/sdig-contract-content.ts
Normal file
920
ts_web/elements/sdig-contract-content/sdig-contract-content.ts
Normal file
@@ -0,0 +1,920 @@
|
||||
/**
|
||||
* @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:file-text' },
|
||||
{ value: 'definition', label: 'Definition', icon: 'lucide:book-open' },
|
||||
{ value: 'obligation', label: 'Obligation', icon: 'lucide:check-square' },
|
||||
{ value: 'condition', label: 'Condition', icon: 'lucide:git-branch' },
|
||||
{ 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 .iconFA=${'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 .iconFA=${'lucide:list'}></dees-icon>
|
||||
</button>
|
||||
<button
|
||||
class="view-toggle-btn ${this.viewMode === 'outline' ? 'active' : ''}"
|
||||
@click=${() => (this.viewMode = 'outline')}
|
||||
title="Outline view"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:layout-list'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-primary" @click=${() => this.handleAddParagraph()}>
|
||||
<dees-icon .iconFA=${'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 .iconFA=${'lucide:file-text'}></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 .iconFA=${'lucide:plus'}></dees-icon>
|
||||
Add paragraph
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:file-plus'}></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 .iconFA=${'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 .iconFA=${'lucide:grip-vertical'}></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 .iconFA=${'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 .iconFA=${'lucide:chevron-up'}></dees-icon>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this.handleMoveParagraph(paragraph.uniqueId, 'down'); }}
|
||||
title="Move down"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:chevron-down'}></dees-icon>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-ghost btn-danger"
|
||||
@click=${(e: Event) => { e.stopPropagation(); this.handleDeleteParagraph(paragraph.uniqueId); }}
|
||||
title="Delete"
|
||||
>
|
||||
<dees-icon .iconFA=${'lucide:trash-2'}></dees-icon>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
${childParagraphs.length > 0
|
||||
? html`
|
||||
<div class="child-paragraphs">
|
||||
${childParagraphs.map((child, childIndex) => this.renderParagraph(child, childIndex))}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
`;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/sdig-contract-header/index.ts
Normal file
1
ts_web/elements/sdig-contract-header/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-header.js';
|
||||
558
ts_web/elements/sdig-contract-header/sdig-contract-header.ts
Normal file
558
ts_web/elements/sdig-contract-header/sdig-contract-header.ts
Normal file
@@ -0,0 +1,558 @@
|
||||
/**
|
||||
* @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 .iconFA=${'lucide:chevron-down'} 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 .iconFA=${'lucide:download'}></dees-icon>
|
||||
</button>
|
||||
<button class="action-btn" @click=${this.handleDuplicate} title="Duplicate">
|
||||
<dees-icon .iconFA=${'lucide:copy'}></dees-icon>
|
||||
</button>
|
||||
<button class="action-btn" @click=${this.handleShare} title="Share">
|
||||
<dees-icon .iconFA=${'lucide:share-2'}></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
ts_web/elements/sdig-contract-metadata/index.ts
Normal file
1
ts_web/elements/sdig-contract-metadata/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-metadata.js';
|
||||
820
ts_web/elements/sdig-contract-metadata/sdig-contract-metadata.ts
Normal file
820
ts_web/elements/sdig-contract-metadata/sdig-contract-metadata.ts
Normal file
@@ -0,0 +1,820 @@
|
||||
/**
|
||||
* @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 .iconFA=${'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 .iconFA=${'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 .iconFA=${'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 .iconFA=${'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
ts_web/elements/sdig-contract-parties/index.ts
Normal file
1
ts_web/elements/sdig-contract-parties/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-parties.js';
|
||||
736
ts_web/elements/sdig-contract-parties/sdig-contract-parties.ts
Normal file
736
ts_web/elements/sdig-contract-parties/sdig-contract-parties.ts
Normal file
@@ -0,0 +1,736 @@
|
||||
/**
|
||||
* @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:pen-tool' },
|
||||
{ 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:check-circle' },
|
||||
{ value: 'guarantor', label: 'Guarantor', icon: 'lucide:shield' },
|
||||
{ value: 'beneficiary', label: 'Beneficiary', icon: 'lucide:user-check' },
|
||||
];
|
||||
|
||||
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 .iconFA=${'lucide:users-2'}></dees-icon>
|
||||
Available Roles
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-secondary btn-sm" @click=${this.handleAddRole}>
|
||||
<dees-icon .iconFA=${'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 .iconFA=${'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 .iconFA=${'lucide:user-plus'}></dees-icon>
|
||||
Involved Parties (${parties.length})
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-primary btn-sm" @click=${this.handleAddParty}>
|
||||
<dees-icon .iconFA=${'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 .iconFA=${'lucide:user-plus'}></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
|
||||
.iconFA=${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 .iconFA=${'lucide:pen-tool'}></dees-icon>
|
||||
Signature required
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
${role.defaultSigningOrder > 0
|
||||
? html`
|
||||
<span class="role-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:list-ordered'}></dees-icon>
|
||||
Order: ${role.defaultSigningOrder}
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
${role.minParties
|
||||
? html`
|
||||
<span class="role-meta-item">
|
||||
<dees-icon .iconFA=${'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 .iconFA=${'lucide:mail'}></dees-icon>
|
||||
${deliveryEmail}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${deliveryPhone
|
||||
? html`
|
||||
<div class="party-detail">
|
||||
<dees-icon .iconFA=${'lucide:phone'}></dees-icon>
|
||||
${deliveryPhone}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
${actingAsProxy
|
||||
? html`
|
||||
<div class="party-detail">
|
||||
<dees-icon .iconFA=${'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
ts_web/elements/sdig-contract-signatures/index.ts
Normal file
1
ts_web/elements/sdig-contract-signatures/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-signatures.js';
|
||||
@@ -0,0 +1,840 @@
|
||||
/**
|
||||
* @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:pen-tool' },
|
||||
{ value: 'signed', label: 'Signed', color: '#10b981', icon: 'lucide:check-circle' },
|
||||
{ value: 'declined', label: 'Declined', color: '#ef4444', icon: 'lucide:x-circle' },
|
||||
];
|
||||
|
||||
const FIELD_TYPES = [
|
||||
{ value: 'signature', label: 'Full Signature', icon: 'lucide:pen-tool' },
|
||||
{ value: 'initials', label: 'Initials', icon: 'lucide:type' },
|
||||
{ value: 'date', label: 'Date', icon: 'lucide:calendar' },
|
||||
{ value: 'text', label: 'Text Field', icon: 'lucide:text-cursor' },
|
||||
];
|
||||
|
||||
@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 .iconFA=${'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 .iconFA=${'lucide:pen-tool'}></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 .iconFA=${'lucide:check-circle'}></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 .iconFA=${'lucide:x-circle'}></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 .iconFA=${'lucide:pen-tool'}></dees-icon>
|
||||
Signature Fields
|
||||
</div>
|
||||
${!this.readonly
|
||||
? html`
|
||||
<button class="btn btn-primary" @click=${this.handleAddField}>
|
||||
<dees-icon .iconFA=${'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 .iconFA=${'lucide:pen-tool'}></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 .iconFA=${'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 .iconFA=${'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 .iconFA=${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 .iconFA=${'lucide:user'}></dees-icon>
|
||||
${this.getPartyRoleName(field.roleId)}
|
||||
</span>
|
||||
<span class="type-badge">
|
||||
<dees-icon .iconFA=${typeConfig.icon}></dees-icon>
|
||||
${typeConfig.label}
|
||||
</span>
|
||||
${field.required
|
||||
? html`
|
||||
<span class="field-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:asterisk'}></dees-icon>
|
||||
Required
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
${field.signedAt
|
||||
? html`
|
||||
<span class="field-meta-item">
|
||||
<dees-icon .iconFA=${'lucide:calendar'}></dees-icon>
|
||||
${this.formatDate(field.signedAt)}
|
||||
</span>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-status ${field.status}">
|
||||
<dees-icon .iconFA=${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 .iconFA=${'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 .iconFA=${'lucide:trash-2'}></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
ts_web/elements/sdig-contract-terms/index.ts
Normal file
1
ts_web/elements/sdig-contract-terms/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-contract-terms.js';
|
||||
873
ts_web/elements/sdig-contract-terms/sdig-contract-terms.ts
Normal file
873
ts_web/elements/sdig-contract-terms/sdig-contract-terms.ts
Normal file
@@ -0,0 +1,873 @@
|
||||
/**
|
||||
* @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:check-square', 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 .iconFA=${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 .iconFA=${'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 .iconFA=${'lucide:pencil'}></dees-icon>
|
||||
</button>
|
||||
</td>
|
||||
`
|
||||
: ''}
|
||||
</tr>
|
||||
`
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'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 .iconFA=${'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 .iconFA=${'lucide:pencil'}></dees-icon>
|
||||
</button>
|
||||
</td>
|
||||
`
|
||||
: ''}
|
||||
</tr>
|
||||
`
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'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 .iconFA=${'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 .iconFA=${'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 .iconFA=${'lucide:pencil'}></dees-icon>
|
||||
</button>
|
||||
</td>
|
||||
`
|
||||
: ''}
|
||||
</tr>
|
||||
`
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
: html`
|
||||
<div class="empty-state">
|
||||
<dees-icon .iconFA=${'lucide:check-square'}></dees-icon>
|
||||
<h4>No Obligations</h4>
|
||||
<p>Add obligations to track party responsibilities</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
css,
|
||||
cssManager,
|
||||
domtools,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as plugins from '../plugins.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sdig-contracteditor': ContractEditor;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('sdig-contracteditor')
|
||||
export class ContractEditor extends DeesElement {
|
||||
public static demo = () => html` <sdig-contracteditor
|
||||
.contract=${plugins.sdDemodata.demoContract}
|
||||
></sdig-contracteditor> `;
|
||||
|
||||
// INSTANCE
|
||||
public localStateInstance = new domtools.plugins.smartstate.Smartstate();
|
||||
public contractState =
|
||||
this.localStateInstance.getStatePart<plugins.sdInterfaces.IPortableContract>('contract');
|
||||
|
||||
@property({ type: Object })
|
||||
public contract: plugins.sdInterfaces.IPortableContract;
|
||||
|
||||
public async firstUpdated(_changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.firstUpdated(_changedProperties);
|
||||
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html` <div class="mainbox"></div> `;
|
||||
}
|
||||
}
|
||||
8
ts_web/elements/sdig-contracteditor/index.ts
Normal file
8
ts_web/elements/sdig-contracteditor/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @file index.ts
|
||||
* @description Export barrel for sdig-contracteditor module
|
||||
*/
|
||||
|
||||
export * from './sdig-contracteditor.js';
|
||||
export * from './types.js';
|
||||
export * from './state.js';
|
||||
839
ts_web/elements/sdig-contracteditor/sdig-contracteditor.ts
Normal file
839
ts_web/elements/sdig-contracteditor/sdig-contracteditor.ts
Normal file
@@ -0,0 +1,839 @@
|
||||
/**
|
||||
* @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';
|
||||
|
||||
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: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();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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 .iconFA=${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 .iconFA=${'lucide:undo-2'}></dees-icon>
|
||||
</button>
|
||||
<button class="btn btn-ghost" @click=${this.handleRedo} ?disabled=${!this.store?.canRedo()}>
|
||||
<dees-icon .iconFA=${'lucide:redo-2'}></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 .iconFA=${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">
|
||||
<!-- Sidebar content - collaboration panel -->
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
407
ts_web/elements/sdig-contracteditor/state.ts
Normal file
407
ts_web/elements/sdig-contracteditor/state.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* @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>>;
|
||||
228
ts_web/elements/sdig-contracteditor/types.ts
Normal file
228
ts_web/elements/sdig-contracteditor/types.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* @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:file-text' },
|
||||
{ id: 'parties', label: 'Parties & Roles', icon: 'lucide:users' },
|
||||
{ id: 'content', label: 'Content', icon: 'lucide:file-edit' },
|
||||
{ id: 'terms', label: 'Terms', icon: 'lucide:calculator' },
|
||||
{ id: 'signatures', label: 'Signatures', icon: 'lucide:pen-tool' },
|
||||
{ id: 'attachments', label: 'Attachments', icon: 'lucide:paperclip' },
|
||||
{ id: 'collaboration', label: 'Collaboration', icon: 'lucide:message-circle' },
|
||||
{ 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
ts_web/elements/sdig-signbox/index.ts
Normal file
1
ts_web/elements/sdig-signbox/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-signbox.js';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeesElement, property, 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 {
|
||||
interface HTMLElementTagNameMap {
|
||||
1
ts_web/elements/sdig-signpad/index.ts
Normal file
1
ts_web/elements/sdig-signpad/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './sdig-signpad.js';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DeesElement, property, 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 {
|
||||
interface HTMLElementTagNameMap {
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
|
||||
Reference in New Issue
Block a user