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