feat(workspace): introduce a responsive signature workspace demo and remove legacy contract editor components

This commit is contained in:
2026-05-02 18:37:48 +00:00
parent 90836f1c72
commit 57cbb739d2
48 changed files with 4387 additions and 13348 deletions
+8
View File
@@ -0,0 +1,8 @@
export * from './sdig-workspace.shared.js';
export * from './sdig-workspace-inbox.js';
export * from './sdig-workspace-compose.js';
export * from './sdig-workspace-sign.js';
export * from './sdig-workspace-audit.js';
export * from './sdig-workspace-developers.js';
export * from './sdig-workspace-placeholder.js';
export * from './sdig-workspace.js';
@@ -0,0 +1,37 @@
import { DeesElement, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
import { actionButton, demoRecipients, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js';
declare global {
interface HTMLElementTagNameMap {
'sdig-workspace-audit': SdigWorkspaceAudit;
}
}
@customElement('sdig-workspace-audit')
export class SdigWorkspaceAudit extends DeesElement {
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-audit></sdig-workspace-audit>`);
public static demoGroups = ['Signature Digital Workspace'];
public static styles = [workspaceBaseStyles, css`
.audit-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 20px; }
.event-row { display: grid; grid-template-columns: 24px 180px 1fr 200px; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--border-subtle); align-items: center; }
@media (max-width: 920px) { .audit-grid { grid-template-columns: 1fr; } .event-row { grid-template-columns: 24px 1fr; } .event-row .hide-mobile { display: none; } }
`];
public render(): TemplateResult {
const events = [
['2026-05-02 14:32:18 UTC', 'Sarah Chen', 'Document signed', '81.221.4.18 · Brussels, BE', '0x4a7b…f29c', 'success'],
['2026-05-02 14:31:54 UTC', 'Sarah Chen', 'Signature adopted (typed)', '81.221.4.18 · Brussels, BE', '0x4a7b…f29c', 'info'],
['2026-05-02 14:28:02 UTC', 'Sarah Chen', 'Document opened', '81.221.4.18 · Brussels, BE', '', 'default'],
['2026-05-02 11:02:11 UTC', 'Philipp K.', 'Document sent for signature', '92.42.114.7 · Berlin, DE', '0x1c8a…3b6f', 'info'],
['2026-05-02 10:54:22 UTC', 'Philipp K.', 'Document created', '92.42.114.7 · Berlin, DE', '0x1c8a…3b6f', 'default'],
];
return html`
${topBar({ breadcrumb: ['signature.digital', 'Inbox', 'doc_8mK3pL', 'Audit Trail'], title: 'Audit Trail', subtitle: pill('completed · cryptographically sealed', 'success', true), actions: html`${actionButton('Certificate (PDF)', 'outline', 'download')}${actionButton('Verify on chain', 'outline', 'hash')}` })}
<div class="content-scroll audit-grid">
<div class="card"><div style="height: 36px; padding: 0 16px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; justify-content: space-between;"><span style="font-size: 12px; font-weight: 600;">Event log</span><span class="mono" style="font-size: 10px; color: var(--text-muted);">${events.length} events · immutable</span></div>${events.map((event) => html`<div class="event-row"><div><span style="display: block; width: 8px; height: 8px; border-radius: 50%; background: ${event[5] === 'success' ? 'var(--success)' : event[5] === 'info' ? 'var(--accent)' : 'var(--text-dim)'};"></span></div><div class="mono hide-mobile" style="font-size: 11px; color: var(--text-muted);">${event[0]}</div><div><div style="font-size: 12px; font-weight: 500;">${event[2]}</div><div style="font-size: 10px; color: var(--text-muted); margin-top: 2px;">by ${event[1]} ${event[4] ? html`<span class="mono" style="color: var(--accent); margin-left: 8px;">${event[4]}</span>` : ''}</div></div><div class="mono hide-mobile" style="font-size: 10px; color: var(--text-muted); text-align: right;">${event[3]}</div></div>`)}</div>
<div style="display: flex; flex-direction: column; gap: 16px;"><div class="card" style="padding: 16px;"><div class="label-upper">Document hash</div><div class="mono" style="font-size: 11px; color: var(--accent); word-break: break-all; line-height: 1.5; padding: 10px; background: var(--bg-el); border-radius: 4px; border: 1px solid var(--border-subtle);">0x4a7b8f29c91e3d2a5b6c8e0f1d3c5a7b9d2e4f6a8c1e3d5f7b9c1e3a5b7d9f0e</div></div><div class="card" style="padding: 16px;"><div class="label-upper">Signers</div>${demoRecipients.map((recipient) => html`<div class="recipient-line"><span class="avatar" style="background: ${recipient.color};">${recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join('')}</span><div style="flex: 1;"><div style="font-size: 12px;">${recipient.name}</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">${recipient.email}</div></div>${icon('check', 12)}</div>`)}</div><div class="card" style="padding: 16px; border-color: rgba(34,197,94,0.2);"><div style="display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: var(--success); margin-bottom: 6px;">${icon('shield', 13)} eIDAS Qualified · ESIGN Act compliant</div><div style="font-size: 11px; color: var(--text-muted); line-height: 1.55;">Open-source verifier available. Anyone can independently validate this signature against the public ledger.</div></div></div>
</div>
`;
}
}
@@ -0,0 +1,369 @@
import { DeesElement, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
import { actionButton, demoFields, demoRecipients, fakeDocument, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame, type IFieldPlacement, type IRecipient } from './sdig-workspace.shared.js';
declare global {
interface HTMLElementTagNameMap {
'sdig-workspace-compose': SdigWorkspaceCompose;
}
}
type TFieldDefinition = {
type: IFieldPlacement['type'];
icon: string;
label: string;
w: number;
h: number;
};
type TResizeHandle = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
type TFieldInteraction = {
fieldId: string;
mode: 'move' | 'resize';
handle?: TResizeHandle;
startClientX: number;
startClientY: number;
startField: IFieldPlacement;
pageWidth: number;
pageHeight: number;
};
const fieldDefinitions: TFieldDefinition[] = [
{ type: 'signature', icon: 'sign', label: 'Signature', w: 200, h: 50 },
{ type: 'initials', icon: 'type', label: 'Initials', w: 120, h: 32 },
{ type: 'date', icon: 'calendar', label: 'Date', w: 120, h: 32 },
{ type: 'text', icon: 'type', label: 'Text field', w: 220, h: 32 },
{ type: 'check', icon: 'check', label: 'Checkbox', w: 120, h: 32 },
];
const resizeHandles: TResizeHandle[] = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
@customElement('sdig-workspace-compose')
export class SdigWorkspaceCompose extends DeesElement {
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-compose></sdig-workspace-compose>`);
public static demoGroups = ['Signature Digital Workspace'];
@state() private accessor step: number = 2;
@state() private accessor activeRecipient: number = 0;
@state() private accessor draggedRecipientId: number | null = null;
@state() private accessor selectedFieldId: string | null = null;
@state() private accessor recipients: IRecipient[] = [...demoRecipients];
@state() private accessor fields: IFieldPlacement[] = [...demoFields];
private draggedFieldDefinition: TFieldDefinition | null = null;
private draggedFieldGrabOffset: { x: number; y: number } | null = null;
private fieldInteraction: TFieldInteraction | null = null;
public static styles = [workspaceBaseStyles, css`
.stepper { height: 44px; flex-shrink: 0; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; padding: 0 24px; gap: 24px; overflow-x: auto; }
.step { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); white-space: nowrap; background: transparent; }
.step-number { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--bg-input); border: 1px solid var(--border); color: var(--text-muted); font-size: 10px; font-weight: 700; }
.step.active { color: var(--text); font-weight: 500; }
.step.active .step-number { background: var(--accent); border-color: var(--accent); color: white; }
.step.done .step-number { background: var(--success); border-color: var(--success); color: white; }
.compose-workspace { flex: 1; display: flex; overflow: hidden; }
.palette { width: 260px; border-right: 1px solid var(--border-subtle); padding: 16px; background: var(--bg-el); overflow-y: auto; flex-shrink: 0; }
.right-panel { width: 280px; border-left: 1px solid var(--border-subtle); padding: 16px; background: var(--bg-el); overflow-y: auto; flex-shrink: 0; }
.field-tool { width: var(--tool-w); height: var(--tool-h); display: flex; align-items: center; gap: 8px; padding: 0 10px; background: color-mix(in srgb, var(--recipient-color) 8%, var(--bg-card)); border: 1.5px dashed var(--recipient-color); border-radius: 4px; font-size: 12px; color: var(--recipient-color); margin-bottom: 8px; cursor: grab; }
.field-tool:active { cursor: grabbing; }
.field-tool span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.swatch { width: 10px; height: 10px; border-radius: 2px; background: var(--recipient-color, var(--accent)); flex-shrink: 0; }
.document-stage { flex: 1; overflow: auto; background: hsl(0 0% 8%); display: flex; flex-direction: column; align-items: center; padding: 32px; gap: 20px; }
:host-context(sdig-workspace[theme='light']) .document-stage { background: hsl(0 0% 92%); }
.recipient-line { cursor: grab; }
.recipient-line.dragging { opacity: 0.45; border-color: var(--accent); }
.page-drop-target { outline: 1px dashed transparent; outline-offset: 8px; }
.page-drop-target.drag-over { outline-color: var(--accent); }
.field-box { user-select: none; touch-action: none; }
.field-box.selected { z-index: 5; cursor: move; }
.field-content { width: 100%; height: 100%; display: flex; align-items: center; gap: 6px; pointer-events: none; overflow: hidden; }
.field-content span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.resize-handle { position: absolute; z-index: 2; width: 9px; height: 9px; border-radius: 50%; background: var(--bg-card); border: 1.5px solid var(--field-color); box-shadow: 0 0 0 2px var(--bg-card); touch-action: none; }
.resize-handle.n { top: -6px; left: 50%; transform: translateX(-50%); cursor: ns-resize; }
.resize-handle.ne { top: -6px; right: -6px; cursor: nesw-resize; }
.resize-handle.e { right: -6px; top: 50%; transform: translateY(-50%); cursor: ew-resize; }
.resize-handle.se { right: -6px; bottom: -6px; cursor: nwse-resize; }
.resize-handle.s { bottom: -6px; left: 50%; transform: translateX(-50%); cursor: ns-resize; }
.resize-handle.sw { left: -6px; bottom: -6px; cursor: nesw-resize; }
.resize-handle.w { left: -6px; top: 50%; transform: translateY(-50%); cursor: ew-resize; }
.resize-handle.nw { left: -6px; top: -6px; cursor: nwse-resize; }
.field-editor { margin-top: 16px; padding: 12px; }
.field-editor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.field-control { display: flex; flex-direction: column; gap: 4px; font-size: 10px; color: var(--text-muted); }
.field-control.full { grid-column: 1 / -1; }
.field-control input, .field-control select { width: 100%; height: 30px; padding: 0 8px; border: 1px solid var(--border); border-radius: 5px; background: var(--bg-input); color: var(--text); font-size: 12px; outline: none; }
.field-control input:focus, .field-control select:focus { border-color: var(--accent); }
@media (max-width: 920px) { .compose-workspace { flex-direction: column; overflow: auto; } .palette, .right-panel { width: 100%; border: 0; border-bottom: 1px solid var(--border-subtle); } .document-page { width: 560px; } }
`];
public disconnectedCallback = async () => {
this.stopFieldInteraction();
await super.disconnectedCallback();
};
private recipientColor(id: number): string {
return this.recipients.find((recipient) => recipient.id === id)?.color || 'var(--accent)';
}
private fieldIcon(type: IFieldPlacement['type']): string {
if (type === 'signature') return 'sign';
if (type === 'date') return 'calendar';
if (type === 'check') return 'check';
return 'type';
}
private fieldDefinition(type: IFieldPlacement['type']): TFieldDefinition {
return fieldDefinitions.find((definition) => definition.type === type) || fieldDefinitions[0];
}
private clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
private updateField(fieldId: string, patch: Partial<IFieldPlacement>) {
this.fields = this.fields.map((field) => field.id === fieldId ? { ...field, ...patch } : field);
}
private updateSelectedField(patch: Partial<IFieldPlacement>) {
if (!this.selectedFieldId) return;
this.updateField(this.selectedFieldId, patch);
}
private updateSelectedFieldNumber(property: 'x' | 'y' | 'w' | 'h', event: Event) {
const value = Number((event.target as HTMLInputElement).value);
if (!Number.isFinite(value)) return;
const min = property === 'w' || property === 'h' ? 16 : 0;
this.updateSelectedField({ [property]: Math.max(min, Math.round(value)) } as Partial<IFieldPlacement>);
}
private resetSelectedFieldSize(field: IFieldPlacement) {
const definition = this.fieldDefinition(field.type);
this.updateSelectedField({ w: definition.w, h: definition.h });
}
private removeSelectedField() {
if (!this.selectedFieldId) return;
this.fields = this.fields.filter((field) => field.id !== this.selectedFieldId);
this.selectedFieldId = null;
}
private handleDocumentClick = (event: MouseEvent) => {
const target = event.target as HTMLElement | null;
if (target?.closest('.field-box')) return;
this.selectedFieldId = null;
};
private startFieldInteraction(event: PointerEvent, field: IFieldPlacement, mode: TFieldInteraction['mode'], handle?: TResizeHandle) {
if (event.button !== 0) return;
const page = this.shadowRoot?.querySelector('.document-page') as HTMLElement | null;
if (!page) return;
const pageRect = page.getBoundingClientRect();
this.selectedFieldId = field.id;
this.fieldInteraction = {
fieldId: field.id,
mode,
handle,
startClientX: event.clientX,
startClientY: event.clientY,
startField: { ...field },
pageWidth: pageRect.width,
pageHeight: pageRect.height,
};
event.preventDefault();
event.stopPropagation();
window.addEventListener('pointermove', this.handleFieldPointerMove, { passive: false });
window.addEventListener('pointerup', this.stopFieldInteraction);
window.addEventListener('pointercancel', this.stopFieldInteraction);
}
private startFieldMove(event: PointerEvent, field: IFieldPlacement) {
this.startFieldInteraction(event, field, 'move');
}
private startFieldResize(event: PointerEvent, field: IFieldPlacement, handle: TResizeHandle) {
this.startFieldInteraction(event, field, 'resize', handle);
}
private handleFieldPointerMove = (event: PointerEvent) => {
if (!this.fieldInteraction) return;
event.preventDefault();
const interaction = this.fieldInteraction;
const dx = event.clientX - interaction.startClientX;
const dy = event.clientY - interaction.startClientY;
const start = interaction.startField;
if (interaction.mode === 'move') {
this.updateField(interaction.fieldId, {
x: Math.round(this.clamp(start.x + dx, 0, interaction.pageWidth - start.w)),
y: Math.round(this.clamp(start.y + dy, 0, interaction.pageHeight - start.h)),
});
return;
}
const minWidth = 32;
const minHeight = 24;
let x = start.x;
let y = start.y;
let w = start.w;
let h = start.h;
const handle = interaction.handle || 'se';
if (handle.includes('e')) {
w = this.clamp(start.w + dx, minWidth, interaction.pageWidth - start.x);
}
if (handle.includes('s')) {
h = this.clamp(start.h + dy, minHeight, interaction.pageHeight - start.y);
}
if (handle.includes('w')) {
x = this.clamp(start.x + dx, 0, start.x + start.w - minWidth);
w = start.x + start.w - x;
}
if (handle.includes('n')) {
y = this.clamp(start.y + dy, 0, start.y + start.h - minHeight);
h = start.y + start.h - y;
}
this.updateField(interaction.fieldId, {
x: Math.round(x),
y: Math.round(y),
w: Math.round(w),
h: Math.round(h),
});
};
private stopFieldInteraction = () => {
this.fieldInteraction = null;
window.removeEventListener('pointermove', this.handleFieldPointerMove);
window.removeEventListener('pointerup', this.stopFieldInteraction);
window.removeEventListener('pointercancel', this.stopFieldInteraction);
};
private reorderRecipient(targetId: number) {
if (this.draggedRecipientId === null || this.draggedRecipientId === targetId) return;
const next = [...this.recipients];
const fromIndex = next.findIndex((recipient) => recipient.id === this.draggedRecipientId);
const toIndex = next.findIndex((recipient) => recipient.id === targetId);
if (fromIndex === -1 || toIndex === -1) return;
const [moved] = next.splice(fromIndex, 1);
next.splice(toIndex, 0, moved);
this.recipients = next.map((recipient, index) => ({ ...recipient, order: index + 1 }));
this.draggedRecipientId = null;
}
private addFieldFromDrop(event: DragEvent) {
event.preventDefault();
const page = event.currentTarget as HTMLElement;
page.classList.remove('drag-over');
const transferredType = event.dataTransfer?.getData('application/x-signature-field') as IFieldPlacement['type'];
if (!this.draggedFieldDefinition && !transferredType) return;
const definition = this.draggedFieldDefinition || this.fieldDefinition(transferredType);
const transferredOffset = event.dataTransfer?.getData('application/x-signature-field-offset');
const offset = this.draggedFieldGrabOffset || (transferredOffset ? JSON.parse(transferredOffset) as { x: number; y: number } : { x: definition.w / 2, y: definition.h / 2 });
const rect = page.getBoundingClientRect();
const x = Math.round(event.clientX - rect.left - offset.x);
const y = Math.round(event.clientY - rect.top - offset.y);
const nextField: IFieldPlacement = {
id: `field_${Date.now()}`,
type: definition.type,
x: Math.max(0, Math.min(Math.max(0, rect.width - definition.w), x)),
y: Math.max(0, Math.min(Math.max(0, rect.height - definition.h), y)),
w: definition.w,
h: definition.h,
page: 1,
recipient: this.activeRecipient,
label: definition.label,
};
this.fields = [...this.fields, nextField];
this.selectedFieldId = nextField.id;
this.draggedFieldDefinition = null;
this.draggedFieldGrabOffset = null;
}
private startFieldToolDrag(event: DragEvent, fieldType: TFieldDefinition) {
const toolRect = (event.currentTarget as HTMLElement).getBoundingClientRect();
const offset = {
x: Math.round(event.clientX - toolRect.left),
y: Math.round(event.clientY - toolRect.top),
};
this.draggedFieldDefinition = fieldType;
this.draggedFieldGrabOffset = offset;
event.dataTransfer?.setData('application/x-signature-field', fieldType.type);
event.dataTransfer?.setData('application/x-signature-field-offset', JSON.stringify(offset));
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'copy';
}
private endFieldToolDrag() {
this.draggedFieldDefinition = null;
this.draggedFieldGrabOffset = null;
}
private renderFieldEditor(field: IFieldPlacement): TemplateResult {
return html`
<div class="card field-editor">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
<div style="font-size: 11px; font-weight: 600;">Field editor</div>
${pill(this.fieldDefinition(field.type).label, 'info', true)}
</div>
<div class="field-editor-grid">
<label class="field-control full">Label<input .value=${field.label} @input=${(event: Event) => this.updateSelectedField({ label: (event.target as HTMLInputElement).value })} /></label>
<label class="field-control full">Assigned signer<select .value=${String(field.recipient)} @change=${(event: Event) => this.updateSelectedField({ recipient: Number((event.target as HTMLSelectElement).value) })}>${this.recipients.map((recipient) => html`<option value=${String(recipient.id)}>${recipient.order}. ${recipient.name}</option>`)}</select></label>
<label class="field-control">X<input type="number" min="0" .value=${String(field.x)} @input=${(event: Event) => this.updateSelectedFieldNumber('x', event)} /></label>
<label class="field-control">Y<input type="number" min="0" .value=${String(field.y)} @input=${(event: Event) => this.updateSelectedFieldNumber('y', event)} /></label>
<label class="field-control">Width<input type="number" min="16" .value=${String(field.w)} @input=${(event: Event) => this.updateSelectedFieldNumber('w', event)} /></label>
<label class="field-control">Height<input type="number" min="16" .value=${String(field.h)} @input=${(event: Event) => this.updateSelectedFieldNumber('h', event)} /></label>
</div>
<div style="display: flex; gap: 8px; margin-top: 12px;">
<button class="btn outline small" style="flex: 1;" @click=${() => this.resetSelectedFieldSize(field)}>Reset size</button>
<button class="btn ghost small" style="color: var(--error);" @click=${() => this.removeSelectedField()}>Delete</button>
</div>
</div>
`;
}
private renderResizeHandles(field: IFieldPlacement): TemplateResult {
return html`${resizeHandles.map((handle) => html`<span class="resize-handle ${handle}" @pointerdown=${(event: PointerEvent) => this.startFieldResize(event, field, handle)}></span>`)}`;
}
private renderStepper(): TemplateResult {
const labels = ['Upload', 'Place fields', 'Recipients & routing', 'Review & send'];
return html`
<div class="stepper">
${labels.map((label, index) => {
const stepNumber = index + 1;
return html`<button class="step ${stepNumber === this.step ? 'active' : stepNumber < this.step ? 'done' : ''}" @click=${() => this.step = stepNumber}><span class="step-number">${stepNumber < this.step ? '✓' : stepNumber}</span><span>${label}</span>${index < labels.length - 1 ? html`<span style="width: 24px; height: 1px; background: var(--border); margin-left: 8px;"></span>` : ''}</button>`;
})}
<div style="flex: 1;"></div><span class="mono" style="font-size: 11px; color: var(--text-muted);">doc_8mK3pL · 14 pages · 2.4 MB</span>
</div>
`;
}
public render(): TemplateResult {
const selectedField = this.fields.find((field) => field.id === this.selectedFieldId);
return html`
${topBar({ breadcrumb: ['signature.digital', 'Inbox', 'Compose'], title: 'Master Services Agreement', subtitle: pill('Draft · auto-saved'), actions: html`${actionButton('Save draft', 'ghost')}${actionButton('Preview', 'outline', 'eye')}${actionButton('Send for signature', 'primary', 'send')}` })}
${this.renderStepper()}
<div class="compose-workspace">
<div class="palette">
<div class="label-upper">Drag onto document</div>
${fieldDefinitions.map((fieldType) => html`<div class="field-tool" style="--tool-w: ${fieldType.w}px; --tool-h: ${fieldType.h}px; --recipient-color: ${this.recipientColor(this.activeRecipient)};" draggable="true" @dragstart=${(event: DragEvent) => this.startFieldToolDrag(event, fieldType)} @dragend=${() => this.endFieldToolDrag()}>${icon(fieldType.icon, 14)}<span style="flex: 1;">${fieldType.label}</span></div>`)}
<div style="height: 1px; background: var(--border-subtle); margin: 20px 0 16px;"></div>
<div class="label-upper">Active for</div>
${this.recipients.map((recipient) => html`<div class="recipient-line ${this.activeRecipient === recipient.id ? 'active' : ''}" @click=${() => this.activeRecipient = recipient.id}><span class="swatch" style="--recipient-color: ${recipient.color};"></span><span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${recipient.name.split(' ')[0]}</span><span class="mono" style="font-size: 10px; color: var(--text-muted);">${this.fields.filter((field) => field.recipient === recipient.id).length}</span></div>`)}
</div>
<div class="document-stage">
<div class="document-page page-drop-target" @click=${this.handleDocumentClick} @dragover=${(event: DragEvent) => { event.preventDefault(); if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'; (event.currentTarget as HTMLElement).classList.add('drag-over'); }} @dragleave=${(event: DragEvent) => (event.currentTarget as HTMLElement).classList.remove('drag-over')} @drop=${(event: DragEvent) => this.addFieldFromDrop(event)}>
${fakeDocument()}
${this.fields.map((field) => html`<div class="field-box ${this.selectedFieldId === field.id ? 'selected' : ''}" style="--x: ${field.x}px; --y: ${field.y}px; --w: ${field.w}px; --h: ${field.h}px; --field-color: ${this.recipientColor(field.recipient)};" @click=${() => this.selectedFieldId = field.id} @pointerdown=${(event: PointerEvent) => this.startFieldMove(event, field)}><div class="field-content">${icon(this.fieldIcon(field.type), 12)}<span>${field.label}</span></div>${this.selectedFieldId === field.id ? this.renderResizeHandles(field) : ''}</div>`)}
<div class="mono" style="position: absolute; bottom: 12px; right: 16px; font-size: 9px; color: hsl(0 0% 60%);">Page 1 of 14</div>
</div>
<div style="display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--text-muted);">${actionButton('Prev', 'outline')}${html`<span class="mono">1 / 14</span>`}${actionButton('Next', 'outline')}</div>
</div>
<div class="right-panel">
<div class="label-upper">Signing order · drag to reorder</div>
${this.recipients.map((recipient) => html`<div class="recipient-line ${this.draggedRecipientId === recipient.id ? 'dragging' : ''}" draggable="true" @dragstart=${() => this.draggedRecipientId = recipient.id} @dragover=${(event: DragEvent) => event.preventDefault()} @drop=${() => this.reorderRecipient(recipient.id)} @dragend=${() => this.draggedRecipientId = null}><span class="mono" style="width: 14px; font-size: 10px; color: var(--text-muted);">${recipient.order}</span><span class="avatar" style="background: ${recipient.color};">${recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join('')}</span><div style="flex: 1; min-width: 0;"><div style="font-size: 12px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${recipient.name}</div><div class="mono" style="font-size: 10px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${recipient.email}</div></div>${icon('more', 12)}</div>`)}
${selectedField ? this.renderFieldEditor(selectedField) : ''}
</div>
</div>
`;
}
}
@@ -0,0 +1,47 @@
import { DeesElement, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
import { actionButton, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js';
declare global {
interface HTMLElementTagNameMap {
'sdig-workspace-developers': SdigWorkspaceDevelopers;
}
}
@customElement('sdig-workspace-developers')
export class SdigWorkspaceDevelopers extends DeesElement {
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-developers></sdig-workspace-developers>`);
public static demoGroups = ['Signature Digital Workspace'];
public static styles = [workspaceBaseStyles, css`
.developer-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 20px; }
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.metric-card { padding: 14px; }
.metric-value { font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
pre.code { margin: 0; padding: 16px; background: var(--bg-el); border: 1px solid var(--border-subtle); border-radius: 6px; font-size: 12px; line-height: 1.7; color: var(--text-sec); overflow: auto; }
@media (max-width: 920px) { .developer-grid, .stats-grid { grid-template-columns: 1fr; } }
`];
public render(): TemplateResult {
return html`
${topBar({ breadcrumb: ['signature.digital', 'Developers'], title: 'Developers', subtitle: pill('API · v0.42', 'info', true), actions: html`${actionButton('View on GitHub', 'outline', 'github')}${actionButton('New API key', 'primary', 'plus')}` })}
<div class="content-scroll developer-grid">
<div style="display: flex; flex-direction: column; gap: 16px;">
<div class="card" style="padding: 20px;"><div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px;"><span style="font-size: 14px; font-weight: 600;">Send a document in 8 lines</span><span class="pill">node</span></div><pre class="code mono">import { Signature } from '@signature.digital/sdk';
const sig = new Signature(process.env.SIGD_KEY);
await sig.documents.send({
file: './msa.pdf',
recipients: [{ name: 'Sarah Chen', email: 'sarah@acme.com' }],
fields: 'auto',
});</pre></div>
<div class="stats-grid">${[['Requests this month', '14,892', '+8.2%'], ['P95 latency', '142ms', '-12ms'], ['Error rate', '0.04%', '✓']].map((metric) => html`<div class="card metric-card"><div class="metric-value">${metric[1]}</div><div style="font-size: 10px; color: var(--text-muted); margin-top: 2px;">${metric[0]}</div><div class="mono" style="font-size: 10px; color: var(--success); margin-top: 6px;">${metric[2]}</div></div>`)}</div>
</div>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div class="card" style="padding: 16px;"><div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">${icon('github', 14)}<span style="font-size: 12px; font-weight: 600;">signature-digital/core</span></div><div style="font-size: 11px; color: var(--text-muted); line-height: 1.5;">MIT-licensed. Self-host on your own infra, or use signature.digital cloud.</div></div>
<div class="card" style="padding: 16px;"><div class="label-upper">Self-host status</div>${[['Docker image', 'ghcr.io/signature-digital'], ['Helm chart', 'v0.42.1'], ['Postgres ≥ 14', 'required'], ['S3-compatible', 'optional']].map((row) => html`<div style="display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 6px;"><span style="color: var(--text-muted);">${row[0]}</span><span class="mono" style="color: var(--text-sec);">${row[1]}</span></div>`)}</div>
</div>
</div>
`;
}
}
@@ -0,0 +1,117 @@
import { DeesElement, property, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
import { actionButton, demoDocuments, icon, pill, requestWorkspaceView, topBar, workspaceBaseStyles, type IDocumentRow, type TDensity } from './sdig-workspace.shared.js';
import { workspaceDemoFrame } from './sdig-workspace.shared.js';
declare global {
interface HTMLElementTagNameMap {
'sdig-workspace-inbox': SdigWorkspaceInbox;
}
}
@customElement('sdig-workspace-inbox')
export class SdigWorkspaceInbox extends DeesElement {
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-inbox></sdig-workspace-inbox>`);
public static demoGroups = ['Signature Digital Workspace'];
@property({ type: String }) public accessor density: TDensity = 'comfortable';
@state() private accessor filter: string = 'all';
@state() private accessor search: string = '';
public static styles = [workspaceBaseStyles, css`
.filterbar { padding: 14px 24px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 8px; }
.searchbox { display: flex; align-items: center; gap: 8px; padding: 0 10px; height: 32px; width: 280px; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; }
.searchbox input { flex: 1; min-width: 0; background: transparent; border: none; outline: none; color: var(--text); font-size: 12px; }
.segmented { display: flex; gap: 2px; padding: 2px; background: var(--bg-el); border-radius: 6px; border: 1px solid var(--border-subtle); }
.segmented button { padding: 4px 10px; font-size: 11px; font-weight: 500; border-radius: 4px; background: transparent; color: var(--text-muted); display: inline-flex; align-items: center; gap: 5px; }
.segmented button.active { background: var(--bg-card); color: var(--text); box-shadow: inset 0 0 0 1px var(--border); }
.doc-table { min-width: 880px; }
.doc-head, .doc-row { display: grid; grid-template-columns: 32px minmax(220px,2.4fr) 150px 160px 90px 60px 32px; align-items: center; gap: 14px; padding: 0 16px; }
.doc-head { height: 36px; border-bottom: 1px solid var(--border-subtle); color: var(--text-dim); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
.doc-row { height: 60px; border-bottom: 1px solid var(--border-subtle); cursor: pointer; transition: background 0.1s ease; }
.doc-row.compact { height: 48px; }
.doc-row:last-child { border-bottom: 0; }
.doc-row:hover { background: var(--row-hover); }
.doc-icon { width: 28px; height: 32px; border-radius: 4px; background: var(--bg-input); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; }
.doc-title { font-size: 13px; color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.doc-meta { margin-top: 2px; font-size: 11px; color: var(--text-muted); }
.recipient-stack { display: flex; align-items: center; }
.recipient-dot { width: 22px; height: 22px; border-radius: 50%; background: var(--bg-input); border: 1.5px solid var(--border); margin-left: -6px; font-size: 9px; font-weight: 600; color: var(--text-sec); display: flex; align-items: center; justify-content: center; }
.recipient-dot:first-child { margin-left: 0; }
.recipient-dot.signed { border-color: var(--success); color: var(--success); background: var(--bg-el); }
.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-top: 24px; }
.metric-card { padding: 16px; }
.metric-value { font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
@media (max-width: 920px) { .filterbar { padding: 12px 16px; display: block; } .searchbox { width: 100%; margin-bottom: 10px; } .stats-grid { grid-template-columns: 1fr; } }
`];
private get filteredDocuments(): IDocumentRow[] {
return demoDocuments
.filter((doc) => this.filter === 'all' || doc.status === this.filter)
.filter((doc) => !this.search || doc.title.toLowerCase().includes(this.search.toLowerCase()));
}
private statusPill(status: IDocumentRow['status']): TemplateResult {
const map = {
awaiting: ['warning', 'awaiting signature'],
signed: ['success', 'completed'],
draft: ['default', 'draft'],
declined: ['error', 'declined'],
} as const;
const [tone, label] = map[status];
return pill(label, tone, true);
}
private openDocument(doc: IDocumentRow) {
requestWorkspaceView(this, doc.status === 'signed' ? 'audit' : 'sign');
}
public render(): TemplateResult {
const filters = [
{ id: 'all', label: 'All', count: demoDocuments.length },
{ id: 'awaiting', label: 'Awaiting', count: demoDocuments.filter((doc) => doc.status === 'awaiting').length },
{ id: 'signed', label: 'Completed', count: demoDocuments.filter((doc) => doc.status === 'signed').length },
{ id: 'draft', label: 'Drafts', count: demoDocuments.filter((doc) => doc.status === 'draft').length },
{ id: 'declined', label: 'Declined', count: demoDocuments.filter((doc) => doc.status === 'declined').length },
];
return html`
${topBar({
breadcrumb: ['signature.digital', 'Lossless GmbH', 'Inbox'],
title: 'Inbox',
subtitle: pill(`${demoDocuments.filter((doc) => doc.status === 'awaiting').length} need attention`, 'info'),
actions: html`${actionButton('Import', 'outline', 'upload')}${actionButton('New document', 'primary', 'plus', () => requestWorkspaceView(this, 'compose'))}`,
})}
<div class="filterbar">
<div class="searchbox">${icon('search', 13)}<input .value=${this.search} @input=${(event: Event) => this.search = (event.target as HTMLInputElement).value} placeholder="Search documents, recipients, IDs..." /><span class="mono" style="font-size: 10px; color: var(--text-dim); border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px;">⌘K</span></div>
<div style="flex: 1;"></div>
<div class="segmented">${filters.map((filter) => html`<button class=${this.filter === filter.id ? 'active' : ''} @click=${() => this.filter = filter.id}>${filter.label}<span class="mono" style="color: var(--text-dim);">${filter.count}</span></button>`)}</div>
</div>
<div class="content-scroll">
<div class="card" style="overflow-x: auto;">
<div class="doc-table">
<div class="doc-head"><span></span><span>Document</span><span>Status</span><span>Recipients</span><span>Deadline</span><span style="text-align: right;">Pages</span><span></span></div>
${this.filteredDocuments.map((doc) => html`
<div class="doc-row ${this.density === 'compact' ? 'compact' : ''}" @click=${() => this.openDocument(doc)}>
<div class="doc-icon">${icon('file', 14)}</div>
<div style="min-width: 0;"><div class="doc-title">${doc.title}</div><div class="doc-meta mono">${doc.id} · ${doc.sender} · ${doc.updated}</div></div>
<div>${this.statusPill(doc.status)}</div>
<div style="display: flex; align-items: center; gap: 8px;"><div class="recipient-stack">${doc.recipients.slice(0, 4).map((recipient) => html`<span class="recipient-dot ${recipient.signed ? 'signed' : ''}" title=${recipient.name}>${recipient.initials}</span>`)}</div><span style="font-size: 11px; color: var(--text-muted);">${doc.recipients.filter((recipient) => recipient.signed).length}/${doc.recipients.length}</span></div>
<div class="mono" style="font-size: 11px; color: ${doc.deadline && doc.status === 'awaiting' ? 'var(--warning)' : 'var(--text-dim)'};">${doc.deadline ? html`${icon('clock', 11)} ${doc.deadline}` : '—'}</div>
<div class="mono" style="font-size: 11px; color: var(--text-muted); text-align: right;">${doc.pages}</div>
<div>${icon('more', 14)}</div>
</div>
`)}
</div>
</div>
<div class="stats-grid">
${[
{ label: 'Sent this month', value: '127', delta: '+24%', icon: 'send' },
{ label: 'Avg time to sign', value: '4.2h', delta: '-18%', icon: 'clock' },
{ label: 'Completion rate', value: '94.1%', delta: '+2.1%', icon: 'check' },
{ label: 'API signatures', value: '2,481', delta: '+312', icon: 'code' },
].map((metric) => html`<div class="card metric-card"><div style="display: flex; justify-content: space-between; margin-bottom: 12px;">${icon(metric.icon, 14)}<span class="mono" style="font-size: 10px; color: var(--success);">${metric.delta}</span></div><div class="metric-value">${metric.value}</div><div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">${metric.label}</div></div>`)}
</div>
</div>
`;
}
}
@@ -0,0 +1,22 @@
import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element';
import { icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js';
declare global {
interface HTMLElementTagNameMap {
'sdig-workspace-placeholder': SdigWorkspacePlaceholder;
}
}
@customElement('sdig-workspace-placeholder')
export class SdigWorkspacePlaceholder extends DeesElement {
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-placeholder label="Templates" subtitle="Reusable agreement templates"></sdig-workspace-placeholder>`);
public static demoGroups = ['Signature Digital Workspace'];
@property({ type: String }) public accessor label: string = 'Section';
@property({ type: String }) public accessor subtitle: string = 'Coming soon';
public static styles = [workspaceBaseStyles];
public render(): TemplateResult {
return html`${topBar({ breadcrumb: ['signature.digital', this.label], title: this.label, subtitle: pill('coming soon') })}<div class="content-scroll" style="display: flex; align-items: center; justify-content: center; flex-direction: column; color: var(--text-muted); gap: 8px;">${icon('folder', 32)}<div style="font-size: 13px; color: var(--text-sec);">${this.label}</div><div style="font-size: 11px;">${this.subtitle}</div></div>`;
}
}
@@ -0,0 +1,91 @@
import { DeesElement, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
import { actionButton, demoFields, fakeDocument, icon, pill, workspaceBaseStyles, workspaceDemoFrame, type IFieldPlacement } from './sdig-workspace.shared.js';
declare global {
interface HTMLElementTagNameMap {
'sdig-workspace-sign': SdigWorkspaceSign;
}
}
@customElement('sdig-workspace-sign')
export class SdigWorkspaceSign extends DeesElement {
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-sign></sdig-workspace-sign>`);
public static demoGroups = ['Signature Digital Workspace'];
@state() private accessor activeFieldId: string = 'f1';
@state() private accessor signedFieldIds: string[] = [];
public static styles = [workspaceBaseStyles, css`
.recipient-header { height: 56px; flex-shrink: 0; padding: 0 24px; background: var(--bg-card); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
.logomark { width: 28px; height: 28px; border-radius: 6px; background: var(--bg-el); border: 1px solid var(--border-strong); display: inline-flex; align-items: center; justify-content: center; font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-weight: 700; position: relative; }
.logomark::after { content: ''; position: absolute; right: 5px; bottom: 5px; width: 4px; height: 4px; border-radius: 50%; background: var(--accent); }
.sign-layout { flex: 1; display: flex; overflow: hidden; background: hsl(0 0% 96%); color: hsl(0 0% 10%); }
:host-context(sdig-workspace[theme='dark']) .sign-layout { background: hsl(0 0% 6%); color: hsl(0 0% 95%); }
.sign-body { flex: 1; overflow: auto; padding: 32px 32px 80px; display: flex; flex-direction: column; align-items: center; gap: 20px; }
.sign-panel { width: 320px; border-left: 1px solid var(--border); background: var(--bg-card); padding: 20px; overflow: auto; flex-shrink: 0; }
@media (max-width: 920px) { .recipient-header .actions { display: none; } .sign-layout { flex-direction: column; overflow: auto; } .sign-panel { width: 100%; border-left: 0; border-top: 1px solid var(--border); } .document-page { width: 560px; } }
`];
private get signFields() {
return demoFields.slice(0, 3);
}
private fieldIcon(type: IFieldPlacement['type']): string {
if (type === 'signature') return 'sign';
if (type === 'date') return 'calendar';
return 'type';
}
private signField(fieldId: string) {
if (!this.signedFieldIds.includes(fieldId)) {
this.signedFieldIds = [...this.signedFieldIds, fieldId];
}
const next = this.signFields.find((field) => !this.signedFieldIds.includes(field.id) && field.id !== fieldId);
if (next) this.activeFieldId = next.id;
}
private renderSignedValue(field: IFieldPlacement): TemplateResult {
if (field.type === 'signature') return html`<span style="font-family: 'Plus Jakarta Sans', cursive; font-size: 22px; font-weight: 600; font-style: italic; color: hsl(220 50% 30%);">Sarah Chen</span>`;
if (field.type === 'date') return html`<span class="mono" style="font-size: 12px; color: hsl(0 0% 18%);">2026-05-02</span>`;
return html`<span style="font-size: 12px; color: hsl(0 0% 18%);">Sarah Chen</span>`;
}
public render(): TemplateResult {
const completed = this.signedFieldIds.length;
const progress = Math.round((completed / this.signFields.length) * 100);
const activeField = this.signFields.find((field) => field.id === this.activeFieldId) || this.signFields[0];
return html`
<div class="recipient-header">
<div style="display: flex; align-items: center; gap: 12px;"><span class="logomark">s</span><div><div style="font-size: 12px; font-weight: 600;">Master Services Agreement</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">From Lossless GmbH · doc_8mK3pL · 14 pages</div></div></div>
<div class="actions"><span class="pill success">${icon('shield', 12)} Verified sender · DKIM ✓</span>${actionButton('Decline', 'outline')}${actionButton('PDF', 'outline', 'download')}</div>
</div>
<div class="progress-track"><div class="progress-fill" style="width: ${progress}%"></div></div>
<div class="sign-layout">
<div class="sign-body">
<div class="document-page" style="width: 620px; min-height: 820px;">
${fakeDocument()}
${this.signFields.map((field) => {
const filled = this.signedFieldIds.includes(field.id);
const active = this.activeFieldId === field.id && !filled;
return html`<div class="field-box ${active ? 'selected' : ''}" style="--x: ${field.x}px; --y: ${field.y}px; --w: ${field.w}px; --h: ${field.h}px; --field-color: ${filled ? 'transparent' : 'var(--accent)'}; color: ${filled ? 'hsl(220 50% 30%)' : 'var(--accent)'}; background: ${filled ? 'transparent' : 'color-mix(in srgb, var(--accent) 12%, transparent)'};" @click=${() => !filled ? this.signField(field.id) : undefined}>${filled ? this.renderSignedValue(field) : html`${icon(this.fieldIcon(field.type), 12)}<span>${active ? html`<span style="display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); animation: pulse 1.4s infinite;"></span>` : ''}${field.label}</span>`}</div>`;
})}
<div class="mono" style="position: absolute; bottom: 14px; right: 18px; font-size: 9px; color: hsl(0 0% 60%);">Page 1 of 14</div>
</div>
</div>
<div class="sign-panel">
<div style="display: flex; align-items: center; gap: 8px;"><span class="avatar" style="background: #60a5fa;">SC</span><div><div style="font-size: 13px; font-weight: 600;">Hi, Sarah</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">sarah@acme.com</div></div></div>
<div class="card" style="padding: 14px; margin-top: 16px;"><div class="label-upper">Your progress</div><div style="font-size: 24px; font-weight: 700;">${completed} <span style="color: var(--text-muted); font-weight: 400;">/ ${this.signFields.length}</span></div><div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">${this.signFields.length - completed === 0 ? 'All fields complete' : `${this.signFields.length - completed} fields remaining`}</div><div class="progress-track" style="margin-top: 12px;"><div class="progress-fill" style="width: ${progress}%"></div></div></div>
<div style="margin-top: 20px;" class="label-upper">Step by step</div>
${this.signFields.map((field, index) => {
const filled = this.signedFieldIds.includes(field.id);
const active = this.activeFieldId === field.id && !filled;
return html`<div class="recipient-line ${active ? 'active' : ''}" @click=${() => !filled ? this.activeFieldId = field.id : undefined}><span class="avatar" style="width: 22px; height: 22px; background: ${filled ? 'var(--success)' : active ? 'var(--accent)' : 'var(--bg-input)'}; color: ${filled || active ? 'white' : 'var(--text-muted)'};">${filled ? '✓' : index + 1}</span><div style="flex: 1;"><div style="font-size: 12px; font-weight: 500; text-decoration: ${filled ? 'line-through' : 'none'};">${field.label}</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">${field.type} · page ${field.page}</div></div>${active ? icon('chevronRight', 12) : ''}</div>`;
})}
<button class="btn primary" style="width: 100%; height: 44px; margin-top: 20px;" @click=${() => this.signField(activeField.id)}>${completed === this.signFields.length ? 'Finish & submit' : `Continue - ${activeField.label}`}</button>
<div style="margin-top: 12px; padding: 10px; font-size: 10px; color: var(--text-muted); line-height: 1.5; text-align: center; border-radius: 6px; background: var(--bg-el);">By signing, you agree to the ESIGN Act & eIDAS terms.<br /><span class="mono">IP 81.221.4.18 · Brussels, BE</span></div>
</div>
</div>
`;
}
}
@@ -0,0 +1,440 @@
import { html, css, type TemplateResult } from '@design.estate/dees-element';
import '@design.estate/dees-catalog/ts_web/elements/00group-utility/dees-icon/dees-icon.js';
export type TWorkspaceView =
| 'inbox'
| 'compose'
| 'sign'
| 'audit'
| 'developers'
| 'templates'
| 'team'
| 'settings';
export type TWorkspaceTheme = 'dark' | 'light';
export type TDensity = 'compact' | 'comfortable';
export interface IDocumentRow {
id: string;
title: string;
status: 'awaiting' | 'signed' | 'draft' | 'declined';
recipients: Array<{ name: string; initials: string; signed: boolean }>;
updated: string;
sender: string;
pages: number;
deadline?: string;
}
export interface IRecipient {
id: number;
name: string;
email: string;
color: string;
order: number;
}
export interface IFieldPlacement {
id: string;
type: 'signature' | 'date' | 'text' | 'initials' | 'check';
x: number;
y: number;
w: number;
h: number;
page: number;
recipient: number;
label: string;
}
export const demoDocuments: IDocumentRow[] = [
{ id: 'doc_8mK3pL', title: 'Master Services Agreement - Acme Corp', status: 'awaiting', recipients: [{ name: 'Sarah Chen', initials: 'SC', signed: true }, { name: 'David Park', initials: 'DP', signed: false }, { name: 'You', initials: 'PK', signed: true }], updated: '2 min ago', sender: 'You', pages: 14, deadline: 'May 5' },
{ id: 'doc_2nQ7vR', title: 'NDA - Helio Robotics', status: 'signed', recipients: [{ name: 'Marcus Tan', initials: 'MT', signed: true }, { name: 'You', initials: 'PK', signed: true }], updated: '1h ago', sender: 'You', pages: 3 },
{ id: 'doc_5tH1zM', title: 'Series B Term Sheet (Lead) v3', status: 'awaiting', recipients: [{ name: 'Anna Lindqvist', initials: 'AL', signed: false }, { name: 'Roy Banerjee', initials: 'RB', signed: true }, { name: 'You', initials: 'PK', signed: false }], updated: '3h ago', sender: 'Sequoia Counsel', pages: 22, deadline: 'May 3' },
{ id: 'doc_9wB4cX', title: 'Employment Offer - Mira Abebe', status: 'declined', recipients: [{ name: 'Mira Abebe', initials: 'MA', signed: false }, { name: 'You', initials: 'PK', signed: true }], updated: 'yesterday', sender: 'You', pages: 6 },
{ id: 'doc_1jF6kY', title: 'Lease - Berlin office Q3', status: 'draft', recipients: [{ name: 'You', initials: 'PK', signed: false }], updated: 'yesterday', sender: 'You', pages: 11 },
{ id: 'doc_4dN8sP', title: 'API Reseller Agreement - Northwind', status: 'signed', recipients: [{ name: 'Lila Brooks', initials: 'LB', signed: true }, { name: 'You', initials: 'PK', signed: true }], updated: '2 days ago', sender: 'You', pages: 8 },
];
export const demoRecipients: IRecipient[] = [
{ id: 0, name: 'Sarah Chen', email: 'sarah@acme.com', color: '#60a5fa', order: 1 },
{ id: 1, name: 'David Park', email: 'd.park@acme.com', color: '#fbbf24', order: 2 },
{ id: 2, name: 'Philipp K.', email: 'philipp@lossless.com', color: '#3b82f6', order: 3 },
];
export const demoFields: IFieldPlacement[] = [
{ id: 'f1', type: 'signature', x: 60, y: 580, w: 200, h: 50, page: 1, recipient: 0, label: 'Signature' },
{ id: 'f2', type: 'date', x: 320, y: 580, w: 120, h: 30, page: 1, recipient: 0, label: 'Date' },
{ id: 'f3', type: 'text', x: 60, y: 460, w: 280, h: 30, page: 1, recipient: 1, label: 'Full legal name' },
{ id: 'f4', type: 'signature', x: 60, y: 700, w: 200, h: 50, page: 1, recipient: 1, label: 'Counter-signature' },
];
export const workspaceBaseStyles = css`
:host {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
color: var(--text);
background: var(--bg);
font-family: Geist, Inter, Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
font-feature-settings: 'cv11', 'tnum', 'cv05' 1;
}
* { box-sizing: border-box; }
button, input, textarea { font: inherit; }
button { border: 0; cursor: pointer; }
dees-icon { flex-shrink: 0; }
.mono {
font-family: 'Intel One Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
font-variant-numeric: tabular-nums;
}
.topbar {
height: 56px;
flex-shrink: 0;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-subtle);
background: var(--bg);
gap: 12px;
}
.breadcrumb {
font-size: 11px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.top-title {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.top-title > span:first-child {
font-family: 'Plus Jakarta Sans', Inter, sans-serif;
font-size: 18px;
font-weight: 600;
letter-spacing: -0.02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.btn {
height: 34px;
padding: 0 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
letter-spacing: -0.01em;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
white-space: nowrap;
transition: all 0.12s ease;
}
.btn.small {
height: 28px;
padding: 0 10px;
font-size: 12px;
}
.btn.primary {
background: var(--accent);
color: white;
border: 1px solid var(--accent);
}
.btn.outline {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.btn.ghost {
background: transparent;
color: var(--text);
border: 1px solid transparent;
}
.btn:hover { background-color: var(--hover); }
.pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 8px;
border-radius: 999px;
background: var(--bg-el);
color: var(--text-sec);
font-size: 11px;
font-weight: 500;
white-space: nowrap;
}
.pill::before {
content: '';
width: 5px;
height: 5px;
display: none;
border-radius: 50%;
background: currentColor;
}
.pill.dot::before { display: block; }
.pill.success { background: rgba(34,197,94,0.12); color: #4ade80; }
.pill.warning { background: rgba(245,158,11,0.12); color: #fbbf24; }
.pill.error { background: rgba(239,68,68,0.12); color: #f87171; }
.pill.info { background: rgba(59,130,246,0.12); color: #60a5fa; }
.content-scroll {
flex: 1;
overflow: auto;
padding: 24px;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.label-upper {
font-size: 10px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 10px;
}
.avatar {
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--accent);
color: white;
font-size: 11px;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.document-page {
position: relative;
width: 600px;
min-height: 800px;
background: white;
border-radius: 4px;
box-shadow: 0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.05);
color: hsl(0 0% 20%);
}
.fake-document {
padding: 48px 56px;
font-size: 11px;
line-height: 1.7;
}
.fake-title {
font-family: 'Plus Jakarta Sans', Inter, sans-serif;
font-size: 18px;
font-weight: 700;
margin-bottom: 4px;
color: hsl(0 0% 10%);
}
.fake-line {
height: 6px;
background: hsl(0 0% 82%);
margin-bottom: 7px;
border-radius: 1px;
}
.fake-line.heavy { background: hsl(0 0% 65%); }
.fake-line.short { width: 70%; }
.field-box {
position: absolute;
left: var(--x);
top: var(--y);
width: var(--w);
height: var(--h);
background: color-mix(in srgb, var(--field-color) 13%, transparent);
border: 1.5px dashed var(--field-color);
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
padding: 0 8px;
font-size: 10px;
font-weight: 500;
color: var(--field-color);
}
.field-box.selected {
border-style: solid;
box-shadow: 0 0 0 4px color-mix(in srgb, var(--field-color) 18%, transparent);
}
.recipient-line {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 12px;
color: var(--text-sec);
margin-bottom: 6px;
}
.recipient-line.active {
background: var(--hover);
border-color: var(--border-strong);
}
.progress-track {
height: 4px;
background: var(--bg-el);
flex-shrink: 0;
}
.progress-fill {
height: 100%;
background: var(--accent);
transition: width 0.4s ease;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (max-width: 920px) {
.topbar { padding: 0 16px; }
.actions { display: none; }
.content-scroll { padding: 16px; }
}
`;
export function icon(name: string, size = 14): TemplateResult {
const iconMap: Record<string, string> = {
inbox: 'lucide:Inbox', plus: 'lucide:Plus', folder: 'lucide:Folder', shield: 'lucide:Shield', code: 'lucide:Code2',
user: 'lucide:User', settings: 'lucide:Settings', upload: 'lucide:Upload', file: 'lucide:FileText', sign: 'lucide:PenTool',
clock: 'lucide:Clock', search: 'lucide:Search', more: 'lucide:MoreHorizontal', send: 'lucide:Send', check: 'lucide:Check',
eye: 'lucide:Eye', calendar: 'lucide:Calendar', type: 'lucide:Type', download: 'lucide:Download', hash: 'lucide:Hash',
github: 'lucide:GitBranch', git: 'lucide:GitBranch', server: 'lucide:Server', star: 'lucide:Star', sparkle: 'lucide:Sparkles',
chevronRight: 'lucide:ChevronRight', chevronDown: 'lucide:ChevronDown', x: 'lucide:X', activity: 'lucide:Activity',
};
return html`<dees-icon .icon=${iconMap[name] || iconMap.file} style="font-size: ${size}px;"></dees-icon>`;
}
export function pill(label: string, tone: 'default' | 'success' | 'warning' | 'error' | 'info' = 'default', dot = false): TemplateResult {
return html`<span class="pill ${tone} ${dot ? 'dot' : ''}">${label}</span>`;
}
export function actionButton(label: string, variant: 'primary' | 'outline' | 'ghost' = 'outline', iconName?: string, onClick?: () => void): TemplateResult {
return html`<button class="btn ${variant}" @click=${onClick || (() => undefined)}>${iconName ? icon(iconName, 13) : ''}${label}</button>`;
}
export function topBar(config: { breadcrumb: string[]; title: string; subtitle?: TemplateResult; actions?: TemplateResult }): TemplateResult {
return html`
<div class="topbar">
<div style="min-width: 0; flex: 1;">
<div class="breadcrumb">
${config.breadcrumb.map((part, index) => html`${index > 0 ? icon('chevronRight', 10) : ''}<span>${part}</span>`)}
</div>
<div class="top-title"><span>${config.title}</span>${config.subtitle || ''}</div>
</div>
<div class="actions">${config.actions || ''}</div>
</div>
`;
}
export function workspaceDemoFrame(content: TemplateResult, theme: TWorkspaceTheme = 'dark'): TemplateResult {
const darkVars = `
--accent: #3b82f6;
--bg: hsl(0 0% 3.9%);
--bg-el: hsl(0 0% 6%);
--bg-card: hsl(0 0% 7%);
--bg-input: hsl(0 0% 9%);
--border: hsl(0 0% 14.9%);
--border-subtle: hsl(0 0% 11%);
--border-strong: hsl(0 0% 20%);
--text: hsl(0 0% 98%);
--text-sec: hsl(0 0% 63.9%);
--text-muted: hsl(0 0% 48%);
--text-dim: hsl(0 0% 32%);
--hover: rgba(255,255,255,0.06);
--hover-subtle: rgba(255,255,255,0.03);
--row-hover: rgba(255,255,255,0.025);
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
`;
const lightVars = `
--accent: #3b82f6;
--bg: hsl(0 0% 99%);
--bg-el: hsl(0 0% 97%);
--bg-card: hsl(0 0% 100%);
--bg-input: hsl(0 0% 98%);
--border: hsl(0 0% 90%);
--border-subtle: hsl(0 0% 93%);
--border-strong: hsl(0 0% 80%);
--text: hsl(0 0% 9%);
--text-sec: hsl(0 0% 32%);
--text-muted: hsl(0 0% 45%);
--text-dim: hsl(0 0% 62%);
--hover: rgba(0,0,0,0.04);
--hover-subtle: rgba(0,0,0,0.02);
--row-hover: rgba(0,0,0,0.02);
--success: #16a34a;
--warning: #d97706;
--error: #dc2626;
`;
return html`<div style="${theme === 'dark' ? darkVars : lightVars} height: 720px; min-height: 720px; background: var(--bg); color: var(--text); overflow: hidden; font-family: Geist, Inter, Roboto, -apple-system, BlinkMacSystemFont, sans-serif;">${content}</div>`;
}
export function fakeDocument(): TemplateResult {
return html`
<div class="fake-document">
<div class="fake-title">Master Services Agreement</div>
<div class="mono" style="font-size: 10px; color: hsl(0 0% 45%); margin-bottom: 24px;">Effective: May 2, 2026 · Acme Corp ↔ Lossless GmbH</div>
${Array.from({ length: 18 }).map((_, index) => html`<div class="fake-line ${index % 5 === 0 ? 'heavy' : ''} ${index % 4 === 3 ? 'short' : ''}"></div>`)}
<div style="height: 16px;"></div>
${Array.from({ length: 8 }).map((_, index) => html`<div class="fake-line ${index % 3 === 2 ? 'short' : ''}"></div>`)}
<div style="margin-top: 60px; font-size: 10px; font-weight: 600; color: hsl(0 0% 35%);">SIGNED ON BEHALF OF ACME CORP</div>
<div style="margin-top: 70px; font-size: 10px; font-weight: 600; color: hsl(0 0% 35%);">SIGNED ON BEHALF OF LOSSLESS GMBH</div>
</div>
`;
}
export function requestWorkspaceView(element: HTMLElement, view: TWorkspaceView) {
element.dispatchEvent(new CustomEvent('workspace-view-request', {
detail: { view },
bubbles: true,
composed: true,
}));
}
@@ -0,0 +1,175 @@
import { DeesElement, property, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
import { icon, type TDensity, type TWorkspaceTheme, type TWorkspaceView } from './sdig-workspace.shared.js';
import './sdig-workspace-inbox.js';
import './sdig-workspace-compose.js';
import './sdig-workspace-sign.js';
import './sdig-workspace-audit.js';
import './sdig-workspace-developers.js';
import './sdig-workspace-placeholder.js';
declare global {
interface HTMLElementTagNameMap {
'sdig-workspace': SdigWorkspace;
}
}
@customElement('sdig-workspace')
export class SdigWorkspace extends DeesElement {
public static demo = () => html`<sdig-workspace></sdig-workspace>`;
public static demoGroups = ['Signature Digital Workspace'];
@property({ type: String }) public accessor accent: string = '#3b82f6';
@property({ type: String }) public accessor density: TDensity = 'comfortable';
@property({ type: String, reflect: true }) public accessor theme: TWorkspaceTheme = 'dark';
@property({ type: String }) public accessor initialView: TWorkspaceView = 'inbox';
@state() private accessor view: TWorkspaceView = 'inbox';
public connectedCallback = async () => {
await super.connectedCallback();
this.view = this.initialView || 'inbox';
this.addEventListener('workspace-view-request', this.handleViewRequest as EventListener);
};
public disconnectedCallback = async () => {
this.removeEventListener('workspace-view-request', this.handleViewRequest as EventListener);
await super.disconnectedCallback();
};
public static styles = css`
:host {
display: block;
width: 100%;
height: 100%;
min-height: 720px;
--accent: #3b82f6;
--bg: hsl(0 0% 3.9%);
--bg-el: hsl(0 0% 6%);
--bg-card: hsl(0 0% 7%);
--bg-input: hsl(0 0% 9%);
--border: hsl(0 0% 14.9%);
--border-subtle: hsl(0 0% 11%);
--border-strong: hsl(0 0% 20%);
--text: hsl(0 0% 98%);
--text-sec: hsl(0 0% 63.9%);
--text-muted: hsl(0 0% 48%);
--text-dim: hsl(0 0% 32%);
--hover: rgba(255,255,255,0.06);
--hover-subtle: rgba(255,255,255,0.03);
--row-hover: rgba(255,255,255,0.025);
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
font-family: Geist, Inter, Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
}
:host([theme='light']) {
--bg: hsl(0 0% 99%);
--bg-el: hsl(0 0% 97%);
--bg-card: hsl(0 0% 100%);
--bg-input: hsl(0 0% 98%);
--border: hsl(0 0% 90%);
--border-subtle: hsl(0 0% 93%);
--border-strong: hsl(0 0% 80%);
--text: hsl(0 0% 9%);
--text-sec: hsl(0 0% 32%);
--text-muted: hsl(0 0% 45%);
--text-dim: hsl(0 0% 62%);
--hover: rgba(0,0,0,0.04);
--hover-subtle: rgba(0,0,0,0.02);
--row-hover: rgba(0,0,0,0.02);
--success: #16a34a;
--warning: #d97706;
--error: #dc2626;
}
* { box-sizing: border-box; }
button { font: inherit; border: 0; cursor: pointer; }
.workspace { display: flex; height: 100%; min-height: 720px; background: var(--bg); color: var(--text); overflow: hidden; }
.sidebar { width: 220px; background: var(--bg); border-right: 1px solid var(--border-subtle); display: flex; flex-direction: column; flex-shrink: 0; height: 100%; }
.brand { padding: 14px 16px 12px; display: flex; align-items: center; gap: 8px; }
.logomark { width: 26px; height: 26px; border-radius: 6px; background: var(--bg-el); border: 1px solid var(--border-strong); display: inline-flex; align-items: center; justify-content: center; font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-size: 13px; font-weight: 700; position: relative; }
.logomark::after, .wordmark::after { content: ''; display: inline-block; border-radius: 50%; background: var(--accent); }
.logomark::after { width: 4px; height: 4px; position: absolute; right: 5px; bottom: 5px; }
.wordmark { font-size: 13px; font-weight: 500; letter-spacing: -0.02em; white-space: nowrap; }
.wordmark .dot { color: var(--text-muted); }
.wordmark::after { width: 4px; height: 4px; margin-left: 3px; transform: translateY(-1px); }
.workspace-card { margin: 0 12px 8px; padding: 7px 10px; background: var(--bg-el); border: 1px solid var(--border-subtle); border-radius: 6px; display: flex; align-items: center; gap: 8px; }
.workspace-badge { width: 18px; height: 18px; border-radius: 4px; background: linear-gradient(135deg, var(--accent), hsl(280 70% 60%)); color: white; font-size: 10px; font-weight: 700; display: flex; align-items: center; justify-content: center; }
.workspace-name { font-size: 12px; font-weight: 500; line-height: 1.2; }
.workspace-plan { font-size: 10px; color: var(--text-muted); }
.nav-block { padding: 4px 0; }
.nav-item { display: flex; align-items: center; gap: 10px; padding: 7px 10px; margin: 1px 8px; border-radius: 6px; color: var(--text-muted); background: transparent; transition: all 0.1s ease; font-size: 13px; position: relative; width: calc(100% - 16px); text-align: left; }
.compact .nav-item { padding: 5px 10px; }
.nav-item:hover { background: var(--hover-subtle); color: var(--text-sec); }
.nav-item.active { background: var(--hover); color: var(--text); font-weight: 500; }
.nav-item.active::before { content: ''; position: absolute; left: -8px; width: 2px; height: 14px; border-radius: 2px; background: var(--accent); }
.nav-count { margin-left: auto; min-width: 18px; padding: 1px 6px; border-radius: 999px; background: var(--bg-el); color: var(--text-muted); font-size: 10px; text-align: center; }
.github-card { margin: 8px 12px; padding: 10px; border: 1px solid var(--border-subtle); border-radius: 6px; background: var(--bg-el); }
.sparkline { margin-top: 8px; display: flex; gap: 2px; align-items: flex-end; height: 18px; }
.sparkline span { flex: 1; background: var(--border-strong); border-radius: 1px; }
.sparkline span:nth-last-child(-n+4) { background: var(--accent); }
.user-card { padding: 8px 12px; border-top: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 10px; }
.avatar { width: 26px; height: 26px; border-radius: 50%; background: var(--accent); color: white; font-size: 11px; font-weight: 700; display: flex; align-items: center; justify-content: center; }
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow: hidden; }
.view-host { flex: 1; min-height: 0; display: flex; flex-direction: column; }
.statusbar { height: 24px; flex-shrink: 0; border-top: 1px solid var(--border-subtle); background: var(--bg); display: flex; align-items: center; padding: 0 16px; gap: 16px; font-size: 10px; color: var(--text-dim); font-family: 'Intel One Mono', ui-monospace, SFMono-Regular, Menlo, monospace; }
@media (max-width: 920px) { .workspace { flex-direction: column; min-height: 100vh; } .sidebar { width: 100%; height: auto; border-right: 0; border-bottom: 1px solid var(--border-subtle); } .brand, .workspace-card, .github-card, .user-card { display: none; } .nav-block { display: flex; overflow-x: auto; padding: 8px; } .nav-item { width: auto; margin: 0 2px; } .statusbar { display: none; } }
`;
private handleViewRequest = (event: CustomEvent<{ view: TWorkspaceView }>) => {
this.setView(event.detail.view);
};
private setView(viewArg: TWorkspaceView) {
this.view = viewArg;
this.dispatchEvent(new CustomEvent('view-change', { detail: { view: viewArg }, bubbles: true, composed: true }));
}
private navButton(item: { id: TWorkspaceView; label: string; icon: string; count?: number }): TemplateResult {
return html`<button class="nav-item ${this.view === item.id ? 'active' : ''}" @click=${() => this.setView(item.id)}>${icon(item.icon, 15)}<span>${item.label}</span>${item.count !== undefined ? html`<span class="nav-count">${item.count}</span>` : ''}</button>`;
}
private renderSidebar(): TemplateResult {
const navItems = [
{ id: 'inbox', label: 'Inbox', icon: 'inbox', count: 4 },
{ id: 'compose', label: 'Compose', icon: 'plus' },
{ id: 'templates', label: 'Templates', icon: 'folder', count: 12 },
{ id: 'audit', label: 'Audit Trail', icon: 'shield' },
{ id: 'developers', label: 'Developers', icon: 'code' },
] as Array<{ id: TWorkspaceView; label: string; icon: string; count?: number }>;
const lowerItems = [
{ id: 'team', label: 'Team', icon: 'user' },
{ id: 'settings', label: 'Settings', icon: 'settings' },
] as Array<{ id: TWorkspaceView; label: string; icon: string }>;
return html`
<aside class="sidebar">
<div class="brand"><span class="logomark">s</span><span class="wordmark">signature<span class="dot">.</span>digital</span></div>
<div class="workspace-card"><span class="workspace-badge">L</span><div style="flex: 1; min-width: 0;"><div class="workspace-name">Lossless GmbH</div><div class="workspace-plan">Cloud · Pro</div></div>${icon('chevronDown', 12)}</div>
<div class="nav-block">${navItems.map((item) => this.navButton(item))}</div>
<div style="flex: 1;"></div>
<div class="github-card"><div style="display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 11px; color: var(--text-sec); font-family: 'Intel One Mono', ui-monospace;">${icon('github', 13)} signature-digital/core</div><div style="display: flex; gap: 12px; font-size: 11px; color: var(--text-muted);"><span>${icon('star', 11)} 8.2k</span><span>${icon('git', 11)} 248</span></div><div class="sparkline">${[3, 5, 2, 7, 4, 6, 8, 5, 9, 6, 4, 8, 7, 10].map((height) => html`<span style="height: ${height * 10}%"></span>`)}</div></div>
<div class="nav-block" style="border-top: 1px solid var(--border-subtle); padding-top: 8px;">${lowerItems.map((item) => this.navButton(item))}</div>
<div class="user-card"><span class="avatar">PK</span><div style="flex: 1; min-width: 0;"><div style="font-size: 12px; font-weight: 500;">Philipp K.</div><div style="font-family: 'Intel One Mono', ui-monospace; font-size: 10px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis;">philipp@lossless.com</div></div>${icon('more', 14)}</div>
</aside>
`;
}
private renderView(): TemplateResult {
switch (this.view) {
case 'inbox': return html`<sdig-workspace-inbox class="view-host" .density=${this.density}></sdig-workspace-inbox>`;
case 'compose': return html`<sdig-workspace-compose class="view-host"></sdig-workspace-compose>`;
case 'sign': return html`<sdig-workspace-sign class="view-host"></sdig-workspace-sign>`;
case 'audit': return html`<sdig-workspace-audit class="view-host"></sdig-workspace-audit>`;
case 'developers': return html`<sdig-workspace-developers class="view-host"></sdig-workspace-developers>`;
case 'templates': return html`<sdig-workspace-placeholder class="view-host" label="Templates" subtitle="Reusable agreement templates"></sdig-workspace-placeholder>`;
case 'team': return html`<sdig-workspace-placeholder class="view-host" label="Team" subtitle="Workspace members & roles"></sdig-workspace-placeholder>`;
case 'settings': return html`<sdig-workspace-placeholder class="view-host" label="Settings" subtitle="Workspace, billing, security"></sdig-workspace-placeholder>`;
default: return html`<sdig-workspace-inbox class="view-host" .density=${this.density}></sdig-workspace-inbox>`;
}
}
public render(): TemplateResult {
return html`<div class="workspace ${this.density === 'compact' ? 'compact' : ''}" style="--accent: ${this.accent};" data-screen-label=${this.view}>${this.renderSidebar()}<main class="main">${this.renderView()}<div class="statusbar"><span style="display: inline-flex; align-items: center; gap: 5px;"><span style="width: 6px; height: 6px; border-radius: 50%; background: var(--success);"></span>api.signature.digital</span><span>eu-central-1</span><span>4 sigs queued</span><div style="flex: 1;"></div><span style="color: var(--accent);">Open Source · MIT</span><span>v0.42.1</span><span>${icon('git', 11)} main</span></div></main></div>`;
}
}