@signature.digital/catalog 1.7.0 → 1.9.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.
Files changed (45) hide show
  1. package/dist_ts_web/00_commitinfo_data.js +3 -3
  2. package/dist_ts_web/elements/index.d.ts +1 -0
  3. package/dist_ts_web/elements/index.js +3 -1
  4. package/dist_ts_web/elements/sdig-app-shell/index.d.ts +1 -0
  5. package/dist_ts_web/elements/sdig-app-shell/index.js +2 -0
  6. package/dist_ts_web/elements/sdig-app-shell/sdig-app-shell.d.ts +38 -0
  7. package/dist_ts_web/elements/sdig-app-shell/sdig-app-shell.js +309 -0
  8. package/dist_ts_web/elements/sdig-signbox/sdig-signbox.d.ts +1 -0
  9. package/dist_ts_web/elements/sdig-signbox/sdig-signbox.js +28 -9
  10. package/dist_ts_web/elements/sdig-signpad/sdig-signpad.d.ts +9 -2
  11. package/dist_ts_web/elements/sdig-signpad/sdig-signpad.js +23 -5
  12. package/dist_ts_web/elements/sdig-workspace/sdig-workspace-audit.js +5 -5
  13. package/dist_ts_web/elements/sdig-workspace/sdig-workspace-compose.d.ts +1 -0
  14. package/dist_ts_web/elements/sdig-workspace/sdig-workspace-compose.js +18 -6
  15. package/dist_ts_web/elements/sdig-workspace/sdig-workspace-developers.js +13 -15
  16. package/dist_ts_web/elements/sdig-workspace/sdig-workspace-inbox.js +14 -13
  17. package/dist_ts_web/elements/sdig-workspace/sdig-workspace-sign.d.ts +3 -0
  18. package/dist_ts_web/elements/sdig-workspace/sdig-workspace-sign.js +55 -18
  19. package/dist_ts_web/elements/sdig-workspace/sdig-workspace.js +28 -28
  20. package/dist_ts_web/elements/sdig-workspace/sdig-workspace.shared.js +44 -42
  21. package/dist_ts_web/plugins.d.ts +3 -3
  22. package/dist_ts_web/plugins.js +3 -4
  23. package/package.json +15 -21
  24. package/readme.md +54 -2
  25. package/ts_web/00_commitinfo_data.ts +2 -2
  26. package/ts_web/elements/index.ts +3 -0
  27. package/ts_web/elements/sdig-app-shell/index.ts +1 -0
  28. package/ts_web/elements/sdig-app-shell/sdig-app-shell.ts +200 -0
  29. package/ts_web/elements/sdig-signbox/sdig-signbox.ts +26 -8
  30. package/ts_web/elements/sdig-signpad/sdig-signpad.ts +27 -6
  31. package/ts_web/elements/sdig-workspace/sdig-workspace-audit.ts +4 -4
  32. package/ts_web/elements/sdig-workspace/sdig-workspace-compose.ts +16 -5
  33. package/ts_web/elements/sdig-workspace/sdig-workspace-developers.ts +12 -14
  34. package/ts_web/elements/sdig-workspace/sdig-workspace-inbox.ts +13 -12
  35. package/ts_web/elements/sdig-workspace/sdig-workspace-sign.ts +53 -17
  36. package/ts_web/elements/sdig-workspace/sdig-workspace.shared.ts +44 -41
  37. package/ts_web/elements/sdig-workspace/sdig-workspace.ts +27 -27
  38. package/ts_web/plugins.ts +3 -4
  39. package/.smartconfig.json +0 -82
  40. package/dist_bundle/bundle.js +0 -6214
  41. package/dist_bundle/bundle.js.map +0 -7
  42. package/dist_bundle/index.html +0 -28
  43. package/dist_watch/bundle.js +0 -135739
  44. package/dist_watch/bundle.js.map +0 -7
  45. package/dist_watch/index.html +0 -28
package/readme.md CHANGED
@@ -28,6 +28,14 @@ Render the product workspace:
28
28
  <sdig-workspace accent="#3b82f6" density="comfortable" theme="dark"></sdig-workspace>
29
29
  ```
30
30
 
31
+ Render the host-controlled application shell:
32
+
33
+ ```html
34
+ <sdig-app-shell workspace-name="Example workspace" active-view="inbox">
35
+ <your-application-view></your-application-view>
36
+ </sdig-app-shell>
37
+ ```
38
+
31
39
  Render a standalone signature box:
32
40
 
33
41
  ```html
@@ -38,6 +46,7 @@ Render a standalone signature box:
38
46
 
39
47
  | Component | Tag | Purpose |
40
48
  |-----------|-----|---------|
49
+ | `SdigAppShell` | `<sdig-app-shell>` | Host-controlled responsive application chrome with a content slot and navigation intents |
41
50
  | `SdigWorkspace` | `<sdig-workspace>` | Product workspace shell with inbox, compose, signing, audit, developer, template, team, and settings views |
42
51
  | `SignBox` | `<sdig-signbox>` | Complete signature capture UI with clear, undo, and submit actions |
43
52
  | `SignPad` | `<sdig-signpad>` | Canvas-based signature capture primitive backed by `signature_pad` |
