diff --git a/package.json b/package.json index a10daf1..f9e5c17 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "license": "MIT", "dependencies": { "@design.estate/dees-element": "^2.2.4", - "@signature.digital/catalog": "^1.6.1", + "@signature.digital/catalog": "^1.7.0", "@signature.digital/interfaces": "file:../interfaces", "@signature.digital/tools": "file:../tools" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8b7714..b81c3f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^2.2.4 version: 2.2.4 '@signature.digital/catalog': - specifier: ^1.6.1 - version: 1.6.1(@tiptap/pm@2.27.2) + specifier: ^1.7.0 + version: 1.7.0(@tiptap/pm@2.27.2) '@signature.digital/interfaces': specifier: file:../interfaces version: file:../interfaces @@ -1035,8 +1035,8 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@signature.digital/catalog@1.6.1': - resolution: {integrity: sha512-WER9SO5gYsnYCpcz5+2gP+8ayZMrbQ76oK1hbuO935Yef2Fv6rtyY63W5bB27uhptly4BAD1A3RHGrh3BZMtjg==} + '@signature.digital/catalog@1.7.0': + resolution: {integrity: sha512-UDgcW2sx5tangR+8omnISYIy4Nqrej17Za3nBPsRCW7hT6+veB8Bu5ZFssDGQqYld+aPe4o2+H3ZiJgdJJ84nQ==} '@signature.digital/interfaces@1.2.0': resolution: {integrity: sha512-Aym4CYEJ5TDXshNI/kiBPPjDIkQ+TRXeu5u1d0aVC7aShwHmLj38mWLXHqFnAnQSvpXDRje4vAZML9NTU9Q7sg==} @@ -4512,7 +4512,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@signature.digital/catalog@1.6.1(@tiptap/pm@2.27.2)': + '@signature.digital/catalog@1.7.0(@tiptap/pm@2.27.2)': dependencies: '@design.estate/dees-catalog': 3.81.0(@tiptap/pm@2.27.2) '@design.estate/dees-domtools': 2.5.6 diff --git a/ts_web/index.ts b/ts_web/index.ts index 340f03c..f942049 100644 --- a/ts_web/index.ts +++ b/ts_web/index.ts @@ -1,41 +1,10 @@ import '@signature.digital/catalog'; import { html, render } from '@design.estate/dees-element'; - -type TWorkspaceView = 'inbox' | 'compose' | 'sign' | 'audit' | 'developers' | 'templates' | 'team' | 'settings'; - -type TDocumentRow = { - 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; -}; - -const workspaceViews: TWorkspaceView[] = ['inbox', 'compose', 'sign', 'audit', 'developers', 'templates', 'team', 'settings']; - -const appDocuments: TDocumentRow[] = [ - { id: 'app_doc_001', title: 'Platform Services Agreement - App-owned data', status: 'awaiting', recipients: [{ name: 'Sarah Chen', initials: 'SC', signed: true }, { name: 'David Park', initials: 'DP', signed: false }], updated: 'just now', sender: 'App Service', pages: 14, deadline: 'May 5' }, - { id: 'app_doc_002', title: 'Security Addendum - Catalog Integration', status: 'draft', recipients: [{ name: 'Philipp K.', initials: 'PK', signed: false }], updated: '12 min ago', sender: 'App Service', pages: 5 }, - { id: 'app_doc_003', title: 'Completed Vendor NDA - Signed from App', status: 'signed', recipients: [{ name: 'Lila Brooks', initials: 'LB', signed: true }, { name: 'Philipp K.', initials: 'PK', signed: true }], updated: '1h ago', sender: 'App Service', pages: 3 }, -]; - -const isWorkspaceView = (view: string): view is TWorkspaceView => workspaceViews.includes(view as TWorkspaceView); - -const routeFromLocation = (): { view: TWorkspaceView; documentId: string } => { - const [view = '', documentId = ''] = globalThis.location.hash.replace(/^#/, '').split(':'); - return { view: isWorkspaceView(view) ? view : 'inbox', documentId }; -}; - -const hashForRoute = (view: TWorkspaceView, documentId = ''): string => { - return documentId && (view === 'sign' || view === 'audit') ? `#${view}:${documentId}` : `#${view}`; -}; +import { appDocuments, appRecipients, documentIdOrDefault, fieldsForDocument, hashForRoute, isWorkspaceView, routeFromLocation, setAppRecipients, setFieldsForDocument, type IAppDocumentRow, type IAppFieldPlacement, type IAppRecipient, type TWorkspaceView } from './state.js'; let currentRoute = routeFromLocation(); let currentView = currentRoute.view; -let activeDocumentId = currentRoute.documentId; +let activeDocumentId = currentRoute.documentId ? documentIdOrDefault(currentRoute.documentId) : ''; let appRoot: HTMLElement | null = null; let routeChangeFrame: number | null = null; @@ -57,11 +26,24 @@ const handleViewChange = (event: Event) => { }; const handleDocumentOpen = (event: Event) => { - const document = (event as CustomEvent<{ document?: TDocumentRow }>).detail?.document; + const document = (event as CustomEvent<{ document?: IAppDocumentRow }>).detail?.document; if (!document) return; activeDocumentId = document.id; }; +const handleFieldsChange = (event: Event) => { + if (!activeDocumentId) return; + const fields = (event as CustomEvent<{ fields?: IAppFieldPlacement[] }>).detail?.fields; + if (!fields) return; + setFieldsForDocument(activeDocumentId, fields); +}; + +const handleRecipientsChange = (event: Event) => { + const recipients = (event as CustomEvent<{ recipients?: IAppRecipient[] }>).detail?.recipients; + if (!recipients) return; + setAppRecipients(recipients); +}; + const handleWorkspaceViewRequest = (event: Event) => { const view = (event as CustomEvent<{ view?: TWorkspaceView }>).detail?.view; if (!view || !isWorkspaceView(view)) return; @@ -82,13 +64,14 @@ const handleRouteChange = () => { routeChangeFrame = null; currentRoute = routeFromLocation(); currentView = currentRoute.view; - activeDocumentId = currentRoute.documentId; + activeDocumentId = currentRoute.documentId ? documentIdOrDefault(currentRoute.documentId) : ''; renderApp(); }); }; const renderApp = () => { if (!appRoot) createAppRoot(); + const documentId = activeDocumentId || appDocuments[0].id; render( html` - + `, appRoot! ); @@ -120,6 +103,9 @@ const run = async () => { createAppRoot(); document.body.addEventListener('view-change', handleViewChange); document.body.addEventListener('document-open', handleDocumentOpen); + document.body.addEventListener('fields-change', handleFieldsChange); + document.body.addEventListener('recipients-change', handleRecipientsChange); + document.body.addEventListener('routing-change', handleRecipientsChange); document.body.addEventListener('workspace-view-request', handleWorkspaceViewRequest); globalThis.addEventListener('popstate', handleRouteChange); globalThis.addEventListener('hashchange', handleRouteChange); diff --git a/ts_web/state.ts b/ts_web/state.ts new file mode 100644 index 0000000..f7fe219 --- /dev/null +++ b/ts_web/state.ts @@ -0,0 +1,87 @@ +export type TWorkspaceView = 'inbox' | 'compose' | 'sign' | 'audit' | 'developers' | 'templates' | 'team' | 'settings'; + +export type TRecipientRole = 'signer' | 'copy' | 'updates'; + +export interface IAppDocumentRow { + 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 IAppRecipient { + id: number; + name: string; + email: string; + color: string; + order: number; + role: TRecipientRole; +} + +export interface IAppFieldPlacement { + id: string; + type: 'signature' | 'date' | 'text' | 'initials' | 'check'; + x: number; + y: number; + w: number; + h: number; + page: number; + recipient: number; + label: string; +} + +export const workspaceViews: TWorkspaceView[] = ['inbox', 'compose', 'sign', 'audit', 'developers', 'templates', 'team', 'settings']; + +export const appDocuments: IAppDocumentRow[] = [ + { id: 'app_doc_001', title: 'Platform Services Agreement - App-owned data', status: 'awaiting', recipients: [{ name: 'Sarah Chen', initials: 'SC', signed: true }, { name: 'David Park', initials: 'DP', signed: false }], updated: 'just now', sender: 'App Service', pages: 14, deadline: 'May 5' }, + { id: 'app_doc_002', title: 'Security Addendum - Catalog Integration', status: 'draft', recipients: [{ name: 'Philipp K.', initials: 'PK', signed: false }], updated: '12 min ago', sender: 'App Service', pages: 5 }, + { id: 'app_doc_003', title: 'Completed Vendor NDA - Signed from App', status: 'signed', recipients: [{ name: 'Lila Brooks', initials: 'LB', signed: true }, { name: 'Philipp K.', initials: 'PK', signed: true }], updated: '1h ago', sender: 'App Service', pages: 3 }, +]; + +export let appRecipients: IAppRecipient[] = [ + { id: 0, name: 'Sarah Chen', email: 'sarah@acme.com', color: '#60a5fa', order: 1, role: 'signer' }, + { id: 1, name: 'David Park', email: 'd.park@acme.com', color: '#fbbf24', order: 2, role: 'signer' }, + { id: 2, name: 'Philipp K.', email: 'philipp@lossless.com', color: '#3b82f6', order: 3, role: 'updates' }, +]; + +const defaultFields: IAppFieldPlacement[] = [ + { id: 'app_f1', type: 'signature', x: 60, y: 580, w: 200, h: 50, page: 1, recipient: 0, label: 'Signature' }, + { id: 'app_f2', type: 'date', x: 320, y: 580, w: 120, h: 30, page: 1, recipient: 0, label: 'Date' }, + { id: 'app_f3', type: 'text', x: 60, y: 460, w: 280, h: 30, page: 1, recipient: 1, label: 'Full legal name' }, + { id: 'app_f4', type: 'signature', x: 60, y: 700, w: 200, h: 50, page: 1, recipient: 1, label: 'Counter-signature' }, +]; + +const appFieldsByDocument: Record = Object.fromEntries( + appDocuments.map((document) => [document.id, defaultFields.map((field) => ({ ...field, id: `${document.id}_${field.id}` }))]) +); + +export const isWorkspaceView = (view: string): view is TWorkspaceView => workspaceViews.includes(view as TWorkspaceView); + +export const routeFromLocation = (): { view: TWorkspaceView; documentId: string } => { + const [view = '', documentId = ''] = globalThis.location.hash.replace(/^#/, '').split(':'); + return { view: isWorkspaceView(view) ? view : 'inbox', documentId }; +}; + +export const hashForRoute = (view: TWorkspaceView, documentId = ''): string => { + return documentId && (view === 'compose' || view === 'sign' || view === 'audit') ? `#${view}:${documentId}` : `#${view}`; +}; + +export const documentIdOrDefault = (documentId: string): string => { + return appDocuments.some((document) => document.id === documentId) ? documentId : appDocuments[0].id; +}; + +export const fieldsForDocument = (documentId: string): IAppFieldPlacement[] => { + return appFieldsByDocument[documentId] || []; +}; + +export const setFieldsForDocument = (documentId: string, fields: IAppFieldPlacement[]) => { + appFieldsByDocument[documentId] = fields; +}; + +export const setAppRecipients = (recipients: IAppRecipient[]) => { + appRecipients = recipients; +};