@signature.digital/catalog 1.3.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/.smartconfig.json +82 -0
- package/dist_bundle/bundle.js +6077 -0
- package/dist_bundle/bundle.js.map +7 -0
- package/dist_bundle/index.html +28 -0
- package/dist_ts_web/00_commitinfo_data.d.ts +8 -0
- package/dist_ts_web/00_commitinfo_data.js +9 -0
- package/dist_ts_web/elements/index.d.ts +3 -0
- package/dist_ts_web/elements/index.js +6 -0
- package/dist_ts_web/elements/sdig-signbox/index.d.ts +1 -0
- package/dist_ts_web/elements/sdig-signbox/index.js +2 -0
- package/dist_ts_web/elements/sdig-signbox/sdig-signbox.d.ts +13 -0
- package/dist_ts_web/elements/sdig-signbox/sdig-signbox.js +153 -0
- package/dist_ts_web/elements/sdig-signpad/index.d.ts +1 -0
- package/dist_ts_web/elements/sdig-signpad/index.js +2 -0
- package/dist_ts_web/elements/sdig-signpad/sdig-signpad.d.ts +22 -0
- package/dist_ts_web/elements/sdig-signpad/sdig-signpad.js +149 -0
- package/dist_ts_web/elements/sdig-workspace/index.d.ts +8 -0
- package/dist_ts_web/elements/sdig-workspace/index.js +9 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-audit.d.ts +12 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-audit.js +81 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-compose.d.ts +44 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-compose.js +418 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-developers.d.ts +12 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-developers.js +91 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-inbox.d.ts +19 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-inbox.js +181 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-placeholder.d.ts +14 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-placeholder.js +84 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-sign.d.ts +18 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-sign.js +152 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace.d.ts +31 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace.js +245 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace.shared.d.ts +53 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace.shared.js +385 -0
- package/dist_ts_web/index.d.ts +1 -0
- package/dist_ts_web/index.js +2 -0
- package/dist_ts_web/pages/index.d.ts +2 -0
- package/dist_ts_web/pages/index.js +3 -0
- package/dist_ts_web/pages/page1.d.ts +1 -0
- package/dist_ts_web/pages/page1.js +5 -0
- package/dist_ts_web/pages/page2.d.ts +1 -0
- package/dist_ts_web/pages/page2.js +5 -0
- package/dist_ts_web/plugins.d.ts +3 -0
- package/dist_ts_web/plugins.js +5 -0
- package/dist_watch/bundle.js +135319 -0
- package/dist_watch/bundle.js.map +7 -0
- package/dist_watch/index.html +28 -0
- package/license +19 -0
- package/package.json +63 -0
- package/readme.hints.md +31 -0
- package/readme.md +172 -0
- package/ts_web/00_commitinfo_data.ts +8 -0
- package/ts_web/elements/index.ts +6 -0
- package/ts_web/elements/sdig-signbox/index.ts +1 -0
- package/ts_web/elements/sdig-signbox/sdig-signbox.ts +109 -0
- package/ts_web/elements/sdig-signpad/index.ts +1 -0
- package/ts_web/elements/sdig-signpad/sdig-signpad.ts +115 -0
- package/ts_web/elements/sdig-workspace/index.ts +8 -0
- package/ts_web/elements/sdig-workspace/sdig-workspace-audit.ts +37 -0
- package/ts_web/elements/sdig-workspace/sdig-workspace-compose.ts +369 -0
- package/ts_web/elements/sdig-workspace/sdig-workspace-developers.ts +47 -0
- package/ts_web/elements/sdig-workspace/sdig-workspace-inbox.ts +117 -0
- package/ts_web/elements/sdig-workspace/sdig-workspace-placeholder.ts +22 -0
- package/ts_web/elements/sdig-workspace/sdig-workspace-sign.ts +91 -0
- package/ts_web/elements/sdig-workspace/sdig-workspace.shared.ts +440 -0
- package/ts_web/elements/sdig-workspace/sdig-workspace.ts +175 -0
- package/ts_web/index.ts +1 -0
- package/ts_web/pages/index.ts +2 -0
- package/ts_web/pages/page1.ts +5 -0
- package/ts_web/pages/page2.ts +5 -0
- package/ts_web/plugins.ts +8 -0
|
@@ -0,0 +1,369 @@
|
|
|
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';
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
interface HTMLElementTagNameMap {
|
|
6
|
+
'sdig-workspace-compose': SdigWorkspaceCompose;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type TFieldDefinition = {
|
|
11
|
+
type: IFieldPlacement['type'];
|
|
12
|
+
icon: string;
|
|
13
|
+
label: string;
|
|
14
|
+
w: number;
|
|
15
|
+
h: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type TResizeHandle = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw';
|
|
19
|
+
|
|
20
|
+
type TFieldInteraction = {
|
|
21
|
+
fieldId: string;
|
|
22
|
+
mode: 'move' | 'resize';
|
|
23
|
+
handle?: TResizeHandle;
|
|
24
|
+
startClientX: number;
|
|
25
|
+
startClientY: number;
|
|
26
|
+
startField: IFieldPlacement;
|
|
27
|
+
pageWidth: number;
|
|
28
|
+
pageHeight: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const fieldDefinitions: TFieldDefinition[] = [
|
|
32
|
+
{ type: 'signature', icon: 'sign', label: 'Signature', w: 200, h: 50 },
|
|
33
|
+
{ type: 'initials', icon: 'type', label: 'Initials', w: 120, h: 32 },
|
|
34
|
+
{ type: 'date', icon: 'calendar', label: 'Date', w: 120, h: 32 },
|
|
35
|
+
{ type: 'text', icon: 'type', label: 'Text field', w: 220, h: 32 },
|
|
36
|
+
{ type: 'check', icon: 'check', label: 'Checkbox', w: 120, h: 32 },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const resizeHandles: TResizeHandle[] = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'];
|
|
40
|
+
|
|
41
|
+
@customElement('sdig-workspace-compose')
|
|
42
|
+
export class SdigWorkspaceCompose extends DeesElement {
|
|
43
|
+
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-compose></sdig-workspace-compose>`);
|
|
44
|
+
public static demoGroups = ['Signature Digital Workspace'];
|
|
45
|
+
|
|
46
|
+
@state() private accessor step: number = 2;
|
|
47
|
+
@state() private accessor activeRecipient: number = 0;
|
|
48
|
+
@state() private accessor draggedRecipientId: number | null = null;
|
|
49
|
+
@state() private accessor selectedFieldId: string | null = null;
|
|
50
|
+
@state() private accessor recipients: IRecipient[] = [...demoRecipients];
|
|
51
|
+
@state() private accessor fields: IFieldPlacement[] = [...demoFields];
|
|
52
|
+
private draggedFieldDefinition: TFieldDefinition | null = null;
|
|
53
|
+
private draggedFieldGrabOffset: { x: number; y: number } | null = null;
|
|
54
|
+
private fieldInteraction: TFieldInteraction | null = null;
|
|
55
|
+
|
|
56
|
+
public static styles = [workspaceBaseStyles, css`
|
|
57
|
+
.stepper { height: 44px; flex-shrink: 0; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; padding: 0 24px; gap: 24px; overflow-x: auto; }
|
|
58
|
+
.step { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-muted); white-space: nowrap; background: transparent; }
|
|
59
|
+
.step-number { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; background: var(--bg-input); border: 1px solid var(--border); color: var(--text-muted); font-size: 10px; font-weight: 700; }
|
|
60
|
+
.step.active { color: var(--text); font-weight: 500; }
|
|
61
|
+
.step.active .step-number { background: var(--accent); border-color: var(--accent); color: white; }
|
|
62
|
+
.step.done .step-number { background: var(--success); border-color: var(--success); color: white; }
|
|
63
|
+
.compose-workspace { flex: 1; display: flex; overflow: hidden; }
|
|
64
|
+
.palette { width: 260px; border-right: 1px solid var(--border-subtle); padding: 16px; background: var(--bg-el); overflow-y: auto; flex-shrink: 0; }
|
|
65
|
+
.right-panel { width: 280px; border-left: 1px solid var(--border-subtle); padding: 16px; background: var(--bg-el); overflow-y: auto; flex-shrink: 0; }
|
|
66
|
+
.field-tool { width: var(--tool-w); height: var(--tool-h); display: flex; align-items: center; gap: 8px; padding: 0 10px; background: color-mix(in srgb, var(--recipient-color) 8%, var(--bg-card)); border: 1.5px dashed var(--recipient-color); border-radius: 4px; font-size: 12px; color: var(--recipient-color); margin-bottom: 8px; cursor: grab; }
|
|
67
|
+
.field-tool:active { cursor: grabbing; }
|
|
68
|
+
.field-tool span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
69
|
+
.swatch { width: 10px; height: 10px; border-radius: 2px; background: var(--recipient-color, var(--accent)); flex-shrink: 0; }
|
|
70
|
+
.document-stage { flex: 1; overflow: auto; background: hsl(0 0% 8%); display: flex; flex-direction: column; align-items: center; padding: 32px; gap: 20px; }
|
|
71
|
+
:host-context(sdig-workspace[theme='light']) .document-stage { background: hsl(0 0% 92%); }
|
|
72
|
+
.recipient-line { cursor: grab; }
|
|
73
|
+
.recipient-line.dragging { opacity: 0.45; border-color: var(--accent); }
|
|
74
|
+
.page-drop-target { outline: 1px dashed transparent; outline-offset: 8px; }
|
|
75
|
+
.page-drop-target.drag-over { outline-color: var(--accent); }
|
|
76
|
+
.field-box { user-select: none; touch-action: none; }
|
|
77
|
+
.field-box.selected { z-index: 5; cursor: move; }
|
|
78
|
+
.field-content { width: 100%; height: 100%; display: flex; align-items: center; gap: 6px; pointer-events: none; overflow: hidden; }
|
|
79
|
+
.field-content span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
80
|
+
.resize-handle { position: absolute; z-index: 2; width: 9px; height: 9px; border-radius: 50%; background: var(--bg-card); border: 1.5px solid var(--field-color); box-shadow: 0 0 0 2px var(--bg-card); touch-action: none; }
|
|
81
|
+
.resize-handle.n { top: -6px; left: 50%; transform: translateX(-50%); cursor: ns-resize; }
|
|
82
|
+
.resize-handle.ne { top: -6px; right: -6px; cursor: nesw-resize; }
|
|
83
|
+
.resize-handle.e { right: -6px; top: 50%; transform: translateY(-50%); cursor: ew-resize; }
|
|
84
|
+
.resize-handle.se { right: -6px; bottom: -6px; cursor: nwse-resize; }
|
|
85
|
+
.resize-handle.s { bottom: -6px; left: 50%; transform: translateX(-50%); cursor: ns-resize; }
|
|
86
|
+
.resize-handle.sw { left: -6px; bottom: -6px; cursor: nesw-resize; }
|
|
87
|
+
.resize-handle.w { left: -6px; top: 50%; transform: translateY(-50%); cursor: ew-resize; }
|
|
88
|
+
.resize-handle.nw { left: -6px; top: -6px; cursor: nwse-resize; }
|
|
89
|
+
.field-editor { margin-top: 16px; padding: 12px; }
|
|
90
|
+
.field-editor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
91
|
+
.field-control { display: flex; flex-direction: column; gap: 4px; font-size: 10px; color: var(--text-muted); }
|
|
92
|
+
.field-control.full { grid-column: 1 / -1; }
|
|
93
|
+
.field-control input, .field-control select { width: 100%; height: 30px; padding: 0 8px; border: 1px solid var(--border); border-radius: 5px; background: var(--bg-input); color: var(--text); font-size: 12px; outline: none; }
|
|
94
|
+
.field-control input:focus, .field-control select:focus { border-color: var(--accent); }
|
|
95
|
+
@media (max-width: 920px) { .compose-workspace { flex-direction: column; overflow: auto; } .palette, .right-panel { width: 100%; border: 0; border-bottom: 1px solid var(--border-subtle); } .document-page { width: 560px; } }
|
|
96
|
+
`];
|
|
97
|
+
|
|
98
|
+
public disconnectedCallback = async () => {
|
|
99
|
+
this.stopFieldInteraction();
|
|
100
|
+
await super.disconnectedCallback();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
private recipientColor(id: number): string {
|
|
104
|
+
return this.recipients.find((recipient) => recipient.id === id)?.color || 'var(--accent)';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fieldIcon(type: IFieldPlacement['type']): string {
|
|
108
|
+
if (type === 'signature') return 'sign';
|
|
109
|
+
if (type === 'date') return 'calendar';
|
|
110
|
+
if (type === 'check') return 'check';
|
|
111
|
+
return 'type';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private fieldDefinition(type: IFieldPlacement['type']): TFieldDefinition {
|
|
115
|
+
return fieldDefinitions.find((definition) => definition.type === type) || fieldDefinitions[0];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private clamp(value: number, min: number, max: number): number {
|
|
119
|
+
return Math.max(min, Math.min(max, value));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private updateField(fieldId: string, patch: Partial<IFieldPlacement>) {
|
|
123
|
+
this.fields = this.fields.map((field) => field.id === fieldId ? { ...field, ...patch } : field);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private updateSelectedField(patch: Partial<IFieldPlacement>) {
|
|
127
|
+
if (!this.selectedFieldId) return;
|
|
128
|
+
this.updateField(this.selectedFieldId, patch);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private updateSelectedFieldNumber(property: 'x' | 'y' | 'w' | 'h', event: Event) {
|
|
132
|
+
const value = Number((event.target as HTMLInputElement).value);
|
|
133
|
+
if (!Number.isFinite(value)) return;
|
|
134
|
+
const min = property === 'w' || property === 'h' ? 16 : 0;
|
|
135
|
+
this.updateSelectedField({ [property]: Math.max(min, Math.round(value)) } as Partial<IFieldPlacement>);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private resetSelectedFieldSize(field: IFieldPlacement) {
|
|
139
|
+
const definition = this.fieldDefinition(field.type);
|
|
140
|
+
this.updateSelectedField({ w: definition.w, h: definition.h });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private removeSelectedField() {
|
|
144
|
+
if (!this.selectedFieldId) return;
|
|
145
|
+
this.fields = this.fields.filter((field) => field.id !== this.selectedFieldId);
|
|
146
|
+
this.selectedFieldId = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private handleDocumentClick = (event: MouseEvent) => {
|
|
150
|
+
const target = event.target as HTMLElement | null;
|
|
151
|
+
if (target?.closest('.field-box')) return;
|
|
152
|
+
this.selectedFieldId = null;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
private startFieldInteraction(event: PointerEvent, field: IFieldPlacement, mode: TFieldInteraction['mode'], handle?: TResizeHandle) {
|
|
156
|
+
if (event.button !== 0) return;
|
|
157
|
+
const page = this.shadowRoot?.querySelector('.document-page') as HTMLElement | null;
|
|
158
|
+
if (!page) return;
|
|
159
|
+
const pageRect = page.getBoundingClientRect();
|
|
160
|
+
this.selectedFieldId = field.id;
|
|
161
|
+
this.fieldInteraction = {
|
|
162
|
+
fieldId: field.id,
|
|
163
|
+
mode,
|
|
164
|
+
handle,
|
|
165
|
+
startClientX: event.clientX,
|
|
166
|
+
startClientY: event.clientY,
|
|
167
|
+
startField: { ...field },
|
|
168
|
+
pageWidth: pageRect.width,
|
|
169
|
+
pageHeight: pageRect.height,
|
|
170
|
+
};
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
event.stopPropagation();
|
|
173
|
+
window.addEventListener('pointermove', this.handleFieldPointerMove, { passive: false });
|
|
174
|
+
window.addEventListener('pointerup', this.stopFieldInteraction);
|
|
175
|
+
window.addEventListener('pointercancel', this.stopFieldInteraction);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private startFieldMove(event: PointerEvent, field: IFieldPlacement) {
|
|
179
|
+
this.startFieldInteraction(event, field, 'move');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private startFieldResize(event: PointerEvent, field: IFieldPlacement, handle: TResizeHandle) {
|
|
183
|
+
this.startFieldInteraction(event, field, 'resize', handle);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private handleFieldPointerMove = (event: PointerEvent) => {
|
|
187
|
+
if (!this.fieldInteraction) return;
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
const interaction = this.fieldInteraction;
|
|
190
|
+
const dx = event.clientX - interaction.startClientX;
|
|
191
|
+
const dy = event.clientY - interaction.startClientY;
|
|
192
|
+
const start = interaction.startField;
|
|
193
|
+
|
|
194
|
+
if (interaction.mode === 'move') {
|
|
195
|
+
this.updateField(interaction.fieldId, {
|
|
196
|
+
x: Math.round(this.clamp(start.x + dx, 0, interaction.pageWidth - start.w)),
|
|
197
|
+
y: Math.round(this.clamp(start.y + dy, 0, interaction.pageHeight - start.h)),
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const minWidth = 32;
|
|
203
|
+
const minHeight = 24;
|
|
204
|
+
let x = start.x;
|
|
205
|
+
let y = start.y;
|
|
206
|
+
let w = start.w;
|
|
207
|
+
let h = start.h;
|
|
208
|
+
const handle = interaction.handle || 'se';
|
|
209
|
+
|
|
210
|
+
if (handle.includes('e')) {
|
|
211
|
+
w = this.clamp(start.w + dx, minWidth, interaction.pageWidth - start.x);
|
|
212
|
+
}
|
|
213
|
+
if (handle.includes('s')) {
|
|
214
|
+
h = this.clamp(start.h + dy, minHeight, interaction.pageHeight - start.y);
|
|
215
|
+
}
|
|
216
|
+
if (handle.includes('w')) {
|
|
217
|
+
x = this.clamp(start.x + dx, 0, start.x + start.w - minWidth);
|
|
218
|
+
w = start.x + start.w - x;
|
|
219
|
+
}
|
|
220
|
+
if (handle.includes('n')) {
|
|
221
|
+
y = this.clamp(start.y + dy, 0, start.y + start.h - minHeight);
|
|
222
|
+
h = start.y + start.h - y;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.updateField(interaction.fieldId, {
|
|
226
|
+
x: Math.round(x),
|
|
227
|
+
y: Math.round(y),
|
|
228
|
+
w: Math.round(w),
|
|
229
|
+
h: Math.round(h),
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
private stopFieldInteraction = () => {
|
|
234
|
+
this.fieldInteraction = null;
|
|
235
|
+
window.removeEventListener('pointermove', this.handleFieldPointerMove);
|
|
236
|
+
window.removeEventListener('pointerup', this.stopFieldInteraction);
|
|
237
|
+
window.removeEventListener('pointercancel', this.stopFieldInteraction);
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
private reorderRecipient(targetId: number) {
|
|
241
|
+
if (this.draggedRecipientId === null || this.draggedRecipientId === targetId) return;
|
|
242
|
+
const next = [...this.recipients];
|
|
243
|
+
const fromIndex = next.findIndex((recipient) => recipient.id === this.draggedRecipientId);
|
|
244
|
+
const toIndex = next.findIndex((recipient) => recipient.id === targetId);
|
|
245
|
+
if (fromIndex === -1 || toIndex === -1) return;
|
|
246
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
247
|
+
next.splice(toIndex, 0, moved);
|
|
248
|
+
this.recipients = next.map((recipient, index) => ({ ...recipient, order: index + 1 }));
|
|
249
|
+
this.draggedRecipientId = null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private addFieldFromDrop(event: DragEvent) {
|
|
253
|
+
event.preventDefault();
|
|
254
|
+
const page = event.currentTarget as HTMLElement;
|
|
255
|
+
page.classList.remove('drag-over');
|
|
256
|
+
const transferredType = event.dataTransfer?.getData('application/x-signature-field') as IFieldPlacement['type'];
|
|
257
|
+
if (!this.draggedFieldDefinition && !transferredType) return;
|
|
258
|
+
const definition = this.draggedFieldDefinition || this.fieldDefinition(transferredType);
|
|
259
|
+
const transferredOffset = event.dataTransfer?.getData('application/x-signature-field-offset');
|
|
260
|
+
const offset = this.draggedFieldGrabOffset || (transferredOffset ? JSON.parse(transferredOffset) as { x: number; y: number } : { x: definition.w / 2, y: definition.h / 2 });
|
|
261
|
+
const rect = page.getBoundingClientRect();
|
|
262
|
+
const x = Math.round(event.clientX - rect.left - offset.x);
|
|
263
|
+
const y = Math.round(event.clientY - rect.top - offset.y);
|
|
264
|
+
const nextField: IFieldPlacement = {
|
|
265
|
+
id: `field_${Date.now()}`,
|
|
266
|
+
type: definition.type,
|
|
267
|
+
x: Math.max(0, Math.min(Math.max(0, rect.width - definition.w), x)),
|
|
268
|
+
y: Math.max(0, Math.min(Math.max(0, rect.height - definition.h), y)),
|
|
269
|
+
w: definition.w,
|
|
270
|
+
h: definition.h,
|
|
271
|
+
page: 1,
|
|
272
|
+
recipient: this.activeRecipient,
|
|
273
|
+
label: definition.label,
|
|
274
|
+
};
|
|
275
|
+
this.fields = [...this.fields, nextField];
|
|
276
|
+
this.selectedFieldId = nextField.id;
|
|
277
|
+
this.draggedFieldDefinition = null;
|
|
278
|
+
this.draggedFieldGrabOffset = null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private startFieldToolDrag(event: DragEvent, fieldType: TFieldDefinition) {
|
|
282
|
+
const toolRect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
283
|
+
const offset = {
|
|
284
|
+
x: Math.round(event.clientX - toolRect.left),
|
|
285
|
+
y: Math.round(event.clientY - toolRect.top),
|
|
286
|
+
};
|
|
287
|
+
this.draggedFieldDefinition = fieldType;
|
|
288
|
+
this.draggedFieldGrabOffset = offset;
|
|
289
|
+
event.dataTransfer?.setData('application/x-signature-field', fieldType.type);
|
|
290
|
+
event.dataTransfer?.setData('application/x-signature-field-offset', JSON.stringify(offset));
|
|
291
|
+
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'copy';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private endFieldToolDrag() {
|
|
295
|
+
this.draggedFieldDefinition = null;
|
|
296
|
+
this.draggedFieldGrabOffset = null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private renderFieldEditor(field: IFieldPlacement): TemplateResult {
|
|
300
|
+
return html`
|
|
301
|
+
<div class="card field-editor">
|
|
302
|
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
|
|
303
|
+
<div style="font-size: 11px; font-weight: 600;">Field editor</div>
|
|
304
|
+
${pill(this.fieldDefinition(field.type).label, 'info', true)}
|
|
305
|
+
</div>
|
|
306
|
+
<div class="field-editor-grid">
|
|
307
|
+
<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.recipients.map((recipient) => html`<option value=${String(recipient.id)}>${recipient.order}. ${recipient.name}</option>`)}</select></label>
|
|
309
|
+
<label class="field-control">X<input type="number" min="0" .value=${String(field.x)} @input=${(event: Event) => this.updateSelectedFieldNumber('x', event)} /></label>
|
|
310
|
+
<label class="field-control">Y<input type="number" min="0" .value=${String(field.y)} @input=${(event: Event) => this.updateSelectedFieldNumber('y', event)} /></label>
|
|
311
|
+
<label class="field-control">Width<input type="number" min="16" .value=${String(field.w)} @input=${(event: Event) => this.updateSelectedFieldNumber('w', event)} /></label>
|
|
312
|
+
<label class="field-control">Height<input type="number" min="16" .value=${String(field.h)} @input=${(event: Event) => this.updateSelectedFieldNumber('h', event)} /></label>
|
|
313
|
+
</div>
|
|
314
|
+
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
|
315
|
+
<button class="btn outline small" style="flex: 1;" @click=${() => this.resetSelectedFieldSize(field)}>Reset size</button>
|
|
316
|
+
<button class="btn ghost small" style="color: var(--error);" @click=${() => this.removeSelectedField()}>Delete</button>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private renderResizeHandles(field: IFieldPlacement): TemplateResult {
|
|
323
|
+
return html`${resizeHandles.map((handle) => html`<span class="resize-handle ${handle}" @pointerdown=${(event: PointerEvent) => this.startFieldResize(event, field, handle)}></span>`)}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private renderStepper(): TemplateResult {
|
|
327
|
+
const labels = ['Upload', 'Place fields', 'Recipients & routing', 'Review & send'];
|
|
328
|
+
return html`
|
|
329
|
+
<div class="stepper">
|
|
330
|
+
${labels.map((label, index) => {
|
|
331
|
+
const stepNumber = index + 1;
|
|
332
|
+
return html`<button class="step ${stepNumber === this.step ? 'active' : stepNumber < this.step ? 'done' : ''}" @click=${() => this.step = stepNumber}><span class="step-number">${stepNumber < this.step ? '✓' : stepNumber}</span><span>${label}</span>${index < labels.length - 1 ? html`<span style="width: 24px; height: 1px; background: var(--border); margin-left: 8px;"></span>` : ''}</button>`;
|
|
333
|
+
})}
|
|
334
|
+
<div style="flex: 1;"></div><span class="mono" style="font-size: 11px; color: var(--text-muted);">doc_8mK3pL · 14 pages · 2.4 MB</span>
|
|
335
|
+
</div>
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
public render(): TemplateResult {
|
|
340
|
+
const selectedField = this.fields.find((field) => field.id === this.selectedFieldId);
|
|
341
|
+
|
|
342
|
+
return html`
|
|
343
|
+
${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
|
+
${this.renderStepper()}
|
|
345
|
+
<div class="compose-workspace">
|
|
346
|
+
<div class="palette">
|
|
347
|
+
<div class="label-upper">Drag onto document</div>
|
|
348
|
+
${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
|
+
<div style="height: 1px; background: var(--border-subtle); margin: 20px 0 16px;"></div>
|
|
350
|
+
<div class="label-upper">Active for</div>
|
|
351
|
+
${this.recipients.map((recipient) => html`<div class="recipient-line ${this.activeRecipient === recipient.id ? 'active' : ''}" @click=${() => this.activeRecipient = recipient.id}><span class="swatch" style="--recipient-color: ${recipient.color};"></span><span style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${recipient.name.split(' ')[0]}</span><span class="mono" style="font-size: 10px; color: var(--text-muted);">${this.fields.filter((field) => field.recipient === recipient.id).length}</span></div>`)}
|
|
352
|
+
</div>
|
|
353
|
+
<div class="document-stage">
|
|
354
|
+
<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)}>
|
|
355
|
+
${fakeDocument()}
|
|
356
|
+
${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>`)}
|
|
357
|
+
<div class="mono" style="position: absolute; bottom: 12px; right: 16px; font-size: 9px; color: hsl(0 0% 60%);">Page 1 of 14</div>
|
|
358
|
+
</div>
|
|
359
|
+
<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
|
+
</div>
|
|
361
|
+
<div class="right-panel">
|
|
362
|
+
<div class="label-upper">Signing order · drag to reorder</div>
|
|
363
|
+
${this.recipients.map((recipient) => html`<div class="recipient-line ${this.draggedRecipientId === recipient.id ? 'dragging' : ''}" draggable="true" @dragstart=${() => this.draggedRecipientId = recipient.id} @dragover=${(event: DragEvent) => event.preventDefault()} @drop=${() => this.reorderRecipient(recipient.id)} @dragend=${() => this.draggedRecipientId = null}><span class="mono" style="width: 14px; font-size: 10px; color: var(--text-muted);">${recipient.order}</span><span class="avatar" style="background: ${recipient.color};">${recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join('')}</span><div style="flex: 1; min-width: 0;"><div style="font-size: 12px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${recipient.name}</div><div class="mono" style="font-size: 10px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${recipient.email}</div></div>${icon('more', 12)}</div>`)}
|
|
364
|
+
${selectedField ? this.renderFieldEditor(selectedField) : ''}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DeesElement, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
|
2
|
+
import { actionButton, icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js';
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
interface HTMLElementTagNameMap {
|
|
6
|
+
'sdig-workspace-developers': SdigWorkspaceDevelopers;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@customElement('sdig-workspace-developers')
|
|
11
|
+
export class SdigWorkspaceDevelopers extends DeesElement {
|
|
12
|
+
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-developers></sdig-workspace-developers>`);
|
|
13
|
+
public static demoGroups = ['Signature Digital Workspace'];
|
|
14
|
+
|
|
15
|
+
public static styles = [workspaceBaseStyles, css`
|
|
16
|
+
.developer-grid { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 20px; }
|
|
17
|
+
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
|
|
18
|
+
.metric-card { padding: 14px; }
|
|
19
|
+
.metric-value { font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
|
|
20
|
+
pre.code { margin: 0; padding: 16px; background: var(--bg-el); border: 1px solid var(--border-subtle); border-radius: 6px; font-size: 12px; line-height: 1.7; color: var(--text-sec); overflow: auto; }
|
|
21
|
+
@media (max-width: 920px) { .developer-grid, .stats-grid { grid-template-columns: 1fr; } }
|
|
22
|
+
`];
|
|
23
|
+
|
|
24
|
+
public render(): TemplateResult {
|
|
25
|
+
return html`
|
|
26
|
+
${topBar({ breadcrumb: ['signature.digital', 'Developers'], title: 'Developers', subtitle: pill('API · v0.42', 'info', true), actions: html`${actionButton('View on GitHub', 'outline', 'github')}${actionButton('New API key', 'primary', 'plus')}` })}
|
|
27
|
+
<div class="content-scroll developer-grid">
|
|
28
|
+
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
29
|
+
<div class="card" style="padding: 20px;"><div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px;"><span style="font-size: 14px; font-weight: 600;">Send a document in 8 lines</span><span class="pill">node</span></div><pre class="code mono">import { Signature } from '@signature.digital/sdk';
|
|
30
|
+
|
|
31
|
+
const sig = new Signature(process.env.SIGD_KEY);
|
|
32
|
+
|
|
33
|
+
await sig.documents.send({
|
|
34
|
+
file: './msa.pdf',
|
|
35
|
+
recipients: [{ name: 'Sarah Chen', email: 'sarah@acme.com' }],
|
|
36
|
+
fields: 'auto',
|
|
37
|
+
});</pre></div>
|
|
38
|
+
<div class="stats-grid">${[['Requests this month', '14,892', '+8.2%'], ['P95 latency', '142ms', '-12ms'], ['Error rate', '0.04%', '✓']].map((metric) => html`<div class="card metric-card"><div class="metric-value">${metric[1]}</div><div style="font-size: 10px; color: var(--text-muted); margin-top: 2px;">${metric[0]}</div><div class="mono" style="font-size: 10px; color: var(--success); margin-top: 6px;">${metric[2]}</div></div>`)}</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
41
|
+
<div class="card" style="padding: 16px;"><div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">${icon('github', 14)}<span style="font-size: 12px; font-weight: 600;">signature-digital/core</span></div><div style="font-size: 11px; color: var(--text-muted); line-height: 1.5;">MIT-licensed. Self-host on your own infra, or use signature.digital cloud.</div></div>
|
|
42
|
+
<div class="card" style="padding: 16px;"><div class="label-upper">Self-host status</div>${[['Docker image', 'ghcr.io/signature-digital'], ['Helm chart', 'v0.42.1'], ['Postgres ≥ 14', 'required'], ['S3-compatible', 'optional']].map((row) => html`<div style="display: flex; justify-content: space-between; font-size: 11px; margin-bottom: 6px;"><span style="color: var(--text-muted);">${row[0]}</span><span class="mono" style="color: var(--text-sec);">${row[1]}</span></div>`)}</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { DeesElement, property, state, html, customElement, type TemplateResult, css } from '@design.estate/dees-element';
|
|
2
|
+
import { actionButton, demoDocuments, icon, pill, requestWorkspaceView, topBar, workspaceBaseStyles, type IDocumentRow, type TDensity } from './sdig-workspace.shared.js';
|
|
3
|
+
import { workspaceDemoFrame } from './sdig-workspace.shared.js';
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface HTMLElementTagNameMap {
|
|
7
|
+
'sdig-workspace-inbox': SdigWorkspaceInbox;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@customElement('sdig-workspace-inbox')
|
|
12
|
+
export class SdigWorkspaceInbox extends DeesElement {
|
|
13
|
+
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-inbox></sdig-workspace-inbox>`);
|
|
14
|
+
public static demoGroups = ['Signature Digital Workspace'];
|
|
15
|
+
|
|
16
|
+
@property({ type: String }) public accessor density: TDensity = 'comfortable';
|
|
17
|
+
@state() private accessor filter: string = 'all';
|
|
18
|
+
@state() private accessor search: string = '';
|
|
19
|
+
|
|
20
|
+
public static styles = [workspaceBaseStyles, css`
|
|
21
|
+
.filterbar { padding: 14px 24px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; gap: 8px; }
|
|
22
|
+
.searchbox { display: flex; align-items: center; gap: 8px; padding: 0 10px; height: 32px; width: 280px; background: var(--bg-input); border: 1px solid var(--border); border-radius: 6px; }
|
|
23
|
+
.searchbox input { flex: 1; min-width: 0; background: transparent; border: none; outline: none; color: var(--text); font-size: 12px; }
|
|
24
|
+
.segmented { display: flex; gap: 2px; padding: 2px; background: var(--bg-el); border-radius: 6px; border: 1px solid var(--border-subtle); }
|
|
25
|
+
.segmented button { padding: 4px 10px; font-size: 11px; font-weight: 500; border-radius: 4px; background: transparent; color: var(--text-muted); display: inline-flex; align-items: center; gap: 5px; }
|
|
26
|
+
.segmented button.active { background: var(--bg-card); color: var(--text); box-shadow: inset 0 0 0 1px var(--border); }
|
|
27
|
+
.doc-table { min-width: 880px; }
|
|
28
|
+
.doc-head, .doc-row { display: grid; grid-template-columns: 32px minmax(220px,2.4fr) 150px 160px 90px 60px 32px; align-items: center; gap: 14px; padding: 0 16px; }
|
|
29
|
+
.doc-head { height: 36px; border-bottom: 1px solid var(--border-subtle); color: var(--text-dim); font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
30
|
+
.doc-row { height: 60px; border-bottom: 1px solid var(--border-subtle); cursor: pointer; transition: background 0.1s ease; }
|
|
31
|
+
.doc-row.compact { height: 48px; }
|
|
32
|
+
.doc-row:last-child { border-bottom: 0; }
|
|
33
|
+
.doc-row:hover { background: var(--row-hover); }
|
|
34
|
+
.doc-icon { width: 28px; height: 32px; border-radius: 4px; background: var(--bg-input); border: 1px solid var(--border); display: flex; align-items: center; justify-content: center; }
|
|
35
|
+
.doc-title { font-size: 13px; color: var(--text); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
36
|
+
.doc-meta { margin-top: 2px; font-size: 11px; color: var(--text-muted); }
|
|
37
|
+
.recipient-stack { display: flex; align-items: center; }
|
|
38
|
+
.recipient-dot { width: 22px; height: 22px; border-radius: 50%; background: var(--bg-input); border: 1.5px solid var(--border); margin-left: -6px; font-size: 9px; font-weight: 600; color: var(--text-sec); display: flex; align-items: center; justify-content: center; }
|
|
39
|
+
.recipient-dot:first-child { margin-left: 0; }
|
|
40
|
+
.recipient-dot.signed { border-color: var(--success); color: var(--success); background: var(--bg-el); }
|
|
41
|
+
.stats-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-top: 24px; }
|
|
42
|
+
.metric-card { padding: 16px; }
|
|
43
|
+
.metric-value { font-family: 'Plus Jakarta Sans', Inter, sans-serif; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
|
|
44
|
+
@media (max-width: 920px) { .filterbar { padding: 12px 16px; display: block; } .searchbox { width: 100%; margin-bottom: 10px; } .stats-grid { grid-template-columns: 1fr; } }
|
|
45
|
+
`];
|
|
46
|
+
|
|
47
|
+
private get filteredDocuments(): IDocumentRow[] {
|
|
48
|
+
return demoDocuments
|
|
49
|
+
.filter((doc) => this.filter === 'all' || doc.status === this.filter)
|
|
50
|
+
.filter((doc) => !this.search || doc.title.toLowerCase().includes(this.search.toLowerCase()));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private statusPill(status: IDocumentRow['status']): TemplateResult {
|
|
54
|
+
const map = {
|
|
55
|
+
awaiting: ['warning', 'awaiting signature'],
|
|
56
|
+
signed: ['success', 'completed'],
|
|
57
|
+
draft: ['default', 'draft'],
|
|
58
|
+
declined: ['error', 'declined'],
|
|
59
|
+
} as const;
|
|
60
|
+
const [tone, label] = map[status];
|
|
61
|
+
return pill(label, tone, true);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private openDocument(doc: IDocumentRow) {
|
|
65
|
+
requestWorkspaceView(this, doc.status === 'signed' ? 'audit' : 'sign');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public render(): TemplateResult {
|
|
69
|
+
const filters = [
|
|
70
|
+
{ id: 'all', label: 'All', count: demoDocuments.length },
|
|
71
|
+
{ id: 'awaiting', label: 'Awaiting', count: demoDocuments.filter((doc) => doc.status === 'awaiting').length },
|
|
72
|
+
{ id: 'signed', label: 'Completed', count: demoDocuments.filter((doc) => doc.status === 'signed').length },
|
|
73
|
+
{ id: 'draft', label: 'Drafts', count: demoDocuments.filter((doc) => doc.status === 'draft').length },
|
|
74
|
+
{ id: 'declined', label: 'Declined', count: demoDocuments.filter((doc) => doc.status === 'declined').length },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
return html`
|
|
78
|
+
${topBar({
|
|
79
|
+
breadcrumb: ['signature.digital', 'Lossless GmbH', 'Inbox'],
|
|
80
|
+
title: 'Inbox',
|
|
81
|
+
subtitle: pill(`${demoDocuments.filter((doc) => doc.status === 'awaiting').length} need attention`, 'info'),
|
|
82
|
+
actions: html`${actionButton('Import', 'outline', 'upload')}${actionButton('New document', 'primary', 'plus', () => requestWorkspaceView(this, 'compose'))}`,
|
|
83
|
+
})}
|
|
84
|
+
<div class="filterbar">
|
|
85
|
+
<div class="searchbox">${icon('search', 13)}<input .value=${this.search} @input=${(event: Event) => this.search = (event.target as HTMLInputElement).value} placeholder="Search documents, recipients, IDs..." /><span class="mono" style="font-size: 10px; color: var(--text-dim); border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px;">⌘K</span></div>
|
|
86
|
+
<div style="flex: 1;"></div>
|
|
87
|
+
<div class="segmented">${filters.map((filter) => html`<button class=${this.filter === filter.id ? 'active' : ''} @click=${() => this.filter = filter.id}>${filter.label}<span class="mono" style="color: var(--text-dim);">${filter.count}</span></button>`)}</div>
|
|
88
|
+
</div>
|
|
89
|
+
<div class="content-scroll">
|
|
90
|
+
<div class="card" style="overflow-x: auto;">
|
|
91
|
+
<div class="doc-table">
|
|
92
|
+
<div class="doc-head"><span></span><span>Document</span><span>Status</span><span>Recipients</span><span>Deadline</span><span style="text-align: right;">Pages</span><span></span></div>
|
|
93
|
+
${this.filteredDocuments.map((doc) => html`
|
|
94
|
+
<div class="doc-row ${this.density === 'compact' ? 'compact' : ''}" @click=${() => this.openDocument(doc)}>
|
|
95
|
+
<div class="doc-icon">${icon('file', 14)}</div>
|
|
96
|
+
<div style="min-width: 0;"><div class="doc-title">${doc.title}</div><div class="doc-meta mono">${doc.id} · ${doc.sender} · ${doc.updated}</div></div>
|
|
97
|
+
<div>${this.statusPill(doc.status)}</div>
|
|
98
|
+
<div style="display: flex; align-items: center; gap: 8px;"><div class="recipient-stack">${doc.recipients.slice(0, 4).map((recipient) => html`<span class="recipient-dot ${recipient.signed ? 'signed' : ''}" title=${recipient.name}>${recipient.initials}</span>`)}</div><span style="font-size: 11px; color: var(--text-muted);">${doc.recipients.filter((recipient) => recipient.signed).length}/${doc.recipients.length}</span></div>
|
|
99
|
+
<div class="mono" style="font-size: 11px; color: ${doc.deadline && doc.status === 'awaiting' ? 'var(--warning)' : 'var(--text-dim)'};">${doc.deadline ? html`${icon('clock', 11)} ${doc.deadline}` : '—'}</div>
|
|
100
|
+
<div class="mono" style="font-size: 11px; color: var(--text-muted); text-align: right;">${doc.pages}</div>
|
|
101
|
+
<div>${icon('more', 14)}</div>
|
|
102
|
+
</div>
|
|
103
|
+
`)}
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="stats-grid">
|
|
107
|
+
${[
|
|
108
|
+
{ label: 'Sent this month', value: '127', delta: '+24%', icon: 'send' },
|
|
109
|
+
{ label: 'Avg time to sign', value: '4.2h', delta: '-18%', icon: 'clock' },
|
|
110
|
+
{ label: 'Completion rate', value: '94.1%', delta: '+2.1%', icon: 'check' },
|
|
111
|
+
{ label: 'API signatures', value: '2,481', delta: '+312', icon: 'code' },
|
|
112
|
+
].map((metric) => html`<div class="card metric-card"><div style="display: flex; justify-content: space-between; margin-bottom: 12px;">${icon(metric.icon, 14)}<span class="mono" style="font-size: 10px; color: var(--success);">${metric.delta}</span></div><div class="metric-value">${metric.value}</div><div style="font-size: 11px; color: var(--text-muted); margin-top: 2px;">${metric.label}</div></div>`)}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DeesElement, property, html, customElement, type TemplateResult } from '@design.estate/dees-element';
|
|
2
|
+
import { icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js';
|
|
3
|
+
|
|
4
|
+
declare global {
|
|
5
|
+
interface HTMLElementTagNameMap {
|
|
6
|
+
'sdig-workspace-placeholder': SdigWorkspacePlaceholder;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@customElement('sdig-workspace-placeholder')
|
|
11
|
+
export class SdigWorkspacePlaceholder extends DeesElement {
|
|
12
|
+
public static demo = () => workspaceDemoFrame(html`<sdig-workspace-placeholder label="Templates" subtitle="Reusable agreement templates"></sdig-workspace-placeholder>`);
|
|
13
|
+
public static demoGroups = ['Signature Digital Workspace'];
|
|
14
|
+
|
|
15
|
+
@property({ type: String }) public accessor label: string = 'Section';
|
|
16
|
+
@property({ type: String }) public accessor subtitle: string = 'Coming soon';
|
|
17
|
+
public static styles = [workspaceBaseStyles];
|
|
18
|
+
|
|
19
|
+
public render(): TemplateResult {
|
|
20
|
+
return html`${topBar({ breadcrumb: ['signature.digital', this.label], title: this.label, subtitle: pill('coming soon') })}<div class="content-scroll" style="display: flex; align-items: center; justify-content: center; flex-direction: column; color: var(--text-muted); gap: 8px;">${icon('folder', 32)}<div style="font-size: 13px; color: var(--text-sec);">${this.label}</div><div style="font-size: 11px;">${this.subtitle}</div></div>`;
|
|
21
|
+
}
|
|
22
|
+
}
|