@@ -90,6 +99,45 @@ workspace?.addEventListener('view-change', (event) => {
90
99
 
91
100
  Child views can request navigation by dispatching `workspace-view-request` with `{ view }` in `detail`. The event bubbles and crosses the shadow boundary.
92
101
 
102
+ ## Controlled Application Shell
103
+
104
+ `sdig-app-shell` supplies presentation only. The host application owns routes, sessions, authorization, persistence, document state, and the slotted view. Set `navigationItems` as a property and respond to the composed `navigation-request` and `signout-request` events.
105
+
106
+ | Property | Type | Default | Description |
107
+ |----------|------|---------|-------------|
108
+ | `accent` | `string` | `#007aff` | Application accent color |
109
+ | `activeView` | `string` | `inbox` | ID of the active host view |
110
+ | `workspaceName` | `string` | `Workspace` | Visible workspace label |
111
+ | `deploymentLabel` | `string` | `Host configured` | Visible deployment label |
112
+ | `actorName` | `string` | `Current user` | Current actor label |
113
+ | `actorEmail` | `string` | empty | Current actor email label |
114
+ | `documentCount` | `number` | `0` | Factual document total shown in the status bar |
115
+ | `statusLabel` | `string` | `host controlled` | Host-supplied status text |
116
+ | `productLabel` | `string` | empty | Optional host-supplied product text |
117
+ | `stackLabel` | `string` | `Application services` | Host-supplied service-stack heading |
118
+ | `stackDescription` | `string` | `Configured by host application` | Host-supplied service-stack detail |
119
+ | `navigationItems` | `ISdigShellNavigationItem[]` | Inbox, Compose, Audit Trail | Host-owned navigation model |
120
+
121
+ ```typescript
122
+ const shell = document.querySelector('sdig-app-shell');
123
+
124
+ if (shell) {
125
+ shell.navigationItems = [
126
+ { id: 'inbox', label: 'Inbox', icon: 'inbox', count: 4 },
127
+ { id: 'compose', label: 'Compose', icon: 'plus' },
128
+ ];
129
+ }
130
+
131
+ shell?.addEventListener('navigation-request', (event) => {
132
+ const { view } = (event as CustomEvent<{ view: string }>).detail;
133
+ // Route using host application state.
134
+ });
135
+
136
+ shell?.addEventListener('signout-request', () => {
137
+ // End the host-owned session.
138
+ });
139
+ ```
140
+
93
141
  ## Signature Box
94
142
 
95
143
  Use `sdig-signbox` when you want the default capture UI with a heading, a signature pad, and built-in action controls:
@@ -102,12 +150,16 @@ Use `sdig-signbox` when you want the default capture UI with a heading, a signat
102
150
  const signbox = document.querySelector('sdig-signbox');
103
151
 
104
152
  signbox?.addEventListener('signature', (event) => {
105
- const { signature } = (event as CustomEvent<{ signature: unknown[] }>).detail;
153
+ const { signature, canvasWidth, canvasHeight } = (event as CustomEvent<{
154
+ signature: unknown[];
155
+ canvasWidth: number;
156
+ canvasHeight: number;
157
+ }>).detail;
106
158
  console.log(signature);
107
159
  });
108
160
  ```
109
161
 
110
- The `signature` event detail contains the `signature_pad` stroke data returned by `sdig-signpad.toData()`.
162
+ The `signature` event detail contains the `signature_pad` stroke data returned by `sdig-signpad.toData()` plus the actual responsive canvas dimensions used to capture it. Existing consumers that only read `signature` remain compatible.
111
163
 
112
164
  ## Signature Pad
113
165
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@signature.digital/catalog',
6
- version: '1.7.0',
7
- description: 'A comprehensive catalog of customizable web components designed for building and managing e-signature applications.'
6
+ version: '1.9.0',
7
+ description: 'Reusable web components for browser-based signature capture and host-controlled e-signature application surfaces.'
8
8
  }
@@ -3,5 +3,8 @@ export * from './sdig-contextmenu/index.js';
3
3
  export * from './sdig-signbox/index.js';
4
4
  export * from './sdig-signpad/index.js';
5
5
 
6
+ // Controlled product application primitives
7
+ export * from './sdig-app-shell/index.js';
8
+
6
9
  // Product workspace component
7
10
  export * from './sdig-workspace/index.js';
@@ -0,0 +1 @@
1
+ export * from './sdig-app-shell.js';
@@ -0,0 +1,200 @@
1
+ import {
2
+ DeesElement,
3
+ property,
4
+ html,
5
+ customElement,
6
+ type TemplateResult,
7
+ css,
8
+ } from '@design.estate/dees-element';
9
+
10
+ import { icon } from '../sdig-workspace/sdig-workspace.shared.js';
11
+
12
+ export interface ISdigShellNavigationItem {
13
+ id: string;
14
+ label: string;
15
+ icon: string;
16
+ count?: number;
17
+ disabled?: boolean;
18
+ title?: string;
19
+ }
20
+
21
+ export interface ISdigShellNavigationRequestDetail {
22
+ view: string;
23
+ }
24
+
25
+ declare global {
26
+ interface HTMLElementTagNameMap {
27
+ 'sdig-app-shell': SdigAppShell;
28
+ }
29
+ }
30
+
31
+ @customElement('sdig-app-shell')
32
+ export class SdigAppShell extends DeesElement {
33
+ public static demo = () => html`
34
+ <sdig-app-shell
35
+ workspace-name="Example workspace"
36
+ deployment-label="Self-hosted · single instance"
37
+ actor-name="Workspace owner"
38
+ actor-email="owner@example.test"
39
+ active-view="inbox"
40
+ status-label="self-hosted"
41
+ product-label="Open Source · MIT"
42
+ stack-label="foss.global stack"
43
+ stack-description="Lit · TypedRequest · SmartData · SmartBucket · MongoDB · S3"
44
+ .documentCount=${4}
45
+ style="height: 720px"
46
+ >
47
+ <div style="height:100%;display:grid;place-items:center;color:var(--text-muted)">Controlled application content</div>
48
+ </sdig-app-shell>
49
+ `;
50
+ public static demoGroups = ['Signature Digital Application'];
51
+
52
+ @property({ type: String }) public accessor accent = '#007aff';
53
+ @property({ type: String, attribute: 'active-view' }) public accessor activeView = 'inbox';
54
+ @property({ type: String, attribute: 'workspace-name' }) public accessor workspaceName = 'Workspace';
55
+ @property({ type: String, attribute: 'deployment-label' }) public accessor deploymentLabel = 'Host configured';
56
+ @property({ type: String, attribute: 'actor-name' }) public accessor actorName = 'Current user';
57
+ @property({ type: String, attribute: 'actor-email' }) public accessor actorEmail = '';
58
+ @property({ type: Number, attribute: 'document-count' }) public accessor documentCount = 0;
59
+ @property({ type: String, attribute: 'status-label' }) public accessor statusLabel = 'host controlled';
60
+ @property({ type: String, attribute: 'product-label' }) public accessor productLabel = '';
61
+ @property({ type: String, attribute: 'stack-label' }) public accessor stackLabel = 'Application services';
62
+ @property({ type: String, attribute: 'stack-description' }) public accessor stackDescription = 'Configured by host application';
63
+ @property({ attribute: false }) public accessor navigationItems: ISdigShellNavigationItem[] = [
64
+ { id: 'inbox', label: 'Inbox', icon: 'inbox' },
65
+ { id: 'compose', label: 'Compose', icon: 'plus' },
66
+ { id: 'audit', label: 'Audit Trail', icon: 'shield' },
67
+ ];
68
+
69
+ public static styles = css`
70
+ :host {
71
+ --accent: #007aff;
72
+ --bg: hsl(0 0% 100%);
73
+ --bg-el: hsl(240 9% 96.5%);
74
+ --bg-card: hsl(0 0% 100%);
75
+ --sidebar: hsl(240 11% 96%);
76
+ --border: hsl(240 6% 89%);
77
+ --border-subtle: hsl(240 6% 92.5%);
78
+ --text: hsl(240 6% 11%);
79
+ --text-sec: hsl(240 4% 34%);
80
+ --text-muted: hsl(240 3% 52%);
81
+ --text-dim: hsl(240 3% 66%);
82
+ --success: #34c759;
83
+ --hover: rgba(0, 0, 0, 0.05);
84
+ --ui: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
85
+ --display: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
86
+ --mono: ui-monospace, "SF Mono", Menlo, "Cascadia Mono", monospace;
87
+ display: block;
88
+ container-type: inline-size;
89
+ width: 100%;
90
+ min-height: 100vh;
91
+ color: var(--text);
92
+ background: var(--bg);
93
+ font-family: var(--ui);
94
+ -webkit-font-smoothing: antialiased;
95
+ }
96
+
97
+ * { box-sizing: border-box; }
98
+ button { font: inherit; cursor: pointer; }
99
+ button:disabled { cursor: not-allowed; opacity: .42; }
100
+ .shell { min-height: inherit; height: 100%; display: grid; grid-template-columns: 224px minmax(0, 1fr); background: var(--bg); }
101
+ .sidebar { height: 100%; min-height: inherit; display: flex; flex-direction: column; border-right: 1px solid var(--border-subtle); background: color-mix(in srgb, var(--sidebar) 94%, transparent); backdrop-filter: saturate(180%) blur(20px); }
102
+ .traffic { display: flex; gap: 8px; padding: 13px 16px 0; }
103
+ .traffic span { width: 12px; height: 12px; border-radius: 50%; box-shadow: inset 0 0 0 .5px rgba(0,0,0,.12); }
104
+ .traffic span:nth-child(1) { background: #ff5f57; }
105
+ .traffic span:nth-child(2) { background: #febc2e; }
106
+ .traffic span:nth-child(3) { background: #28c840; }
107
+ .brand { display: flex; align-items: center; gap: 8px; padding: 12px 16px; }
108
+ .mark { width: 26px; height: 26px; display: grid; place-items: center; position: relative; border: .5px solid rgba(0,0,0,.15); border-radius: 7px; color: white; background: linear-gradient(160deg, var(--accent), color-mix(in srgb, var(--accent) 70%, black)); box-shadow: 0 1px 2px rgba(0,0,0,.18), inset 0 1px 0 rgba(255,255,255,.25); font-family: var(--display); font-size: 13px; font-weight: 700; }
109
+ .mark::after { content: ''; width: 4px; height: 4px; position: absolute; right: 5px; bottom: 5px; border-radius: 50%; background: rgba(255,255,255,.95); }
110
+ .wordmark { font-size: 13px; font-weight: 500; letter-spacing: -.02em; white-space: nowrap; }
111
+ .wordmark .dot { color: var(--text-muted); }
112
+ .wordmark::after { content: ''; width: 4px; height: 4px; display: inline-block; margin-left: 3px; border-radius: 50%; background: var(--accent); transform: translateY(-1px); }
113
+ .workspace { display: flex; align-items: center; gap: 8px; margin: 0 12px 8px; padding: 7px 10px; border: 1px solid var(--border); border-radius: 9px; background: var(--bg-card); box-shadow: 0 1px 1.5px rgba(0,0,0,.05); }
114
+ .workspace-badge { width: 20px; height: 20px; display: grid; place-items: center; flex: none; border-radius: 5px; color: white; background: linear-gradient(135deg, var(--accent), #af52de); font-size: 10px; font-weight: 700; }
115
+ .workspace strong { display: block; overflow: hidden; font-size: 12px; font-weight: 500; line-height: 1.2; text-overflow: ellipsis; white-space: nowrap; }
116
+ .workspace small { display: block; margin-top: 1px; color: var(--text-muted); font-size: 10px; }
117
+ nav { display: grid; gap: 2px; padding: 4px 0; }
118
+ .nav-item { width: calc(100% - 18px); min-height: 30px; display: flex; align-items: center; gap: 9px; margin: 1px 9px; padding: 6px 9px; border: 0; border-radius: 7px; color: var(--text-sec); background: transparent; text-align: left; font-size: 13px; font-weight: 450; letter-spacing: -.01em; transition: color .1s ease, background .1s ease, box-shadow .1s ease; }
119
+ .nav-item:hover:not(:disabled) { color: var(--text); background: var(--hover); }
120
+ .nav-item.active { color: white; background: var(--accent); box-shadow: 0 1px 2px color-mix(in srgb, var(--accent) 42%, transparent); font-weight: 510; }
121
+ .count { min-width: 18px; margin-left: auto; padding: 1px 7px; border-radius: 999px; color: var(--text-muted); background: var(--bg-el); font: 10px var(--mono); text-align: center; }
122
+ .nav-item.active .count { color: white; background: rgba(255,255,255,.22); }
123
+ .stack { margin: auto 12px 8px; padding: 11px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-card); box-shadow: 0 1px 2px rgba(0,0,0,.04); }
124
+ .stack strong { display: block; color: var(--text-sec); font: 11px var(--mono); }
125
+ .stack p { margin: 7px 0 0; color: var(--text-muted); font-size: 10px; line-height: 1.45; }
126
+ .actor { display: flex; align-items: center; gap: 9px; padding: 8px 12px; border-top: 1px solid var(--border-subtle); }
127
+ .avatar { width: 26px; height: 26px; display: grid; place-items: center; flex: none; border-radius: 50%; color: white; background: var(--accent); font-size: 10px; font-weight: 700; }
128
+ .actor-name, .actor-email { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
129
+ .actor-name { font-size: 12px; font-weight: 500; }
130
+ .actor-email { color: var(--text-muted); font: 10px var(--mono); }
131
+ .signout { width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: 6px; color: var(--text-muted); background: transparent; }
132
+ .signout:hover { color: #d70015; background: var(--hover); }
133
+ .mobile-signout { display: none; margin-left: auto; }
134
+ .main { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
135
+ .content { flex: 1; min-height: 0; overflow: auto; }
136
+ ::slotted(*) { min-height: 100%; }
137
+ .status { height: 24px; display: flex; align-items: center; gap: 16px; flex: none; padding: 0 16px; border-top: 1px solid var(--border-subtle); color: var(--text-dim); background: var(--bg); font: 10px var(--mono); }
138
+ .status-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--success); }
139
+ :focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 25%, transparent); outline-offset: 2px; }
140
+
141
+ @container (max-width: 760px) {
142
+ .shell { grid-template-columns: 1fr; }
143
+ .sidebar { height: auto; min-height: 0; border-right: 0; border-bottom: 1px solid var(--border-subtle); }
144
+ .traffic, .workspace, .stack, .actor { display: none; }
145
+ .brand { padding: 10px 14px 4px; }
146
+ .mobile-signout { display: grid; }
147
+ nav { display: flex; overflow-x: auto; padding: 4px 8px 8px; }
148
+ .nav-item { width: auto; flex: none; margin: 0 2px; }
149
+ .status { display: none; }
150
+ }
151
+
152
+ @media (prefers-reduced-motion: reduce) {
153
+ *, *::before, *::after { transition-duration: .001ms !important; animation-duration: .001ms !important; animation-iteration-count: 1 !important; }
154
+ }
155
+ `;
156
+
157
+ private initials(valueArg: string, fallbackArg = ''): string {
158
+ const source = valueArg.trim() || fallbackArg.trim();
159
+ if (!source) return '?';
160
+ return source.split(/\s+|@/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() || '').join('');
161
+ }
162
+
163
+ private requestNavigation(itemArg: ISdigShellNavigationItem) {
164
+ if (itemArg.disabled) return;
165
+ this.dispatchEvent(new CustomEvent<ISdigShellNavigationRequestDetail>('navigation-request', {
166
+ detail: { view: itemArg.id },
167
+ bubbles: true,
168
+ composed: true,
169
+ }));
170
+ }
171
+
172
+ private requestSignout() {
173
+ this.dispatchEvent(new CustomEvent('signout-request', { bubbles: true, composed: true }));
174
+ }
175
+
176
+ public render(): TemplateResult {
177
+ return html`
178
+ <div class="shell" style=${`--accent:${this.accent}`}>
179
+ <aside class="sidebar">
180
+ <div class="traffic" aria-hidden="true"><span></span><span></span><span></span></div>
181
+ <div class="brand"><span class="mark">s</span><span class="wordmark">signature<span class="dot">.</span>digital</span><button class="signout mobile-signout" title="Sign out" aria-label="Sign out" @click=${this.requestSignout}>↪</button></div>
182
+ <div class="workspace"><span class="workspace-badge">${this.initials(this.workspaceName)}</span><div style="min-width:0;flex:1"><strong>${this.workspaceName}</strong><small>${this.deploymentLabel}</small></div></div>
183
+ <nav aria-label="Workspace navigation">
184
+ ${this.navigationItems.map((item) => html`
185
+ <button class="nav-item ${this.activeView === item.id ? 'active' : ''}" ?disabled=${item.disabled} title=${item.title || ''} @click=${() => this.requestNavigation(item)}>
186
+ ${icon(item.icon, 16)}<span>${item.label}</span>${item.count === undefined ? '' : html`<span class="count">${item.count}</span>`}
187
+ </button>
188
+ `)}
189
+ </nav>
190
+ <div class="stack"><strong>${this.stackLabel}</strong><p>${this.stackDescription}</p></div>
191
+ <div class="actor"><span class="avatar">${this.initials(this.actorName, this.actorEmail)}</span><div style="min-width:0;flex:1"><div class="actor-name">${this.actorName}</div><div class="actor-email">${this.actorEmail}</div></div><button class="signout" title="Sign out" aria-label="Sign out" @click=${this.requestSignout}>↪</button></div>
192
+ </aside>
193
+ <main class="main">
194
+ <div class="content"><slot></slot></div>
195
+ <div class="status"><span class="status-dot"></span><span>${this.statusLabel}</span><span>${this.documentCount} documents</span><div style="flex:1"></div>${this.productLabel ? html`<span style="color:var(--accent)">${this.productLabel}</span>` : ''}</div>
196
+ </main>
197
+ </div>
198
+ `;
199
+ }
200
+ }
@@ -1,4 +1,5 @@
1
1
  import { DeesElement, html, customElement, type TemplateResult, css, cssManager } from '@design.estate/dees-element';
2
+ import '../sdig-signpad/index.js';
2
3
 
3
4
  declare global {
4
5
  interface HTMLElementTagNameMap {
@@ -22,6 +23,7 @@ export class SignBox extends DeesElement {
22
23
  css`
23
24
  .mainbox {
24
25
  position: relative;
26
+ width: 100%;
25
27
  background: ${cssManager.bdTheme('#eeeeeb', '#111111')};
26
28
  border-radius: 16px;
27
29
  max-width: 600px;
@@ -54,6 +56,7 @@ export class SignBox extends DeesElement {
54
56
  box-shadow: ${cssManager.bdTheme('0px 0px 8px 0px #00000040', 'none')};
55
57
  z-index: 2;
56
58
  display: flex;
59
+ flex-wrap: wrap;
57
60
  justify-content: flex-end;
58
61
  }
59
62
 
@@ -68,6 +71,13 @@ export class SignBox extends DeesElement {
68
71
  .button:hover {
69
72
  color: ${cssManager.bdTheme('#111111', '#eeeeeb')};
70
73
  }
74
+
75
+ button.button {
76
+ appearance: none;
77
+ border: 0;
78
+ background: transparent;
79
+ font: inherit;
80
+ }
71
81
  `
72
82
  ];
73
83
 
@@ -79,29 +89,37 @@ export class SignBox extends DeesElement {
79
89
  </div>
80
90
  <sdig-signpad></sdig-signpad>
81
91
  <div class="actions">
82
- <div class="button" @click=${async () => {
92
+ <button class="button" data-action="clear" @click=${async () => {
83
93
  await this.shadowRoot?.querySelector('sdig-signpad')?.clear();
84
94
  }}>
85
95
  Clear
86
- </div>
87
- <div class="button" @click=${async () => {
96
+ </button>
97
+ <button class="button" data-action="undo" @click=${async () => {
88
98
  await this.shadowRoot?.querySelector('sdig-signpad')?.undo();
89
99
  }}>
90
100
  Undo
91
- </div>
92
- <div class="button" @click=${async () => {
101
+ </button>
102
+ <button class="button" data-action="submit" @click=${async () => {
93
103
  const signaturePad = this.shadowRoot?.querySelector('sdig-signpad');
94
104
  if (!signaturePad) return;
105
+ if (await signaturePad.isEmpty()) return;
95
106
  const signature = await signaturePad.toData();
107
+ const svg = await signaturePad.toSVG();
108
+ const canvasSize = signaturePad.getCanvasSize();
96
109
  this.dispatchEvent(new CustomEvent('signature', {
97
110
  detail: {
98
111
  signature,
99
- }
112
+ svg,
113
+ createdAt: Date.now(),
114
+ canvasWidth: canvasSize.width,
115
+ canvasHeight: canvasSize.height,
116
+ },
117
+ bubbles: true,
118
+ composed: true,
100
119
  }));
101
- console.log(signature);
102
120
  }}>
103
121
  Submit Signature
104
- </div>
122
+ </button>
105
123
  </div>
106
124
  </div>
107
125
  `;
@@ -24,6 +24,7 @@ export class SignPad extends DeesElement {
24
24
  css`
25
25
  :host {
26
26
  display: block;
27
+ width: 100%;
27
28
  color: white;
28
29
  position: relative;
29
30
  max-width: 600px;
@@ -33,7 +34,8 @@ export class SignPad extends DeesElement {
33
34
 
34
35
  .mainbox {
35
36
  position: relative;
36
- width: 600px;
37
+ width: 100%;
38
+ max-width: 600px;
37
39
  height: 280px;
38
40
  }
39
41
 
@@ -47,6 +49,8 @@ export class SignPad extends DeesElement {
47
49
  }
48
50
 
49
51
  canvas {
52
+ display: block;
53
+ max-width: 100%;
50
54
  filter: ${cssManager.bdTheme('invert(0)', 'invert(1)')};
51
55
  }
52
56
  `
@@ -61,20 +65,26 @@ export class SignPad extends DeesElement {
61
65
  }
62
66
 
63
67
  public signaturePad?: typeof plugins.signaturePad.prototype;
68
+ private canvas?: HTMLCanvasElement;
64
69
 
65
70
  public async firstUpdated() {
66
71
  await this.domtoolsPromise;
67
72
  const mainbox = this.shadowRoot?.querySelector('.mainbox');
68
73
  if (!mainbox) return;
69
- const canvas = document.createElement('canvas');
70
- mainbox.appendChild(canvas);
74
+ this.canvas = document.createElement('canvas');
75
+ mainbox.appendChild(this.canvas);
71
76
  await this.resizeCanvas();
72
- this.signaturePad = new plugins.signaturePad(canvas, {
77
+ this.signaturePad = new plugins.signaturePad(this.canvas, {
73
78
 
74
79
  });
75
80
  this.signaturePad.on();
76
81
  }
77
82
 
83
+ public async disconnectedCallback() {
84
+ this.signaturePad?.off();
85
+ await super.disconnectedCallback();
86
+ }
87
+
78
88
  public async resizeCanvas() {
79
89
  const mainbox = this.shadowRoot?.querySelector('.mainbox');
80
90
  const canvas = this.shadowRoot?.querySelector('canvas');
@@ -92,12 +102,23 @@ export class SignPad extends DeesElement {
92
102
  this.signaturePad?.clear();
93
103
  }
94
104
 
95
- public async toData() {
105
+ public async isEmpty(): Promise<boolean> {
106
+ return this.signaturePad?.isEmpty() ?? true;
107
+ }
108
+
109
+ public async toData(): Promise<plugins.TSignaturePadPointGroup[]> {
96
110
  const returnData = this.signaturePad?.toData() || [];
97
111
  return returnData;
98
112
  }
99
113
 
100
- public async fromData(dataArrayArg: any[]) {
114
+ public getCanvasSize(): { width: number; height: number } {
115
+ return {
116
+ width: this.canvas?.width || 0,
117
+ height: this.canvas?.height || 0,
118
+ };
119
+ }
120
+
121
+ public async fromData(dataArrayArg: plugins.TSignaturePadPointGroup[]) {
101
122
  this.signaturePad?.fromData(dataArrayArg);
102
123
  }
103
124
 
@@ -24,16 +24,16 @@ export class SdigWorkspaceAudit extends DeesElement {
24
24
  const document = this.document || demoDocuments[1];
25
25
  const events = [
26
26
  ['2026-05-02 14:32:18 UTC', 'Sarah Chen', 'Document signed', '81.221.4.18 · Brussels, BE', '0x4a7b…f29c', 'success'],
27
- ['2026-05-02 14:31:54 UTC', 'Sarah Chen', 'Signature adopted (typed)', '81.221.4.18 · Brussels, BE', '0x4a7b…f29c', 'info'],
27
+ ['2026-05-02 14:31:54 UTC', 'Sarah Chen', 'Signature strokes captured', 'recipient session', '0x4a7b…f29c', 'info'],
28
28
  ['2026-05-02 14:28:02 UTC', 'Sarah Chen', 'Document opened', '81.221.4.18 · Brussels, BE', '', 'default'],
29
29
  ['2026-05-02 11:02:11 UTC', 'Philipp K.', 'Document sent for signature', '92.42.114.7 · Berlin, DE', '0x1c8a…3b6f', 'info'],
30
30
  ['2026-05-02 10:54:22 UTC', 'Philipp K.', 'Document created', '92.42.114.7 · Berlin, DE', '0x1c8a…3b6f', 'default'],
31
31
  ];
32
32
  return html`
33
- ${topBar({ breadcrumb: ['signature.digital', 'Inbox', document.id, 'Audit Trail'], title: `Audit Trail · ${document.title}`, subtitle: pill(`${document.status} · cryptographically sealed`, document.status === 'declined' ? 'error' : document.status === 'signed' ? 'success' : 'info', true), actions: html`${actionButton('Certificate (PDF)', 'outline', 'download')}${actionButton('Verify on chain', 'outline', 'hash')}` })}
33
+ ${topBar({ breadcrumb: ['signature.digital', 'Inbox', document.id, 'Audit Trail'], title: `Audit Trail · ${document.title}`, subtitle: pill(`${document.status} · demo application hash chain`, document.status === 'declined' ? 'error' : document.status === 'signed' ? 'success' : 'info', true), actions: html`${actionButton('Evidence export', 'outline', 'download')}` })}
34
34
  <div class="content-scroll audit-grid">
35
- <div class="card"><div style="height: 36px; padding: 0 16px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; justify-content: space-between;"><span style="font-size: 12px; font-weight: 600;">Event log</span><span class="mono" style="font-size: 10px; color: var(--text-muted);">${events.length} events · immutable</span></div>${events.map((event) => html`<div class="event-row"><div><span style="display: block; width: 8px; height: 8px; border-radius: 50%; background: ${event[5] === 'success' ? 'var(--success)' : event[5] === 'info' ? 'var(--accent)' : 'var(--text-dim)'};"></span></div><div class="mono hide-mobile" style="font-size: 11px; color: var(--text-muted);">${event[0]}</div><div><div style="font-size: 12px; font-weight: 500;">${event[2]}</div><div style="font-size: 10px; color: var(--text-muted); margin-top: 2px;">by ${event[1]} ${event[4] ? html`<span class="mono" style="color: var(--accent); margin-left: 8px;">${event[4]}</span>` : ''}</div></div><div class="mono hide-mobile" style="font-size: 10px; color: var(--text-muted); text-align: right;">${event[3]}</div></div>`)}</div>
36
- <div style="display: flex; flex-direction: column; gap: 16px;"><div class="card" style="padding: 16px;"><div class="label-upper">Document hash</div><div class="mono" style="font-size: 11px; color: var(--accent); word-break: break-all; line-height: 1.5; padding: 10px; background: var(--bg-el); border-radius: 4px; border: 1px solid var(--border-subtle);">0x4a7b8f29c91e3d2a5b6c8e0f1d3c5a7b9d2e4f6a8c1e3d5f7b9c1e3a5b7d9f0e</div></div><div class="card" style="padding: 16px;"><div class="label-upper">Signers</div>${demoRecipients.map((recipient) => html`<div class="recipient-line"><span class="avatar" style="background: ${recipient.color};">${recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join('')}</span><div style="flex: 1;"><div style="font-size: 12px;">${recipient.name}</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">${recipient.email}</div></div>${icon('check', 12)}</div>`)}</div><div class="card" style="padding: 16px; border-color: rgba(34,197,94,0.2);"><div style="display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: var(--success); margin-bottom: 6px;">${icon('shield', 13)} eIDAS Qualified · ESIGN Act compliant</div><div style="font-size: 11px; color: var(--text-muted); line-height: 1.55;">Open-source verifier available. Anyone can independently validate this signature against the public ledger.</div></div></div>
35
+ <div class="card"><div style="height: 36px; padding: 0 16px; border-bottom: 1px solid var(--border-subtle); display: flex; align-items: center; justify-content: space-between;"><span style="font-size: 12px; font-weight: 600;">Demo event log</span><span class="mono" style="font-size: 10px; color: var(--text-muted);">${events.length} local demo events</span></div>${events.map((event) => html`<div class="event-row"><div><span style="display: block; width: 8px; height: 8px; border-radius: 50%; background: ${event[5] === 'success' ? 'var(--success)' : event[5] === 'info' ? 'var(--accent)' : 'var(--text-dim)'};"></span></div><div class="mono hide-mobile" style="font-size: 11px; color: var(--text-muted);">${event[0]}</div><div><div style="font-size: 12px; font-weight: 500;">${event[2]}</div><div style="font-size: 10px; color: var(--text-muted); margin-top: 2px;">by ${event[1]} ${event[4] ? html`<span class="mono" style="color: var(--accent); margin-left: 8px;">${event[4]}</span>` : ''}</div></div><div class="mono hide-mobile" style="font-size: 10px; color: var(--text-muted); text-align: right;">${event[3]}</div></div>`)}</div>
36
+ <div style="display: flex; flex-direction: column; gap: 16px;"><div class="card" style="padding: 16px;"><div class="label-upper">Demo document hash</div><div class="mono" style="font-size: 11px; color: var(--accent); word-break: break-all; line-height: 1.5; padding: 10px; background: var(--bg-el); border-radius: 8px; border: 1px solid var(--border-subtle);">4a7b8f29c91e3d2a5b6c8e0f1d3c5a7b9d2e4f6a8c1e3d5f7b9c1e3a5b7d9f0e</div></div><div class="card" style="padding: 16px;"><div class="label-upper">Demo recipients</div>${demoRecipients.map((recipient) => html`<div class="recipient-line"><span class="avatar" style="background: ${recipient.color};">${recipient.name.split(' ').map((part) => part[0]).slice(0, 2).join('')}</span><div style="flex: 1;"><div style="font-size: 12px;">${recipient.name}</div><div class="mono" style="font-size: 10px; color: var(--text-muted);">${recipient.email}</div></div>${recipient.role === 'signer' ? icon('check', 12) : ''}</div>`)}</div><div class="card" style="padding: 16px; border-color: rgba(0,122,255,0.2);"><div style="display: inline-flex; align-items: center; gap: 6px; font-size: 11px; color: var(--accent); margin-bottom: 6px;">${icon('shield', 13)} Application evidence preview</div><div style="font-size: 11px; color: var(--text-muted); line-height: 1.55;">The host application supplies recorded events and SHA-256 values. This demo component does not assert identity, trusted timestamps, blockchain anchoring, or legal signature level.</div></div></div>
37
37
  </div>
38
38
  `;
39
39
  }
@@ -60,7 +60,7 @@ const resizeHandles: TResizeHandle[] = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'n
60
60
  const recipientRoleDefinitions: Array<{ role: TRecipientRole; label: string; shortLabel: string; description: string }> = [
61
61
  { role: 'signer', label: 'Needs signature', shortLabel: 'Signer', description: 'Can receive fields and must sign in order.' },
62
62
  { role: 'copy', label: 'Final copy only', shortLabel: 'Copy', description: 'Receives the completed document after signing.' },
63
- { role: 'updates', label: 'Every step update', shortLabel: 'Updates', description: 'Receives notifications for every routing step.' },
63
+ { role: 'updates', label: 'Host-defined update', shortLabel: 'Updates', description: 'Emits an update-role intent for the host application.' },
64
64
  ];
65
65
 
66
66
  @customElement('sdig-workspace-compose')
@@ -170,6 +170,16 @@ export class SdigWorkspaceCompose extends DeesElement {
170
170
  return Math.max(min, Math.min(max, value));
171
171
  }
172
172
 
173
+ private parseDragOffset(rawOffset: string | undefined, fallback: { x: number; y: number }): { x: number; y: number } {
174
+ if (!rawOffset) return fallback;
175
+ try {
176
+ const parsed = JSON.parse(rawOffset) as Partial<{ x: number; y: number }>;
177
+ return Number.isFinite(parsed.x) && Number.isFinite(parsed.y) ? { x: parsed.x!, y: parsed.y! } : fallback;
178
+ } catch {
179
+ return fallback;
180
+ }
181
+ }
182
+
173
183
  private emitFieldsChange() {
174
184
  this.dispatchEvent(new CustomEvent('fields-change', {
175
185
  detail: { fields: this.fields },
@@ -473,12 +483,13 @@ export class SdigWorkspaceCompose extends DeesElement {
473
483
  if (!this.draggedFieldDefinition && !transferredType) return;
474
484
  const definition = this.draggedFieldDefinition || this.fieldDefinition(transferredType);
475
485
  const transferredOffset = event.dataTransfer?.getData('application/x-signature-field-offset');
476
- const offset = this.draggedFieldGrabOffset || (transferredOffset ? JSON.parse(transferredOffset) as { x: number; y: number } : { x: definition.w / 2, y: definition.h / 2 });
486
+ const fallbackOffset = { x: definition.w / 2, y: definition.h / 2 };
487
+ const offset = this.draggedFieldGrabOffset || this.parseDragOffset(transferredOffset, fallbackOffset);
477
488
  const rect = page.getBoundingClientRect();
478
489
  const x = Math.round(event.clientX - rect.left - offset.x);
479
490
  const y = Math.round(event.clientY - rect.top - offset.y);
480
491
  const nextField: IFieldPlacement = {
481
- id: `field_${Date.now()}`,
492
+ id: globalThis.crypto?.randomUUID?.() || `field_${Date.now()}_${Math.random().toString(36).slice(2)}`,
482
493
  type: definition.type,
483
494
  x: Math.max(0, Math.min(Math.max(0, rect.width - definition.w), x)),
484
495
  y: Math.max(0, Math.min(Math.max(0, rect.height - definition.h), y)),
@@ -615,7 +626,7 @@ export class SdigWorkspaceCompose extends DeesElement {
615
626
  const selectedField = this.fields.find((field) => field.id === this.selectedFieldId);
616
627
 
617
628
  return html`
618
- ${topBar({ breadcrumb: ['signature.digital', 'Inbox', 'Compose'], title: document.title, subtitle: pill('Draft · auto-saved'), actions: html`${actionButton('Save draft', 'ghost')}${actionButton('Preview', 'outline', 'eye')}${actionButton('Send for signature', 'primary', 'send')}` })}
629
+ ${topBar({ breadcrumb: ['signature.digital', 'Inbox', 'Compose'], title: document.title, subtitle: pill('Draft · host controlled'), actions: html`${actionButton('Save draft', 'ghost')}${actionButton('Preview', 'outline', 'eye')}${actionButton('Send for signature', 'primary', 'send')}` })}
619
630
  ${this.renderStepper()}
620
631
  <div class="compose-workspace">
621
632
  ${this.renderRecipientContextMenu()}
@@ -636,7 +647,7 @@ export class SdigWorkspaceCompose extends DeesElement {
636
647
  </div>
637
648
  <div class="right-panel">
638
649
  <div class="label-upper">Routing order · drag to reorder</div>
639
- <div class="role-hint">Choose who signs, who gets the completed copy, and who is notified at every step.</div>
650
+ <div class="role-hint">Choose visual routing roles. Delivery and notifications remain the host application's responsibility.</div>
640
651
  ${this.renderSigningOrder()}
641
652
  ${selectedField ? this.renderFieldEditor(selectedField) : ''}
642
653
  </div>
@@ -1,5 +1,5 @@
1
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';
2
+ import { icon, pill, topBar, workspaceBaseStyles, workspaceDemoFrame } from './sdig-workspace.shared.js';
3
3
 
4
4
  declare global {
5
5
  interface HTMLElementTagNameMap {
@@ -23,23 +23,21 @@ export class SdigWorkspaceDevelopers extends DeesElement {
23
23
 
24
24
  public render(): TemplateResult {
25
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')}` })}
26
+ ${topBar({ breadcrumb: ['signature.digital', 'Developers'], title: 'Developers', subtitle: pill('foss.global · typed end to end', 'info', true) })}
27
27
  <div class="content-scroll developer-grid">
28
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>
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;">Production application boundary</span><span class="pill">TypeScript</span></div><pre class="code mono">const stack = {
30
+ ui: 'Lit + design.estate',
31
+ api: 'TypedRequest + TypedServer',
32
+ state: 'SmartData + MongoDB',
33
+ blobs: 'SmartBucket + S3',
34
+ pdf: 'pdf-lib + PDF.js',
35
+ } as const;</pre></div>
36
+ <div class="stats-grid">${[['Typed boundary', 'TypedRequest', 'request/response'], ['Persistence', 'SmartData', 'MongoDB'], ['Artifacts', 'SmartBucket', 'S3-compatible']].map((metric) => html`<div class="card metric-card"><div class="metric-value" style="font-size:15px">${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(--accent); margin-top: 6px;">${metric[2]}</div></div>`)}</div>
39
37
  </div>
40
38
  <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>
39
+ <div class="card" style="padding: 16px;"><div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">${icon('code', 14)}<span style="font-size: 12px; font-weight: 600;">Controlled component contract</span></div><div style="font-size: 11px; color: var(--text-muted); line-height: 1.5;">Hosts inject workspace data and own authentication, routing, persistence, and delivery. Catalog components emit bubbling, composed intent events.</div></div>
40
+ <div class="card" style="padding: 16px;"><div class="label-upper">Self-hosted application</div>${[['Runtime', 'Node + TypeScript'], ['State', 'MongoDB'], ['Artifacts', 'S3-compatible'], ['License', 'MIT core']].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
41
  </div>
44
42
  </div>
45
43
  `;