feat(catalog): add ContractEditor and many editor subcomponents; implement SignPad and SignBox; update README and bump dependencies

This commit is contained in:
2025-12-18 15:27:22 +00:00
parent 6d53259b75
commit 56c087bc3a
35 changed files with 10914 additions and 1959 deletions

View 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>
`;
}
}