8 Commits

Author SHA1 Message Date
jkunz 5ac46728a7 v1.7.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-05 20:45:05 +00:00
jkunz b6b4698028 feat(workspace): make compose and sign views accept document, recipient, and field state via properties and emit field and routing change events 2026-05-05 20:45:05 +00:00
jkunz 7baff5004c v1.6.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-05 19:46:01 +00:00
jkunz b3a30bfb96 fix(workspace): pass the active document into sign and audit views 2026-05-05 19:46:01 +00:00
jkunz 79ca3a07f1 v1.6.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-02 21:26:03 +00:00
jkunz a6324f867f feat(workspace): support configurable inbox documents and emit document-open events 2026-05-02 21:26:03 +00:00
jkunz a0dd552628 v1.5.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-02 19:53:40 +00:00
jkunz d693af8a26 fix(sdig-workspace): make workspace view externally controllable and preserve explicit initial state 2026-05-02 19:53:40 +00:00
8 changed files with 126 additions and 34 deletions
+26
View File
@@ -1,5 +1,31 @@
# Changelog # Changelog
## 2026-05-05 - 1.7.0 - feat(workspace)
make compose and sign views accept document, recipient, and field state via properties and emit field and routing change events
- Pass active document, recipients, and fields into workspace compose and sign views instead of relying on demo-only state.
- Emit field-create, field-update, field-delete, fields-change, recipients-change, and routing-change events from the compose workspace.
- Use document metadata for compose titles and page counts so the UI reflects the selected document.
## 2026-05-05 - 1.6.1 - fix(workspace)
pass the active document into sign and audit views
- Add activeDocumentId support in the workspace container to resolve the selected document for nested views.
- Update sign and audit components to accept a document property and render dynamic title, sender, page count, breadcrumb, and status data instead of hardcoded values.
## 2026-05-02 - 1.6.0 - feat(workspace)
support configurable inbox documents and emit document-open events
- Adds a documents property to the workspace and inbox components so document lists, counts, filters, and attention metrics use injected data instead of demo data.
- Passes documents from the workspace container into the inbox view for consistent sidebar and inbox counts.
- Emits a bubbling document-open event when an inbox item is opened to enable parent integrations and external handling.
## 2026-05-02 - 1.5.1 - fix(sdig-workspace)
make workspace view externally controllable and preserve explicit initial state
- change the workspace view from internal state to a reflected public property
- only apply initialView during connection when no non-default view has already been set
## 2026-05-02 - 1.5.0 - feat(elements) ## 2026-05-02 - 1.5.0 - feat(elements)
add reusable context menu element for recipient role selection add reusable context menu element for recipient role selection
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@signature.digital/catalog", "name": "@signature.digital/catalog",
"version": "1.5.0", "version": "1.7.0",
"private": false, "private": false,
"description": "A comprehensive catalog of customizable web components designed for building and managing e-signature applications.", "description": "A comprehensive catalog of customizable web components designed for building and managing e-signature applications.",
"exports": { "exports": {
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@signature.digital/catalog', name: '@signature.digital/catalog',
version: '1.5.0', version: '1.7.0',
description: 'A comprehensive catalog of customizable web components designed for building and managing e-signature applications.' description: 'A comprehensive catalog of customizable web components designed for building and managing e-signature applications.'
} }
@@ -1,5 +1,5 @@
import { DeesElement, html, customElement, type TemplateResult, css } from '@design.estate/dees-element'; import { DeesElement, property, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
import { actionButton, demoRecipients, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js'; import { actionButton, demoDocuments, demoRecipients, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame, type IDocumentRow } from './sdig-workspace.shared.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -12,6 +12,8 @@ export class SdigWorkspaceAudit extends DeesElement {
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-audit></sdig-workspace-audit>`); public static demo = () => workspaceDemoFrame(html`<sdig-workspace-audit></sdig-workspace-audit>`);
public static demoGroups = ['Signature Digital Workspace']; public static demoGroups = ['Signature Digital Workspace'];
@property({ attribute: false }) public accessor document: IDocumentRow = demoDocuments[1];
public static styles = [workspaceBaseStyles, css` public static styles = [workspaceBaseStyles, css`
.audit-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 20px; } .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; } .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; }
@@ -19,6 +21,7 @@ export class SdigWorkspaceAudit extends DeesElement {
`]; `];
public render(): TemplateResult { public render(): TemplateResult {
const document = this.document || demoDocuments[1];
const events = [ 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: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:31:54 UTC', 'Sarah Chen', 'Signature adopted (typed)', '81.221.4.18 · Brussels, BE', '0x4a7b…f29c', 'info'],
@@ -27,7 +30,7 @@ export class SdigWorkspaceAudit extends DeesElement {
['2026-05-02 10:54:22 UTC', 'Philipp K.', 'Document created', '92.42.114.7 · Berlin, DE', '0x1c8a…3b6f', 'default'], ['2026-05-02 10:54:22 UTC', 'Philipp K.', 'Document created', '92.42.114.7 · Berlin, DE', '0x1c8a…3b6f', 'default'],
]; ];
return html` 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')}` })} ${topBar({ breadcrumb: ['signature.digital', 'Inbox', document.id, 'Audit Trail'], title: `Audit Trail · ${document.title}`, subtitle: pill(`${document.status} · cryptographically sealed`, document.status === 'declined' ? 'error' : document.status === 'signed' ? 'success' : 'info', true), actions: html`${actionButton('Certificate (PDF)', 'outline', 'download')}${actionButton('Verify on chain', 'outline', 'hash')}` })}
<div class="content-scroll audit-grid"> <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 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 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>
@@ -1,5 +1,5 @@
import { DeesElement, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element'; import { DeesElement, property, 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, type TRecipientRole } from './sdig-workspace.shared.js'; import { actionButton, demoDocuments, demoFields, demoRecipients, fakeDocument, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame, type IDocumentRow, type IFieldPlacement, type IRecipient, type TRecipientRole } from './sdig-workspace.shared.js';
import '../sdig-contextmenu/index.js'; import '../sdig-contextmenu/index.js';
import { type ISdigContextMenuAction, type ISdigContextMenuActionEventDetail } from '../sdig-contextmenu/index.js'; import { type ISdigContextMenuAction, type ISdigContextMenuActionEventDetail } from '../sdig-contextmenu/index.js';
@@ -71,8 +71,9 @@ export class SdigWorkspaceCompose extends DeesElement {
@state() private accessor step: number = 2; @state() private accessor step: number = 2;
@state() private accessor activeRecipient: number = 0; @state() private accessor activeRecipient: number = 0;
@state() private accessor selectedFieldId: string | null = null; @state() private accessor selectedFieldId: string | null = null;
@state() private accessor recipients: IRecipient[] = [...demoRecipients]; @property({ attribute: false }) public accessor document: IDocumentRow = demoDocuments[0];
@state() private accessor fields: IFieldPlacement[] = [...demoFields]; @property({ attribute: false }) public accessor recipients: IRecipient[] = [...demoRecipients];
@property({ attribute: false }) public accessor fields: IFieldPlacement[] = [...demoFields];
@state() private accessor signingOrderDrag: TSigningOrderDrag | null = null; @state() private accessor signingOrderDrag: TSigningOrderDrag | null = null;
@state() private accessor recipientContextMenu: TRecipientContextMenu | null = null; @state() private accessor recipientContextMenu: TRecipientContextMenu | null = null;
private draggedFieldDefinition: TFieldDefinition | null = null; private draggedFieldDefinition: TFieldDefinition | null = null;
@@ -169,8 +170,36 @@ export class SdigWorkspaceCompose extends DeesElement {
return Math.max(min, Math.min(max, value)); return Math.max(min, Math.min(max, value));
} }
private emitFieldsChange() {
this.dispatchEvent(new CustomEvent('fields-change', {
detail: { fields: this.fields },
bubbles: true,
composed: true,
}));
}
private emitRecipientsChange() {
this.dispatchEvent(new CustomEvent('recipients-change', {
detail: { recipients: this.recipients },
bubbles: true,
composed: true,
}));
this.dispatchEvent(new CustomEvent('routing-change', {
detail: { recipients: this.recipients },
bubbles: true,
composed: true,
}));
}
private updateField(fieldId: string, patch: Partial<IFieldPlacement>) { private updateField(fieldId: string, patch: Partial<IFieldPlacement>) {
this.fields = this.fields.map((field) => field.id === fieldId ? { ...field, ...patch } : field); this.fields = this.fields.map((field) => field.id === fieldId ? { ...field, ...patch } : field);
const field = this.fields.find((currentField) => currentField.id === fieldId);
this.dispatchEvent(new CustomEvent('field-update', {
detail: { fieldId, field, patch, fields: this.fields },
bubbles: true,
composed: true,
}));
this.emitFieldsChange();
} }
private updateSelectedField(patch: Partial<IFieldPlacement>) { private updateSelectedField(patch: Partial<IFieldPlacement>) {
@@ -192,8 +221,15 @@ export class SdigWorkspaceCompose extends DeesElement {
private removeSelectedField() { private removeSelectedField() {
if (!this.selectedFieldId) return; if (!this.selectedFieldId) return;
const field = this.fields.find((currentField) => currentField.id === this.selectedFieldId);
this.fields = this.fields.filter((field) => field.id !== this.selectedFieldId); this.fields = this.fields.filter((field) => field.id !== this.selectedFieldId);
this.dispatchEvent(new CustomEvent('field-delete', {
detail: { fieldId: this.selectedFieldId, field, fields: this.fields },
bubbles: true,
composed: true,
}));
this.selectedFieldId = null; this.selectedFieldId = null;
this.emitFieldsChange();
} }
private updateRecipientRole(recipientId: number, role: TRecipientRole) { private updateRecipientRole(recipientId: number, role: TRecipientRole) {
@@ -220,6 +256,7 @@ export class SdigWorkspaceCompose extends DeesElement {
targetMembers.splice(insertIndex, 0, { ...recipient, role: nextRole }); targetMembers.splice(insertIndex, 0, { ...recipient, role: nextRole });
nextByRole.set(nextRole, targetMembers); nextByRole.set(nextRole, targetMembers);
this.recipients = recipientRoleDefinitions.flatMap((roleDefinition) => nextByRole.get(roleDefinition.role) || []).map((currentRecipient, index) => ({ ...currentRecipient, order: index + 1 })); this.recipients = recipientRoleDefinitions.flatMap((roleDefinition) => nextByRole.get(roleDefinition.role) || []).map((currentRecipient, index) => ({ ...currentRecipient, order: index + 1 }));
this.emitRecipientsChange();
const nextSigners = this.recipients.filter((currentRecipient) => currentRecipient.role === 'signer'); const nextSigners = this.recipients.filter((currentRecipient) => currentRecipient.role === 'signer');
const fallbackSigner = nextSigners[0]; const fallbackSigner = nextSigners[0];
@@ -229,6 +266,7 @@ export class SdigWorkspaceCompose extends DeesElement {
if (this.activeRecipient === recipientId) { if (this.activeRecipient === recipientId) {
this.activeRecipient = fallbackSigner.id; this.activeRecipient = fallbackSigner.id;
} }
this.emitFieldsChange();
} }
} }
@@ -454,6 +492,12 @@ export class SdigWorkspaceCompose extends DeesElement {
this.selectedFieldId = nextField.id; this.selectedFieldId = nextField.id;
this.draggedFieldDefinition = null; this.draggedFieldDefinition = null;
this.draggedFieldGrabOffset = null; this.draggedFieldGrabOffset = null;
this.dispatchEvent(new CustomEvent('field-create', {
detail: { field: nextField, fields: this.fields },
bubbles: true,
composed: true,
}));
this.emitFieldsChange();
} }
private startFieldToolDrag(event: DragEvent, fieldType: TFieldDefinition) { private startFieldToolDrag(event: DragEvent, fieldType: TFieldDefinition) {
@@ -567,10 +611,11 @@ export class SdigWorkspaceCompose extends DeesElement {
} }
public render(): TemplateResult { public render(): TemplateResult {
const document = this.document || demoDocuments[0];
const selectedField = this.fields.find((field) => field.id === this.selectedFieldId); const selectedField = this.fields.find((field) => field.id === this.selectedFieldId);
return html` 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')}` })} ${topBar({ breadcrumb: ['signature.digital', 'Inbox', 'Compose'], title: document.title, subtitle: pill('Draft · auto-saved'), actions: html`${actionButton('Save draft', 'ghost')}${actionButton('Preview', 'outline', 'eye')}${actionButton('Send for signature', 'primary', 'send')}` })}
${this.renderStepper()} ${this.renderStepper()}
<div class="compose-workspace"> <div class="compose-workspace">
${this.renderRecipientContextMenu()} ${this.renderRecipientContextMenu()}
@@ -585,9 +630,9 @@ export class SdigWorkspaceCompose extends DeesElement {
<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)}> <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()} ${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>`)} ${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 class="mono" style="position: absolute; bottom: 12px; right: 16px; font-size: 9px; color: hsl(0 0% 60%);">Page 1 of ${document.pages}</div>
</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 style="display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--text-muted);">${actionButton('Prev', 'outline')}${html`<span class="mono">1 / ${document.pages}</span>`}${actionButton('Next', 'outline')}</div>
</div> </div>
<div class="right-panel"> <div class="right-panel">
<div class="label-upper">Routing order · drag to reorder</div> <div class="label-upper">Routing order · drag to reorder</div>
@@ -14,6 +14,7 @@ export class SdigWorkspaceInbox extends DeesElement {
public static demoGroups = ['Signature Digital Workspace']; public static demoGroups = ['Signature Digital Workspace'];
@property({ type: String }) public accessor density: TDensity = 'comfortable'; @property({ type: String }) public accessor density: TDensity = 'comfortable';
@property({ attribute: false }) public accessor documents: IDocumentRow[] = demoDocuments;
@state() private accessor filter: string = 'all'; @state() private accessor filter: string = 'all';
@state() private accessor search: string = ''; @state() private accessor search: string = '';
@@ -45,7 +46,7 @@ export class SdigWorkspaceInbox extends DeesElement {
`]; `];
private get filteredDocuments(): IDocumentRow[] { private get filteredDocuments(): IDocumentRow[] {
return demoDocuments return this.documents
.filter((doc) => this.filter === 'all' || doc.status === this.filter) .filter((doc) => this.filter === 'all' || doc.status === this.filter)
.filter((doc) => !this.search || doc.title.toLowerCase().includes(this.search.toLowerCase())); .filter((doc) => !this.search || doc.title.toLowerCase().includes(this.search.toLowerCase()));
} }
@@ -62,23 +63,30 @@ export class SdigWorkspaceInbox extends DeesElement {
} }
private openDocument(doc: IDocumentRow) { private openDocument(doc: IDocumentRow) {
this.dispatchEvent(new CustomEvent('document-open', {
detail: { document: doc },
bubbles: true,
composed: true,
}));
requestWorkspaceView(this, doc.status === 'signed' ? 'audit' : 'sign'); requestWorkspaceView(this, doc.status === 'signed' ? 'audit' : 'sign');
} }
public render(): TemplateResult { public render(): TemplateResult {
const documents = this.documents;
const filters = [ const filters = [
{ id: 'all', label: 'All', count: demoDocuments.length }, { id: 'all', label: 'All', count: documents.length },
{ id: 'awaiting', label: 'Awaiting', count: demoDocuments.filter((doc) => doc.status === 'awaiting').length }, { id: 'awaiting', label: 'Awaiting', count: documents.filter((doc) => doc.status === 'awaiting').length },
{ id: 'signed', label: 'Completed', count: demoDocuments.filter((doc) => doc.status === 'signed').length }, { id: 'signed', label: 'Completed', count: documents.filter((doc) => doc.status === 'signed').length },
{ id: 'draft', label: 'Drafts', count: demoDocuments.filter((doc) => doc.status === 'draft').length }, { id: 'draft', label: 'Drafts', count: documents.filter((doc) => doc.status === 'draft').length },
{ id: 'declined', label: 'Declined', count: demoDocuments.filter((doc) => doc.status === 'declined').length }, { id: 'declined', label: 'Declined', count: documents.filter((doc) => doc.status === 'declined').length },
]; ];
const attentionCount = documents.filter((doc) => doc.status === 'awaiting').length;
return html` return html`
${topBar({ ${topBar({
breadcrumb: ['signature.digital', 'Lossless GmbH', 'Inbox'], breadcrumb: ['signature.digital', 'Lossless GmbH', 'Inbox'],
title: 'Inbox', title: 'Inbox',
subtitle: pill(`${demoDocuments.filter((doc) => doc.status === 'awaiting').length} need attention`, 'info'), subtitle: pill(`${attentionCount} need attention`, 'info'),
actions: html`${actionButton('Import', 'outline', 'upload')}${actionButton('New document', 'primary', 'plus', () => requestWorkspaceView(this, 'compose'))}`, actions: html`${actionButton('Import', 'outline', 'upload')}${actionButton('New document', 'primary', 'plus', () => requestWorkspaceView(this, 'compose'))}`,
})} })}
<div class="filterbar"> <div class="filterbar">
@@ -1,5 +1,5 @@
import { DeesElement, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element'; import { DeesElement, property, 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'; import { actionButton, demoDocuments, demoFields, fakeDocument, icon, pill, workspaceBaseStyles, workspaceDemoFrame, type IDocumentRow, type IFieldPlacement } from './sdig-workspace.shared.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -12,6 +12,8 @@ export class SdigWorkspaceSign extends DeesElement {
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-sign></sdig-workspace-sign>`); public static demo = () => workspaceDemoFrame(html`<sdig-workspace-sign></sdig-workspace-sign>`);
public static demoGroups = ['Signature Digital Workspace']; public static demoGroups = ['Signature Digital Workspace'];
@property({ attribute: false }) public accessor document: IDocumentRow = demoDocuments[0];
@property({ attribute: false }) public accessor fields: IFieldPlacement[] = demoFields;
@state() private accessor activeFieldId: string = 'f1'; @state() private accessor activeFieldId: string = 'f1';
@state() private accessor signedFieldIds: string[] = []; @state() private accessor signedFieldIds: string[] = [];
@@ -27,7 +29,7 @@ export class SdigWorkspaceSign extends DeesElement {
`]; `];
private get signFields() { private get signFields() {
return demoFields.slice(0, 3); return this.fields.slice(0, 3);
} }
private fieldIcon(type: IFieldPlacement['type']): string { private fieldIcon(type: IFieldPlacement['type']): string {
@@ -51,13 +53,14 @@ export class SdigWorkspaceSign extends DeesElement {
} }
public render(): TemplateResult { public render(): TemplateResult {
const document = this.document || demoDocuments[0];
const completed = this.signedFieldIds.length; const completed = this.signedFieldIds.length;
const progress = Math.round((completed / this.signFields.length) * 100); const progress = Math.round((completed / this.signFields.length) * 100);
const activeField = this.signFields.find((field) => field.id === this.activeFieldId) || this.signFields[0]; const activeField = this.signFields.find((field) => field.id === this.activeFieldId) || this.signFields[0];
return html` return html`
<div class="recipient-header"> <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 style="display: flex; align-items: center; gap: 12px;"><span class="logomark">s</span><div><div style="font-size: 12px; font-weight: 600;">${document.title}</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">From ${document.sender} · ${document.id} · ${document.pages} 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 class="actions"><span class="pill success">${icon('shield', 12)} Verified sender · DKIM ✓</span>${actionButton('Decline', 'outline')}${actionButton('PDF', 'outline', 'download')}</div>
</div> </div>
<div class="progress-track"><div class="progress-fill" style="width: ${progress}%"></div></div> <div class="progress-track"><div class="progress-fill" style="width: ${progress}%"></div></div>
@@ -70,7 +73,7 @@ export class SdigWorkspaceSign extends DeesElement {
const active = this.activeFieldId === field.id && !filled; 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>`; 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 class="mono" style="position: absolute; bottom: 14px; right: 18px; font-size: 9px; color: hsl(0 0% 60%);">Page 1 of ${document.pages}</div>
</div> </div>
</div> </div>
<div class="sign-panel"> <div class="sign-panel">
@@ -1,5 +1,5 @@
import { DeesElement, property, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element'; import { DeesElement, property, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
import { icon, type TDensity, type TWorkspaceTheme, type TWorkspaceView } from './sdig-workspace.shared.js'; import { demoDocuments, demoFields, demoRecipients, icon, type IDocumentRow, type IFieldPlacement, type IRecipient, type TDensity, type TWorkspaceTheme, type TWorkspaceView } from './sdig-workspace.shared.js';
import './sdig-workspace-inbox.js'; import './sdig-workspace-inbox.js';
import './sdig-workspace-compose.js'; import './sdig-workspace-compose.js';
import './sdig-workspace-sign.js'; import './sdig-workspace-sign.js';
@@ -22,11 +22,17 @@ export class SdigWorkspace extends DeesElement {
@property({ type: String }) public accessor density: TDensity = 'comfortable'; @property({ type: String }) public accessor density: TDensity = 'comfortable';
@property({ type: String, reflect: true }) public accessor theme: TWorkspaceTheme = 'dark'; @property({ type: String, reflect: true }) public accessor theme: TWorkspaceTheme = 'dark';
@property({ type: String }) public accessor initialView: TWorkspaceView = 'inbox'; @property({ type: String }) public accessor initialView: TWorkspaceView = 'inbox';
@state() private accessor view: TWorkspaceView = 'inbox'; @property({ type: String, reflect: true }) public accessor view: TWorkspaceView = 'inbox';
@property({ attribute: false }) public accessor documents: IDocumentRow[] = demoDocuments;
@property({ type: String }) public accessor activeDocumentId: string = '';
@property({ attribute: false }) public accessor recipients: IRecipient[] = demoRecipients;
@property({ attribute: false }) public accessor fields: IFieldPlacement[] = demoFields;
public connectedCallback = async () => { public connectedCallback = async () => {
await super.connectedCallback(); await super.connectedCallback();
this.view = this.initialView || 'inbox'; if (this.view === 'inbox' && this.initialView !== 'inbox') {
this.view = this.initialView;
}
this.addEventListener('workspace-view-request', this.handleViewRequest as EventListener); this.addEventListener('workspace-view-request', this.handleViewRequest as EventListener);
}; };
@@ -131,7 +137,7 @@ export class SdigWorkspace extends DeesElement {
private renderSidebar(): TemplateResult { private renderSidebar(): TemplateResult {
const navItems = [ const navItems = [
{ id: 'inbox', label: 'Inbox', icon: 'inbox', count: 4 }, { id: 'inbox', label: 'Inbox', icon: 'inbox', count: this.documents.length },
{ id: 'compose', label: 'Compose', icon: 'plus' }, { id: 'compose', label: 'Compose', icon: 'plus' },
{ id: 'templates', label: 'Templates', icon: 'folder', count: 12 }, { id: 'templates', label: 'Templates', icon: 'folder', count: 12 },
{ id: 'audit', label: 'Audit Trail', icon: 'shield' }, { id: 'audit', label: 'Audit Trail', icon: 'shield' },
@@ -156,16 +162,17 @@ export class SdigWorkspace extends DeesElement {
} }
private renderView(): TemplateResult { private renderView(): TemplateResult {
const activeDocument = this.documents.find((document) => document.id === this.activeDocumentId) || this.documents[0] || demoDocuments[0];
switch (this.view) { switch (this.view) {
case 'inbox': return html`<sdig-workspace-inbox class="view-host" .density=${this.density}></sdig-workspace-inbox>`; case 'inbox': return html`<sdig-workspace-inbox class="view-host" .density=${this.density} .documents=${this.documents}></sdig-workspace-inbox>`;
case 'compose': return html`<sdig-workspace-compose class="view-host"></sdig-workspace-compose>`; case 'compose': return html`<sdig-workspace-compose class="view-host" .document=${activeDocument} .recipients=${this.recipients} .fields=${this.fields}></sdig-workspace-compose>`;
case 'sign': return html`<sdig-workspace-sign class="view-host"></sdig-workspace-sign>`; case 'sign': return html`<sdig-workspace-sign class="view-host" .document=${activeDocument} .fields=${this.fields}></sdig-workspace-sign>`;
case 'audit': return html`<sdig-workspace-audit class="view-host"></sdig-workspace-audit>`; case 'audit': return html`<sdig-workspace-audit class="view-host" .document=${activeDocument}></sdig-workspace-audit>`;
case 'developers': return html`<sdig-workspace-developers class="view-host"></sdig-workspace-developers>`; 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 '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 '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>`; 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>`; default: return html`<sdig-workspace-inbox class="view-host" .density=${this.density} .documents=${this.documents}></sdig-workspace-inbox>`;
} }
} }