@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.
- package/dist_ts_web/00_commitinfo_data.js +3 -3
- package/dist_ts_web/elements/index.d.ts +1 -0
- package/dist_ts_web/elements/index.js +3 -1
- package/dist_ts_web/elements/sdig-app-shell/index.d.ts +1 -0
- package/dist_ts_web/elements/sdig-app-shell/index.js +2 -0
- package/dist_ts_web/elements/sdig-app-shell/sdig-app-shell.d.ts +38 -0
- package/dist_ts_web/elements/sdig-app-shell/sdig-app-shell.js +309 -0
- package/dist_ts_web/elements/sdig-signbox/sdig-signbox.d.ts +1 -0
- package/dist_ts_web/elements/sdig-signbox/sdig-signbox.js +28 -9
- package/dist_ts_web/elements/sdig-signpad/sdig-signpad.d.ts +9 -2
- package/dist_ts_web/elements/sdig-signpad/sdig-signpad.js +23 -5
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-audit.js +5 -5
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-compose.d.ts +1 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-compose.js +18 -6
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-developers.js +13 -15
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-inbox.js +14 -13
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-sign.d.ts +3 -0
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace-sign.js +55 -18
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace.js +28 -28
- package/dist_ts_web/elements/sdig-workspace/sdig-workspace.shared.js +44 -42
- package/dist_ts_web/plugins.d.ts +3 -3
- package/dist_ts_web/plugins.js +3 -4
- package/package.json +15 -21
- package/readme.md +54 -2
- package/ts_web/00_commitinfo_data.ts +2 -2
- package/ts_web/elements/index.ts +3 -0
- package/ts_web/elements/sdig-app-shell/index.ts +1 -0
- package/ts_web/elements/sdig-app-shell/sdig-app-shell.ts +200 -0
- package/ts_web/elements/sdig-signbox/sdig-signbox.ts +26 -8
- package/ts_web/elements/sdig-signpad/sdig-signpad.ts +27 -6
- package/ts_web/elements/sdig-workspace/sdig-workspace-audit.ts +4 -4
- package/ts_web/elements/sdig-workspace/sdig-workspace-compose.ts +16 -5
- package/ts_web/elements/sdig-workspace/sdig-workspace-developers.ts +12 -14
- package/ts_web/elements/sdig-workspace/sdig-workspace-inbox.ts +13 -12
- package/ts_web/elements/sdig-workspace/sdig-workspace-sign.ts +53 -17
- package/ts_web/elements/sdig-workspace/sdig-workspace.shared.ts +44 -41
- package/ts_web/elements/sdig-workspace/sdig-workspace.ts +27 -27
- package/ts_web/plugins.ts +3 -4
- package/.smartconfig.json +0 -82
- package/dist_bundle/bundle.js +0 -6214
- package/dist_bundle/bundle.js.map +0 -7
- package/dist_bundle/index.html +0 -28
- package/dist_watch/bundle.js +0 -135739
- package/dist_watch/bundle.js.map +0 -7
- 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<{
|
|
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
|
-
description: '
|
|
6
|
+
version: '1.9.0',
|
|
7
|
+
description: 'Reusable web components for browser-based signature capture and host-controlled e-signature application surfaces.'
|
|
8
8
|
}
|
package/ts_web/elements/index.ts
CHANGED
|
@@ -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
|
-
<
|
|
92
|
+
<button class="button" data-action="clear" @click=${async () => {
|
|
83
93
|
await this.shadowRoot?.querySelector('sdig-signpad')?.clear();
|
|
84
94
|
}}>
|
|
85
95
|
Clear
|
|
86
|
-
</
|
|
87
|
-
<
|
|
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
|
-
</
|
|
92
|
-
<
|
|
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
|
-
</
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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} ·
|
|
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;">
|
|
36
|
-
<div style="display: flex; flex-direction: column; gap: 16px;"><div class="card" style="padding: 16px;"><div class="label-upper">
|
|
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: '
|
|
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
|
|
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 ·
|
|
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
|
|
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 {
|
|
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('
|
|
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;">
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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('
|
|
42
|
-
<div class="card" style="padding: 16px;"><div class="label-upper">Self-
|
|
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
|
`;
|