@signature.digital/catalog 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_bundle/bundle.js +240 -212
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-compose.d.ts +17 -2
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-compose.js +220 -26
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace.shared.d.ts +2 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace.shared.js +4 -4
- package/dist_watch/bundle.js +207 -26
- package/dist_watch/bundle.js.map +3 -3
- package/package.json +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/sdig-workspace/sdig-workspace-compose.ts +231 -17
- package/ts_web/elements/sdig-workspace/sdig-workspace.shared.ts +5 -3
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DeesElement, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
|
2
|
-
import { actionButton, demoFields, demoRecipients, fakeDocument, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame, type IFieldPlacement, type IRecipient } from './sdig-workspace.shared.js';
|
|
2
|
+
import { actionButton, demoFields, demoRecipients, fakeDocument, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame, type IFieldPlacement, type IRecipient, type TRecipientRole } from './sdig-workspace.shared.js';
|
|
3
3
|
|
|
4
4
|
declare global {
|
|
5
5
|
interface HTMLElementTagNameMap {
|
|
@@ -28,6 +28,23 @@ type TFieldInteraction = {
|
|
|
28
28
|
pageHeight: number;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
type TSigningOrderDrag = {
|
|
32
|
+
recipientId: number;
|
|
33
|
+
pointerY: number;
|
|
34
|
+
listTop: number;
|
|
35
|
+
grabOffsetY: number;
|
|
36
|
+
itemHeight: number;
|
|
37
|
+
itemStep: number;
|
|
38
|
+
targetRole: TRecipientRole;
|
|
39
|
+
targetIndex: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type TRecipientContextMenu = {
|
|
43
|
+
recipientId: number;
|
|
44
|
+
x: number;
|
|
45
|
+
y: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
31
48
|
const fieldDefinitions: TFieldDefinition[] = [
|
|
32
49
|
{ type: 'signature', icon: 'sign', label: 'Signature', w: 200, h: 50 },
|
|
33
50
|
{ type: 'initials', icon: 'type', label: 'Initials', w: 120, h: 32 },
|
|
@@ -38,6 +55,12 @@ const fieldDefinitions: TFieldDefinition[] = [
|
|
|
38
55
|
|
|
39
56
|
const resizeHandles: TResizeHandle[] = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
|
|
40
57
|
|
|
58
|
+
const recipientRoleDefinitions: Array<{ role: TRecipientRole; label: string; shortLabel: string; description: string }> = [
|
|
59
|
+
{ role: 'signer', label: 'Needs signature', shortLabel: 'Signer', description: 'Can receive fields and must sign in order.' },
|
|
60
|
+
{ role: 'copy', label: 'Final copy only', shortLabel: 'Copy', description: 'Receives the completed document after signing.' },
|
|
61
|
+
{ role: 'updates', label: 'Every step update', shortLabel: 'Updates', description: 'Receives notifications for every routing step.' },
|
|
62
|
+
];
|
|
63
|
+
|
|
41
64
|
@customElement('sdig-workspace-compose')
|
|
42
65
|
export class SdigWorkspaceCompose extends DeesElement {
|
|
43
66
|
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-compose></sdig-workspace-compose>`);
|
|
@@ -45,10 +68,11 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
45
68
|
|
|
46
69
|
@state() private accessor step: number = 2;
|
|
47
70
|
@state() private accessor activeRecipient: number = 0;
|
|
48
|
-
@state() private accessor draggedRecipientId: number | null = null;
|
|
49
71
|
@state() private accessor selectedFieldId: string | null = null;
|
|
50
72
|
@state() private accessor recipients: IRecipient[] = [...demoRecipients];
|
|
51
73
|
@state() private accessor fields: IFieldPlacement[] = [...demoFields];
|
|
74
|
+
@state() private accessor signingOrderDrag: TSigningOrderDrag | null = null;
|
|
75
|
+
@state() private accessor recipientContextMenu: TRecipientContextMenu | null = null;
|
|
52
76
|
private draggedFieldDefinition: TFieldDefinition | null = null;
|
|
53
77
|
private draggedFieldGrabOffset: { x: number; y: number } | null = null;
|
|
54
78
|
private fieldInteraction: TFieldInteraction | null = null;
|
|
@@ -70,7 +94,26 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
70
94
|
.document-stage { flex: 1; overflow: auto; background: hsl(0 0% 8%); display: flex; flex-direction: column; align-items: center; padding: 32px; gap: 20px; }
|
|
71
95
|
:host-context(sdig-workspace[theme='light']) .document-stage { background: hsl(0 0% 92%); }
|
|
72
96
|
.recipient-line { cursor: grab; }
|
|
73
|
-
.
|
|
97
|
+
.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; }
|
|
98
|
+
.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)); }
|
|
99
|
+
.routing-role-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
100
|
+
.routing-role-title { font-size: 11px; font-weight: 700; color: var(--text-sec); }
|
|
101
|
+
.routing-role-description { margin-bottom: 8px; font-size: 10px; line-height: 1.35; color: var(--text-muted); }
|
|
102
|
+
.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; }
|
|
103
|
+
.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; }
|
|
104
|
+
.signing-order-list { position: relative; min-height: 44px; }
|
|
105
|
+
.signing-order-list.dragging { height: var(--routing-list-height); min-height: var(--routing-list-height); }
|
|
106
|
+
.signing-order-list::before { content: ''; position: absolute; left: 11px; top: 10px; bottom: 10px; width: 1px; background: var(--border); }
|
|
107
|
+
.signing-recipient { position: relative; z-index: 1; transition: transform 0.14s ease, opacity 0.14s ease, border-color 0.14s ease; }
|
|
108
|
+
.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; }
|
|
109
|
+
.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; }
|
|
110
|
+
.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); }
|
|
111
|
+
.role-hint { margin-top: -2px; margin-bottom: 10px; font-size: 10px; line-height: 1.45; color: var(--text-muted); }
|
|
112
|
+
.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); }
|
|
113
|
+
.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; }
|
|
114
|
+
.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; }
|
|
115
|
+
.context-action:hover { background: var(--hover); color: var(--text); }
|
|
116
|
+
.context-action[disabled] { opacity: 0.45; cursor: not-allowed; }
|
|
74
117
|
.page-drop-target { outline: 1px dashed transparent; outline-offset: 8px; }
|
|
75
118
|
.page-drop-target.drag-over { outline-color: var(--accent); }
|
|
76
119
|
.field-box { user-select: none; touch-action: none; }
|
|
@@ -97,6 +140,8 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
97
140
|
|
|
98
141
|
public disconnectedCallback = async () => {
|
|
99
142
|
this.stopFieldInteraction();
|
|
143
|
+
this.stopSigningOrderDrag();
|
|
144
|
+
window.removeEventListener('click', this.closeRecipientContextMenu);
|
|
100
145
|
await super.disconnectedCallback();
|
|
101
146
|
};
|
|
102
147
|
|
|
@@ -115,6 +160,14 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
115
160
|
return fieldDefinitions.find((definition) => definition.type === type) || fieldDefinitions[0];
|
|
116
161
|
}
|
|
117
162
|
|
|
163
|
+
private recipientRoleDefinition(role: TRecipientRole) {
|
|
164
|
+
return recipientRoleDefinitions.find((definition) => definition.role === role) || recipientRoleDefinitions[0];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private signingRecipients(): IRecipient[] {
|
|
168
|
+
return this.recipients.filter((recipient) => recipient.role === 'signer');
|
|
169
|
+
}
|
|
170
|
+
|
|
118
171
|
private clamp(value: number, min: number, max: number): number {
|
|
119
172
|
return Math.max(min, Math.min(max, value));
|
|
120
173
|
}
|
|
@@ -146,6 +199,54 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
146
199
|
this.selectedFieldId = null;
|
|
147
200
|
}
|
|
148
201
|
|
|
202
|
+
private updateRecipientRole(recipientId: number, role: TRecipientRole) {
|
|
203
|
+
const recipient = this.recipients.find((currentRecipient) => currentRecipient.id === recipientId);
|
|
204
|
+
if (!recipient) return;
|
|
205
|
+
const signerCount = this.signingRecipients().length;
|
|
206
|
+
if (recipient.role === 'signer' && role !== 'signer' && signerCount <= 1) return;
|
|
207
|
+
|
|
208
|
+
this.moveRecipientToRole(recipientId, role);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private moveRecipientToRole(recipientId: number, role: TRecipientRole, targetIndex?: number) {
|
|
212
|
+
const recipient = this.recipients.find((currentRecipient) => currentRecipient.id === recipientId);
|
|
213
|
+
if (!recipient) return;
|
|
214
|
+
const signerCount = this.signingRecipients().length;
|
|
215
|
+
const nextRole = recipient.role === 'signer' && role !== 'signer' && signerCount <= 1 ? 'signer' : role;
|
|
216
|
+
const withoutRecipient = this.recipients.filter((currentRecipient) => currentRecipient.id !== recipientId);
|
|
217
|
+
const nextByRole = new Map<TRecipientRole, IRecipient[]>();
|
|
218
|
+
for (const roleDefinition of recipientRoleDefinitions) {
|
|
219
|
+
nextByRole.set(roleDefinition.role, withoutRecipient.filter((currentRecipient) => currentRecipient.role === roleDefinition.role));
|
|
220
|
+
}
|
|
221
|
+
const targetMembers = [...(nextByRole.get(nextRole) || [])];
|
|
222
|
+
const insertIndex = targetIndex === undefined ? targetMembers.length : this.clamp(targetIndex, 0, targetMembers.length);
|
|
223
|
+
targetMembers.splice(insertIndex, 0, { ...recipient, role: nextRole });
|
|
224
|
+
nextByRole.set(nextRole, targetMembers);
|
|
225
|
+
this.recipients = recipientRoleDefinitions.flatMap((roleDefinition) => nextByRole.get(roleDefinition.role) || []).map((currentRecipient, index) => ({ ...currentRecipient, order: index + 1 }));
|
|
226
|
+
|
|
227
|
+
const nextSigners = this.recipients.filter((currentRecipient) => currentRecipient.role === 'signer');
|
|
228
|
+
const fallbackSigner = nextSigners[0];
|
|
229
|
+
|
|
230
|
+
if (nextRole !== 'signer' && fallbackSigner) {
|
|
231
|
+
this.fields = this.fields.map((field) => field.recipient === recipientId ? { ...field, recipient: fallbackSigner.id } : field);
|
|
232
|
+
if (this.activeRecipient === recipientId) {
|
|
233
|
+
this.activeRecipient = fallbackSigner.id;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private openRecipientContextMenu(event: MouseEvent, recipient: IRecipient) {
|
|
239
|
+
event.preventDefault();
|
|
240
|
+
event.stopPropagation();
|
|
241
|
+
this.recipientContextMenu = { recipientId: recipient.id, x: event.clientX, y: event.clientY };
|
|
242
|
+
window.removeEventListener('click', this.closeRecipientContextMenu);
|
|
243
|
+
setTimeout(() => window.addEventListener('click', this.closeRecipientContextMenu, { once: true }), 0);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private closeRecipientContextMenu = () => {
|
|
247
|
+
this.recipientContextMenu = null;
|
|
248
|
+
};
|
|
249
|
+
|
|
149
250
|
private handleDocumentClick = (event: MouseEvent) => {
|
|
150
251
|
const target = event.target as HTMLElement | null;
|
|
151
252
|
if (target?.closest('.field-box')) return;
|
|
@@ -237,18 +338,81 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
237
338
|
window.removeEventListener('pointercancel', this.stopFieldInteraction);
|
|
238
339
|
};
|
|
239
340
|
|
|
240
|
-
private
|
|
241
|
-
if (this.
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
341
|
+
private visualSigningOrder(): IRecipient[] {
|
|
342
|
+
if (!this.signingOrderDrag) return this.recipients;
|
|
343
|
+
const dragged = this.recipients.find((recipient) => recipient.id === this.signingOrderDrag?.recipientId);
|
|
344
|
+
if (!dragged) return this.recipients;
|
|
345
|
+
const others = this.recipients.filter((recipient) => recipient.role === this.signingOrderDrag?.targetRole && recipient.id !== dragged.id);
|
|
346
|
+
const targetIndex = this.clamp(this.signingOrderDrag.targetIndex, 0, others.length);
|
|
347
|
+
return [...others.slice(0, targetIndex), dragged, ...others.slice(targetIndex)];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private recipientsForRole(role: TRecipientRole): IRecipient[] {
|
|
351
|
+
if (!this.signingOrderDrag) return this.recipients.filter((recipient) => recipient.role === role);
|
|
352
|
+
const dragged = this.recipients.find((recipient) => recipient.id === this.signingOrderDrag?.recipientId);
|
|
353
|
+
const recipients = this.recipients.filter((recipient) => recipient.role === role && recipient.id !== dragged?.id);
|
|
354
|
+
if (!dragged || this.signingOrderDrag.targetRole !== role) return recipients;
|
|
355
|
+
const targetIndex = this.clamp(this.signingOrderDrag.targetIndex, 0, recipients.length);
|
|
356
|
+
return [...recipients.slice(0, targetIndex), { ...dragged, role }, ...recipients.slice(targetIndex)];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private startSigningOrderDrag(event: PointerEvent, recipient: IRecipient) {
|
|
360
|
+
if (event.button !== 0) return;
|
|
361
|
+
const target = event.target as HTMLElement | null;
|
|
362
|
+
if (target?.closest('select, input, button')) return;
|
|
363
|
+
const item = event.currentTarget as HTMLElement;
|
|
364
|
+
const list = item.closest('.signing-order-list') as HTMLElement | null;
|
|
365
|
+
if (!list) return;
|
|
366
|
+
const section = item.closest('.routing-role-section') as HTMLElement | null;
|
|
367
|
+
const role = (section?.dataset.role || recipient.role) as TRecipientRole;
|
|
368
|
+
const itemRect = item.getBoundingClientRect();
|
|
369
|
+
const listRect = list.getBoundingClientRect();
|
|
370
|
+
const marginBottom = Number.parseFloat(globalThis.getComputedStyle(item).marginBottom || '0');
|
|
371
|
+
const startIndex = this.recipients.filter((currentRecipient) => currentRecipient.role === role).findIndex((currentRecipient) => currentRecipient.id === recipient.id);
|
|
372
|
+
this.signingOrderDrag = {
|
|
373
|
+
recipientId: recipient.id,
|
|
374
|
+
pointerY: event.clientY,
|
|
375
|
+
listTop: listRect.top,
|
|
376
|
+
grabOffsetY: event.clientY - itemRect.top,
|
|
377
|
+
itemHeight: itemRect.height,
|
|
378
|
+
itemStep: itemRect.height + marginBottom,
|
|
379
|
+
targetRole: role,
|
|
380
|
+
targetIndex: Math.max(0, startIndex),
|
|
381
|
+
};
|
|
382
|
+
event.preventDefault();
|
|
383
|
+
window.addEventListener('pointermove', this.handleSigningOrderPointerMove, { passive: false });
|
|
384
|
+
window.addEventListener('pointerup', this.stopSigningOrderDrag);
|
|
385
|
+
window.addEventListener('pointercancel', this.stopSigningOrderDrag);
|
|
250
386
|
}
|
|
251
387
|
|
|
388
|
+
private handleSigningOrderPointerMove = (event: PointerEvent) => {
|
|
389
|
+
if (!this.signingOrderDrag) return;
|
|
390
|
+
event.preventDefault();
|
|
391
|
+
const drag = this.signingOrderDrag;
|
|
392
|
+
const target = this.shadowRoot?.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null;
|
|
393
|
+
const section = target?.closest('.routing-role-section') as HTMLElement | null;
|
|
394
|
+
const roleCandidate = (section?.dataset.role || drag.targetRole) as TRecipientRole;
|
|
395
|
+
const draggedRecipient = this.recipients.find((recipient) => recipient.id === drag.recipientId);
|
|
396
|
+
const targetRole = draggedRecipient?.role === 'signer' && roleCandidate !== 'signer' && this.signingRecipients().length <= 1 ? 'signer' : roleCandidate;
|
|
397
|
+
const list = this.shadowRoot?.querySelector(`.routing-role-section[data-role="${targetRole}"] .signing-order-list`) as HTMLElement | null;
|
|
398
|
+
const listRect = list?.getBoundingClientRect();
|
|
399
|
+
const listTop = listRect?.top ?? drag.listTop;
|
|
400
|
+
const targetMemberCount = this.recipients.filter((recipient) => recipient.role === targetRole && recipient.id !== drag.recipientId).length;
|
|
401
|
+
const draggedCenterY = event.clientY - listTop - drag.grabOffsetY + drag.itemStep / 2;
|
|
402
|
+
const targetIndex = Math.round(this.clamp(draggedCenterY / drag.itemStep, 0, targetMemberCount));
|
|
403
|
+
this.signingOrderDrag = { ...drag, pointerY: event.clientY, listTop, targetRole, targetIndex };
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
private stopSigningOrderDrag = () => {
|
|
407
|
+
if (this.signingOrderDrag) {
|
|
408
|
+
this.moveRecipientToRole(this.signingOrderDrag.recipientId, this.signingOrderDrag.targetRole, this.signingOrderDrag.targetIndex);
|
|
409
|
+
}
|
|
410
|
+
this.signingOrderDrag = null;
|
|
411
|
+
window.removeEventListener('pointermove', this.handleSigningOrderPointerMove);
|
|
412
|
+
window.removeEventListener('pointerup', this.stopSigningOrderDrag);
|
|
413
|
+
window.removeEventListener('pointercancel', this.stopSigningOrderDrag);
|
|
414
|
+
};
|
|
415
|
+
|
|
252
416
|
private addFieldFromDrop(event: DragEvent) {
|
|
253
417
|
event.preventDefault();
|
|
254
418
|
const page = event.currentTarget as HTMLElement;
|
|
@@ -305,7 +469,7 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
305
469
|
</div>
|
|
306
470
|
<div class="field-editor-grid">
|
|
307
471
|
<label class="field-control full">Label<input .value=${field.label} @input=${(event: Event) => this.updateSelectedField({ label: (event.target as HTMLInputElement).value })} /></label>
|
|
308
|
-
<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.
|
|
472
|
+
<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.signingRecipients().map((recipient) => html`<option value=${String(recipient.id)}>${recipient.order}. ${recipient.name}</option>`)}</select></label>
|
|
309
473
|
<label class="field-control">X<input type="number" min="0" .value=${String(field.x)} @input=${(event: Event) => this.updateSelectedFieldNumber('x', event)} /></label>
|
|
310
474
|
<label class="field-control">Y<input type="number" min="0" .value=${String(field.y)} @input=${(event: Event) => this.updateSelectedFieldNumber('y', event)} /></label>
|
|
311
475
|
<label class="field-control">Width<input type="number" min="16" .value=${String(field.w)} @input=${(event: Event) => this.updateSelectedFieldNumber('w', event)} /></label>
|
|
@@ -323,6 +487,54 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
323
487
|
return html`${resizeHandles.map((handle) => html`<span class="resize-handle ${handle}" @pointerdown=${(event: PointerEvent) => this.startFieldResize(event, field, handle)}></span>`)}`;
|
|
324
488
|
}
|
|
325
489
|
|
|
490
|
+
private renderSigningRecipient(recipient: IRecipient, orderNumber: number, options: { overlayTop?: number; rowTop?: number; displayRole?: TRecipientRole } = {}): TemplateResult {
|
|
491
|
+
const initials = recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join('');
|
|
492
|
+
const isOverlay = options.overlayTop !== undefined;
|
|
493
|
+
const top = options.overlayTop ?? options.rowTop;
|
|
494
|
+
const displayRole = options.displayRole || recipient.role;
|
|
495
|
+
return html`<div class="recipient-line signing-recipient ${isOverlay ? 'signing-drag-overlay' : ''}" style="${top !== undefined ? `--routing-top: ${top}px;` : ''}" @contextmenu=${(event: MouseEvent) => this.openRecipientContextMenu(event, recipient)} @pointerdown=${!isOverlay ? (event: PointerEvent) => this.startSigningOrderDrag(event, recipient) : undefined}><span class="mono" style="width: 14px; font-size: 10px; color: ${isOverlay ? 'var(--accent)' : 'var(--text-muted)'};">${orderNumber}</span><span class="avatar" style="background: ${recipient.color};">${initials}</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;">${this.recipientRoleDefinition(displayRole).description}</div></div><span class="role-chip">${this.recipientRoleDefinition(displayRole).shortLabel}</span></div>`;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private renderRoleSection(roleDefinition: typeof recipientRoleDefinitions[number]): TemplateResult {
|
|
499
|
+
const role = roleDefinition.role;
|
|
500
|
+
const members = this.recipientsForRole(role);
|
|
501
|
+
const isTargetRole = this.signingOrderDrag?.targetRole === role;
|
|
502
|
+
const draggedRecipientId = this.signingOrderDrag?.recipientId;
|
|
503
|
+
const draggedRecipient = draggedRecipientId !== undefined ? this.recipients.find((recipient) => recipient.id === draggedRecipientId) : undefined;
|
|
504
|
+
|
|
505
|
+
if (!this.signingOrderDrag) {
|
|
506
|
+
return html`<div class="routing-role-section" data-role=${role}><div class="routing-role-head"><span class="routing-role-title">${roleDefinition.label}</span><span class="role-count">${members.length}</span></div><div class="routing-role-description">${roleDefinition.description}</div><div class="signing-order-list">${members.map((recipient, index) => this.renderSigningRecipient(recipient, index + 1))}</div></div>`;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const visualIndexById = new Map(members.map((recipient, index) => [recipient.id, index]));
|
|
510
|
+
const overlayTop = isTargetRole ? this.signingOrderDrag.pointerY - this.signingOrderDrag.listTop - this.signingOrderDrag.grabOffsetY : 0;
|
|
511
|
+
const draggedOrder = draggedRecipient ? (members.findIndex((recipient) => recipient.id === draggedRecipient.id) + 1 || this.signingOrderDrag.targetIndex + 1) : 0;
|
|
512
|
+
|
|
513
|
+
return html`<div class="routing-role-section ${isTargetRole ? 'active-drop' : ''}" data-role=${role}><div class="routing-role-head"><span class="routing-role-title">${roleDefinition.label}</span><span class="role-count">${members.filter((recipient) => recipient.id !== draggedRecipientId).length + (isTargetRole ? 1 : 0)}</span></div><div class="routing-role-description">${roleDefinition.description}</div><div class="signing-order-list dragging" style="--routing-list-height: ${Math.max(1, members.length) * this.signingOrderDrag.itemStep}px;">
|
|
514
|
+
${members.filter((recipient) => recipient.id !== draggedRecipientId).map((recipient) => {
|
|
515
|
+
const visualIndex = visualIndexById.get(recipient.id) ?? 0;
|
|
516
|
+
return this.renderSigningRecipient(recipient, visualIndex + 1, { rowTop: visualIndex * this.signingOrderDrag!.itemStep, displayRole: role });
|
|
517
|
+
})}
|
|
518
|
+
${isTargetRole ? html`<div class="signing-placeholder" style="--routing-top: ${this.signingOrderDrag.targetIndex * this.signingOrderDrag.itemStep}px; --routing-row-height: ${this.signingOrderDrag.itemHeight}px;"></div>` : ''}
|
|
519
|
+
${isTargetRole && draggedRecipient ? this.renderSigningRecipient(draggedRecipient, draggedOrder, { overlayTop, displayRole: role }) : ''}
|
|
520
|
+
</div></div>`;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private renderSigningOrder(): TemplateResult {
|
|
524
|
+
return html`${recipientRoleDefinitions.map((roleDefinition) => this.renderRoleSection(roleDefinition))}`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private renderRecipientContextMenu(): TemplateResult {
|
|
528
|
+
if (!this.recipientContextMenu) return html``;
|
|
529
|
+
const recipient = this.recipients.find((currentRecipient) => currentRecipient.id === this.recipientContextMenu?.recipientId);
|
|
530
|
+
if (!recipient) return html``;
|
|
531
|
+
const signerCount = this.signingRecipients().length;
|
|
532
|
+
return html`<div class="recipient-context-menu" style="left: ${this.recipientContextMenu.x}px; top: ${this.recipientContextMenu.y}px;" @click=${(event: Event) => event.stopPropagation()}>
|
|
533
|
+
<div class="recipient-context-title">${recipient.name}</div>
|
|
534
|
+
${recipientRoleDefinitions.map((roleDefinition) => html`<button class="context-action" ?disabled=${recipient.role === 'signer' && roleDefinition.role !== 'signer' && signerCount <= 1} @click=${() => { this.updateRecipientRole(recipient.id, roleDefinition.role); this.closeRecipientContextMenu(); }}>${recipient.role === roleDefinition.role ? icon('check', 12) : html`<span style="width: 12px;"></span>`}<span>${roleDefinition.label}</span></button>`)}
|
|
535
|
+
</div>`;
|
|
536
|
+
}
|
|
537
|
+
|
|
326
538
|
private renderStepper(): TemplateResult {
|
|
327
539
|
const labels = ['Upload', 'Place fields', 'Recipients & routing', 'Review & send'];
|
|
328
540
|
return html`
|
|
@@ -343,12 +555,13 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
343
555
|
${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')}` })}
|
|
344
556
|
${this.renderStepper()}
|
|
345
557
|
<div class="compose-workspace">
|
|
558
|
+
${this.renderRecipientContextMenu()}
|
|
346
559
|
<div class="palette">
|
|
347
560
|
<div class="label-upper">Drag onto document</div>
|
|
348
561
|
${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>`)}
|
|
349
562
|
<div style="height: 1px; background: var(--border-subtle); margin: 20px 0 16px;"></div>
|
|
350
563
|
<div class="label-upper">Active for</div>
|
|
351
|
-
${this.
|
|
564
|
+
${this.signingRecipients().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>`)}
|
|
352
565
|
</div>
|
|
353
566
|
<div class="document-stage">
|
|
354
567
|
<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)}>
|
|
@@ -359,8 +572,9 @@ export class SdigWorkspaceCompose extends DeesElement {
|
|
|
359
572
|
<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>
|
|
360
573
|
</div>
|
|
361
574
|
<div class="right-panel">
|
|
362
|
-
<div class="label-upper">
|
|
363
|
-
|
|
575
|
+
<div class="label-upper">Routing order · drag to reorder</div>
|
|
576
|
+
<div class="role-hint">Choose who signs, who gets the completed copy, and who is notified at every step.</div>
|
|
577
|
+
${this.renderSigningOrder()}
|
|
364
578
|
${selectedField ? this.renderFieldEditor(selectedField) : ''}
|
|
365
579
|
</div>
|
|
366
580
|
</div>
|
|
@@ -13,6 +13,7 @@ export type TWorkspaceView =
|
|
|
13
13
|
|
|
14
14
|
export type TWorkspaceTheme = 'dark' | 'light';
|
|
15
15
|
export type TDensity = 'compact' | 'comfortable';
|
|
16
|
+
export type TRecipientRole = 'signer' | 'copy' | 'updates';
|
|
16
17
|
|
|
17
18
|
export interface IDocumentRow {
|
|
18
19
|
id: string;
|
|
@@ -31,6 +32,7 @@ export interface IRecipient {
|
|
|
31
32
|
email: string;
|
|
32
33
|
color: string;
|
|
33
34
|
order: number;
|
|
35
|
+
role: TRecipientRole;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface IFieldPlacement {
|
|
@@ -55,9 +57,9 @@ export const demoDocuments: IDocumentRow[] = [
|
|
|
55
57
|
];
|
|
56
58
|
|
|
57
59
|
export const demoRecipients: IRecipient[] = [
|
|
58
|
-
{ id: 0, name: 'Sarah Chen', email: 'sarah@acme.com', color: '#60a5fa', order: 1 },
|
|
59
|
-
{ id: 1, name: 'David Park', email: 'd.park@acme.com', color: '#fbbf24', order: 2 },
|
|
60
|
-
{ id: 2, name: 'Philipp K.', email: 'philipp@lossless.com', color: '#3b82f6', order: 3 },
|
|
60
|
+
{ id: 0, name: 'Sarah Chen', email: 'sarah@acme.com', color: '#60a5fa', order: 1, role: 'signer' },
|
|
61
|
+
{ id: 1, name: 'David Park', email: 'd.park@acme.com', color: '#fbbf24', order: 2, role: 'signer' },
|
|
62
|
+
{ id: 2, name: 'Philipp K.', email: 'philipp@lossless.com', color: '#3b82f6', order: 3, role: 'updates' },
|
|
61
63
|
];
|
|
62
64
|
|
|
63
65
|
export const demoFields: IFieldPlacement[] = [
|