feat(catalog): add ContractEditor and many editor subcomponents; implement SignPad and SignBox; update README and bump dependencies
This commit is contained in:
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;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user