diff --git a/changelog.md b/changelog.md index 0f1bfe2..d3818f0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-05-02 - 1.4.0 - feat(workspace-compose) +add recipient routing roles and drag-and-drop routing management + +- Introduce recipient roles for signers, final-copy recipients, and step-update recipients. +- Replace simple recipient reordering with role-based drag-and-drop routing sections and contextual role assignment. +- Limit field assignment and active field tools to signing recipients, with automatic field reassignment when a signer is moved out of the signing flow. + ## 2026-05-02 - 1.3.0 - feat(workspace) introduce a responsive signature workspace demo and remove legacy contract editor components diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 0341062..c5008b9 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@signature.digital/catalog', - version: '1.3.0', + version: '1.4.0', description: 'A comprehensive catalog of customizable web components designed for building and managing e-signature applications.' } diff --git a/ts_web/elements/sdig-workspace/sdig-workspace-compose.ts b/ts_web/elements/sdig-workspace/sdig-workspace-compose.ts index ca1ff3e..f4fe264 100644 --- a/ts_web/elements/sdig-workspace/sdig-workspace-compose.ts +++ b/ts_web/elements/sdig-workspace/sdig-workspace-compose.ts @@ -1,5 +1,5 @@ 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'; +import { actionButton, demoFields, demoRecipients, fakeDocument, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame, type IFieldPlacement, type IRecipient, type TRecipientRole } from './sdig-workspace.shared.js'; declare global { interface HTMLElementTagNameMap { @@ -28,6 +28,23 @@ type TFieldInteraction = { pageHeight: number; }; +type TSigningOrderDrag = { + recipientId: number; + pointerY: number; + listTop: number; + grabOffsetY: number; + itemHeight: number; + itemStep: number; + targetRole: TRecipientRole; + targetIndex: number; +}; + +type TRecipientContextMenu = { + recipientId: number; + x: number; + y: number; +}; + const fieldDefinitions: TFieldDefinition[] = [ { type: 'signature', icon: 'sign', label: 'Signature', w: 200, h: 50 }, { type: 'initials', icon: 'type', label: 'Initials', w: 120, h: 32 }, @@ -38,6 +55,12 @@ const fieldDefinitions: TFieldDefinition[] = [ const resizeHandles: TResizeHandle[] = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']; +const recipientRoleDefinitions: Array<{ role: TRecipientRole; label: string; shortLabel: string; description: string }> = [ + { role: 'signer', label: 'Needs signature', shortLabel: 'Signer', description: 'Can receive fields and must sign in order.' }, + { role: 'copy', label: 'Final copy only', shortLabel: 'Copy', description: 'Receives the completed document after signing.' }, + { role: 'updates', label: 'Every step update', shortLabel: 'Updates', description: 'Receives notifications for every routing step.' }, +]; + @customElement('sdig-workspace-compose') export class SdigWorkspaceCompose extends DeesElement { public static demo = () => workspaceDemoFrame(html``); @@ -45,10 +68,11 @@ export class SdigWorkspaceCompose extends DeesElement { @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]; + @state() private accessor signingOrderDrag: TSigningOrderDrag | null = null; + @state() private accessor recipientContextMenu: TRecipientContextMenu | null = null; private draggedFieldDefinition: TFieldDefinition | null = null; private draggedFieldGrabOffset: { x: number; y: number } | null = null; private fieldInteraction: TFieldInteraction | null = null; @@ -70,7 +94,26 @@ export class SdigWorkspaceCompose extends DeesElement { .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); } + .routing-role-section { margin-bottom: 14px; padding: 10px; border: 1px solid var(--border-subtle); border-radius: 8px; background: color-mix(in srgb, var(--bg-card) 72%, transparent); transition: border-color 0.14s ease, background 0.14s ease; } + .routing-role-section.active-drop { border-color: color-mix(in srgb, var(--accent) 48%, var(--border)); background: color-mix(in srgb, var(--accent) 7%, var(--bg-card)); } + .routing-role-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; } + .routing-role-title { font-size: 11px; font-weight: 700; color: var(--text-sec); } + .routing-role-description { margin-bottom: 8px; font-size: 10px; line-height: 1.35; color: var(--text-muted); } + .role-count { min-width: 18px; height: 18px; padding: 0 6px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center; background: var(--bg-input); color: var(--text-muted); font-size: 10px; } + .role-chip { height: 22px; padding: 0 7px; border-radius: 999px; display: inline-flex; align-items: center; background: var(--bg-input); color: var(--text-muted); font-size: 10px; font-weight: 600; } + .signing-order-list { position: relative; min-height: 44px; } + .signing-order-list.dragging { height: var(--routing-list-height); min-height: var(--routing-list-height); } + .signing-order-list::before { content: ''; position: absolute; left: 11px; top: 10px; bottom: 10px; width: 1px; background: var(--border); } + .signing-recipient { position: relative; z-index: 1; transition: transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease; } + .signing-order-list.dragging .signing-recipient:not(.signing-drag-overlay) { position: absolute; left: 0; right: 0; top: var(--routing-top); margin-bottom: 0; transition: top 0.16s ease, transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease; } + .signing-placeholder { position: absolute; left: 0; right: 0; top: var(--routing-top); height: var(--routing-row-height); border: 1.5px dashed var(--accent); border-radius: 6px; background: transparent; pointer-events: none; transition: top 0.16s ease; } + .signing-drag-overlay { position: absolute; left: 0; right: 0; z-index: 6; top: var(--routing-top); margin-bottom: 0; cursor: grabbing; pointer-events: none; border-color: var(--accent); box-shadow: 0 10px 28px rgba(0,0,0,0.28); transform: scale(1.015); } + .role-hint { margin-top: -2px; margin-bottom: 10px; font-size: 10px; line-height: 1.45; color: var(--text-muted); } + .recipient-context-menu { position: fixed; z-index: 100; min-width: 190px; padding: 6px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-card); box-shadow: 0 16px 42px rgba(0,0,0,0.36); } + .recipient-context-title { padding: 7px 8px; font-size: 11px; font-weight: 700; color: var(--text-sec); border-bottom: 1px solid var(--border-subtle); margin-bottom: 4px; } + .context-action { width: 100%; padding: 8px; border-radius: 6px; background: transparent; color: var(--text-sec); display: flex; align-items: center; gap: 8px; text-align: left; font-size: 11px; } + .context-action:hover { background: var(--hover); color: var(--text); } + .context-action[disabled] { opacity: 0.45; cursor: not-allowed; } .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; } @@ -97,6 +140,8 @@ export class SdigWorkspaceCompose extends DeesElement { public disconnectedCallback = async () => { this.stopFieldInteraction(); + this.stopSigningOrderDrag(); + window.removeEventListener('click', this.closeRecipientContextMenu); await super.disconnectedCallback(); }; @@ -115,6 +160,14 @@ export class SdigWorkspaceCompose extends DeesElement { return fieldDefinitions.find((definition) => definition.type === type) || fieldDefinitions[0]; } + private recipientRoleDefinition(role: TRecipientRole) { + return recipientRoleDefinitions.find((definition) => definition.role === role) || recipientRoleDefinitions[0]; + } + + private signingRecipients(): IRecipient[] { + return this.recipients.filter((recipient) => recipient.role === 'signer'); + } + private clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); } @@ -146,6 +199,54 @@ export class SdigWorkspaceCompose extends DeesElement { this.selectedFieldId = null; } + private updateRecipientRole(recipientId: number, role: TRecipientRole) { + const recipient = this.recipients.find((currentRecipient) => currentRecipient.id === recipientId); + if (!recipient) return; + const signerCount = this.signingRecipients().length; + if (recipient.role === 'signer' && role !== 'signer' && signerCount <= 1) return; + + this.moveRecipientToRole(recipientId, role); + } + + private moveRecipientToRole(recipientId: number, role: TRecipientRole, targetIndex?: number) { + const recipient = this.recipients.find((currentRecipient) => currentRecipient.id === recipientId); + if (!recipient) return; + const signerCount = this.signingRecipients().length; + const nextRole = recipient.role === 'signer' && role !== 'signer' && signerCount <= 1 ? 'signer' : role; + const withoutRecipient = this.recipients.filter((currentRecipient) => currentRecipient.id !== recipientId); + const nextByRole = new Map(); + for (const roleDefinition of recipientRoleDefinitions) { + nextByRole.set(roleDefinition.role, withoutRecipient.filter((currentRecipient) => currentRecipient.role === roleDefinition.role)); + } + const targetMembers = [...(nextByRole.get(nextRole) || [])]; + const insertIndex = targetIndex === undefined ? targetMembers.length : this.clamp(targetIndex, 0, targetMembers.length); + targetMembers.splice(insertIndex, 0, { ...recipient, role: nextRole }); + nextByRole.set(nextRole, targetMembers); + this.recipients = recipientRoleDefinitions.flatMap((roleDefinition) => nextByRole.get(roleDefinition.role) || []).map((currentRecipient, index) => ({ ...currentRecipient, order: index + 1 })); + + const nextSigners = this.recipients.filter((currentRecipient) => currentRecipient.role === 'signer'); + const fallbackSigner = nextSigners[0]; + + if (nextRole !== 'signer' && fallbackSigner) { + this.fields = this.fields.map((field) => field.recipient === recipientId ? { ...field, recipient: fallbackSigner.id } : field); + if (this.activeRecipient === recipientId) { + this.activeRecipient = fallbackSigner.id; + } + } + } + + private openRecipientContextMenu(event: MouseEvent, recipient: IRecipient) { + event.preventDefault(); + event.stopPropagation(); + this.recipientContextMenu = { recipientId: recipient.id, x: event.clientX, y: event.clientY }; + window.removeEventListener('click', this.closeRecipientContextMenu); + setTimeout(() => window.addEventListener('click', this.closeRecipientContextMenu, { once: true }), 0); + } + + private closeRecipientContextMenu = () => { + this.recipientContextMenu = null; + }; + private handleDocumentClick = (event: MouseEvent) => { const target = event.target as HTMLElement | null; if (target?.closest('.field-box')) return; @@ -237,18 +338,81 @@ export class SdigWorkspaceCompose extends DeesElement { 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 visualSigningOrder(): IRecipient[] { + if (!this.signingOrderDrag) return this.recipients; + const dragged = this.recipients.find((recipient) => recipient.id === this.signingOrderDrag?.recipientId); + if (!dragged) return this.recipients; + const others = this.recipients.filter((recipient) => recipient.role === this.signingOrderDrag?.targetRole && recipient.id !== dragged.id); + const targetIndex = this.clamp(this.signingOrderDrag.targetIndex, 0, others.length); + return [...others.slice(0, targetIndex), dragged, ...others.slice(targetIndex)]; } + private recipientsForRole(role: TRecipientRole): IRecipient[] { + if (!this.signingOrderDrag) return this.recipients.filter((recipient) => recipient.role === role); + const dragged = this.recipients.find((recipient) => recipient.id === this.signingOrderDrag?.recipientId); + const recipients = this.recipients.filter((recipient) => recipient.role === role && recipient.id !== dragged?.id); + if (!dragged || this.signingOrderDrag.targetRole !== role) return recipients; + const targetIndex = this.clamp(this.signingOrderDrag.targetIndex, 0, recipients.length); + return [...recipients.slice(0, targetIndex), { ...dragged, role }, ...recipients.slice(targetIndex)]; + } + + private startSigningOrderDrag(event: PointerEvent, recipient: IRecipient) { + if (event.button !== 0) return; + const target = event.target as HTMLElement | null; + if (target?.closest('select, input, button')) return; + const item = event.currentTarget as HTMLElement; + const list = item.closest('.signing-order-list') as HTMLElement | null; + if (!list) return; + const section = item.closest('.routing-role-section') as HTMLElement | null; + const role = (section?.dataset.role || recipient.role) as TRecipientRole; + const itemRect = item.getBoundingClientRect(); + const listRect = list.getBoundingClientRect(); + const marginBottom = Number.parseFloat(globalThis.getComputedStyle(item).marginBottom || '0'); + const startIndex = this.recipients.filter((currentRecipient) => currentRecipient.role === role).findIndex((currentRecipient) => currentRecipient.id === recipient.id); + this.signingOrderDrag = { + recipientId: recipient.id, + pointerY: event.clientY, + listTop: listRect.top, + grabOffsetY: event.clientY - itemRect.top, + itemHeight: itemRect.height, + itemStep: itemRect.height + marginBottom, + targetRole: role, + targetIndex: Math.max(0, startIndex), + }; + event.preventDefault(); + window.addEventListener('pointermove', this.handleSigningOrderPointerMove, { passive: false }); + window.addEventListener('pointerup', this.stopSigningOrderDrag); + window.addEventListener('pointercancel', this.stopSigningOrderDrag); + } + + private handleSigningOrderPointerMove = (event: PointerEvent) => { + if (!this.signingOrderDrag) return; + event.preventDefault(); + const drag = this.signingOrderDrag; + const target = this.shadowRoot?.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null; + const section = target?.closest('.routing-role-section') as HTMLElement | null; + const roleCandidate = (section?.dataset.role || drag.targetRole) as TRecipientRole; + const draggedRecipient = this.recipients.find((recipient) => recipient.id === drag.recipientId); + const targetRole = draggedRecipient?.role === 'signer' && roleCandidate !== 'signer' && this.signingRecipients().length <= 1 ? 'signer' : roleCandidate; + const list = this.shadowRoot?.querySelector(`.routing-role-section[data-role="${targetRole}"] .signing-order-list`) as HTMLElement | null; + const listRect = list?.getBoundingClientRect(); + const listTop = listRect?.top ?? drag.listTop; + const targetMemberCount = this.recipients.filter((recipient) => recipient.role === targetRole && recipient.id !== drag.recipientId).length; + const draggedCenterY = event.clientY - listTop - drag.grabOffsetY + drag.itemStep / 2; + const targetIndex = Math.round(this.clamp(draggedCenterY / drag.itemStep, 0, targetMemberCount)); + this.signingOrderDrag = { ...drag, pointerY: event.clientY, listTop, targetRole, targetIndex }; + }; + + private stopSigningOrderDrag = () => { + if (this.signingOrderDrag) { + this.moveRecipientToRole(this.signingOrderDrag.recipientId, this.signingOrderDrag.targetRole, this.signingOrderDrag.targetIndex); + } + this.signingOrderDrag = null; + window.removeEventListener('pointermove', this.handleSigningOrderPointerMove); + window.removeEventListener('pointerup', this.stopSigningOrderDrag); + window.removeEventListener('pointercancel', this.stopSigningOrderDrag); + }; + private addFieldFromDrop(event: DragEvent) { event.preventDefault(); const page = event.currentTarget as HTMLElement; @@ -305,7 +469,7 @@ export class SdigWorkspaceCompose extends DeesElement {
- + @@ -323,6 +487,54 @@ export class SdigWorkspaceCompose extends DeesElement { return html`${resizeHandles.map((handle) => html` this.startFieldResize(event, field, handle)}>`)}`; } + private renderSigningRecipient(recipient: IRecipient, orderNumber: number, options: { overlayTop?: number; rowTop?: number; displayRole?: TRecipientRole } = {}): TemplateResult { + const initials = recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join(''); + const isOverlay = options.overlayTop !== undefined; + const top = options.overlayTop ?? options.rowTop; + const displayRole = options.displayRole || recipient.role; + return html`
this.openRecipientContextMenu(event, recipient)} @pointerdown=${!isOverlay ? (event: PointerEvent) => this.startSigningOrderDrag(event, recipient) : undefined}>${orderNumber}${initials}
${recipient.name}
${this.recipientRoleDefinition(displayRole).description}
${this.recipientRoleDefinition(displayRole).shortLabel}
`; + } + + private renderRoleSection(roleDefinition: typeof recipientRoleDefinitions[number]): TemplateResult { + const role = roleDefinition.role; + const members = this.recipientsForRole(role); + const isTargetRole = this.signingOrderDrag?.targetRole === role; + const draggedRecipientId = this.signingOrderDrag?.recipientId; + const draggedRecipient = draggedRecipientId !== undefined ? this.recipients.find((recipient) => recipient.id === draggedRecipientId) : undefined; + + if (!this.signingOrderDrag) { + return html`
${roleDefinition.label}${members.length}
${roleDefinition.description}
${members.map((recipient, index) => this.renderSigningRecipient(recipient, index + 1))}
`; + } + + const visualIndexById = new Map(members.map((recipient, index) => [recipient.id, index])); + const overlayTop = isTargetRole ? this.signingOrderDrag.pointerY - this.signingOrderDrag.listTop - this.signingOrderDrag.grabOffsetY : 0; + const draggedOrder = draggedRecipient ? (members.findIndex((recipient) => recipient.id === draggedRecipient.id) + 1 || this.signingOrderDrag.targetIndex + 1) : 0; + + return html`
${roleDefinition.label}${members.filter((recipient) => recipient.id !== draggedRecipientId).length + (isTargetRole ? 1 : 0)}
${roleDefinition.description}
+ ${members.filter((recipient) => recipient.id !== draggedRecipientId).map((recipient) => { + const visualIndex = visualIndexById.get(recipient.id) ?? 0; + return this.renderSigningRecipient(recipient, visualIndex + 1, { rowTop: visualIndex * this.signingOrderDrag!.itemStep, displayRole: role }); + })} + ${isTargetRole ? html`
` : ''} + ${isTargetRole && draggedRecipient ? this.renderSigningRecipient(draggedRecipient, draggedOrder, { overlayTop, displayRole: role }) : ''} +
`; + } + + private renderSigningOrder(): TemplateResult { + return html`${recipientRoleDefinitions.map((roleDefinition) => this.renderRoleSection(roleDefinition))}`; + } + + private renderRecipientContextMenu(): TemplateResult { + if (!this.recipientContextMenu) return html``; + const recipient = this.recipients.find((currentRecipient) => currentRecipient.id === this.recipientContextMenu?.recipientId); + if (!recipient) return html``; + const signerCount = this.signingRecipients().length; + return html`
event.stopPropagation()}> +
${recipient.name}
+ ${recipientRoleDefinitions.map((roleDefinition) => html``)} +
`; + } + private renderStepper(): TemplateResult { const labels = ['Upload', 'Place fields', 'Recipients & routing', 'Review & send']; return html` @@ -343,12 +555,13 @@ export class SdigWorkspaceCompose extends DeesElement { ${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()}
+ ${this.renderRecipientContextMenu()}
Drag onto document
${fieldDefinitions.map((fieldType) => html`
this.startFieldToolDrag(event, fieldType)} @dragend=${() => this.endFieldToolDrag()}>${icon(fieldType.icon, 14)}${fieldType.label}
`)}
Active for
- ${this.recipients.map((recipient) => html`
this.activeRecipient = recipient.id}>${recipient.name.split(' ')[0]}${this.fields.filter((field) => field.recipient === recipient.id).length}
`)} + ${this.signingRecipients().map((recipient) => html`
this.activeRecipient = recipient.id}>${recipient.name.split(' ')[0]}${this.fields.filter((field) => field.recipient === recipient.id).length}
`)}
{ 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)}> @@ -359,8 +572,9 @@ export class SdigWorkspaceCompose extends DeesElement {
${actionButton('Prev', 'outline')}${html`1 / 14`}${actionButton('Next', 'outline')}
-
Signing order · drag to reorder
- ${this.recipients.map((recipient) => html`
this.draggedRecipientId = recipient.id} @dragover=${(event: DragEvent) => event.preventDefault()} @drop=${() => this.reorderRecipient(recipient.id)} @dragend=${() => this.draggedRecipientId = null}>${recipient.order}${recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join('')}
${recipient.name}
${recipient.email}
${icon('more', 12)}
`)} +
Routing order · drag to reorder
+
Choose who signs, who gets the completed copy, and who is notified at every step.
+ ${this.renderSigningOrder()} ${selectedField ? this.renderFieldEditor(selectedField) : ''}
diff --git a/ts_web/elements/sdig-workspace/sdig-workspace.shared.ts b/ts_web/elements/sdig-workspace/sdig-workspace.shared.ts index 28b5e9d..09f22e4 100644 --- a/ts_web/elements/sdig-workspace/sdig-workspace.shared.ts +++ b/ts_web/elements/sdig-workspace/sdig-workspace.shared.ts @@ -13,6 +13,7 @@ export type TWorkspaceView = export type TWorkspaceTheme = 'dark' | 'light'; export type TDensity = 'compact' | 'comfortable'; +export type TRecipientRole = 'signer' | 'copy' | 'updates'; export interface IDocumentRow { id: string; @@ -31,6 +32,7 @@ export interface IRecipient { email: string; color: string; order: number; + role: TRecipientRole; } export interface IFieldPlacement { @@ -55,9 +57,9 @@ export const demoDocuments: IDocumentRow[] = [ ]; 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 }, + { id: 0, name: 'Sarah Chen', email: 'sarah@acme.com', color: '#60a5fa', order: 1, role: 'signer' }, + { id: 1, name: 'David Park', email: 'd.park@acme.com', color: '#fbbf24', order: 2, role: 'signer' }, + { id: 2, name: 'Philipp K.', email: 'philipp@lossless.com', color: '#3b82f6', order: 3, role: 'updates' }, ]; export const demoFields: IFieldPlacement[] = [