feat(workspace): introduce a responsive signature workspace demo and remove legacy contract editor components

This commit is contained in:
2026-05-02 18:37:48 +00:00
parent 90836f1c72
commit 57cbb739d2
48 changed files with 4387 additions and 13348 deletions
@@ -0,0 +1,440 @@
import { html, css, type TemplateResult } from '@design.estate/dees-element';
import '@design.estate/dees-catalog/ts_web/elements/00group-utility/dees-icon/dees-icon.js';
export type TWorkspaceView =
| 'inbox'
| 'compose'
| 'sign'
| 'audit'
| 'developers'
| 'templates'
| 'team'
| 'settings';
export type TWorkspaceTheme = 'dark' | 'light';
export type TDensity = 'compact' | 'comfortable';
export interface IDocumentRow {
id: string;
title: string;
status: 'awaiting' | 'signed' | 'draft' | 'declined';
recipients: Array<{ name: string; initials: string; signed: boolean }>;
updated: string;
sender: string;
pages: number;
deadline?: string;
}
export interface IRecipient {
id: number;
name: string;
email: string;
color: string;
order: number;
}
export interface IFieldPlacement {
id: string;
type: 'signature' | 'date' | 'text' | 'initials' | 'check';
x: number;
y: number;
w: number;
h: number;
page: number;
recipient: number;
label: string;
}
export const demoDocuments: IDocumentRow[] = [
{ id: 'doc_8mK3pL', title: 'Master Services Agreement - Acme Corp', status: 'awaiting', recipients: [{ name: 'Sarah Chen', initials: 'SC', signed: true }, { name: 'David Park', initials: 'DP', signed: false }, { name: 'You', initials: 'PK', signed: true }], updated: '2 min ago', sender: 'You', pages: 14, deadline: 'May 5' },
{ id: 'doc_2nQ7vR', title: 'NDA - Helio Robotics', status: 'signed', recipients: [{ name: 'Marcus Tan', initials: 'MT', signed: true }, { name: 'You', initials: 'PK', signed: true }], updated: '1h ago', sender: 'You', pages: 3 },
{ id: 'doc_5tH1zM', title: 'Series B Term Sheet (Lead) v3', status: 'awaiting', recipients: [{ name: 'Anna Lindqvist', initials: 'AL', signed: false }, { name: 'Roy Banerjee', initials: 'RB', signed: true }, { name: 'You', initials: 'PK', signed: false }], updated: '3h ago', sender: 'Sequoia Counsel', pages: 22, deadline: 'May 3' },
{ id: 'doc_9wB4cX', title: 'Employment Offer - Mira Abebe', status: 'declined', recipients: [{ name: 'Mira Abebe', initials: 'MA', signed: false }, { name: 'You', initials: 'PK', signed: true }], updated: 'yesterday', sender: 'You', pages: 6 },
{ id: 'doc_1jF6kY', title: 'Lease - Berlin office Q3', status: 'draft', recipients: [{ name: 'You', initials: 'PK', signed: false }], updated: 'yesterday', sender: 'You', pages: 11 },
{ id: 'doc_4dN8sP', title: 'API Reseller Agreement - Northwind', status: 'signed', recipients: [{ name: 'Lila Brooks', initials: 'LB', signed: true }, { name: 'You', initials: 'PK', signed: true }], updated: '2 days ago', sender: 'You', pages: 8 },
];
export const demoRecipients: IRecipient[] = [
{ id: 0, name: 'Sarah Chen', email: 'sarah@acme.com', color: '#60a5fa', order: 1 },
{ id: 1, name: 'David Park', email: 'd.park@acme.com', color: '#fbbf24', order: 2 },
{ id: 2, name: 'Philipp K.', email: 'philipp@lossless.com', color: '#3b82f6', order: 3 },
];
export const demoFields: IFieldPlacement[] = [
{ id: 'f1', type: 'signature', x: 60, y: 580, w: 200, h: 50, page: 1, recipient: 0, label: 'Signature' },
{ id: 'f2', type: 'date', x: 320, y: 580, w: 120, h: 30, page: 1, recipient: 0, label: 'Date' },
{ id: 'f3', type: 'text', x: 60, y: 460, w: 280, h: 30, page: 1, recipient: 1, label: 'Full legal name' },
{ id: 'f4', type: 'signature', x: 60, y: 700, w: 200, h: 50, page: 1, recipient: 1, label: 'Counter-signature' },
];
export const workspaceBaseStyles = css`
:host {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
color: var(--text);
background: var(--bg);
font-family: Geist, Inter, Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
font-feature-settings: 'cv11', 'tnum', 'cv05' 1;
}
* { box-sizing: border-box; }
button, input, textarea { font: inherit; }
button { border: 0; cursor: pointer; }
dees-icon { flex-shrink: 0; }
.mono {
font-family: 'Intel One Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
font-variant-numeric: tabular-nums;
}
.topbar {
height: 56px;
flex-shrink: 0;
padding: 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border-subtle);
background: var(--bg);
gap: 12px;
}
.breadcrumb {
font-size: 11px;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.top-title {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.top-title > span:first-child {
font-family: 'Plus Jakarta Sans', Inter, sans-serif;
font-size: 18px;
font-weight: 600;
letter-spacing: -0.02em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.btn {
height: 34px;
padding: 0 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
letter-spacing: -0.01em;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
white-space: nowrap;
transition: all 0.12s ease;
}
.btn.small {
height: 28px;
padding: 0 10px;
font-size: 12px;
}
.btn.primary {
background: var(--accent);
color: white;
border: 1px solid var(--accent);
}
.btn.outline {
background: transparent;
color: var(--text);
border: 1px solid var(--border);
}
.btn.ghost {
background: transparent;
color: var(--text);
border: 1px solid transparent;
}
.btn:hover { background-color: var(--hover); }
.pill {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 2px 8px;
border-radius: 999px;
background: var(--bg-el);
color: var(--text-sec);
font-size: 11px;
font-weight: 500;
white-space: nowrap;
}
.pill::before {
content: '';
width: 5px;
height: 5px;
display: none;
border-radius: 50%;
background: currentColor;
}
.pill.dot::before { display: block; }
.pill.success { background: rgba(34,197,94,0.12); color: #4ade80; }
.pill.warning { background: rgba(245,158,11,0.12); color: #fbbf24; }
.pill.error { background: rgba(239,68,68,0.12); color: #f87171; }
.pill.info { background: rgba(59,130,246,0.12); color: #60a5fa; }
.content-scroll {
flex: 1;
overflow: auto;
padding: 24px;
}
.card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.label-upper {
font-size: 10px;
font-weight: 600;
color: var(--text-dim);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 10px;
}
.avatar {
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--accent);
color: white;
font-size: 11px;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.document-page {
position: relative;
width: 600px;
min-height: 800px;
background: white;
border-radius: 4px;
box-shadow: 0 8px 32px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.05);
color: hsl(0 0% 20%);
}
.fake-document {
padding: 48px 56px;
font-size: 11px;
line-height: 1.7;
}
.fake-title {
font-family: 'Plus Jakarta Sans', Inter, sans-serif;
font-size: 18px;
font-weight: 700;
margin-bottom: 4px;
color: hsl(0 0% 10%);
}
.fake-line {
height: 6px;
background: hsl(0 0% 82%);
margin-bottom: 7px;
border-radius: 1px;
}
.fake-line.heavy { background: hsl(0 0% 65%); }
.fake-line.short { width: 70%; }
.field-box {
position: absolute;
left: var(--x);
top: var(--y);
width: var(--w);
height: var(--h);
background: color-mix(in srgb, var(--field-color) 13%, transparent);
border: 1.5px dashed var(--field-color);
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
padding: 0 8px;
font-size: 10px;
font-weight: 500;
color: var(--field-color);
}
.field-box.selected {
border-style: solid;
box-shadow: 0 0 0 4px color-mix(in srgb, var(--field-color) 18%, transparent);
}
.recipient-line {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 12px;
color: var(--text-sec);
margin-bottom: 6px;
}
.recipient-line.active {
background: var(--hover);
border-color: var(--border-strong);
}
.progress-track {
height: 4px;
background: var(--bg-el);
flex-shrink: 0;
}
.progress-fill {
height: 100%;
background: var(--accent);
transition: width 0.4s ease;
}
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (max-width: 920px) {
.topbar { padding: 0 16px; }
.actions { display: none; }
.content-scroll { padding: 16px; }
}
`;
export function icon(name: string, size = 14): TemplateResult {
const iconMap: Record<string, string> = {
inbox: 'lucide:Inbox', plus: 'lucide:Plus', folder: 'lucide:Folder', shield: 'lucide:Shield', code: 'lucide:Code2',
user: 'lucide:User', settings: 'lucide:Settings', upload: 'lucide:Upload', file: 'lucide:FileText', sign: 'lucide:PenTool',
clock: 'lucide:Clock', search: 'lucide:Search', more: 'lucide:MoreHorizontal', send: 'lucide:Send', check: 'lucide:Check',
eye: 'lucide:Eye', calendar: 'lucide:Calendar', type: 'lucide:Type', download: 'lucide:Download', hash: 'lucide:Hash',
github: 'lucide:GitBranch', git: 'lucide:GitBranch', server: 'lucide:Server', star: 'lucide:Star', sparkle: 'lucide:Sparkles',
chevronRight: 'lucide:ChevronRight', chevronDown: 'lucide:ChevronDown', x: 'lucide:X', activity: 'lucide:Activity',
};
return html`<dees-icon .icon=${iconMap[name] || iconMap.file} style="font-size: ${size}px;"></dees-icon>`;
}
export function pill(label: string, tone: 'default' | 'success' | 'warning' | 'error' | 'info' = 'default', dot = false): TemplateResult {
return html`<span class="pill ${tone} ${dot ? 'dot' : ''}">${label}</span>`;
}
export function actionButton(label: string, variant: 'primary' | 'outline' | 'ghost' = 'outline', iconName?: string, onClick?: () => void): TemplateResult {
return html`<button class="btn ${variant}" @click=${onClick || (() => undefined)}>${iconName ? icon(iconName, 13) : ''}${label}</button>`;
}
export function topBar(config: { breadcrumb: string[]; title: string; subtitle?: TemplateResult; actions?: TemplateResult }): TemplateResult {
return html`
<div class="topbar">
<div style="min-width: 0; flex: 1;">
<div class="breadcrumb">
${config.breadcrumb.map((part, index) => html`${index > 0 ? icon('chevronRight', 10) : ''}<span>${part}</span>`)}
</div>
<div class="top-title"><span>${config.title}</span>${config.subtitle || ''}</div>
</div>
<div class="actions">${config.actions || ''}</div>
</div>
`;
}
export function workspaceDemoFrame(content: TemplateResult, theme: TWorkspaceTheme = 'dark'): TemplateResult {
const darkVars = `
--accent: #3b82f6;
--bg: hsl(0 0% 3.9%);
--bg-el: hsl(0 0% 6%);
--bg-card: hsl(0 0% 7%);
--bg-input: hsl(0 0% 9%);
--border: hsl(0 0% 14.9%);
--border-subtle: hsl(0 0% 11%);
--border-strong: hsl(0 0% 20%);
--text: hsl(0 0% 98%);
--text-sec: hsl(0 0% 63.9%);
--text-muted: hsl(0 0% 48%);
--text-dim: hsl(0 0% 32%);
--hover: rgba(255,255,255,0.06);
--hover-subtle: rgba(255,255,255,0.03);
--row-hover: rgba(255,255,255,0.025);
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
`;
const lightVars = `
--accent: #3b82f6;
--bg: hsl(0 0% 99%);
--bg-el: hsl(0 0% 97%);
--bg-card: hsl(0 0% 100%);
--bg-input: hsl(0 0% 98%);
--border: hsl(0 0% 90%);
--border-subtle: hsl(0 0% 93%);
--border-strong: hsl(0 0% 80%);
--text: hsl(0 0% 9%);
--text-sec: hsl(0 0% 32%);
--text-muted: hsl(0 0% 45%);
--text-dim: hsl(0 0% 62%);
--hover: rgba(0,0,0,0.04);
--hover-subtle: rgba(0,0,0,0.02);
--row-hover: rgba(0,0,0,0.02);
--success: #16a34a;
--warning: #d97706;
--error: #dc2626;
`;
return html`<div style="${theme === 'dark' ? darkVars : lightVars} height: 720px; min-height: 720px; background: var(--bg); color: var(--text); overflow: hidden; font-family: Geist, Inter, Roboto, -apple-system, BlinkMacSystemFont, sans-serif;">${content}</div>`;
}
export function fakeDocument(): TemplateResult {
return html`
<div class="fake-document">
<div class="fake-title">Master Services Agreement</div>
<div class="mono" style="font-size: 10px; color: hsl(0 0% 45%); margin-bottom: 24px;">Effective: May 2, 2026 · Acme Corp ↔ Lossless GmbH</div>
${Array.from({ length: 18 }).map((_, index) => html`<div class="fake-line ${index % 5 === 0 ? 'heavy' : ''} ${index % 4 === 3 ? 'short' : ''}"></div>`)}
<div style="height: 16px;"></div>
${Array.from({ length: 8 }).map((_, index) => html`<div class="fake-line ${index % 3 === 2 ? 'short' : ''}"></div>`)}
<div style="margin-top: 60px; font-size: 10px; font-weight: 600; color: hsl(0 0% 35%);">SIGNED ON BEHALF OF ACME CORP</div>
<div style="margin-top: 70px; font-size: 10px; font-weight: 600; color: hsl(0 0% 35%);">SIGNED ON BEHALF OF LOSSLESS GMBH</div>
</div>
`;
}
export function requestWorkspaceView(element: HTMLElement, view: TWorkspaceView) {
element.dispatchEvent(new CustomEvent('workspace-view-request', {
detail: { view },
bubbles: true,
composed: true,
}));
}