@echothink-ui/domain-widgets 0.1.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/src/index.tsx ADDED
@@ -0,0 +1,3925 @@
1
+ import "./styles.css";
2
+
3
+ import * as React from "react";
4
+ import { DownloadIcon, PlusIcon, TableIcon } from "@echothink-ui/icons";
5
+ import {
6
+ Badge,
7
+ Button,
8
+ Checkbox,
9
+ LinkButton,
10
+ Select,
11
+ Surface,
12
+ StatusDot,
13
+ TextInput,
14
+ createSurfaceComponent,
15
+ statusLabel,
16
+ type EthAction,
17
+ type EthListItem,
18
+ type EthOperationalStatus,
19
+ type SurfaceComponentProps
20
+ } from "@echothink-ui/core";
21
+
22
+ export type ApprovalWorkflowMode = "sequential" | "parallel" | "conditional";
23
+
24
+ export interface CRMContactPanelProps extends SurfaceComponentProps {
25
+ contacts?: EthListItem[];
26
+ relationshipHealthLabel?: React.ReactNode;
27
+ engagementLabel?: React.ReactNode;
28
+ nextStep?: React.ReactNode;
29
+ }
30
+
31
+ export interface SupportTicketQueueProps extends SurfaceComponentProps {
32
+ tickets?: EthListItem[];
33
+ }
34
+
35
+ function textFromNode(node: React.ReactNode): string {
36
+ if (typeof node === "string" || typeof node === "number") return String(node);
37
+ if (Array.isArray(node)) return node.map(textFromNode).join(" ");
38
+ return "";
39
+ }
40
+
41
+ function getContactInitials(label: React.ReactNode) {
42
+ const words = textFromNode(label).trim().split(/\s+/).filter(Boolean);
43
+ if (!words.length) return "CT";
44
+ return words
45
+ .slice(0, 2)
46
+ .map((word) => word.charAt(0).toUpperCase())
47
+ .join("");
48
+ }
49
+
50
+ function safeDomId(value: string) {
51
+ return value.replace(/[^A-Za-z0-9_-]+/g, "-");
52
+ }
53
+
54
+ function splitTicketPriority(label: React.ReactNode) {
55
+ if (typeof label !== "string" && typeof label !== "number") {
56
+ return { priority: undefined, label };
57
+ }
58
+
59
+ const text = String(label);
60
+ const match = text.match(/^\[(P[0-4])\]\s*(.+)$/i);
61
+ if (!match) return { priority: undefined, label };
62
+
63
+ return {
64
+ priority: match[1].toUpperCase(),
65
+ label: match[2]
66
+ };
67
+ }
68
+
69
+ function healthTone(status?: EthOperationalStatus) {
70
+ if (status === "blocked" || status === "failed" || status === "stale") return "risk";
71
+ if (status === "warning" || status === "pending-approval" || status === "approval-required") {
72
+ return "watch";
73
+ }
74
+ if (status === "queued" || status === "not-started") return "new";
75
+ return "healthy";
76
+ }
77
+
78
+ function healthLabel(status?: EthOperationalStatus) {
79
+ if (status === "blocked" || status === "failed" || status === "stale") return "Needs attention";
80
+ if (status === "warning" || status === "pending-approval" || status === "approval-required") {
81
+ return "Monitor closely";
82
+ }
83
+ if (status === "queued" || status === "not-started") return "New relationship";
84
+ return "Healthy";
85
+ }
86
+
87
+ function CRMContactActions({ actions }: { actions?: EthAction[] }) {
88
+ if (!actions?.length) return null;
89
+
90
+ return (
91
+ <div className="eth-domain-crm-contact-panel__contact-actions">
92
+ {actions.slice(0, 2).map((action) =>
93
+ action.href ? (
94
+ <LinkButton
95
+ key={action.id}
96
+ href={action.disabled ? undefined : action.href}
97
+ intent={action.intent ?? "ghost"}
98
+ density="compact"
99
+ aria-disabled={action.disabled ? true : undefined}
100
+ tabIndex={action.disabled ? -1 : undefined}
101
+ >
102
+ {action.label}
103
+ </LinkButton>
104
+ ) : (
105
+ <Button
106
+ key={action.id}
107
+ type="button"
108
+ intent={action.intent ?? "ghost"}
109
+ density="compact"
110
+ disabled={action.disabled}
111
+ onClick={action.onSelect}
112
+ >
113
+ {action.label}
114
+ </Button>
115
+ )
116
+ )}
117
+ </div>
118
+ );
119
+ }
120
+
121
+ function SupportTicketActions({ actions }: { actions?: EthAction[] }) {
122
+ if (!actions?.length) return null;
123
+
124
+ return (
125
+ <div className="eth-domain-support-ticket-queue__actions" aria-label="Ticket actions">
126
+ {actions.slice(0, 2).map((action) =>
127
+ action.href ? (
128
+ <LinkButton
129
+ key={action.id}
130
+ href={action.disabled ? undefined : action.href}
131
+ intent={action.intent ?? "ghost"}
132
+ density="compact"
133
+ aria-disabled={action.disabled ? true : undefined}
134
+ tabIndex={action.disabled ? -1 : undefined}
135
+ >
136
+ {action.label}
137
+ </LinkButton>
138
+ ) : (
139
+ <Button
140
+ key={action.id}
141
+ type="button"
142
+ intent={action.intent ?? "ghost"}
143
+ density="compact"
144
+ disabled={action.disabled}
145
+ onClick={action.onSelect}
146
+ >
147
+ {action.label}
148
+ </Button>
149
+ )
150
+ )}
151
+ </div>
152
+ );
153
+ }
154
+
155
+ export function CRMContactPanel({
156
+ title = "Account relationship",
157
+ description,
158
+ status = "active",
159
+ metadata,
160
+ items,
161
+ contacts,
162
+ actions,
163
+ footer,
164
+ children,
165
+ className,
166
+ relationshipHealthLabel,
167
+ engagementLabel,
168
+ nextStep,
169
+ ...props
170
+ }: CRMContactPanelProps) {
171
+ const contactItems = contacts ?? items ?? [];
172
+ const contactsHeadingId = React.useId();
173
+ const signalsHeadingId = React.useId();
174
+ const tone = healthTone(status);
175
+ const health = relationshipHealthLabel ?? healthLabel(status);
176
+ const engagement =
177
+ engagementLabel ??
178
+ `${contactItems.length} contact${contactItems.length === 1 ? "" : "s"} mapped`;
179
+ const nextActivity = nextStep ?? "Review next activity";
180
+
181
+ return (
182
+ <Surface
183
+ {...props}
184
+ data-eth-component="CRMContactPanel"
185
+ title={title}
186
+ description={description}
187
+ status={status}
188
+ metadata={metadata}
189
+ actions={actions}
190
+ footer={footer}
191
+ className={["eth-domain-crm-contact-panel", className].filter(Boolean).join(" ")}
192
+ >
193
+ <div className="eth-domain-crm-contact-panel__body">
194
+ <section
195
+ className="eth-domain-crm-contact-panel__contacts"
196
+ aria-labelledby={contactsHeadingId}
197
+ >
198
+ <div className="eth-domain-crm-contact-panel__section-header">
199
+ <div>
200
+ <span>Relationship map</span>
201
+ <h3 id={contactsHeadingId}>Key contacts</h3>
202
+ </div>
203
+ <strong>{engagement}</strong>
204
+ </div>
205
+
206
+ {contactItems.length ? (
207
+ <div className="eth-domain-crm-contact-panel__contact-list" role="list">
208
+ {contactItems.map((item) => (
209
+ <article
210
+ key={item.id}
211
+ className="eth-domain-crm-contact-panel__contact"
212
+ role="listitem"
213
+ >
214
+ <span className="eth-domain-crm-contact-panel__avatar" aria-hidden="true">
215
+ {getContactInitials(item.label)}
216
+ </span>
217
+ <div className="eth-domain-crm-contact-panel__contact-main">
218
+ <strong>{item.href ? <a href={item.href}>{item.label}</a> : item.label}</strong>
219
+ {item.description ? <p>{item.description}</p> : null}
220
+ {item.meta ? (
221
+ <span className="eth-domain-crm-contact-panel__contact-meta">
222
+ {item.meta}
223
+ </span>
224
+ ) : null}
225
+ </div>
226
+ {item.status || item.actions?.length ? (
227
+ <div className="eth-domain-crm-contact-panel__contact-aside">
228
+ {item.status ? (
229
+ <StatusDot status={item.status} label={statusLabel(item.status)} />
230
+ ) : null}
231
+ <CRMContactActions actions={item.actions} />
232
+ </div>
233
+ ) : null}
234
+ </article>
235
+ ))}
236
+ </div>
237
+ ) : (
238
+ <p className="eth-domain-crm-contact-panel__empty">No contacts linked.</p>
239
+ )}
240
+ </section>
241
+
242
+ <aside className="eth-domain-crm-contact-panel__signals" aria-labelledby={signalsHeadingId}>
243
+ <div className="eth-domain-crm-contact-panel__section-header">
244
+ <div>
245
+ <span>Account signals</span>
246
+ <h3 id={signalsHeadingId}>Relationship health</h3>
247
+ </div>
248
+ </div>
249
+ <div className="eth-domain-crm-contact-panel__signal-list">
250
+ <div className="eth-domain-crm-contact-panel__signal" data-tone={tone}>
251
+ <span>Health</span>
252
+ <strong>{health}</strong>
253
+ </div>
254
+ <div className="eth-domain-crm-contact-panel__signal">
255
+ <span>Coverage</span>
256
+ <strong>{engagement}</strong>
257
+ </div>
258
+ <div className="eth-domain-crm-contact-panel__signal">
259
+ <span>Next step</span>
260
+ <strong>{nextActivity}</strong>
261
+ </div>
262
+ </div>
263
+ </aside>
264
+ </div>
265
+ {children}
266
+ </Surface>
267
+ );
268
+ }
269
+
270
+ export function SupportTicketQueue({
271
+ title = "Support queue",
272
+ description,
273
+ status,
274
+ metadata,
275
+ items,
276
+ tickets,
277
+ actions,
278
+ footer,
279
+ children,
280
+ className,
281
+ ...props
282
+ }: SupportTicketQueueProps) {
283
+ const ticketItems = tickets ?? items ?? [];
284
+ const queueId = React.useId().replace(/:/g, "");
285
+ const blockedCount = ticketItems.filter((item) =>
286
+ ["blocked", "failed", "approval-required"].includes(item.status ?? "")
287
+ ).length;
288
+ const runningCount = ticketItems.filter((item) =>
289
+ ["running", "in-progress"].includes(item.status ?? "")
290
+ ).length;
291
+ const nextSla =
292
+ textFromNode(
293
+ ticketItems.find((item) => item.status === "blocked")?.description ??
294
+ ticketItems[0]?.description
295
+ ) || "No active SLA";
296
+
297
+ const summary =
298
+ metadata ??
299
+ [
300
+ { label: "Open tickets", value: ticketItems.length.toLocaleString() },
301
+ { label: "Blocked", value: blockedCount.toLocaleString() },
302
+ { label: "In progress", value: runningCount.toLocaleString() },
303
+ { label: "Next SLA", value: nextSla }
304
+ ];
305
+
306
+ return (
307
+ <Surface
308
+ {...props}
309
+ data-eth-component="SupportTicketQueue"
310
+ title={title}
311
+ description={description}
312
+ status={status}
313
+ metadata={summary}
314
+ actions={actions}
315
+ footer={footer}
316
+ className={["eth-domain-support-ticket-queue", className].filter(Boolean).join(" ")}
317
+ >
318
+ {ticketItems.length ? (
319
+ <div
320
+ className="eth-domain-support-ticket-queue__list"
321
+ role="list"
322
+ aria-label="Support tickets"
323
+ >
324
+ {ticketItems.map((item) => {
325
+ const { priority, label } = splitTicketPriority(item.label);
326
+ const ticketId = `${queueId}-${safeDomId(item.id)}`;
327
+ const labelId = `${ticketId}-label`;
328
+ const slaId = item.description ? `${ticketId}-sla` : undefined;
329
+
330
+ return (
331
+ <article
332
+ key={item.id}
333
+ className="eth-domain-support-ticket-queue__row"
334
+ data-status={item.status}
335
+ role="listitem"
336
+ aria-labelledby={labelId}
337
+ aria-describedby={slaId}
338
+ >
339
+ <div className="eth-domain-support-ticket-queue__ticket">
340
+ {priority ? (
341
+ <span
342
+ className="eth-domain-support-ticket-queue__priority"
343
+ data-priority={priority}
344
+ >
345
+ {priority}
346
+ </span>
347
+ ) : null}
348
+ <strong id={labelId}>
349
+ {item.href ? <a href={item.href}>{label}</a> : label}
350
+ </strong>
351
+ </div>
352
+
353
+ {item.description ? (
354
+ <div
355
+ className={[
356
+ "eth-domain-support-ticket-queue__field",
357
+ "eth-domain-support-ticket-queue__field--sla"
358
+ ].join(" ")}
359
+ id={slaId}
360
+ >
361
+ <span>SLA</span>
362
+ <strong>{item.description}</strong>
363
+ </div>
364
+ ) : null}
365
+
366
+ {item.meta ? (
367
+ <div
368
+ className={[
369
+ "eth-domain-support-ticket-queue__field",
370
+ "eth-domain-support-ticket-queue__field--owner"
371
+ ].join(" ")}
372
+ >
373
+ <span>Owner</span>
374
+ <strong>{item.meta}</strong>
375
+ </div>
376
+ ) : null}
377
+
378
+ <div className="eth-domain-support-ticket-queue__status">
379
+ {item.status ? (
380
+ <StatusDot status={item.status} label={statusLabel(item.status)} />
381
+ ) : null}
382
+ <SupportTicketActions actions={item.actions} />
383
+ </div>
384
+ </article>
385
+ );
386
+ })}
387
+ </div>
388
+ ) : (
389
+ <p className="eth-domain-support-ticket-queue__empty">No active support tickets.</p>
390
+ )}
391
+ {children}
392
+ </Surface>
393
+ );
394
+ }
395
+
396
+ export interface ApprovalWorkflowStep extends Record<string, unknown> {
397
+ id: string;
398
+ label: string;
399
+ approver?: string;
400
+ condition?: string;
401
+ required?: boolean;
402
+ status?: EthOperationalStatus;
403
+ }
404
+
405
+ export interface ComplianceChecklistItem extends EthListItem {
406
+ controlId?: React.ReactNode;
407
+ evidence?: React.ReactNode;
408
+ evidenceHref?: string;
409
+ evidenceLabel?: React.ReactNode;
410
+ owner?: React.ReactNode;
411
+ dueDate?: React.ReactNode;
412
+ updatedAt?: React.ReactNode;
413
+ required?: boolean;
414
+ }
415
+
416
+ export interface ComplianceChecklistProps extends Omit<SurfaceComponentProps, "items"> {
417
+ items?: ComplianceChecklistItem[];
418
+ }
419
+
420
+ export type RiskMatrixImpact = "low" | "medium" | "high" | "critical";
421
+ export type RiskMatrixProbability = "low" | "medium" | "high";
422
+ export type RiskMatrixSeverity = "low" | "medium" | "high" | "critical";
423
+ export type RiskMatrixView = "matrix" | "register";
424
+
425
+ export interface RiskMatrixItem extends EthListItem {
426
+ impact?: RiskMatrixImpact;
427
+ probability?: RiskMatrixProbability;
428
+ severity?: RiskMatrixSeverity;
429
+ owner?: React.ReactNode;
430
+ mitigation?: React.ReactNode;
431
+ dueDate?: React.ReactNode;
432
+ score?: React.ReactNode;
433
+ }
434
+
435
+ export interface RiskMatrixProps extends Omit<SurfaceComponentProps, "items"> {
436
+ items?: RiskMatrixItem[];
437
+ selectedItemId?: string;
438
+ view?: RiskMatrixView;
439
+ reviewCadence?: React.ReactNode;
440
+ onViewChange?: (view: RiskMatrixView) => void;
441
+ }
442
+
443
+ export interface InvoiceLineItem extends EthListItem {
444
+ quantity?: React.ReactNode;
445
+ rate?: React.ReactNode;
446
+ amount?: React.ReactNode;
447
+ }
448
+
449
+ export interface InvoicePaymentMilestone extends EthListItem {
450
+ dueDate?: React.ReactNode;
451
+ amount?: React.ReactNode;
452
+ }
453
+
454
+ export interface InvoiceViewerProps extends Omit<SurfaceComponentProps, "items"> {
455
+ items?: InvoiceLineItem[];
456
+ customer?: React.ReactNode;
457
+ dueDate?: React.ReactNode;
458
+ terms?: React.ReactNode;
459
+ owner?: React.ReactNode;
460
+ subtotal?: React.ReactNode;
461
+ tax?: React.ReactNode;
462
+ total?: React.ReactNode;
463
+ amountDue?: React.ReactNode;
464
+ payments?: InvoicePaymentMilestone[];
465
+ }
466
+
467
+ export interface OrderFulfillmentItem extends EthListItem {
468
+ owner?: React.ReactNode;
469
+ timestamp?: React.ReactNode;
470
+ location?: React.ReactNode;
471
+ quantity?: React.ReactNode;
472
+ exception?: React.ReactNode;
473
+ }
474
+
475
+ export interface OrderManagementPanelProps extends Omit<SurfaceComponentProps, "items"> {
476
+ items?: OrderFulfillmentItem[];
477
+ customer?: React.ReactNode;
478
+ orderTotal?: React.ReactNode;
479
+ paymentTerms?: React.ReactNode;
480
+ shipTo?: React.ReactNode;
481
+ carrier?: React.ReactNode;
482
+ eta?: React.ReactNode;
483
+ fulfillmentOwner?: React.ReactNode;
484
+ }
485
+
486
+ export interface KnowledgeBaseArticleSection {
487
+ id: string;
488
+ title: React.ReactNode;
489
+ body: React.ReactNode;
490
+ status?: EthOperationalStatus;
491
+ }
492
+
493
+ export interface KnowledgeBaseRelatedArticle extends EthListItem {
494
+ type?: React.ReactNode;
495
+ updatedAt?: React.ReactNode;
496
+ }
497
+
498
+ export interface KnowledgeBaseCitation {
499
+ id: string;
500
+ label: React.ReactNode;
501
+ source?: React.ReactNode;
502
+ excerpt?: React.ReactNode;
503
+ href?: string;
504
+ status?: EthOperationalStatus;
505
+ }
506
+
507
+ export interface KnowledgeBaseArticleViewerProps extends Omit<SurfaceComponentProps, "items"> {
508
+ body?: React.ReactNode;
509
+ sections?: KnowledgeBaseArticleSection[];
510
+ relatedArticles?: KnowledgeBaseRelatedArticle[];
511
+ citations?: KnowledgeBaseCitation[];
512
+ items?: KnowledgeBaseRelatedArticle[];
513
+ articleStatusLabel?: React.ReactNode;
514
+ }
515
+
516
+ export interface DataImportWizardStep extends EthListItem {
517
+ owner?: React.ReactNode;
518
+ }
519
+
520
+ export interface DataImportFieldMapping {
521
+ id: string;
522
+ sourceField: React.ReactNode;
523
+ targetField: React.ReactNode;
524
+ sample?: React.ReactNode;
525
+ status?: EthOperationalStatus;
526
+ required?: boolean;
527
+ }
528
+
529
+ export interface DataImportValidationIssue {
530
+ id: string;
531
+ field: React.ReactNode;
532
+ message: React.ReactNode;
533
+ count?: React.ReactNode;
534
+ status?: EthOperationalStatus;
535
+ }
536
+
537
+ export interface DataImportWizardProps extends Omit<SurfaceComponentProps, "items"> {
538
+ items?: DataImportWizardStep[];
539
+ fileName?: React.ReactNode;
540
+ sourceLabel?: React.ReactNode;
541
+ rowCount?: React.ReactNode;
542
+ columnCount?: React.ReactNode;
543
+ mappingCount?: React.ReactNode;
544
+ validationSummary?: React.ReactNode;
545
+ mappings?: DataImportFieldMapping[];
546
+ validationIssues?: DataImportValidationIssue[];
547
+ }
548
+
549
+ export interface MappingConfigurationItem extends EthListItem {
550
+ sourceField?: React.ReactNode;
551
+ targetField?: React.ReactNode;
552
+ sourceObject?: React.ReactNode;
553
+ targetObject?: React.ReactNode;
554
+ sourceType?: React.ReactNode;
555
+ targetType?: React.ReactNode;
556
+ sample?: React.ReactNode;
557
+ transform?: React.ReactNode;
558
+ required?: boolean;
559
+ confidence?: React.ReactNode;
560
+ }
561
+
562
+ export interface MappingConfigurationIssue {
563
+ id: string;
564
+ field: React.ReactNode;
565
+ message: React.ReactNode;
566
+ status?: EthOperationalStatus;
567
+ count?: React.ReactNode;
568
+ }
569
+
570
+ export interface MappingConfigurationWizardProps extends Omit<SurfaceComponentProps, "items"> {
571
+ items?: MappingConfigurationItem[];
572
+ sourceObject?: React.ReactNode;
573
+ targetObject?: React.ReactNode;
574
+ mappingCount?: React.ReactNode;
575
+ validationSummary?: React.ReactNode;
576
+ targetOptions?: Array<{ value: string; label: string; disabled?: boolean }>;
577
+ validationIssues?: MappingConfigurationIssue[];
578
+ onMappingChange?: (id: string, targetField: string) => void;
579
+ onAutoMap?: () => void;
580
+ onValidate?: () => void;
581
+ onSubmit?: () => void;
582
+ }
583
+
584
+ export interface ReportBuilderSection extends EthListItem {
585
+ owner?: React.ReactNode;
586
+ source?: React.ReactNode;
587
+ updatedAt?: React.ReactNode;
588
+ required?: boolean;
589
+ }
590
+
591
+ export interface ReportBuilderProps extends Omit<SurfaceComponentProps, "items"> {
592
+ items?: ReportBuilderSection[];
593
+ formatLabel?: React.ReactNode;
594
+ owner?: React.ReactNode;
595
+ updatedAt?: React.ReactNode;
596
+ onAddSection?: () => void;
597
+ onExportPdf?: () => void;
598
+ onExportCsv?: () => void;
599
+ }
600
+
601
+ export interface InventoryTableItem extends EthListItem {
602
+ sku?: React.ReactNode;
603
+ location?: React.ReactNode;
604
+ onHand?: React.ReactNode;
605
+ available?: React.ReactNode;
606
+ allocated?: React.ReactNode;
607
+ reorderPoint?: React.ReactNode;
608
+ incoming?: React.ReactNode;
609
+ updatedAt?: React.ReactNode;
610
+ }
611
+
612
+ export interface InventoryTableProps extends Omit<SurfaceComponentProps, "items"> {
613
+ items?: InventoryTableItem[];
614
+ }
615
+
616
+ export interface ApprovalWorkflowEditorProps extends Omit<
617
+ SurfaceComponentProps,
618
+ "children" | "items" | "metadata" | "onChange"
619
+ > {
620
+ steps?: ApprovalWorkflowStep[];
621
+ approverOptions?: Array<{ value: string; label: string; disabled?: boolean }>;
622
+ mode?: ApprovalWorkflowMode;
623
+ onChange?: (steps: ApprovalWorkflowStep[]) => void;
624
+ onModeChange?: (mode: ApprovalWorkflowMode) => void;
625
+ }
626
+
627
+ export type ProcessDesignerNodeKind = "start" | "task" | "decision" | "end";
628
+
629
+ export interface ProcessDesignerItem extends EthListItem {
630
+ kind?: ProcessDesignerNodeKind;
631
+ owner?: React.ReactNode;
632
+ lane?: React.ReactNode;
633
+ duration?: React.ReactNode;
634
+ x?: number;
635
+ y?: number;
636
+ }
637
+
638
+ export interface ProcessDesignerConnection {
639
+ from: string;
640
+ to: string;
641
+ label?: React.ReactNode;
642
+ status?: EthOperationalStatus;
643
+ }
644
+
645
+ export interface ProcessDesignerValidationIssue {
646
+ id: string;
647
+ target?: React.ReactNode;
648
+ message: React.ReactNode;
649
+ status?: EthOperationalStatus;
650
+ }
651
+
652
+ export interface ProcessDesignerProps extends Omit<SurfaceComponentProps, "items"> {
653
+ items?: ProcessDesignerItem[];
654
+ connections?: ProcessDesignerConnection[];
655
+ validationIssues?: ProcessDesignerValidationIssue[];
656
+ selectedItemId?: string;
657
+ onAddStep?: () => void;
658
+ onValidate?: () => void;
659
+ }
660
+
661
+ const defaultApproverOptions = [
662
+ { value: "", label: "Select approver", disabled: true },
663
+ { value: "team-lead", label: "Team lead" },
664
+ { value: "finance-ops", label: "Finance operations" },
665
+ { value: "legal-review", label: "Legal review" },
666
+ { value: "security-office", label: "Security office" },
667
+ { value: "executive-sponsor", label: "Executive sponsor" }
668
+ ];
669
+
670
+ const defaultApprovalSteps: ApprovalWorkflowStep[] = [
671
+ {
672
+ id: "manager-review",
673
+ label: "Manager review",
674
+ approver: "team-lead",
675
+ condition: "Spend is above requester limit",
676
+ required: true,
677
+ status: "completed"
678
+ },
679
+ {
680
+ id: "finance-check",
681
+ label: "Finance control check",
682
+ approver: "finance-ops",
683
+ condition: "Amount is greater than $10,000",
684
+ required: true,
685
+ status: "pending-approval"
686
+ },
687
+ {
688
+ id: "legal-signoff",
689
+ label: "Legal sign-off",
690
+ approver: "legal-review",
691
+ condition: "Vendor terms changed",
692
+ required: false,
693
+ status: "queued"
694
+ }
695
+ ];
696
+
697
+ const modeOptions: Array<{ value: ApprovalWorkflowMode; label: string }> = [
698
+ { value: "sequential", label: "Sequential route" },
699
+ { value: "parallel", label: "Parallel quorum" },
700
+ { value: "conditional", label: "Conditional branch" }
701
+ ];
702
+
703
+ const defaultProcessItems: ProcessDesignerItem[] = [
704
+ {
705
+ id: "start",
706
+ label: "HRIS trigger",
707
+ description: "New employee record received from Workday.",
708
+ status: "completed",
709
+ kind: "start",
710
+ owner: "People ops",
711
+ lane: "Intake",
712
+ duration: "Instant",
713
+ x: 96,
714
+ y: 170
715
+ },
716
+ {
717
+ id: "profile",
718
+ label: "Collect profile",
719
+ description: "Manager confirms role, location, and start date.",
720
+ status: "completed",
721
+ kind: "task",
722
+ owner: "Hiring manager",
723
+ lane: "Intake",
724
+ duration: "4h SLA",
725
+ x: 278,
726
+ y: 170
727
+ },
728
+ {
729
+ id: "approval",
730
+ label: "Approval required?",
731
+ description: "Route privileged roles through security approval.",
732
+ status: "warning",
733
+ kind: "decision",
734
+ owner: "People systems",
735
+ lane: "Policy",
736
+ duration: "Conditional",
737
+ x: 460,
738
+ y: 170
739
+ },
740
+ {
741
+ id: "security",
742
+ label: "Security review",
743
+ description: "Validate device, access groups, and background check.",
744
+ status: "pending-approval",
745
+ kind: "task",
746
+ owner: "Security office",
747
+ lane: "Controls",
748
+ duration: "24h SLA",
749
+ x: 642,
750
+ y: 92
751
+ },
752
+ {
753
+ id: "provision",
754
+ label: "Provision access",
755
+ description: "Create accounts and assign application entitlements.",
756
+ status: "in-progress",
757
+ kind: "task",
758
+ owner: "IT operations",
759
+ lane: "Fulfillment",
760
+ duration: "2h SLA",
761
+ x: 642,
762
+ y: 248
763
+ },
764
+ {
765
+ id: "complete",
766
+ label: "Day-one ready",
767
+ description: "Welcome package, access, and training tasks are complete.",
768
+ status: "queued",
769
+ kind: "end",
770
+ owner: "People ops",
771
+ lane: "Fulfillment",
772
+ duration: "Start date",
773
+ x: 824,
774
+ y: 248
775
+ }
776
+ ];
777
+
778
+ const defaultProcessConnections: ProcessDesignerConnection[] = [
779
+ { from: "start", to: "profile" },
780
+ { from: "profile", to: "approval" },
781
+ { from: "approval", to: "security", label: "Needs review", status: "warning" },
782
+ { from: "approval", to: "provision", label: "Auto approve" },
783
+ { from: "security", to: "provision", label: "Approved" },
784
+ { from: "provision", to: "complete" }
785
+ ];
786
+
787
+ const defaultProcessValidationIssues: ProcessDesignerValidationIssue[] = [
788
+ {
789
+ id: "backup-owner",
790
+ target: "Approval required?",
791
+ message: "Privileged access branch needs a backup owner.",
792
+ status: "warning"
793
+ },
794
+ {
795
+ id: "sla-policy",
796
+ target: "Security review",
797
+ message: "Security review SLA is waiting on policy confirmation.",
798
+ status: "pending-approval"
799
+ }
800
+ ];
801
+
802
+ function nextApprovalStepId(steps: ApprovalWorkflowStep[]) {
803
+ let index = steps.length + 1;
804
+ let id = `approval-step-${index}`;
805
+ while (steps.some((step) => step.id === id)) {
806
+ index += 1;
807
+ id = `approval-step-${index}`;
808
+ }
809
+ return id;
810
+ }
811
+
812
+ function formatApprovalStatus(status: EthOperationalStatus | undefined, required?: boolean) {
813
+ if (status === "completed") return "Completed";
814
+ if (status === "pending-approval") return "Pending";
815
+ if (status === "blocked" || status === "failed") return "Blocked";
816
+ if (status === "queued") return "Queued";
817
+ return required ? "Required" : "Optional";
818
+ }
819
+
820
+ function isComplianceComplete(status: EthOperationalStatus | undefined) {
821
+ return status === "completed" || status === "succeeded" || status === "synced";
822
+ }
823
+
824
+ function needsComplianceAttention(status: EthOperationalStatus | undefined) {
825
+ return (
826
+ status === "approval-required" ||
827
+ status === "blocked" ||
828
+ status === "failed" ||
829
+ status === "pending-approval" ||
830
+ status === "stale" ||
831
+ status === "warning"
832
+ );
833
+ }
834
+
835
+ function deriveComplianceStatus(
836
+ items: ComplianceChecklistItem[]
837
+ ): EthOperationalStatus | undefined {
838
+ if (!items.length) return undefined;
839
+ if (items.some((item) => item.status === "blocked" || item.status === "failed")) {
840
+ return "blocked";
841
+ }
842
+ if (items.some((item) => item.status === "approval-required")) {
843
+ return "approval-required";
844
+ }
845
+ if (items.every((item) => isComplianceComplete(item.status))) {
846
+ return "completed";
847
+ }
848
+ return "in-progress";
849
+ }
850
+
851
+ function hasComplianceEvidence(item: ComplianceChecklistItem) {
852
+ return Boolean(item.evidence ?? item.evidenceLabel ?? item.evidenceHref ?? item.meta);
853
+ }
854
+
855
+ function hasDisplayValue(value: React.ReactNode) {
856
+ return value !== undefined && value !== null && value !== "";
857
+ }
858
+
859
+ function isReportSectionIncluded(status: EthOperationalStatus | undefined) {
860
+ return (
861
+ status !== "inactive" &&
862
+ status !== "not-started" &&
863
+ status !== "blocked" &&
864
+ status !== "failed"
865
+ );
866
+ }
867
+
868
+ function needsReportSectionAttention(status: EthOperationalStatus | undefined) {
869
+ return (
870
+ status === "approval-required" ||
871
+ status === "blocked" ||
872
+ status === "failed" ||
873
+ status === "pending-approval" ||
874
+ status === "stale" ||
875
+ status === "warning"
876
+ );
877
+ }
878
+
879
+ function deriveReportBuilderStatus(items: ReportBuilderSection[]): EthOperationalStatus {
880
+ if (!items.length) return "not-started";
881
+ if (items.some((item) => item.status === "blocked" || item.status === "failed")) {
882
+ return "blocked";
883
+ }
884
+ if (items.some((item) => needsReportSectionAttention(item.status))) return "warning";
885
+ if (items.some((item) => item.status === "in-progress" || item.status === "running")) {
886
+ return "in-progress";
887
+ }
888
+ return "active";
889
+ }
890
+
891
+ function reportBuilderReadinessLabel(status: EthOperationalStatus, attentionCount: number) {
892
+ if (attentionCount) {
893
+ return `${attentionCount} ${attentionCount === 1 ? "issue" : "issues"} to resolve`;
894
+ }
895
+ if (status === "not-started") return "Not started";
896
+ if (status === "in-progress" || status === "running") return "Draft in progress";
897
+ if (status === "blocked" || status === "failed") return "Blocked";
898
+ return "Ready";
899
+ }
900
+
901
+ function reportSectionMeta(item: ReportBuilderSection, index: number) {
902
+ return item.meta ?? item.updatedAt ?? item.source ?? `Section ${index + 1}`;
903
+ }
904
+
905
+ const riskImpactLevels: RiskMatrixImpact[] = ["critical", "high", "medium", "low"];
906
+ const riskProbabilityLevels: RiskMatrixProbability[] = ["low", "medium", "high"];
907
+ const riskImpactRank: Record<RiskMatrixImpact, number> = {
908
+ low: 1,
909
+ medium: 2,
910
+ high: 3,
911
+ critical: 4
912
+ };
913
+ const riskProbabilityRank: Record<RiskMatrixProbability, number> = {
914
+ low: 1,
915
+ medium: 2,
916
+ high: 3
917
+ };
918
+ const riskSeverityRank: Record<RiskMatrixSeverity, number> = {
919
+ low: 1,
920
+ medium: 2,
921
+ high: 3,
922
+ critical: 4
923
+ };
924
+ const riskImpactLabels: Record<RiskMatrixImpact, string> = {
925
+ low: "Low",
926
+ medium: "Medium",
927
+ high: "High",
928
+ critical: "Critical"
929
+ };
930
+ const riskProbabilityLabels: Record<RiskMatrixProbability, string> = {
931
+ low: "Low",
932
+ medium: "Medium",
933
+ high: "High"
934
+ };
935
+
936
+ interface ResolvedRiskMatrixItem extends RiskMatrixItem {
937
+ resolvedImpact: RiskMatrixImpact;
938
+ resolvedProbability: RiskMatrixProbability;
939
+ resolvedSeverity: RiskMatrixSeverity;
940
+ resolvedScore: number;
941
+ }
942
+
943
+ function isRiskImpact(value: string): value is RiskMatrixImpact {
944
+ return value === "low" || value === "medium" || value === "high" || value === "critical";
945
+ }
946
+
947
+ function isRiskProbability(value: string): value is RiskMatrixProbability {
948
+ return value === "low" || value === "medium" || value === "high";
949
+ }
950
+
951
+ function normalizeRiskImpact(value: unknown): RiskMatrixImpact | undefined {
952
+ if (typeof value !== "string") return undefined;
953
+ const normalized = value.trim().toLowerCase();
954
+ return isRiskImpact(normalized) ? normalized : undefined;
955
+ }
956
+
957
+ function normalizeRiskProbability(value: unknown): RiskMatrixProbability | undefined {
958
+ if (typeof value !== "string") return undefined;
959
+ const normalized = value.trim().toLowerCase();
960
+ return isRiskProbability(normalized) ? normalized : undefined;
961
+ }
962
+
963
+ function riskFieldFromDescription(item: RiskMatrixItem, field: "impact" | "probability") {
964
+ const text = textFromNode(item.description);
965
+ const match = text.match(new RegExp(`${field}\\s*[:=]\\s*(critical|high|medium|low)`, "i"));
966
+ return match?.[1];
967
+ }
968
+
969
+ function fallbackImpactForStatus(status: EthOperationalStatus | undefined): RiskMatrixImpact {
970
+ if (status === "blocked" || status === "failed" || status === "approval-required") {
971
+ return "critical";
972
+ }
973
+ if (status === "warning" || status === "stale" || status === "pending-approval") {
974
+ return "high";
975
+ }
976
+ if (status === "completed" || status === "succeeded" || status === "synced") return "low";
977
+ return "medium";
978
+ }
979
+
980
+ function riskSeverityFromScore(
981
+ impact: RiskMatrixImpact,
982
+ probability: RiskMatrixProbability
983
+ ): RiskMatrixSeverity {
984
+ const score = riskImpactRank[impact] * riskProbabilityRank[probability];
985
+ if (score >= 9) return "critical";
986
+ if (score >= 6) return "high";
987
+ if (score >= 3) return "medium";
988
+ return "low";
989
+ }
990
+
991
+ function resolveRiskMatrixItem(item: RiskMatrixItem): ResolvedRiskMatrixItem {
992
+ const resolvedImpact =
993
+ normalizeRiskImpact(item.impact) ??
994
+ normalizeRiskImpact(riskFieldFromDescription(item, "impact")) ??
995
+ fallbackImpactForStatus(item.status);
996
+ const resolvedProbability =
997
+ normalizeRiskProbability(item.probability) ??
998
+ normalizeRiskProbability(riskFieldFromDescription(item, "probability")) ??
999
+ "medium";
1000
+ const resolvedSeverity =
1001
+ item.severity ??
1002
+ (item.status === "blocked" || item.status === "failed"
1003
+ ? "critical"
1004
+ : riskSeverityFromScore(resolvedImpact, resolvedProbability));
1005
+
1006
+ return {
1007
+ ...item,
1008
+ resolvedImpact,
1009
+ resolvedProbability,
1010
+ resolvedSeverity,
1011
+ resolvedScore: riskImpactRank[resolvedImpact] * riskProbabilityRank[resolvedProbability]
1012
+ };
1013
+ }
1014
+
1015
+ function compareRiskPriority(a: ResolvedRiskMatrixItem, b: ResolvedRiskMatrixItem) {
1016
+ return (
1017
+ riskSeverityRank[b.resolvedSeverity] - riskSeverityRank[a.resolvedSeverity] ||
1018
+ b.resolvedScore - a.resolvedScore ||
1019
+ riskImpactRank[b.resolvedImpact] - riskImpactRank[a.resolvedImpact] ||
1020
+ riskProbabilityRank[b.resolvedProbability] - riskProbabilityRank[a.resolvedProbability]
1021
+ );
1022
+ }
1023
+
1024
+ function deriveRiskMatrixStatus(items: ResolvedRiskMatrixItem[]): EthOperationalStatus | undefined {
1025
+ if (!items.length) return undefined;
1026
+ if (items.some((item) => item.resolvedSeverity === "critical")) return "blocked";
1027
+ if (items.some((item) => item.resolvedSeverity === "high")) return "warning";
1028
+ if (items.some((item) => item.resolvedSeverity === "medium")) return "in-progress";
1029
+ return "active";
1030
+ }
1031
+
1032
+ function deriveRiskMatrixSurfaceSeverity(items: ResolvedRiskMatrixItem[]) {
1033
+ if (items.some((item) => item.resolvedSeverity === "critical")) return "danger" as const;
1034
+ if (items.some((item) => item.resolvedSeverity === "high")) return "warning" as const;
1035
+ return undefined;
1036
+ }
1037
+
1038
+ function riskCountLabel(count: number) {
1039
+ return `${count} risk${count === 1 ? "" : "s"}`;
1040
+ }
1041
+
1042
+ function isCompletedProcessStep(status: EthOperationalStatus | undefined) {
1043
+ return status === "completed" || status === "succeeded" || status === "synced";
1044
+ }
1045
+
1046
+ function processStepKind(
1047
+ item: ProcessDesignerItem,
1048
+ index: number,
1049
+ total: number
1050
+ ): ProcessDesignerNodeKind {
1051
+ if (item.kind) return item.kind;
1052
+ if (index === 0) return "start";
1053
+ if (index === total - 1) return "end";
1054
+ return "task";
1055
+ }
1056
+
1057
+ function deriveProcessStatus(
1058
+ items: ProcessDesignerItem[],
1059
+ validationIssues: ProcessDesignerValidationIssue[]
1060
+ ): EthOperationalStatus | undefined {
1061
+ if (validationIssues.some((issue) => issue.status === "blocked" || issue.status === "failed")) {
1062
+ return "blocked";
1063
+ }
1064
+ if (
1065
+ validationIssues.some(
1066
+ (issue) =>
1067
+ issue.status === "warning" ||
1068
+ issue.status === "pending-approval" ||
1069
+ issue.status === "approval-required"
1070
+ )
1071
+ ) {
1072
+ return "warning";
1073
+ }
1074
+ if (!items.length) return undefined;
1075
+ if (items.some((item) => item.status === "blocked" || item.status === "failed")) {
1076
+ return "blocked";
1077
+ }
1078
+ if (items.every((item) => isCompletedProcessStep(item.status))) {
1079
+ return "completed";
1080
+ }
1081
+ if (
1082
+ items.some(
1083
+ (item) =>
1084
+ item.status === "active" || item.status === "in-progress" || item.status === "running"
1085
+ )
1086
+ ) {
1087
+ return "in-progress";
1088
+ }
1089
+ return "queued";
1090
+ }
1091
+
1092
+ function sequentialProcessConnections(items: ProcessDesignerItem[]): ProcessDesignerConnection[] {
1093
+ return items.slice(1).map((item, index) => ({ from: items[index].id, to: item.id }));
1094
+ }
1095
+
1096
+ function processNodePoint(item: ProcessDesignerItem, index: number) {
1097
+ return {
1098
+ x: item.x ?? 110 + (index % 4) * 210,
1099
+ y: item.y ?? 140 + Math.floor(index / 4) * 140
1100
+ };
1101
+ }
1102
+
1103
+ function graphLabelLines(value: React.ReactNode, fallback: string) {
1104
+ const label = textFromNode(value).trim() || fallback;
1105
+ if (label.length <= 18) return [label];
1106
+
1107
+ const lines: string[] = [];
1108
+ let current = "";
1109
+ for (const word of label.split(/\s+/)) {
1110
+ const next = current ? `${current} ${word}` : word;
1111
+ if (next.length > 18 && current) {
1112
+ lines.push(current);
1113
+ current = word;
1114
+ } else {
1115
+ current = next;
1116
+ }
1117
+ }
1118
+ if (current) lines.push(current);
1119
+
1120
+ if (lines.length <= 2) return lines;
1121
+ return [lines[0], `${lines.slice(1).join(" ").slice(0, 16)}...`];
1122
+ }
1123
+
1124
+ const defaultOrderFulfillmentItems: OrderFulfillmentItem[] = [
1125
+ {
1126
+ id: "received",
1127
+ label: "Order received",
1128
+ description: "Purchase order matched against contract pricing and customer terms.",
1129
+ owner: "Sales operations",
1130
+ timestamp: "May 29, 08:12",
1131
+ status: "completed"
1132
+ },
1133
+ {
1134
+ id: "payment",
1135
+ label: "Payment terms cleared",
1136
+ description: "Net 30 credit check passed; release hold removed by finance.",
1137
+ owner: "Finance operations",
1138
+ timestamp: "May 29, 09:05",
1139
+ status: "completed"
1140
+ },
1141
+ {
1142
+ id: "pick-pack",
1143
+ label: "Pick and pack",
1144
+ description: "Warehouse B is staging the final cartons for carrier handoff.",
1145
+ owner: "Fulfillment team",
1146
+ location: "Warehouse B",
1147
+ quantity: "18 / 20 cartons",
1148
+ timestamp: "May 29, 14:40",
1149
+ status: "in-progress"
1150
+ },
1151
+ {
1152
+ id: "carrier",
1153
+ label: "Carrier handoff",
1154
+ description: "FedEx pickup booked; ASN will post when scan is received.",
1155
+ owner: "FedEx Ground",
1156
+ location: "Dock 4",
1157
+ timestamp: "May 29, 17:00",
1158
+ status: "queued"
1159
+ }
1160
+ ];
1161
+
1162
+ function isOrderStepComplete(status: EthOperationalStatus | undefined) {
1163
+ return status === "completed" || status === "succeeded" || status === "synced";
1164
+ }
1165
+
1166
+ function isOrderStepCurrent(status: EthOperationalStatus | undefined) {
1167
+ return status === "active" || status === "in-progress" || status === "running";
1168
+ }
1169
+
1170
+ function needsOrderAttention(item: OrderFulfillmentItem) {
1171
+ return (
1172
+ hasDisplayValue(item.exception) ||
1173
+ item.status === "approval-required" ||
1174
+ item.status === "blocked" ||
1175
+ item.status === "failed" ||
1176
+ item.status === "pending-approval" ||
1177
+ item.status === "stale" ||
1178
+ item.status === "warning"
1179
+ );
1180
+ }
1181
+
1182
+ function deriveOrderStatus(items: OrderFulfillmentItem[]): EthOperationalStatus | undefined {
1183
+ if (!items.length) return undefined;
1184
+ if (items.some((item) => item.status === "blocked" || item.status === "failed")) {
1185
+ return "blocked";
1186
+ }
1187
+ if (items.some((item) => item.status === "approval-required")) {
1188
+ return "approval-required";
1189
+ }
1190
+ if (items.every((item) => isOrderStepComplete(item.status))) return "completed";
1191
+ if (items.some((item) => isOrderStepCurrent(item.status))) return "in-progress";
1192
+ if (items.some((item) => item.status === "pending-approval" || item.status === "warning")) {
1193
+ return "pending-approval";
1194
+ }
1195
+ return "queued";
1196
+ }
1197
+
1198
+ function currentOrderStepIndex(items: OrderFulfillmentItem[]) {
1199
+ const activeIndex = items.findIndex((item) => isOrderStepCurrent(item.status));
1200
+ if (activeIndex >= 0) return activeIndex;
1201
+
1202
+ const nextIndex = items.findIndex((item) => !isOrderStepComplete(item.status));
1203
+ return nextIndex >= 0 ? nextIndex : Math.max(items.length - 1, 0);
1204
+ }
1205
+
1206
+ function OrderFulfillmentActions({ actions }: { actions?: EthAction[] }) {
1207
+ if (!actions?.length) return null;
1208
+
1209
+ return (
1210
+ <div className="eth-domain-order-management__step-actions">
1211
+ {actions.slice(0, 2).map((action) =>
1212
+ action.href ? (
1213
+ <LinkButton
1214
+ key={action.id}
1215
+ href={action.disabled ? undefined : action.href}
1216
+ intent={action.intent ?? "ghost"}
1217
+ density="compact"
1218
+ aria-disabled={action.disabled ? true : undefined}
1219
+ tabIndex={action.disabled ? -1 : undefined}
1220
+ >
1221
+ {action.label}
1222
+ </LinkButton>
1223
+ ) : (
1224
+ <Button
1225
+ key={action.id}
1226
+ type="button"
1227
+ intent={action.intent ?? "ghost"}
1228
+ density="compact"
1229
+ disabled={action.disabled}
1230
+ onClick={action.onSelect}
1231
+ >
1232
+ {action.label}
1233
+ </Button>
1234
+ )
1235
+ )}
1236
+ </div>
1237
+ );
1238
+ }
1239
+
1240
+ function parseInventoryNumber(value: React.ReactNode): number | null {
1241
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
1242
+ if (typeof value !== "string") return null;
1243
+
1244
+ const normalized = value.replace(/,/g, "").trim();
1245
+ if (!/^-?\d+(\.\d+)?$/.test(normalized)) return null;
1246
+
1247
+ const parsed = Number(normalized);
1248
+ return Number.isFinite(parsed) ? parsed : null;
1249
+ }
1250
+
1251
+ function formatInventoryValue(value: React.ReactNode) {
1252
+ if (!hasDisplayValue(value)) return "--";
1253
+ if (typeof value === "number") return new Intl.NumberFormat("en-US").format(value);
1254
+ return value;
1255
+ }
1256
+
1257
+ function parseInventoryStockDescription(description: React.ReactNode) {
1258
+ const text = textFromNode(description);
1259
+ const match = text.match(
1260
+ /\bOn hand:\s*([0-9,]+(?:\.\d+)?)\s*(?:[^A-Za-z0-9]+\s*)?Reorder:\s*([0-9,]+(?:\.\d+)?)/i
1261
+ );
1262
+
1263
+ if (!match) return null;
1264
+ return {
1265
+ onHand: Number(match[1].replace(/,/g, "")),
1266
+ reorderPoint: Number(match[2].replace(/,/g, ""))
1267
+ };
1268
+ }
1269
+
1270
+ function inventoryStatusFromLevels(
1271
+ item: InventoryTableItem,
1272
+ onHandNumber: number | null,
1273
+ reorderNumber: number | null
1274
+ ): EthOperationalStatus {
1275
+ if (item.status) return item.status;
1276
+ if (onHandNumber !== null && onHandNumber <= 0) return "failed";
1277
+ if (onHandNumber !== null && reorderNumber !== null && onHandNumber <= reorderNumber) {
1278
+ return "warning";
1279
+ }
1280
+ return "active";
1281
+ }
1282
+
1283
+ function inventoryStatusLabel(status: EthOperationalStatus) {
1284
+ if (status === "failed" || status === "blocked") return "Stockout";
1285
+ if (status === "warning" || status === "stale") return "Reorder";
1286
+ if (status === "in-progress" || status === "running") return "Replenishing";
1287
+ if (status === "queued" || status === "not-started") return "Pending count";
1288
+ if (
1289
+ status === "active" ||
1290
+ status === "completed" ||
1291
+ status === "succeeded" ||
1292
+ status === "synced"
1293
+ ) {
1294
+ return "In stock";
1295
+ }
1296
+ return statusLabel(status);
1297
+ }
1298
+
1299
+ interface InventoryTableRow {
1300
+ item: InventoryTableItem;
1301
+ parsedStockDescription: boolean;
1302
+ onHand: React.ReactNode;
1303
+ onHandNumber: number | null;
1304
+ reorderPoint: React.ReactNode;
1305
+ reorderNumber: number | null;
1306
+ available: React.ReactNode;
1307
+ status: EthOperationalStatus;
1308
+ }
1309
+
1310
+ function normalizeInventoryRow(item: InventoryTableItem): InventoryTableRow {
1311
+ const parsedStockDescription = parseInventoryStockDescription(item.description);
1312
+ const onHand = item.onHand ?? parsedStockDescription?.onHand;
1313
+ const reorderPoint = item.reorderPoint ?? parsedStockDescription?.reorderPoint;
1314
+ const onHandNumber = parseInventoryNumber(onHand);
1315
+ const reorderNumber = parseInventoryNumber(reorderPoint);
1316
+ const allocatedNumber = parseInventoryNumber(item.allocated);
1317
+ const available =
1318
+ item.available ??
1319
+ (onHandNumber !== null && allocatedNumber !== null
1320
+ ? onHandNumber - allocatedNumber
1321
+ : undefined);
1322
+ const status = inventoryStatusFromLevels(item, onHandNumber, reorderNumber);
1323
+
1324
+ return {
1325
+ item,
1326
+ parsedStockDescription: Boolean(parsedStockDescription),
1327
+ onHand,
1328
+ onHandNumber,
1329
+ reorderPoint,
1330
+ reorderNumber,
1331
+ available,
1332
+ status
1333
+ };
1334
+ }
1335
+
1336
+ function inventoryLevelPercent(row: InventoryTableRow) {
1337
+ if (row.onHandNumber === null || row.reorderNumber === null) return 0;
1338
+
1339
+ const capacity = Math.max(row.reorderNumber * 2, row.onHandNumber, 1);
1340
+ return Math.max(0, Math.min(100, Math.round((row.onHandNumber / capacity) * 100)));
1341
+ }
1342
+
1343
+ function isInventoryReorderRow(row: InventoryTableRow) {
1344
+ if (row.onHandNumber !== null && row.reorderNumber !== null) {
1345
+ return row.onHandNumber <= row.reorderNumber;
1346
+ }
1347
+ return row.status === "warning" || row.status === "failed" || row.status === "blocked";
1348
+ }
1349
+
1350
+ function isInventoryStockoutRow(row: InventoryTableRow) {
1351
+ if (row.onHandNumber !== null) return row.onHandNumber <= 0;
1352
+ return row.status === "failed" || row.status === "blocked";
1353
+ }
1354
+
1355
+ export function InventoryTable({
1356
+ title = "Inventory",
1357
+ description = "SKU availability, reorder thresholds, and fulfillment readiness.",
1358
+ items = [],
1359
+ metadata,
1360
+ actions,
1361
+ footer,
1362
+ children,
1363
+ className,
1364
+ ...props
1365
+ }: InventoryTableProps) {
1366
+ const rows = items.map(normalizeInventoryRow);
1367
+ const totalOnHand = rows.reduce(
1368
+ (sum, row) => (row.onHandNumber === null ? sum : sum + row.onHandNumber),
1369
+ 0
1370
+ );
1371
+ const hasOnHandTotal = rows.some((row) => row.onHandNumber !== null);
1372
+ const reorderCount = rows.filter(isInventoryReorderRow).length;
1373
+ const stockoutCount = rows.filter(isInventoryStockoutRow).length;
1374
+ const computedMetadata =
1375
+ metadata ??
1376
+ (rows.length
1377
+ ? [
1378
+ { label: "SKUs", value: rows.length },
1379
+ { label: "Below reorder", value: reorderCount },
1380
+ { label: "Stockouts", value: stockoutCount },
1381
+ ...(hasOnHandTotal
1382
+ ? [{ label: "On hand", value: new Intl.NumberFormat("en-US").format(totalOnHand) }]
1383
+ : [])
1384
+ ]
1385
+ : undefined);
1386
+
1387
+ return (
1388
+ <Surface
1389
+ {...props}
1390
+ data-eth-component="InventoryTable"
1391
+ title={title}
1392
+ description={description}
1393
+ metadata={computedMetadata}
1394
+ actions={actions}
1395
+ footer={footer}
1396
+ className={["eth-domain-inventory-table", className].filter(Boolean).join(" ")}
1397
+ >
1398
+ {rows.length ? (
1399
+ <div className="eth-domain-inventory-table__table-wrap">
1400
+ <table className="eth-domain-inventory-table__table">
1401
+ <caption className="eth-domain-inventory-table__caption">
1402
+ Inventory stock by SKU and reorder threshold
1403
+ </caption>
1404
+ <colgroup>
1405
+ <col className="eth-domain-inventory-table__sku-col" />
1406
+ <col className="eth-domain-inventory-table__location-col" />
1407
+ <col className="eth-domain-inventory-table__quantity-col" />
1408
+ <col className="eth-domain-inventory-table__quantity-col" />
1409
+ <col className="eth-domain-inventory-table__quantity-col" />
1410
+ <col className="eth-domain-inventory-table__status-col" />
1411
+ </colgroup>
1412
+ <thead>
1413
+ <tr>
1414
+ <th scope="col">SKU</th>
1415
+ <th scope="col">Location</th>
1416
+ <th scope="col">On hand</th>
1417
+ <th scope="col">Available</th>
1418
+ <th scope="col">Reorder point</th>
1419
+ <th scope="col">Stock status</th>
1420
+ </tr>
1421
+ </thead>
1422
+ <tbody>
1423
+ {rows.map((row) => {
1424
+ const { item } = row;
1425
+ const labelContent = item.href ? <a href={item.href}>{item.label}</a> : item.label;
1426
+ const showDescription =
1427
+ hasDisplayValue(item.description) && !row.parsedStockDescription;
1428
+ const location = item.location ?? item.meta;
1429
+ const statusLabelText = inventoryStatusLabel(row.status);
1430
+
1431
+ return (
1432
+ <tr key={item.id} data-status={row.status}>
1433
+ <td>
1434
+ <div className="eth-domain-inventory-table__sku">
1435
+ <span>{item.sku ?? item.id}</span>
1436
+ <strong>{labelContent}</strong>
1437
+ {showDescription ? <p>{item.description}</p> : null}
1438
+ </div>
1439
+ </td>
1440
+ <td>
1441
+ <div className="eth-domain-inventory-table__cell-stack">
1442
+ <strong>{formatInventoryValue(location)}</strong>
1443
+ {hasDisplayValue(item.updatedAt) ? (
1444
+ <span>{item.updatedAt}</span>
1445
+ ) : (
1446
+ <span>Cycle count current</span>
1447
+ )}
1448
+ </div>
1449
+ </td>
1450
+ <td>
1451
+ <div className="eth-domain-inventory-table__quantity">
1452
+ <strong>{formatInventoryValue(row.onHand)}</strong>
1453
+ <span className="eth-domain-inventory-table__level" aria-hidden="true">
1454
+ <span
1455
+ className="eth-domain-inventory-table__level-fill"
1456
+ style={{ inlineSize: `${inventoryLevelPercent(row)}%` }}
1457
+ />
1458
+ </span>
1459
+ </div>
1460
+ </td>
1461
+ <td className="eth-domain-inventory-table__numeric">
1462
+ {formatInventoryValue(row.available)}
1463
+ </td>
1464
+ <td className="eth-domain-inventory-table__numeric">
1465
+ {formatInventoryValue(row.reorderPoint)}
1466
+ </td>
1467
+ <td>
1468
+ <div className="eth-domain-inventory-table__state">
1469
+ <StatusDot status={row.status} label={statusLabelText} />
1470
+ {hasDisplayValue(item.incoming) ? (
1471
+ <span>Incoming {formatInventoryValue(item.incoming)}</span>
1472
+ ) : null}
1473
+ </div>
1474
+ </td>
1475
+ </tr>
1476
+ );
1477
+ })}
1478
+ </tbody>
1479
+ </table>
1480
+ </div>
1481
+ ) : (
1482
+ <div className="eth-domain-inventory-table__empty">No inventory items configured.</div>
1483
+ )}
1484
+ {children}
1485
+ </Surface>
1486
+ );
1487
+ }
1488
+
1489
+ export function OrderManagementPanel({
1490
+ title = "Order",
1491
+ description,
1492
+ status,
1493
+ metadata,
1494
+ actions,
1495
+ footer,
1496
+ children,
1497
+ className,
1498
+ items = defaultOrderFulfillmentItems,
1499
+ customer,
1500
+ orderTotal,
1501
+ paymentTerms,
1502
+ shipTo,
1503
+ carrier,
1504
+ eta,
1505
+ fulfillmentOwner = "Fulfillment operations",
1506
+ ...props
1507
+ }: OrderManagementPanelProps) {
1508
+ const orderStatus = status ?? deriveOrderStatus(items) ?? "not-started";
1509
+ const currentIndex = currentOrderStepIndex(items);
1510
+ const currentStep = items[currentIndex];
1511
+ const completedCount = items.filter((item) => isOrderStepComplete(item.status)).length;
1512
+ const attentionCount = items.filter(needsOrderAttention).length;
1513
+ const lifecycleHeadingId = React.useId();
1514
+ const executionHeadingId = React.useId();
1515
+ const computedMetadata =
1516
+ metadata ??
1517
+ [
1518
+ {
1519
+ label: "Fulfillment",
1520
+ value: items.length ? `${completedCount}/${items.length} checkpoints` : undefined
1521
+ },
1522
+ { label: "Carrier", value: carrier },
1523
+ { label: "ETA", value: eta },
1524
+ {
1525
+ label: "Exceptions",
1526
+ value: attentionCount ? `${attentionCount} open` : "None"
1527
+ }
1528
+ ].filter((item) => hasDisplayValue(item.value));
1529
+ const executionSummary = [
1530
+ { label: "Customer", value: customer },
1531
+ { label: "Current checkpoint", value: currentStep?.label },
1532
+ { label: "Owner", value: currentStep?.owner ?? fulfillmentOwner },
1533
+ { label: "Ship to", value: shipTo },
1534
+ { label: "Payment terms", value: paymentTerms },
1535
+ { label: "Order total", value: orderTotal }
1536
+ ].filter((item) => hasDisplayValue(item.value));
1537
+ const activeException = items.find(needsOrderAttention);
1538
+
1539
+ return (
1540
+ <Surface
1541
+ {...props}
1542
+ data-eth-component="OrderManagementPanel"
1543
+ title={title}
1544
+ description={description}
1545
+ status={orderStatus}
1546
+ metadata={computedMetadata.length ? computedMetadata : undefined}
1547
+ actions={actions}
1548
+ footer={footer}
1549
+ className={["eth-domain-order-management", className].filter(Boolean).join(" ")}
1550
+ >
1551
+ <div className="eth-domain-order-management__body">
1552
+ <section
1553
+ className="eth-domain-order-management__timeline"
1554
+ aria-labelledby={lifecycleHeadingId}
1555
+ >
1556
+ <div className="eth-domain-order-management__section-header">
1557
+ <div>
1558
+ <span>Fulfillment</span>
1559
+ <h3 id={lifecycleHeadingId}>Lifecycle</h3>
1560
+ </div>
1561
+ <strong>
1562
+ {items.length ? `${completedCount} of ${items.length} complete` : "No checkpoints"}
1563
+ </strong>
1564
+ </div>
1565
+
1566
+ {items.length ? (
1567
+ <ol className="eth-domain-order-management__steps">
1568
+ {items.map((item, index) => {
1569
+ const itemStatus = item.status ?? "not-started";
1570
+ const labelContent = item.href ? (
1571
+ <a href={item.href}>{item.label}</a>
1572
+ ) : (
1573
+ item.label
1574
+ );
1575
+ const detailRows = [
1576
+ { label: "Owner", value: item.owner },
1577
+ { label: "Location", value: item.location },
1578
+ { label: "Quantity", value: item.quantity },
1579
+ { label: "Time", value: item.timestamp ?? item.meta }
1580
+ ].filter((detail) => hasDisplayValue(detail.value));
1581
+
1582
+ return (
1583
+ <li
1584
+ key={item.id}
1585
+ aria-current={index === currentIndex ? "step" : undefined}
1586
+ data-current={index === currentIndex ? "true" : undefined}
1587
+ data-status={itemStatus}
1588
+ >
1589
+ <span className="eth-domain-order-management__step-marker" aria-hidden="true">
1590
+ {index + 1}
1591
+ </span>
1592
+ <div className="eth-domain-order-management__step-main">
1593
+ <div className="eth-domain-order-management__step-heading">
1594
+ <strong>{labelContent}</strong>
1595
+ <StatusDot status={itemStatus} label={statusLabel(itemStatus)} />
1596
+ </div>
1597
+ {item.description ? <p>{item.description}</p> : null}
1598
+ {detailRows.length ? (
1599
+ <dl className="eth-domain-order-management__step-meta">
1600
+ {detailRows.map((detail, detailIndex) => (
1601
+ <div key={detailIndex}>
1602
+ <dt>{detail.label}</dt>
1603
+ <dd>{detail.value}</dd>
1604
+ </div>
1605
+ ))}
1606
+ </dl>
1607
+ ) : null}
1608
+ {item.exception ? (
1609
+ <div className="eth-domain-order-management__step-exception">
1610
+ {item.exception}
1611
+ </div>
1612
+ ) : null}
1613
+ <OrderFulfillmentActions actions={item.actions} />
1614
+ </div>
1615
+ </li>
1616
+ );
1617
+ })}
1618
+ </ol>
1619
+ ) : (
1620
+ <div className="eth-domain-order-management__empty">
1621
+ No fulfillment checkpoints configured.
1622
+ </div>
1623
+ )}
1624
+ </section>
1625
+
1626
+ <aside
1627
+ className="eth-domain-order-management__execution"
1628
+ aria-labelledby={executionHeadingId}
1629
+ >
1630
+ <div className="eth-domain-order-management__section-header">
1631
+ <div>
1632
+ <span>Execution</span>
1633
+ <h3 id={executionHeadingId}>Order controls</h3>
1634
+ </div>
1635
+ <StatusDot status={orderStatus} label={statusLabel(orderStatus)} />
1636
+ </div>
1637
+
1638
+ {executionSummary.length ? (
1639
+ <dl className="eth-domain-order-management__summary">
1640
+ {executionSummary.map((item, index) => (
1641
+ <div key={index}>
1642
+ <dt>{item.label}</dt>
1643
+ <dd>{item.value}</dd>
1644
+ </div>
1645
+ ))}
1646
+ </dl>
1647
+ ) : null}
1648
+
1649
+ <div
1650
+ className="eth-domain-order-management__attention"
1651
+ data-status={activeException ? activeException.status ?? "warning" : "completed"}
1652
+ >
1653
+ <span>Exception state</span>
1654
+ <strong>
1655
+ {activeException ? activeException.label : "No active fulfillment exceptions"}
1656
+ </strong>
1657
+ {activeException?.exception ? <p>{activeException.exception}</p> : null}
1658
+ </div>
1659
+ </aside>
1660
+ </div>
1661
+ {children}
1662
+ </Surface>
1663
+ );
1664
+ }
1665
+
1666
+ const defaultKnowledgeBaseSections: KnowledgeBaseArticleSection[] = [
1667
+ {
1668
+ id: "routing",
1669
+ title: "Approval routing",
1670
+ body: "Requests with external recipients enter legal review before delivery. The route records each approver, decision, exception reason, and timestamp for audit export.",
1671
+ status: "active"
1672
+ },
1673
+ {
1674
+ id: "exceptions",
1675
+ title: "Exception handling",
1676
+ body: "If legal requests a change, the article owner receives a revision task and external send remains disabled until the legal approval returns.",
1677
+ status: "pending-approval"
1678
+ }
1679
+ ];
1680
+
1681
+ const defaultKnowledgeBaseRelatedArticles: KnowledgeBaseRelatedArticle[] = [
1682
+ {
1683
+ id: "legal-review-sla",
1684
+ label: "Legal review SLA",
1685
+ description: "Response windows, backup approvers, and overdue escalation paths.",
1686
+ meta: "Policy",
1687
+ type: "Policy",
1688
+ updatedAt: "Updated May 20",
1689
+ status: "active"
1690
+ },
1691
+ {
1692
+ id: "external-send-controls",
1693
+ label: "External send controls",
1694
+ description: "Controls that keep outbound delivery locked while approval is open.",
1695
+ meta: "Runbook",
1696
+ type: "Runbook",
1697
+ updatedAt: "Updated May 18",
1698
+ status: "in-progress"
1699
+ }
1700
+ ];
1701
+
1702
+ const defaultKnowledgeBaseCitations: KnowledgeBaseCitation[] = [
1703
+ {
1704
+ id: "approval-policy",
1705
+ label: "External send approval policy",
1706
+ source: "Policy KB-214",
1707
+ excerpt: "Outbound delivery requires recorded legal approval.",
1708
+ status: "completed"
1709
+ },
1710
+ {
1711
+ id: "audit-control",
1712
+ label: "Audit evidence control",
1713
+ source: "Control AE-09",
1714
+ excerpt: "Approval decisions must include owner, timestamp, and reason.",
1715
+ status: "active"
1716
+ }
1717
+ ];
1718
+
1719
+ function knowledgeBaseStatusLabel(status: EthOperationalStatus | undefined) {
1720
+ return status ? statusLabel(status) : "Published";
1721
+ }
1722
+
1723
+ function hasKnowledgeBaseNode(node: React.ReactNode) {
1724
+ return node !== undefined && node !== null && node !== false && node !== "";
1725
+ }
1726
+
1727
+ function renderKnowledgeBaseNode(node: React.ReactNode) {
1728
+ if (!hasKnowledgeBaseNode(node)) return null;
1729
+ if (typeof node === "string" || typeof node === "number") return <p>{node}</p>;
1730
+ return node;
1731
+ }
1732
+
1733
+ export function ComplianceChecklist({
1734
+ title = "Compliance checklist",
1735
+ description = "Evidence readiness, control ownership, and audit state for the current review.",
1736
+ items = [],
1737
+ metadata,
1738
+ status,
1739
+ footer,
1740
+ children,
1741
+ className,
1742
+ ...props
1743
+ }: ComplianceChecklistProps) {
1744
+ const completeCount = items.filter((item) => isComplianceComplete(item.status)).length;
1745
+ const attentionCount = items.filter((item) => needsComplianceAttention(item.status)).length;
1746
+ const evidenceCount = items.filter(hasComplianceEvidence).length;
1747
+ const computedMetadata =
1748
+ metadata ??
1749
+ (items.length
1750
+ ? [
1751
+ { label: "Complete", value: `${completeCount}/${items.length}` },
1752
+ { label: "Needs attention", value: attentionCount },
1753
+ { label: "Evidence linked", value: `${evidenceCount}/${items.length}` }
1754
+ ]
1755
+ : undefined);
1756
+
1757
+ return (
1758
+ <Surface
1759
+ {...props}
1760
+ data-eth-component="ComplianceChecklist"
1761
+ title={title}
1762
+ description={description}
1763
+ status={status ?? deriveComplianceStatus(items)}
1764
+ metadata={computedMetadata}
1765
+ footer={footer}
1766
+ className={["eth-domain-compliance-checklist", className].filter(Boolean).join(" ")}
1767
+ >
1768
+ {items.length ? (
1769
+ <div className="eth-domain-compliance-checklist__table-wrap">
1770
+ <table className="eth-domain-compliance-checklist__table">
1771
+ <caption className="eth-domain-compliance-checklist__caption">
1772
+ Compliance evidence checklist
1773
+ </caption>
1774
+ <colgroup>
1775
+ <col className="eth-domain-compliance-checklist__control-col" />
1776
+ <col className="eth-domain-compliance-checklist__owner-col" />
1777
+ <col className="eth-domain-compliance-checklist__evidence-col" />
1778
+ <col className="eth-domain-compliance-checklist__state-col" />
1779
+ </colgroup>
1780
+ <thead>
1781
+ <tr>
1782
+ <th scope="col">Control</th>
1783
+ <th scope="col">Owner</th>
1784
+ <th scope="col">Evidence</th>
1785
+ <th scope="col">Audit state</th>
1786
+ </tr>
1787
+ </thead>
1788
+ <tbody>
1789
+ {items.map((item) => {
1790
+ const itemStatus = item.status ?? "not-started";
1791
+ const labelContent = item.href ? <a href={item.href}>{item.label}</a> : item.label;
1792
+ const evidenceContent = item.evidence ?? item.evidenceLabel ?? item.meta;
1793
+
1794
+ return (
1795
+ <tr key={item.id} data-status={itemStatus}>
1796
+ <td>
1797
+ <div className="eth-domain-compliance-checklist__control">
1798
+ {item.controlId ? (
1799
+ <span className="eth-domain-compliance-checklist__control-id">
1800
+ {item.controlId}
1801
+ </span>
1802
+ ) : null}
1803
+ <strong>{labelContent}</strong>
1804
+ {item.description ? <p>{item.description}</p> : null}
1805
+ </div>
1806
+ </td>
1807
+ <td>
1808
+ <div className="eth-domain-compliance-checklist__cell-stack">
1809
+ <strong>{item.owner ?? "Unassigned"}</strong>
1810
+ {item.dueDate ? (
1811
+ <span className="eth-domain-compliance-checklist__secondary">
1812
+ Due {item.dueDate}
1813
+ </span>
1814
+ ) : null}
1815
+ </div>
1816
+ </td>
1817
+ <td>
1818
+ <div className="eth-domain-compliance-checklist__cell-stack">
1819
+ {evidenceContent ? (
1820
+ item.evidenceHref ? (
1821
+ <a
1822
+ className="eth-domain-compliance-checklist__evidence-link"
1823
+ href={item.evidenceHref}
1824
+ >
1825
+ {evidenceContent}
1826
+ </a>
1827
+ ) : (
1828
+ <strong>{evidenceContent}</strong>
1829
+ )
1830
+ ) : (
1831
+ <span className="eth-domain-compliance-checklist__secondary">
1832
+ Evidence not linked
1833
+ </span>
1834
+ )}
1835
+ {item.updatedAt ? (
1836
+ <span className="eth-domain-compliance-checklist__secondary">
1837
+ {item.updatedAt}
1838
+ </span>
1839
+ ) : null}
1840
+ </div>
1841
+ </td>
1842
+ <td>
1843
+ <div className="eth-domain-compliance-checklist__state">
1844
+ <StatusDot status={itemStatus} label={statusLabel(itemStatus)} />
1845
+ {item.required ? (
1846
+ <span className="eth-domain-compliance-checklist__requirement">
1847
+ Required
1848
+ </span>
1849
+ ) : null}
1850
+ </div>
1851
+ </td>
1852
+ </tr>
1853
+ );
1854
+ })}
1855
+ </tbody>
1856
+ </table>
1857
+ </div>
1858
+ ) : (
1859
+ <div className="eth-domain-compliance-checklist__empty">
1860
+ No compliance controls configured.
1861
+ </div>
1862
+ )}
1863
+ {children}
1864
+ </Surface>
1865
+ );
1866
+ }
1867
+
1868
+ function RiskMatrixActions({ actions }: { actions?: EthAction[] }) {
1869
+ if (!actions?.length) return null;
1870
+
1871
+ return (
1872
+ <div className="eth-domain-risk-matrix__item-actions">
1873
+ {actions.map((action) =>
1874
+ action.href ? (
1875
+ <LinkButton
1876
+ key={action.id}
1877
+ href={action.disabled ? undefined : action.href}
1878
+ intent={action.intent ?? "ghost"}
1879
+ density="compact"
1880
+ aria-disabled={action.disabled ? true : undefined}
1881
+ tabIndex={action.disabled ? -1 : undefined}
1882
+ >
1883
+ {action.label}
1884
+ </LinkButton>
1885
+ ) : (
1886
+ <Button
1887
+ key={action.id}
1888
+ type="button"
1889
+ intent={action.intent ?? "ghost"}
1890
+ density="compact"
1891
+ disabled={action.disabled}
1892
+ onClick={action.onSelect}
1893
+ >
1894
+ {action.label}
1895
+ </Button>
1896
+ )
1897
+ )}
1898
+ </div>
1899
+ );
1900
+ }
1901
+
1902
+ function RiskMatrixFact({
1903
+ label,
1904
+ value
1905
+ }: {
1906
+ label: React.ReactNode;
1907
+ value: React.ReactNode;
1908
+ }) {
1909
+ if (!hasDisplayValue(value)) return null;
1910
+
1911
+ return (
1912
+ <div>
1913
+ <dt>{label}</dt>
1914
+ <dd>{value}</dd>
1915
+ </div>
1916
+ );
1917
+ }
1918
+
1919
+ export function RiskMatrix({
1920
+ title = "Risk register",
1921
+ description = "Impact and probability register with mitigation ownership.",
1922
+ status,
1923
+ severity,
1924
+ metadata,
1925
+ actions,
1926
+ footer,
1927
+ children,
1928
+ className,
1929
+ items = [],
1930
+ selectedItemId,
1931
+ view,
1932
+ reviewCadence,
1933
+ onViewChange,
1934
+ ...props
1935
+ }: RiskMatrixProps) {
1936
+ const matrixHeadingId = React.useId();
1937
+ const registerHeadingId = React.useId();
1938
+ const [internalView, setInternalView] = React.useState<RiskMatrixView>("matrix");
1939
+ const currentView = view ?? internalView;
1940
+ const risks = items.map(resolveRiskMatrixItem).sort(compareRiskPriority);
1941
+ const priorityRisk = selectedItemId
1942
+ ? risks.find((item) => item.id === selectedItemId) ?? risks[0]
1943
+ : risks[0];
1944
+ const criticalCount = risks.filter((item) => item.resolvedSeverity === "critical").length;
1945
+ const highCount = risks.filter((item) => item.resolvedSeverity === "high").length;
1946
+ const mediumCount = risks.filter((item) => item.resolvedSeverity === "medium").length;
1947
+ const priorityStatus = priorityRisk
1948
+ ? priorityRisk.status ?? deriveRiskMatrixStatus([priorityRisk]) ?? "active"
1949
+ : "active";
1950
+ const computedMetadata =
1951
+ metadata ??
1952
+ [
1953
+ { label: "Open risks", value: risks.length },
1954
+ { label: "Critical", value: criticalCount },
1955
+ { label: "High", value: highCount },
1956
+ { label: "Medium", value: mediumCount }
1957
+ ];
1958
+ const changeView = (nextView: RiskMatrixView) => {
1959
+ if (view === undefined) setInternalView(nextView);
1960
+ onViewChange?.(nextView);
1961
+ };
1962
+
1963
+ return (
1964
+ <Surface
1965
+ {...props}
1966
+ data-eth-component="RiskMatrix"
1967
+ title={title}
1968
+ description={description}
1969
+ status={status ?? deriveRiskMatrixStatus(risks)}
1970
+ severity={severity ?? deriveRiskMatrixSurfaceSeverity(risks)}
1971
+ metadata={computedMetadata}
1972
+ actions={actions}
1973
+ footer={footer}
1974
+ className={["eth-domain-risk-matrix", className].filter(Boolean).join(" ")}
1975
+ >
1976
+ <div
1977
+ className="eth-domain-risk-matrix__toolbar"
1978
+ role="group"
1979
+ aria-label="Risk matrix controls"
1980
+ >
1981
+ <div className="eth-domain-risk-matrix__view-switch" role="group" aria-label="Risk view">
1982
+ <Button
1983
+ type="button"
1984
+ intent={currentView === "matrix" ? "secondary" : "ghost"}
1985
+ density="compact"
1986
+ aria-label="Matrix view"
1987
+ aria-pressed={currentView === "matrix"}
1988
+ onClick={() => changeView("matrix")}
1989
+ >
1990
+ Matrix
1991
+ </Button>
1992
+ <Button
1993
+ type="button"
1994
+ intent={currentView === "register" ? "secondary" : "ghost"}
1995
+ density="compact"
1996
+ aria-label="Register view"
1997
+ aria-pressed={currentView === "register"}
1998
+ onClick={() => changeView("register")}
1999
+ >
2000
+ Register
2001
+ </Button>
2002
+ </div>
2003
+ {reviewCadence ? (
2004
+ <span className="eth-domain-risk-matrix__cadence">
2005
+ Review <strong>{reviewCadence}</strong>
2006
+ </span>
2007
+ ) : null}
2008
+ {criticalCount || highCount ? (
2009
+ <span className="eth-domain-risk-matrix__attention">
2010
+ {criticalCount} critical / {highCount} high
2011
+ </span>
2012
+ ) : null}
2013
+ </div>
2014
+
2015
+ {risks.length ? (
2016
+ <div className="eth-domain-risk-matrix__workspace" data-view={currentView}>
2017
+ {currentView === "matrix" ? (
2018
+ <section
2019
+ className="eth-domain-risk-matrix__matrix-panel"
2020
+ aria-labelledby={matrixHeadingId}
2021
+ >
2022
+ <div className="eth-domain-risk-matrix__panel-header">
2023
+ <div>
2024
+ <span>Matrix</span>
2025
+ <h3 id={matrixHeadingId}>Impact x probability</h3>
2026
+ </div>
2027
+ <div
2028
+ className="eth-domain-risk-matrix__legend"
2029
+ role="list"
2030
+ aria-label="Risk severity legend"
2031
+ >
2032
+ {(["low", "medium", "high", "critical"] as RiskMatrixSeverity[]).map(
2033
+ (riskSeverity) => (
2034
+ <span key={riskSeverity} data-severity={riskSeverity} role="listitem">
2035
+ {riskImpactLabels[riskSeverity]}
2036
+ </span>
2037
+ )
2038
+ )}
2039
+ </div>
2040
+ </div>
2041
+
2042
+ <div
2043
+ className="eth-domain-risk-matrix__grid"
2044
+ role="grid"
2045
+ aria-label="Impact probability risk matrix"
2046
+ >
2047
+ <div
2048
+ className="eth-domain-risk-matrix__grid-row eth-domain-risk-matrix__grid-row--header"
2049
+ role="row"
2050
+ >
2051
+ <div className="eth-domain-risk-matrix__corner" role="columnheader">
2052
+ Impact
2053
+ </div>
2054
+ {riskProbabilityLevels.map((probability) => (
2055
+ <div
2056
+ key={probability}
2057
+ className="eth-domain-risk-matrix__axis"
2058
+ role="columnheader"
2059
+ >
2060
+ <span>Probability</span>
2061
+ <strong>{riskProbabilityLabels[probability]}</strong>
2062
+ </div>
2063
+ ))}
2064
+ </div>
2065
+
2066
+ {riskImpactLevels.map((impact) => (
2067
+ <div key={impact} className="eth-domain-risk-matrix__grid-row" role="row">
2068
+ <div
2069
+ className="eth-domain-risk-matrix__axis eth-domain-risk-matrix__row-axis"
2070
+ role="rowheader"
2071
+ >
2072
+ <span>Impact</span>
2073
+ <strong>{riskImpactLabels[impact]}</strong>
2074
+ </div>
2075
+ {riskProbabilityLevels.map((probability) => {
2076
+ const cellRisks = risks.filter(
2077
+ (item) =>
2078
+ item.resolvedImpact === impact && item.resolvedProbability === probability
2079
+ );
2080
+ const cellSeverity =
2081
+ cellRisks[0]?.resolvedSeverity ??
2082
+ riskSeverityFromScore(impact, probability);
2083
+
2084
+ return (
2085
+ <div
2086
+ key={`${impact}-${probability}`}
2087
+ className="eth-domain-risk-matrix__cell"
2088
+ data-severity={cellSeverity}
2089
+ role="gridcell"
2090
+ aria-label={`${riskImpactLabels[impact]} impact, ${riskProbabilityLabels[probability]} probability: ${riskCountLabel(cellRisks.length)}`}
2091
+ >
2092
+ {cellRisks.length ? (
2093
+ <div className="eth-domain-risk-matrix__cell-risks" role="list">
2094
+ {cellRisks.map((risk) => {
2095
+ const labelContent = risk.href ? (
2096
+ <a href={risk.href}>{risk.label}</a>
2097
+ ) : (
2098
+ risk.label
2099
+ );
2100
+
2101
+ return (
2102
+ <article
2103
+ key={risk.id}
2104
+ className="eth-domain-risk-matrix__risk-card"
2105
+ data-severity={risk.resolvedSeverity}
2106
+ role="listitem"
2107
+ >
2108
+ <span className="eth-domain-risk-matrix__risk-score">
2109
+ {risk.score ?? risk.resolvedScore}
2110
+ </span>
2111
+ <div className="eth-domain-risk-matrix__risk-card-main">
2112
+ <strong>{labelContent}</strong>
2113
+ <div className="eth-domain-risk-matrix__risk-card-meta">
2114
+ {risk.owner ? <span>{risk.owner}</span> : null}
2115
+ {risk.dueDate ? <span>{risk.dueDate}</span> : null}
2116
+ </div>
2117
+ </div>
2118
+ </article>
2119
+ );
2120
+ })}
2121
+ </div>
2122
+ ) : (
2123
+ <span className="eth-domain-risk-matrix__empty-cell">No risks</span>
2124
+ )}
2125
+ </div>
2126
+ );
2127
+ })}
2128
+ </div>
2129
+ ))}
2130
+ </div>
2131
+ </section>
2132
+ ) : null}
2133
+
2134
+ <aside className="eth-domain-risk-matrix__detail" aria-label="Highest priority risk">
2135
+ {priorityRisk ? (
2136
+ <>
2137
+ <div className="eth-domain-risk-matrix__panel-header">
2138
+ <div>
2139
+ <span>Highest priority</span>
2140
+ <h3>{priorityRisk.label}</h3>
2141
+ </div>
2142
+ <StatusDot status={priorityStatus} label={statusLabel(priorityStatus)} />
2143
+ </div>
2144
+ {priorityRisk.description ? <p>{priorityRisk.description}</p> : null}
2145
+ <dl className="eth-domain-risk-matrix__facts">
2146
+ <RiskMatrixFact
2147
+ label="Impact"
2148
+ value={riskImpactLabels[priorityRisk.resolvedImpact]}
2149
+ />
2150
+ <RiskMatrixFact
2151
+ label="Probability"
2152
+ value={riskProbabilityLabels[priorityRisk.resolvedProbability]}
2153
+ />
2154
+ <RiskMatrixFact label="Owner" value={priorityRisk.owner} />
2155
+ <RiskMatrixFact label="Due" value={priorityRisk.dueDate} />
2156
+ <RiskMatrixFact
2157
+ label="Score"
2158
+ value={priorityRisk.score ?? priorityRisk.resolvedScore}
2159
+ />
2160
+ <RiskMatrixFact label="Review" value={reviewCadence} />
2161
+ </dl>
2162
+ {priorityRisk.mitigation ? (
2163
+ <div className="eth-domain-risk-matrix__mitigation">
2164
+ <span>Mitigation</span>
2165
+ <strong>{priorityRisk.mitigation}</strong>
2166
+ </div>
2167
+ ) : null}
2168
+ <RiskMatrixActions actions={priorityRisk.actions} />
2169
+ </>
2170
+ ) : (
2171
+ <p className="eth-domain-risk-matrix__empty">No priority risk selected.</p>
2172
+ )}
2173
+
2174
+ <section
2175
+ className="eth-domain-risk-matrix__register"
2176
+ aria-labelledby={registerHeadingId}
2177
+ >
2178
+ <div className="eth-domain-risk-matrix__panel-header">
2179
+ <div>
2180
+ <span>Register</span>
2181
+ <h3 id={registerHeadingId}>Mapped risks</h3>
2182
+ </div>
2183
+ <strong>{riskCountLabel(risks.length)}</strong>
2184
+ </div>
2185
+ <div className="eth-domain-risk-matrix__register-list" role="list">
2186
+ {risks.map((risk) => {
2187
+ const itemStatus = risk.status ?? deriveRiskMatrixStatus([risk]) ?? "active";
2188
+ return (
2189
+ <article
2190
+ key={risk.id}
2191
+ className="eth-domain-risk-matrix__register-item"
2192
+ data-severity={risk.resolvedSeverity}
2193
+ role="listitem"
2194
+ >
2195
+ <div>
2196
+ <strong>{risk.label}</strong>
2197
+ <span>
2198
+ {riskImpactLabels[risk.resolvedImpact]} /{" "}
2199
+ {riskProbabilityLabels[risk.resolvedProbability]}
2200
+ </span>
2201
+ </div>
2202
+ <StatusDot status={itemStatus} label={statusLabel(itemStatus)} />
2203
+ </article>
2204
+ );
2205
+ })}
2206
+ </div>
2207
+ </section>
2208
+ </aside>
2209
+ </div>
2210
+ ) : (
2211
+ <div className="eth-domain-risk-matrix__empty">No risks mapped.</div>
2212
+ )}
2213
+
2214
+ {children}
2215
+ </Surface>
2216
+ );
2217
+ }
2218
+
2219
+ export function InvoiceViewer({
2220
+ title = "Invoice",
2221
+ description,
2222
+ status = "pending-approval",
2223
+ items = [],
2224
+ metadata,
2225
+ actions,
2226
+ footer,
2227
+ children,
2228
+ className,
2229
+ customer,
2230
+ dueDate,
2231
+ terms,
2232
+ owner,
2233
+ subtotal,
2234
+ tax,
2235
+ total,
2236
+ amountDue,
2237
+ payments = [],
2238
+ ...props
2239
+ }: InvoiceViewerProps) {
2240
+ const computedMetadata =
2241
+ metadata ??
2242
+ [
2243
+ { label: "Amount due", value: amountDue ?? total },
2244
+ { label: "Due date", value: dueDate },
2245
+ { label: "Terms", value: terms },
2246
+ { label: "Owner", value: owner ?? customer }
2247
+ ].filter((item) => hasDisplayValue(item.value));
2248
+ const summaryRows: Array<{
2249
+ label: React.ReactNode;
2250
+ value: React.ReactNode;
2251
+ emphasis?: boolean;
2252
+ }> = [
2253
+ { label: "Subtotal", value: subtotal },
2254
+ { label: "Tax", value: tax },
2255
+ { label: "Total", value: total },
2256
+ { label: "Amount due", value: amountDue ?? total, emphasis: true }
2257
+ ].filter((item) => hasDisplayValue(item.value));
2258
+
2259
+ return (
2260
+ <Surface
2261
+ {...props}
2262
+ data-eth-component="InvoiceViewer"
2263
+ title={title}
2264
+ description={description}
2265
+ status={status}
2266
+ metadata={computedMetadata.length ? computedMetadata : undefined}
2267
+ actions={actions}
2268
+ footer={footer}
2269
+ className={["eth-domain-invoice-viewer", className].filter(Boolean).join(" ")}
2270
+ >
2271
+ <div className="eth-domain-invoice-viewer__body">
2272
+ <section className="eth-domain-invoice-viewer__section" aria-label="Invoice line items">
2273
+ <div className="eth-domain-invoice-viewer__section-header">
2274
+ <div>
2275
+ <span>Charges</span>
2276
+ <h3>Line items</h3>
2277
+ </div>
2278
+ <strong>
2279
+ {items.length} item{items.length === 1 ? "" : "s"}
2280
+ </strong>
2281
+ </div>
2282
+
2283
+ {items.length ? (
2284
+ <div className="eth-domain-invoice-viewer__table-wrap">
2285
+ <table className="eth-domain-invoice-viewer__table">
2286
+ <caption className="eth-domain-invoice-viewer__caption">Invoice line items</caption>
2287
+ <thead>
2288
+ <tr>
2289
+ <th scope="col">Item</th>
2290
+ <th scope="col">Qty</th>
2291
+ <th scope="col">Rate</th>
2292
+ <th scope="col">Amount</th>
2293
+ </tr>
2294
+ </thead>
2295
+ <tbody>
2296
+ {items.map((item) => {
2297
+ const itemStatus = item.status ?? "not-started";
2298
+ const labelContent = item.href ? (
2299
+ <a href={item.href}>{item.label}</a>
2300
+ ) : (
2301
+ item.label
2302
+ );
2303
+
2304
+ return (
2305
+ <tr key={item.id} data-status={itemStatus}>
2306
+ <td>
2307
+ <div className="eth-domain-invoice-viewer__line-item">
2308
+ <strong>{labelContent}</strong>
2309
+ {item.description ? <p>{item.description}</p> : null}
2310
+ {item.status ? (
2311
+ <StatusDot status={item.status} label={statusLabel(item.status)} />
2312
+ ) : null}
2313
+ </div>
2314
+ </td>
2315
+ <td>{item.quantity ?? "1"}</td>
2316
+ <td>{item.rate ?? "-"}</td>
2317
+ <td>{item.amount ?? item.meta ?? "-"}</td>
2318
+ </tr>
2319
+ );
2320
+ })}
2321
+ </tbody>
2322
+ </table>
2323
+ </div>
2324
+ ) : (
2325
+ <p className="eth-domain-invoice-viewer__empty">No invoice line items available.</p>
2326
+ )}
2327
+ </section>
2328
+
2329
+ <aside className="eth-domain-invoice-viewer__section" aria-label="Payment summary">
2330
+ <div className="eth-domain-invoice-viewer__section-header">
2331
+ <div>
2332
+ <span>Payment</span>
2333
+ <h3>Summary</h3>
2334
+ </div>
2335
+ <StatusDot status={status} label={statusLabel(status)} />
2336
+ </div>
2337
+
2338
+ {summaryRows.length ? (
2339
+ <dl className="eth-domain-invoice-viewer__totals">
2340
+ {summaryRows.map((row, index) => (
2341
+ <div
2342
+ key={index}
2343
+ className={row.emphasis ? "eth-domain-invoice-viewer__total" : undefined}
2344
+ >
2345
+ <dt>{row.label}</dt>
2346
+ <dd>{row.value}</dd>
2347
+ </div>
2348
+ ))}
2349
+ </dl>
2350
+ ) : null}
2351
+
2352
+ {payments.length ? (
2353
+ <ol className="eth-domain-invoice-viewer__payments" aria-label="Payment milestones">
2354
+ {payments.map((payment) => {
2355
+ const paymentStatus = payment.status ?? "queued";
2356
+
2357
+ return (
2358
+ <li key={payment.id} data-status={paymentStatus}>
2359
+ <div className="eth-domain-invoice-viewer__payment-main">
2360
+ <strong>{payment.label}</strong>
2361
+ {payment.dueDate || payment.description ? (
2362
+ <span>{payment.dueDate ?? payment.description}</span>
2363
+ ) : null}
2364
+ </div>
2365
+ {hasDisplayValue(payment.amount ?? payment.meta) ? (
2366
+ <span className="eth-domain-invoice-viewer__payment-amount">
2367
+ {payment.amount ?? payment.meta}
2368
+ </span>
2369
+ ) : null}
2370
+ </li>
2371
+ );
2372
+ })}
2373
+ </ol>
2374
+ ) : null}
2375
+ </aside>
2376
+ </div>
2377
+ {children}
2378
+ </Surface>
2379
+ );
2380
+ }
2381
+
2382
+ export function KnowledgeBaseArticleViewer({
2383
+ title = "Knowledge base article",
2384
+ description,
2385
+ status = "active",
2386
+ metadata,
2387
+ actions,
2388
+ footer,
2389
+ children,
2390
+ className,
2391
+ body,
2392
+ sections,
2393
+ relatedArticles,
2394
+ citations = defaultKnowledgeBaseCitations,
2395
+ items,
2396
+ articleStatusLabel,
2397
+ ...props
2398
+ }: KnowledgeBaseArticleViewerProps) {
2399
+ const hasBody = hasKnowledgeBaseNode(body) || React.Children.count(children) > 0;
2400
+ const articleSections = sections ?? (hasBody ? [] : defaultKnowledgeBaseSections);
2401
+ const related = relatedArticles ?? items ?? defaultKnowledgeBaseRelatedArticles;
2402
+ const articleHeadingId = React.useId();
2403
+ const relatedHeadingId = React.useId();
2404
+ const citationsHeadingId = React.useId();
2405
+
2406
+ return (
2407
+ <Surface
2408
+ {...props}
2409
+ data-eth-component="KnowledgeBaseArticleViewer"
2410
+ title={title}
2411
+ description={description}
2412
+ status={status}
2413
+ metadata={metadata}
2414
+ actions={actions}
2415
+ footer={footer}
2416
+ className={["eth-domain-knowledge-base-viewer", className].filter(Boolean).join(" ")}
2417
+ >
2418
+ <div className="eth-domain-knowledge-base-viewer__workspace">
2419
+ <article
2420
+ className="eth-domain-knowledge-base-viewer__article"
2421
+ aria-labelledby={articleHeadingId}
2422
+ >
2423
+ <div className="eth-domain-knowledge-base-viewer__article-header">
2424
+ <div>
2425
+ <span>Article body</span>
2426
+ <h3 id={articleHeadingId}>Guidance</h3>
2427
+ </div>
2428
+ <StatusDot
2429
+ status={status}
2430
+ label={articleStatusLabel ?? knowledgeBaseStatusLabel(status)}
2431
+ />
2432
+ </div>
2433
+
2434
+ {hasBody ? (
2435
+ <div className="eth-domain-knowledge-base-viewer__body">
2436
+ {renderKnowledgeBaseNode(body)}
2437
+ {children}
2438
+ </div>
2439
+ ) : null}
2440
+
2441
+ {articleSections.length ? (
2442
+ <div className="eth-domain-knowledge-base-viewer__sections">
2443
+ {articleSections.map((section) => (
2444
+ <section
2445
+ key={section.id}
2446
+ className="eth-domain-knowledge-base-viewer__section"
2447
+ data-status={section.status}
2448
+ >
2449
+ <div className="eth-domain-knowledge-base-viewer__section-heading">
2450
+ <h4>{section.title}</h4>
2451
+ {section.status ? (
2452
+ <StatusDot status={section.status} label={statusLabel(section.status)} />
2453
+ ) : null}
2454
+ </div>
2455
+ {renderKnowledgeBaseNode(section.body)}
2456
+ </section>
2457
+ ))}
2458
+ </div>
2459
+ ) : null}
2460
+ </article>
2461
+
2462
+ <aside className="eth-domain-knowledge-base-viewer__rail">
2463
+ <section
2464
+ className="eth-domain-knowledge-base-viewer__panel"
2465
+ aria-labelledby={relatedHeadingId}
2466
+ >
2467
+ <div className="eth-domain-knowledge-base-viewer__panel-header">
2468
+ <span>Knowledge graph</span>
2469
+ <h3 id={relatedHeadingId}>Related content</h3>
2470
+ </div>
2471
+ {related.length ? (
2472
+ <div className="eth-domain-knowledge-base-viewer__related-list" role="list">
2473
+ {related.map((item) => {
2474
+ const labelContent = item.href ? (
2475
+ <a href={item.href}>{item.label}</a>
2476
+ ) : (
2477
+ item.label
2478
+ );
2479
+ return (
2480
+ <article
2481
+ key={item.id}
2482
+ className="eth-domain-knowledge-base-viewer__related-item"
2483
+ role="listitem"
2484
+ >
2485
+ <div>
2486
+ <strong>{labelContent}</strong>
2487
+ {item.description ? <p>{item.description}</p> : null}
2488
+ </div>
2489
+ <div className="eth-domain-knowledge-base-viewer__item-meta">
2490
+ {(item.type ?? item.meta) ? <span>{item.type ?? item.meta}</span> : null}
2491
+ {item.updatedAt ? <span>{item.updatedAt}</span> : null}
2492
+ {item.status ? (
2493
+ <StatusDot status={item.status} label={statusLabel(item.status)} />
2494
+ ) : null}
2495
+ </div>
2496
+ </article>
2497
+ );
2498
+ })}
2499
+ </div>
2500
+ ) : (
2501
+ <p className="eth-domain-knowledge-base-viewer__empty">No related content linked.</p>
2502
+ )}
2503
+ </section>
2504
+
2505
+ <section
2506
+ className="eth-domain-knowledge-base-viewer__panel"
2507
+ aria-labelledby={citationsHeadingId}
2508
+ >
2509
+ <div className="eth-domain-knowledge-base-viewer__panel-header">
2510
+ <span>Evidence</span>
2511
+ <h3 id={citationsHeadingId}>Citations</h3>
2512
+ </div>
2513
+ {citations.length ? (
2514
+ <ol className="eth-domain-knowledge-base-viewer__citations">
2515
+ {citations.map((citation) => {
2516
+ const labelContent = citation.href ? (
2517
+ <a href={citation.href}>{citation.label}</a>
2518
+ ) : (
2519
+ citation.label
2520
+ );
2521
+ return (
2522
+ <li key={citation.id}>
2523
+ <div className="eth-domain-knowledge-base-viewer__citation-main">
2524
+ <strong>{labelContent}</strong>
2525
+ {citation.source ? <span>{citation.source}</span> : null}
2526
+ {renderKnowledgeBaseNode(citation.excerpt)}
2527
+ </div>
2528
+ {citation.status ? (
2529
+ <StatusDot status={citation.status} label={statusLabel(citation.status)} />
2530
+ ) : null}
2531
+ </li>
2532
+ );
2533
+ })}
2534
+ </ol>
2535
+ ) : (
2536
+ <p className="eth-domain-knowledge-base-viewer__empty">No citations attached.</p>
2537
+ )}
2538
+ </section>
2539
+ </aside>
2540
+ </div>
2541
+ </Surface>
2542
+ );
2543
+ }
2544
+
2545
+ const defaultDataImportSteps: DataImportWizardStep[] = [
2546
+ {
2547
+ id: "upload",
2548
+ label: "Upload CSV",
2549
+ description: "File accepted and parsed into a preview set.",
2550
+ status: "completed"
2551
+ },
2552
+ {
2553
+ id: "map",
2554
+ label: "Map fields",
2555
+ description: "Match source columns to the destination object.",
2556
+ status: "in-progress"
2557
+ },
2558
+ {
2559
+ id: "validate",
2560
+ label: "Validate",
2561
+ description: "Run required field and duplicate checks.",
2562
+ status: "queued"
2563
+ },
2564
+ {
2565
+ id: "submit",
2566
+ label: "Submit",
2567
+ description: "Create records after validation passes.",
2568
+ status: "not-started"
2569
+ }
2570
+ ];
2571
+
2572
+ function dataImportStatusFromSteps(
2573
+ steps: DataImportWizardStep[]
2574
+ ): EthOperationalStatus | undefined {
2575
+ if (!steps.length) return undefined;
2576
+ if (steps.some((step) => step.status === "blocked" || step.status === "failed")) {
2577
+ return "blocked";
2578
+ }
2579
+ if (steps.every((step) => step.status === "completed" || step.status === "succeeded")) {
2580
+ return "completed";
2581
+ }
2582
+ if (
2583
+ steps.some(
2584
+ (step) =>
2585
+ step.status === "in-progress" || step.status === "running" || step.status === "active"
2586
+ )
2587
+ ) {
2588
+ return "in-progress";
2589
+ }
2590
+ if (steps.some((step) => step.status === "queued" || step.status === "pending-approval")) {
2591
+ return "queued";
2592
+ }
2593
+ return "not-started";
2594
+ }
2595
+
2596
+ function isCompletedImportStep(status: EthOperationalStatus | undefined) {
2597
+ return status === "completed" || status === "succeeded" || status === "synced";
2598
+ }
2599
+
2600
+ function isCurrentImportStep(status: EthOperationalStatus | undefined) {
2601
+ return status === "in-progress" || status === "running" || status === "active";
2602
+ }
2603
+
2604
+ function currentImportStepIndex(steps: DataImportWizardStep[]) {
2605
+ const activeIndex = steps.findIndex((step) => isCurrentImportStep(step.status));
2606
+ if (activeIndex >= 0) return activeIndex;
2607
+
2608
+ const nextIndex = steps.findIndex((step) => !isCompletedImportStep(step.status));
2609
+ return nextIndex >= 0 ? nextIndex : Math.max(steps.length - 1, 0);
2610
+ }
2611
+
2612
+ export function DataImportWizard({
2613
+ title = "Data import",
2614
+ description = "Import, map, validate, and submit records.",
2615
+ items = defaultDataImportSteps,
2616
+ metadata,
2617
+ status,
2618
+ actions,
2619
+ footer,
2620
+ children,
2621
+ className,
2622
+ fileName,
2623
+ sourceLabel,
2624
+ rowCount,
2625
+ columnCount,
2626
+ mappingCount,
2627
+ validationSummary,
2628
+ mappings = [],
2629
+ validationIssues = [],
2630
+ ...props
2631
+ }: DataImportWizardProps) {
2632
+ const currentIndex = currentImportStepIndex(items);
2633
+ const completeCount = items.filter((item) => isCompletedImportStep(item.status)).length;
2634
+ const computedMetadata =
2635
+ metadata ??
2636
+ [
2637
+ { label: "File", value: fileName },
2638
+ { label: "Rows", value: rowCount },
2639
+ { label: "Columns", value: columnCount },
2640
+ { label: "Mapped", value: mappingCount },
2641
+ { label: "Validation", value: validationSummary }
2642
+ ].filter((item) => hasDisplayValue(item.value));
2643
+
2644
+ return (
2645
+ <Surface
2646
+ {...props}
2647
+ data-eth-component="DataImportWizard"
2648
+ title={title}
2649
+ description={description}
2650
+ status={status ?? dataImportStatusFromSteps(items)}
2651
+ metadata={computedMetadata.length ? computedMetadata : undefined}
2652
+ actions={actions}
2653
+ footer={footer}
2654
+ className={["eth-domain-data-import-wizard", className].filter(Boolean).join(" ")}
2655
+ >
2656
+ <ol className="eth-domain-data-import-wizard__steps" aria-label="Data import progress">
2657
+ {items.map((step, index) => {
2658
+ const stepStatus = step.status ?? "not-started";
2659
+
2660
+ return (
2661
+ <li
2662
+ key={step.id}
2663
+ aria-current={index === currentIndex ? "step" : undefined}
2664
+ className="eth-domain-data-import-wizard__step"
2665
+ data-current={index === currentIndex ? "true" : undefined}
2666
+ data-status={stepStatus}
2667
+ >
2668
+ <span className="eth-domain-data-import-wizard__step-marker" aria-hidden="true">
2669
+ {index + 1}
2670
+ </span>
2671
+ <span className="eth-domain-data-import-wizard__step-copy">
2672
+ <strong>{step.label}</strong>
2673
+ {step.description ? <span>{step.description}</span> : null}
2674
+ </span>
2675
+ <StatusDot status={stepStatus} label={statusLabel(stepStatus)} />
2676
+ </li>
2677
+ );
2678
+ })}
2679
+ </ol>
2680
+
2681
+ <div className="eth-domain-data-import-wizard__workspace">
2682
+ <section className="eth-domain-data-import-wizard__panel" aria-label="Upload summary">
2683
+ <div className="eth-domain-data-import-wizard__panel-header">
2684
+ <span>Source</span>
2685
+ <strong>{fileName ?? "No file selected"}</strong>
2686
+ </div>
2687
+ <dl className="eth-domain-data-import-wizard__facts">
2688
+ {hasDisplayValue(sourceLabel) ? (
2689
+ <div>
2690
+ <dt>Import type</dt>
2691
+ <dd>{sourceLabel}</dd>
2692
+ </div>
2693
+ ) : null}
2694
+ {hasDisplayValue(rowCount) ? (
2695
+ <div>
2696
+ <dt>Rows detected</dt>
2697
+ <dd>{rowCount}</dd>
2698
+ </div>
2699
+ ) : null}
2700
+ {hasDisplayValue(columnCount) ? (
2701
+ <div>
2702
+ <dt>Columns</dt>
2703
+ <dd>{columnCount}</dd>
2704
+ </div>
2705
+ ) : null}
2706
+ <div>
2707
+ <dt>Progress</dt>
2708
+ <dd>
2709
+ {items.length
2710
+ ? `${completeCount}/${items.length} steps complete`
2711
+ : "No steps configured"}
2712
+ </dd>
2713
+ </div>
2714
+ </dl>
2715
+ </section>
2716
+
2717
+ {mappings.length ? (
2718
+ <section className="eth-domain-data-import-wizard__panel eth-domain-data-import-wizard__panel--wide">
2719
+ <div className="eth-domain-data-import-wizard__panel-header">
2720
+ <span>Field mapping</span>
2721
+ <strong>{mappingCount ?? `${mappings.length} fields`}</strong>
2722
+ </div>
2723
+ <div className="eth-domain-data-import-wizard__table-wrap">
2724
+ <table className="eth-domain-data-import-wizard__table">
2725
+ <caption>CSV fields mapped to destination properties</caption>
2726
+ <thead>
2727
+ <tr>
2728
+ <th scope="col">Source field</th>
2729
+ <th scope="col">Destination</th>
2730
+ <th scope="col">Sample</th>
2731
+ <th scope="col">State</th>
2732
+ </tr>
2733
+ </thead>
2734
+ <tbody>
2735
+ {mappings.map((mapping) => {
2736
+ const mappingStatus = mapping.status ?? "not-started";
2737
+
2738
+ return (
2739
+ <tr key={mapping.id} data-status={mappingStatus}>
2740
+ <td>
2741
+ <strong>{mapping.sourceField}</strong>
2742
+ {mapping.required ? <span>Required</span> : null}
2743
+ </td>
2744
+ <td>{mapping.targetField}</td>
2745
+ <td>{mapping.sample ?? "No sample"}</td>
2746
+ <td>
2747
+ <StatusDot status={mappingStatus} label={statusLabel(mappingStatus)} />
2748
+ </td>
2749
+ </tr>
2750
+ );
2751
+ })}
2752
+ </tbody>
2753
+ </table>
2754
+ </div>
2755
+ </section>
2756
+ ) : null}
2757
+
2758
+ {validationIssues.length || hasDisplayValue(validationSummary) ? (
2759
+ <section className="eth-domain-data-import-wizard__panel">
2760
+ <div className="eth-domain-data-import-wizard__panel-header">
2761
+ <span>Validation</span>
2762
+ <strong>{validationSummary ?? "Ready to run"}</strong>
2763
+ </div>
2764
+ {validationIssues.length ? (
2765
+ <ul className="eth-domain-data-import-wizard__issues" aria-label="Validation issues">
2766
+ {validationIssues.map((issue) => {
2767
+ const issueStatus = issue.status ?? "warning";
2768
+
2769
+ return (
2770
+ <li key={issue.id} data-status={issueStatus}>
2771
+ <div>
2772
+ <strong>{issue.field}</strong>
2773
+ <span>{issue.message}</span>
2774
+ </div>
2775
+ {hasDisplayValue(issue.count) ? <span>{issue.count}</span> : null}
2776
+ </li>
2777
+ );
2778
+ })}
2779
+ </ul>
2780
+ ) : (
2781
+ <p className="eth-domain-data-import-wizard__empty-note">
2782
+ No validation issues have been detected.
2783
+ </p>
2784
+ )}
2785
+ </section>
2786
+ ) : null}
2787
+ </div>
2788
+ {children}
2789
+ </Surface>
2790
+ );
2791
+ }
2792
+
2793
+ const defaultMappingTargetOptions = [
2794
+ { value: "", label: "Select target field", disabled: true },
2795
+ { value: "contact.firstName", label: "Contact / First name" },
2796
+ { value: "contact.lastName", label: "Contact / Last name" },
2797
+ { value: "account.name", label: "Account / Name" },
2798
+ { value: "account.domain", label: "Account / Domain" },
2799
+ { value: "opportunity.amount", label: "Opportunity / Amount" },
2800
+ { value: "opportunity.closeDate", label: "Opportunity / Close date" }
2801
+ ];
2802
+
2803
+ function parseMappingLabel(label: React.ReactNode) {
2804
+ const text = textFromNode(label).trim();
2805
+ const match = text.match(/^(.*?)\s*(?:→|->)\s*(.*?)$/);
2806
+ if (!match) return undefined;
2807
+
2808
+ return {
2809
+ source: match[1].trim(),
2810
+ target: match[2].trim()
2811
+ };
2812
+ }
2813
+
2814
+ function mappingSource(item: MappingConfigurationItem) {
2815
+ return item.sourceField ?? parseMappingLabel(item.label)?.source ?? item.label;
2816
+ }
2817
+
2818
+ function mappingTarget(item: MappingConfigurationItem) {
2819
+ return item.targetField ?? parseMappingLabel(item.label)?.target;
2820
+ }
2821
+
2822
+ function selectableValue(value: React.ReactNode) {
2823
+ if (typeof value === "string" || typeof value === "number") return String(value);
2824
+ return undefined;
2825
+ }
2826
+
2827
+ function mappingItemStatus(item: MappingConfigurationItem) {
2828
+ return item.status ?? (hasDisplayValue(mappingTarget(item)) ? "active" : "warning");
2829
+ }
2830
+
2831
+ function isMappingUnresolved(item: MappingConfigurationItem) {
2832
+ const status = mappingItemStatus(item);
2833
+ return (
2834
+ !hasDisplayValue(mappingTarget(item)) ||
2835
+ status === "warning" ||
2836
+ status === "blocked" ||
2837
+ status === "failed" ||
2838
+ status === "approval-required"
2839
+ );
2840
+ }
2841
+
2842
+ function mappingWizardStatus(
2843
+ items: MappingConfigurationItem[],
2844
+ issues: MappingConfigurationIssue[]
2845
+ ): EthOperationalStatus {
2846
+ if (!items.length) return "not-started";
2847
+ if (
2848
+ items.some((item) =>
2849
+ ["blocked", "failed", "approval-required"].includes(mappingItemStatus(item))
2850
+ ) ||
2851
+ issues.some((issue) =>
2852
+ ["blocked", "failed", "approval-required"].includes(issue.status ?? "warning")
2853
+ )
2854
+ ) {
2855
+ return "blocked";
2856
+ }
2857
+ if (items.some(isMappingUnresolved) || issues.length) return "warning";
2858
+ return "active";
2859
+ }
2860
+
2861
+ function optionsForMapping(
2862
+ options: Array<{ value: string; label: string; disabled?: boolean }>,
2863
+ value: string | undefined
2864
+ ) {
2865
+ if (!value || options.some((option) => option.value === value)) return options;
2866
+ return [...options, { value, label: value }];
2867
+ }
2868
+
2869
+ export function MappingConfigurationWizard({
2870
+ title = "Mapping configuration",
2871
+ description = "Configure how source fields map into destination objects.",
2872
+ items = [],
2873
+ sourceObject,
2874
+ targetObject,
2875
+ mappingCount,
2876
+ validationSummary,
2877
+ targetOptions = defaultMappingTargetOptions,
2878
+ validationIssues = [],
2879
+ metadata,
2880
+ status,
2881
+ actions,
2882
+ footer,
2883
+ children,
2884
+ className,
2885
+ onMappingChange,
2886
+ onAutoMap,
2887
+ onValidate,
2888
+ onSubmit,
2889
+ ...props
2890
+ }: MappingConfigurationWizardProps) {
2891
+ const [targetOverrides, setTargetOverrides] = React.useState<Record<string, string>>({});
2892
+
2893
+ React.useEffect(() => {
2894
+ setTargetOverrides({});
2895
+ }, [items]);
2896
+
2897
+ const resolvedMappingTarget = React.useCallback(
2898
+ (item: MappingConfigurationItem) => targetOverrides[item.id] ?? mappingTarget(item),
2899
+ [targetOverrides]
2900
+ );
2901
+ const resolvedMappingStatus = React.useCallback(
2902
+ (item: MappingConfigurationItem) =>
2903
+ item.status ?? (hasDisplayValue(resolvedMappingTarget(item)) ? "active" : "warning"),
2904
+ [resolvedMappingTarget]
2905
+ );
2906
+ const isResolvedMappingUnresolved = React.useCallback(
2907
+ (item: MappingConfigurationItem) => {
2908
+ const itemStatus = resolvedMappingStatus(item);
2909
+
2910
+ return (
2911
+ !hasDisplayValue(resolvedMappingTarget(item)) ||
2912
+ itemStatus === "warning" ||
2913
+ itemStatus === "blocked" ||
2914
+ itemStatus === "failed" ||
2915
+ itemStatus === "approval-required"
2916
+ );
2917
+ },
2918
+ [resolvedMappingStatus, resolvedMappingTarget]
2919
+ );
2920
+ const mappedCount = items.filter((item) => hasDisplayValue(resolvedMappingTarget(item))).length;
2921
+ const unresolvedCount =
2922
+ items.filter(isResolvedMappingUnresolved).length + validationIssues.length;
2923
+ const resolvedItems = items.map((item) => ({
2924
+ ...item,
2925
+ targetField: resolvedMappingTarget(item),
2926
+ status: resolvedMappingStatus(item)
2927
+ }));
2928
+ const computedStatus = status ?? mappingWizardStatus(resolvedItems, validationIssues);
2929
+ const computedValidationSummary =
2930
+ validationSummary ??
2931
+ (unresolvedCount
2932
+ ? `${unresolvedCount} ${unresolvedCount === 1 ? "issue" : "issues"} to resolve`
2933
+ : "Ready to apply");
2934
+ const mappingProgressLabel =
2935
+ mappingCount ?? (items.length ? `${mappedCount}/${items.length}` : "No fields");
2936
+ const computedMetadata =
2937
+ metadata ??
2938
+ [
2939
+ { label: "Source object", value: sourceObject },
2940
+ { label: "Target object", value: targetObject },
2941
+ { label: "Mapped", value: mappingProgressLabel },
2942
+ { label: "Validation", value: computedValidationSummary }
2943
+ ].filter((item) => hasDisplayValue(item.value));
2944
+ const wizardSteps = [
2945
+ {
2946
+ id: "source",
2947
+ label: "Source",
2948
+ description: items.length ? `${items.length} fields detected` : "No fields loaded",
2949
+ status: items.length ? "completed" : "not-started"
2950
+ },
2951
+ {
2952
+ id: "mapping",
2953
+ label: "Map",
2954
+ description: items.length ? `${mappedCount}/${items.length} mapped` : "Awaiting source",
2955
+ status: !items.length
2956
+ ? "not-started"
2957
+ : mappedCount === items.length
2958
+ ? "completed"
2959
+ : "in-progress"
2960
+ },
2961
+ {
2962
+ id: "validation",
2963
+ label: "Validate",
2964
+ description: computedValidationSummary,
2965
+ status: computedStatus === "blocked" ? "blocked" : unresolvedCount ? "warning" : "completed"
2966
+ },
2967
+ {
2968
+ id: "review",
2969
+ label: "Review",
2970
+ description: unresolvedCount ? "Resolve issues first" : "Ready for sync",
2971
+ status: unresolvedCount ? "not-started" : "queued"
2972
+ }
2973
+ ] satisfies Array<{
2974
+ id: string;
2975
+ label: string;
2976
+ description: React.ReactNode;
2977
+ status: EthOperationalStatus;
2978
+ }>;
2979
+ const currentStepIndex = wizardSteps.findIndex((step) => !isCompletedImportStep(step.status));
2980
+ const currentIndex = currentStepIndex >= 0 ? currentStepIndex : wizardSteps.length - 1;
2981
+
2982
+ return (
2983
+ <Surface
2984
+ {...props}
2985
+ data-eth-component="MappingConfigurationWizard"
2986
+ title={title}
2987
+ description={description}
2988
+ status={computedStatus}
2989
+ metadata={computedMetadata.length ? computedMetadata : undefined}
2990
+ actions={actions}
2991
+ className={["eth-domain-mapping-config-wizard", className].filter(Boolean).join(" ")}
2992
+ >
2993
+ <ol className="eth-domain-mapping-config-wizard__steps" aria-label="Mapping progress">
2994
+ {wizardSteps.map((step, index) => (
2995
+ <li
2996
+ key={step.id}
2997
+ aria-current={index === currentIndex ? "step" : undefined}
2998
+ className="eth-domain-mapping-config-wizard__step"
2999
+ data-current={index === currentIndex ? "true" : undefined}
3000
+ data-status={step.status}
3001
+ >
3002
+ <span className="eth-domain-mapping-config-wizard__step-marker" aria-hidden="true">
3003
+ {index + 1}
3004
+ </span>
3005
+ <span className="eth-domain-mapping-config-wizard__step-copy">
3006
+ <strong>{step.label}</strong>
3007
+ <span>{step.description}</span>
3008
+ </span>
3009
+ <StatusDot status={step.status} label={statusLabel(step.status)} />
3010
+ </li>
3011
+ ))}
3012
+ </ol>
3013
+
3014
+ <div className="eth-domain-mapping-config-wizard__workspace">
3015
+ <aside className="eth-domain-mapping-config-wizard__panel" aria-label="Object scope">
3016
+ <div className="eth-domain-mapping-config-wizard__panel-header">
3017
+ <span>Objects</span>
3018
+ <strong>
3019
+ {sourceObject ?? "Source object"}
3020
+ <span aria-hidden="true"> -&gt; </span>
3021
+ {targetObject ?? "Target object"}
3022
+ </strong>
3023
+ </div>
3024
+ <dl className="eth-domain-mapping-config-wizard__facts">
3025
+ <div>
3026
+ <dt>Source object</dt>
3027
+ <dd>{sourceObject ?? "Not selected"}</dd>
3028
+ </div>
3029
+ <div>
3030
+ <dt>Target object</dt>
3031
+ <dd>{targetObject ?? "Not selected"}</dd>
3032
+ </div>
3033
+ <div>
3034
+ <dt>Mapped fields</dt>
3035
+ <dd>{mappingProgressLabel}</dd>
3036
+ </div>
3037
+ <div>
3038
+ <dt>Validation</dt>
3039
+ <dd>{computedValidationSummary}</dd>
3040
+ </div>
3041
+ </dl>
3042
+ </aside>
3043
+
3044
+ <section
3045
+ className="eth-domain-mapping-config-wizard__panel eth-domain-mapping-config-wizard__panel--mapping"
3046
+ aria-label="Field mappings"
3047
+ >
3048
+ <div className="eth-domain-mapping-config-wizard__panel-header eth-domain-mapping-config-wizard__panel-header--inline">
3049
+ <div>
3050
+ <span>Field mapping</span>
3051
+ <strong>
3052
+ {mappingCount ??
3053
+ (items.length ? `${mappedCount}/${items.length} mapped` : "No fields")}
3054
+ </strong>
3055
+ </div>
3056
+ <Button type="button" intent="tertiary" density="compact" onClick={onAutoMap}>
3057
+ Auto-map
3058
+ </Button>
3059
+ </div>
3060
+
3061
+ {items.length ? (
3062
+ <div className="eth-domain-mapping-config-wizard__table-wrap">
3063
+ <table className="eth-domain-mapping-config-wizard__table">
3064
+ <caption>Source fields mapped to destination fields</caption>
3065
+ <thead>
3066
+ <tr>
3067
+ <th scope="col">Source field</th>
3068
+ <th scope="col">Destination field</th>
3069
+ <th scope="col">Transform</th>
3070
+ <th scope="col">State</th>
3071
+ </tr>
3072
+ </thead>
3073
+ <tbody>
3074
+ {items.map((item) => {
3075
+ const source = mappingSource(item);
3076
+ const target = resolvedMappingTarget(item);
3077
+ const targetValue = selectableValue(target);
3078
+ const itemStatus = resolvedMappingStatus(item);
3079
+ const sourceLabel = textFromNode(source) || `mapping-${item.id}`;
3080
+
3081
+ return (
3082
+ <tr key={item.id} data-status={itemStatus}>
3083
+ <td>
3084
+ <div className="eth-domain-mapping-config-wizard__field-stack">
3085
+ <strong>{source}</strong>
3086
+ {hasDisplayValue(item.sourceType) ? (
3087
+ <span>{item.sourceType}</span>
3088
+ ) : null}
3089
+ {hasDisplayValue(item.sample) ? <p>Sample: {item.sample}</p> : null}
3090
+ </div>
3091
+ </td>
3092
+ <td>
3093
+ <div className="eth-domain-mapping-config-wizard__target-field">
3094
+ <Select
3095
+ labelText={`Destination for ${sourceLabel}`}
3096
+ hideLabel
3097
+ density="compact"
3098
+ value={targetValue ?? ""}
3099
+ options={optionsForMapping(targetOptions, targetValue)}
3100
+ invalid={!hasDisplayValue(target)}
3101
+ invalidText="Select a destination field."
3102
+ onChange={(event) => {
3103
+ const nextTarget = event.currentTarget.value;
3104
+ setTargetOverrides((current) => ({
3105
+ ...current,
3106
+ [item.id]: nextTarget
3107
+ }));
3108
+ onMappingChange?.(item.id, nextTarget);
3109
+ }}
3110
+ />
3111
+ {hasDisplayValue(item.targetType) ? (
3112
+ <span>{item.targetType}</span>
3113
+ ) : null}
3114
+ </div>
3115
+ </td>
3116
+ <td>
3117
+ <div className="eth-domain-mapping-config-wizard__field-stack">
3118
+ <strong>{item.transform ?? "Direct copy"}</strong>
3119
+ {hasDisplayValue(item.description) ? <p>{item.description}</p> : null}
3120
+ </div>
3121
+ </td>
3122
+ <td>
3123
+ <div className="eth-domain-mapping-config-wizard__state">
3124
+ <StatusDot status={itemStatus} label={statusLabel(itemStatus)} />
3125
+ <div className="eth-domain-mapping-config-wizard__badges">
3126
+ {item.required ? <Badge severity="info">Required</Badge> : null}
3127
+ {hasDisplayValue(item.confidence) ? (
3128
+ <Badge severity={itemStatus === "warning" ? "warning" : "neutral"}>
3129
+ {item.confidence}
3130
+ </Badge>
3131
+ ) : null}
3132
+ </div>
3133
+ </div>
3134
+ </td>
3135
+ </tr>
3136
+ );
3137
+ })}
3138
+ </tbody>
3139
+ </table>
3140
+ </div>
3141
+ ) : (
3142
+ <p className="eth-domain-mapping-config-wizard__empty">No fields are ready to map.</p>
3143
+ )}
3144
+ </section>
3145
+
3146
+ <aside className="eth-domain-mapping-config-wizard__panel" aria-label="Validation issues">
3147
+ <div className="eth-domain-mapping-config-wizard__panel-header">
3148
+ <span>Validation</span>
3149
+ <strong>{computedValidationSummary}</strong>
3150
+ </div>
3151
+ {validationIssues.length ? (
3152
+ <ul
3153
+ className="eth-domain-mapping-config-wizard__issues"
3154
+ aria-label="Mapping validation issues"
3155
+ >
3156
+ {validationIssues.map((issue) => {
3157
+ const issueStatus = issue.status ?? "warning";
3158
+
3159
+ return (
3160
+ <li key={issue.id} data-status={issueStatus}>
3161
+ <div>
3162
+ <strong>{issue.field}</strong>
3163
+ <span>{issue.message}</span>
3164
+ </div>
3165
+ {hasDisplayValue(issue.count) ? <span>{issue.count}</span> : null}
3166
+ </li>
3167
+ );
3168
+ })}
3169
+ </ul>
3170
+ ) : (
3171
+ <p className="eth-domain-mapping-config-wizard__empty-note">
3172
+ No mapping validation issues.
3173
+ </p>
3174
+ )}
3175
+ </aside>
3176
+ </div>
3177
+
3178
+ {children}
3179
+
3180
+ <footer className="eth-domain-mapping-config-wizard__footer">
3181
+ {footer ?? (
3182
+ <>
3183
+ <span>{computedValidationSummary}</span>
3184
+ <div className="eth-domain-mapping-config-wizard__footer-actions">
3185
+ <Button type="button" intent="ghost" density="compact">
3186
+ Back
3187
+ </Button>
3188
+ <Button type="button" intent="secondary" density="compact" onClick={onValidate}>
3189
+ Validate
3190
+ </Button>
3191
+ <Button
3192
+ type="button"
3193
+ intent="primary"
3194
+ density="compact"
3195
+ disabled={Boolean(unresolvedCount)}
3196
+ onClick={onSubmit}
3197
+ >
3198
+ Apply mapping
3199
+ </Button>
3200
+ </div>
3201
+ </>
3202
+ )}
3203
+ </footer>
3204
+ </Surface>
3205
+ );
3206
+ }
3207
+
3208
+ export function ReportBuilder({
3209
+ title = "Report builder",
3210
+ description,
3211
+ status,
3212
+ metadata,
3213
+ actions,
3214
+ footer,
3215
+ children,
3216
+ className,
3217
+ items = [],
3218
+ formatLabel = "PDF / CSV",
3219
+ owner,
3220
+ updatedAt,
3221
+ onAddSection,
3222
+ onExportPdf,
3223
+ onExportCsv,
3224
+ ...props
3225
+ }: ReportBuilderProps) {
3226
+ const sectionsHeadingId = React.useId();
3227
+ const previewHeadingId = React.useId();
3228
+ const includedSections = items.filter((item) => isReportSectionIncluded(item.status));
3229
+ const attentionCount = items.filter((item) => needsReportSectionAttention(item.status)).length;
3230
+ const computedStatus = status ?? deriveReportBuilderStatus(items);
3231
+ const readinessLabel = reportBuilderReadinessLabel(computedStatus, attentionCount);
3232
+ const computedMetadata =
3233
+ metadata ??
3234
+ [
3235
+ {
3236
+ label: "Sections",
3237
+ value: items.length
3238
+ ? `${items.length} ${items.length === 1 ? "section" : "sections"}`
3239
+ : "No sections"
3240
+ },
3241
+ {
3242
+ label: "Included",
3243
+ value: items.length ? `${includedSections.length}/${items.length}` : "0/0"
3244
+ },
3245
+ { label: "Readiness", value: readinessLabel },
3246
+ { label: "Export", value: formatLabel }
3247
+ ].filter((item) => hasDisplayValue(item.value));
3248
+ const currentSectionIndex = items.findIndex(
3249
+ (item) => item.status === "in-progress" || item.status === "running"
3250
+ );
3251
+ const currentIndex = currentSectionIndex >= 0 ? currentSectionIndex : 0;
3252
+ const exportSummary = items.length
3253
+ ? `${includedSections.length}/${items.length} sections included`
3254
+ : "No sections selected";
3255
+
3256
+ return (
3257
+ <Surface
3258
+ {...props}
3259
+ data-eth-component="ReportBuilder"
3260
+ title={title}
3261
+ description={description}
3262
+ status={computedStatus}
3263
+ metadata={computedMetadata.length ? computedMetadata : undefined}
3264
+ actions={actions}
3265
+ footer={footer}
3266
+ className={["eth-domain-report-builder", className].filter(Boolean).join(" ")}
3267
+ >
3268
+ <div className="eth-domain-report-builder__workspace">
3269
+ <section
3270
+ className="eth-domain-report-builder__sections"
3271
+ aria-labelledby={sectionsHeadingId}
3272
+ >
3273
+ <div className="eth-domain-report-builder__panel-header eth-domain-report-builder__panel-header--inline">
3274
+ <div>
3275
+ <span>Composition</span>
3276
+ <h3 id={sectionsHeadingId}>Report sections</h3>
3277
+ </div>
3278
+ <Button
3279
+ type="button"
3280
+ intent="tertiary"
3281
+ density="compact"
3282
+ icon={<PlusIcon size={16} />}
3283
+ onClick={onAddSection}
3284
+ >
3285
+ Add section
3286
+ </Button>
3287
+ </div>
3288
+
3289
+ {items.length ? (
3290
+ <ol className="eth-domain-report-builder__section-list" aria-label="Report sections">
3291
+ {items.map((item, index) => {
3292
+ const itemStatus = item.status ?? "not-started";
3293
+ const labelContent = item.href ? <a href={item.href}>{item.label}</a> : item.label;
3294
+ const meta = reportSectionMeta(item, index);
3295
+
3296
+ return (
3297
+ <li
3298
+ key={item.id}
3299
+ aria-current={index === currentIndex ? "step" : undefined}
3300
+ className="eth-domain-report-builder__section-item"
3301
+ data-current={index === currentIndex ? "true" : undefined}
3302
+ data-status={itemStatus}
3303
+ >
3304
+ <span className="eth-domain-report-builder__section-index" aria-hidden="true">
3305
+ {index + 1}
3306
+ </span>
3307
+ <div className="eth-domain-report-builder__section-main">
3308
+ <strong>{labelContent}</strong>
3309
+ {item.description ? <p>{item.description}</p> : null}
3310
+ {hasDisplayValue(meta) ? (
3311
+ <span className="eth-domain-report-builder__section-meta">{meta}</span>
3312
+ ) : null}
3313
+ </div>
3314
+ <div className="eth-domain-report-builder__section-state">
3315
+ <StatusDot status={itemStatus} label={statusLabel(itemStatus)} />
3316
+ {item.required ? <Badge severity="info">Required</Badge> : null}
3317
+ </div>
3318
+ </li>
3319
+ );
3320
+ })}
3321
+ </ol>
3322
+ ) : (
3323
+ <p className="eth-domain-report-builder__empty">No report sections configured.</p>
3324
+ )}
3325
+ </section>
3326
+
3327
+ <section className="eth-domain-report-builder__preview" aria-label="Report preview">
3328
+ <div className="eth-domain-report-builder__panel-header eth-domain-report-builder__panel-header--inline">
3329
+ <div>
3330
+ <span>Preview</span>
3331
+ <h3 id={previewHeadingId}>{title}</h3>
3332
+ </div>
3333
+ <StatusDot status={computedStatus} label={statusLabel(computedStatus)} />
3334
+ </div>
3335
+
3336
+ <div
3337
+ className="eth-domain-report-builder__preview-page"
3338
+ aria-labelledby={previewHeadingId}
3339
+ >
3340
+ <span className="eth-domain-report-builder__preview-kicker">Draft report</span>
3341
+ <strong className="eth-domain-report-builder__preview-title">{title}</strong>
3342
+ <div className="eth-domain-report-builder__preview-meta">
3343
+ {hasDisplayValue(owner) ? <span>Owner: {owner}</span> : null}
3344
+ {hasDisplayValue(updatedAt) ? <span>Updated: {updatedAt}</span> : null}
3345
+ <span>{formatLabel}</span>
3346
+ </div>
3347
+ {includedSections.length ? (
3348
+ <div className="eth-domain-report-builder__preview-lines">
3349
+ {includedSections.slice(0, 4).map((item, index) => (
3350
+ <div key={item.id} className="eth-domain-report-builder__preview-line">
3351
+ <span>{item.label}</span>
3352
+ <span>{reportSectionMeta(item, index)}</span>
3353
+ </div>
3354
+ ))}
3355
+ </div>
3356
+ ) : (
3357
+ <p className="eth-domain-report-builder__empty-note">
3358
+ Select at least one section to generate a preview.
3359
+ </p>
3360
+ )}
3361
+ </div>
3362
+
3363
+ <div className="eth-domain-report-builder__export-bar" aria-label="Export actions">
3364
+ <span>{exportSummary}</span>
3365
+ <div className="eth-domain-report-builder__export-actions">
3366
+ <Button
3367
+ type="button"
3368
+ intent="primary"
3369
+ density="compact"
3370
+ icon={<DownloadIcon size={16} />}
3371
+ disabled={!includedSections.length}
3372
+ onClick={onExportPdf}
3373
+ >
3374
+ Export PDF
3375
+ </Button>
3376
+ <Button
3377
+ type="button"
3378
+ intent="secondary"
3379
+ density="compact"
3380
+ icon={<TableIcon size={16} />}
3381
+ disabled={!includedSections.length}
3382
+ onClick={onExportCsv}
3383
+ >
3384
+ Export CSV
3385
+ </Button>
3386
+ </div>
3387
+ </div>
3388
+ </section>
3389
+ </div>
3390
+
3391
+ {children}
3392
+ </Surface>
3393
+ );
3394
+ }
3395
+
3396
+ export function ApprovalWorkflowEditor({
3397
+ title = "Approval workflow",
3398
+ description = "Configure approval routing, ownership, and conditional gates.",
3399
+ steps,
3400
+ approverOptions = defaultApproverOptions,
3401
+ mode,
3402
+ onChange,
3403
+ onModeChange,
3404
+ className,
3405
+ status,
3406
+ ...props
3407
+ }: ApprovalWorkflowEditorProps) {
3408
+ const [internalSteps, setInternalSteps] = React.useState<ApprovalWorkflowStep[]>(() =>
3409
+ defaultApprovalSteps.map((step) => ({ ...step }))
3410
+ );
3411
+ const [internalMode, setInternalMode] = React.useState<ApprovalWorkflowMode>("sequential");
3412
+ const currentSteps = steps ?? internalSteps;
3413
+ const currentMode = mode ?? internalMode;
3414
+ const missingApproverCount = currentSteps.filter((step) => !step.approver).length;
3415
+ const requiredCount = currentSteps.filter((step) => step.required).length;
3416
+ const readyCount = currentSteps.length - missingApproverCount;
3417
+ const validationMessage = missingApproverCount
3418
+ ? `${missingApproverCount} approval ${missingApproverCount === 1 ? "step needs" : "steps need"} an approver.`
3419
+ : "All approval steps have owners and can be simulated.";
3420
+
3421
+ const commitSteps = (nextSteps: ApprovalWorkflowStep[]) => {
3422
+ if (!steps) setInternalSteps(nextSteps);
3423
+ onChange?.(nextSteps);
3424
+ };
3425
+
3426
+ const updateStep = (index: number, next: ApprovalWorkflowStep) => {
3427
+ const nextSteps = [...currentSteps];
3428
+ nextSteps[index] = next;
3429
+ commitSteps(nextSteps);
3430
+ };
3431
+
3432
+ const addStep = () => {
3433
+ commitSteps([
3434
+ ...currentSteps,
3435
+ {
3436
+ id: nextApprovalStepId(currentSteps),
3437
+ label: "Approval step",
3438
+ approver: "",
3439
+ condition: "Define routing condition",
3440
+ required: true,
3441
+ status: "queued"
3442
+ }
3443
+ ]);
3444
+ };
3445
+
3446
+ const removeStep = (id: string) => {
3447
+ commitSteps(currentSteps.filter((step) => step.id !== id));
3448
+ };
3449
+
3450
+ const changeMode = (nextMode: ApprovalWorkflowMode) => {
3451
+ if (!mode) setInternalMode(nextMode);
3452
+ onModeChange?.(nextMode);
3453
+ };
3454
+
3455
+ return (
3456
+ <Surface
3457
+ {...props}
3458
+ data-eth-component="ApprovalWorkflowEditor"
3459
+ title={title}
3460
+ description={description}
3461
+ status={status ?? (missingApproverCount ? "pending-approval" : "active")}
3462
+ metadata={[
3463
+ { label: "Steps", value: currentSteps.length },
3464
+ { label: "Required gates", value: requiredCount },
3465
+ { label: "Ready", value: `${readyCount}/${currentSteps.length || 0}` }
3466
+ ]}
3467
+ className={`eth-domain-approval-workflow-editor ${className ?? ""}`}
3468
+ >
3469
+ <div className="eth-domain-approval-workflow-editor__settings">
3470
+ <Select
3471
+ labelText="Routing mode"
3472
+ density="compact"
3473
+ value={currentMode}
3474
+ options={modeOptions}
3475
+ onChange={(event) => changeMode(event.currentTarget.value as ApprovalWorkflowMode)}
3476
+ />
3477
+ <div className="eth-domain-approval-workflow-editor__validation" role="status">
3478
+ <span>Validation</span>
3479
+ <strong>{validationMessage}</strong>
3480
+ </div>
3481
+ </div>
3482
+
3483
+ {currentSteps.length ? (
3484
+ <>
3485
+ <div className="eth-domain-approval-workflow-editor__route" aria-label="Approval route">
3486
+ {currentSteps.map((step, index) => (
3487
+ <React.Fragment key={step.id}>
3488
+ <span className="eth-domain-approval-workflow-editor__route-node">
3489
+ <strong>{index + 1}</strong>
3490
+ <span>{step.label}</span>
3491
+ </span>
3492
+ {index < currentSteps.length - 1 ? (
3493
+ <span
3494
+ className="eth-domain-approval-workflow-editor__route-arrow"
3495
+ aria-hidden="true"
3496
+ >
3497
+ -&gt;
3498
+ </span>
3499
+ ) : null}
3500
+ </React.Fragment>
3501
+ ))}
3502
+ </div>
3503
+
3504
+ <ol className="eth-domain-approval-workflow-editor__steps">
3505
+ {currentSteps.map((step, index) => (
3506
+ <li key={step.id} className="eth-domain-approval-workflow-editor__step">
3507
+ <div className="eth-domain-approval-workflow-editor__step-index" aria-hidden="true">
3508
+ {index + 1}
3509
+ </div>
3510
+ <div className="eth-domain-approval-workflow-editor__step-main">
3511
+ <div className="eth-domain-approval-workflow-editor__step-header">
3512
+ <div>
3513
+ <span>Gate {index + 1}</span>
3514
+ <strong>{formatApprovalStatus(step.status, step.required)}</strong>
3515
+ </div>
3516
+ <Button
3517
+ type="button"
3518
+ intent="ghost"
3519
+ density="compact"
3520
+ disabled={currentSteps.length <= 1}
3521
+ onClick={() => removeStep(step.id)}
3522
+ >
3523
+ Remove
3524
+ </Button>
3525
+ </div>
3526
+ <div className="eth-domain-approval-workflow-editor__fields">
3527
+ <TextInput
3528
+ labelText="Step name"
3529
+ density="compact"
3530
+ value={step.label}
3531
+ onChange={(event) =>
3532
+ updateStep(index, { ...step, label: event.currentTarget.value })
3533
+ }
3534
+ />
3535
+ <Select
3536
+ labelText="Approver"
3537
+ density="compact"
3538
+ value={step.approver ?? ""}
3539
+ options={approverOptions}
3540
+ invalid={!step.approver}
3541
+ invalidText="Choose an approver."
3542
+ onChange={(event) =>
3543
+ updateStep(index, { ...step, approver: event.currentTarget.value })
3544
+ }
3545
+ />
3546
+ <TextInput
3547
+ labelText="Condition"
3548
+ density="compact"
3549
+ value={step.condition ?? ""}
3550
+ onChange={(event) =>
3551
+ updateStep(index, { ...step, condition: event.currentTarget.value })
3552
+ }
3553
+ />
3554
+ </div>
3555
+ <div className="eth-domain-approval-workflow-editor__step-footer">
3556
+ <Checkbox
3557
+ label="Required gate"
3558
+ checked={Boolean(step.required)}
3559
+ onChange={(event) =>
3560
+ updateStep(index, { ...step, required: event.currentTarget.checked })
3561
+ }
3562
+ />
3563
+ <span>Escalates to workflow owner when overdue by 24h.</span>
3564
+ </div>
3565
+ </div>
3566
+ </li>
3567
+ ))}
3568
+ </ol>
3569
+ </>
3570
+ ) : (
3571
+ <div className="eth-domain-approval-workflow-editor__empty">
3572
+ No approval steps configured.
3573
+ </div>
3574
+ )}
3575
+
3576
+ <div className="eth-domain-approval-workflow-editor__footer">
3577
+ <Button type="button" intent="secondary" density="compact" onClick={addStep}>
3578
+ Add approval
3579
+ </Button>
3580
+ <Button
3581
+ type="button"
3582
+ intent="tertiary"
3583
+ density="compact"
3584
+ disabled={Boolean(missingApproverCount)}
3585
+ >
3586
+ Simulate route
3587
+ </Button>
3588
+ </div>
3589
+ </Surface>
3590
+ );
3591
+ }
3592
+
3593
+ export function ProcessDesigner({
3594
+ title = "Business process",
3595
+ description = "Design, validate, and inspect a multi-step process.",
3596
+ items,
3597
+ connections,
3598
+ validationIssues: validationIssuesProp,
3599
+ selectedItemId,
3600
+ metadata,
3601
+ actions,
3602
+ footer,
3603
+ children,
3604
+ className,
3605
+ status,
3606
+ onAddStep,
3607
+ onValidate,
3608
+ ...props
3609
+ }: ProcessDesignerProps) {
3610
+ const canvasTitleId = React.useId();
3611
+ const graphTitleId = React.useId();
3612
+ const graphDescriptionId = React.useId();
3613
+ const markerId = React.useId().replace(/:/g, "");
3614
+ const panelTitleId = React.useId();
3615
+ const processItems = items ?? defaultProcessItems;
3616
+ const validationIssues =
3617
+ validationIssuesProp ?? (items ? [] : defaultProcessValidationIssues);
3618
+ const processConnections =
3619
+ connections ?? (items ? sequentialProcessConnections(processItems) : defaultProcessConnections);
3620
+ const titleText = textFromNode(title).trim() || "Business process";
3621
+ const [internalSelectedId, setInternalSelectedId] = React.useState<string | undefined>(
3622
+ selectedItemId ?? processItems[0]?.id
3623
+ );
3624
+
3625
+ React.useEffect(() => {
3626
+ if (selectedItemId) {
3627
+ setInternalSelectedId(selectedItemId);
3628
+ return;
3629
+ }
3630
+
3631
+ setInternalSelectedId((current) =>
3632
+ processItems.some((item) => item.id === current) ? current : processItems[0]?.id
3633
+ );
3634
+ }, [processItems, selectedItemId]);
3635
+
3636
+ const activeItem =
3637
+ processItems.find((item) => item.id === (selectedItemId ?? internalSelectedId)) ??
3638
+ processItems[0];
3639
+ const completedCount = processItems.filter((item) => isCompletedProcessStep(item.status)).length;
3640
+ const decisionCount = processItems.filter(
3641
+ (item, index) => processStepKind(item, index, processItems.length) === "decision"
3642
+ ).length;
3643
+ const processStatus = status ?? deriveProcessStatus(processItems, validationIssues);
3644
+ const validationLabel = validationIssues.length
3645
+ ? `${validationIssues.length} open ${validationIssues.length === 1 ? "issue" : "issues"}`
3646
+ : "No open issues";
3647
+
3648
+ const selectNode = (id: string) => {
3649
+ if (!selectedItemId) setInternalSelectedId(id);
3650
+ };
3651
+
3652
+ return (
3653
+ <Surface
3654
+ {...props}
3655
+ data-eth-component="ProcessDesigner"
3656
+ title={title}
3657
+ description={description}
3658
+ status={processStatus}
3659
+ metadata={
3660
+ metadata ?? [
3661
+ { label: "Steps", value: processItems.length },
3662
+ { label: "Completed", value: `${completedCount}/${processItems.length || 0}` },
3663
+ { label: "Branches", value: decisionCount },
3664
+ { label: "Validation", value: validationLabel }
3665
+ ]
3666
+ }
3667
+ actions={actions}
3668
+ footer={
3669
+ footer ?? (
3670
+ <div className="eth-domain-process-designer__footer">
3671
+ <span>Draft process: {titleText}</span>
3672
+ <span>Last validated 12 minutes ago</span>
3673
+ </div>
3674
+ )
3675
+ }
3676
+ className={`eth-domain-process-designer ${className ?? ""}`}
3677
+ >
3678
+ <div className="eth-domain-process-designer__workspace">
3679
+ <section
3680
+ className="eth-domain-process-designer__canvas-shell"
3681
+ aria-labelledby={canvasTitleId}
3682
+ >
3683
+ <div className="eth-domain-process-designer__toolbar">
3684
+ <div>
3685
+ <span>Canvas</span>
3686
+ <strong id={canvasTitleId}>
3687
+ {processItems.length ? titleText : "No workflow steps"}
3688
+ </strong>
3689
+ </div>
3690
+ <div className="eth-domain-process-designer__toolbar-actions">
3691
+ {processStatus ? <StatusDot status={processStatus} label={validationLabel} /> : null}
3692
+ <Button type="button" intent="secondary" density="compact" onClick={onAddStep}>
3693
+ Add step
3694
+ </Button>
3695
+ <Button type="button" intent="tertiary" density="compact" onClick={onValidate}>
3696
+ Validate
3697
+ </Button>
3698
+ </div>
3699
+ </div>
3700
+
3701
+ {processItems.length ? (
3702
+ <div className="eth-domain-process-designer__canvas">
3703
+ <svg
3704
+ viewBox="0 0 920 340"
3705
+ role="group"
3706
+ aria-labelledby={graphTitleId}
3707
+ aria-describedby={graphDescriptionId}
3708
+ >
3709
+ <title id={graphTitleId}>Business process graph</title>
3710
+ <desc id={graphDescriptionId}>
3711
+ Process graph with {processItems.length} steps and {processConnections.length}{" "}
3712
+ connections.
3713
+ </desc>
3714
+ <defs>
3715
+ <marker
3716
+ id={markerId}
3717
+ viewBox="0 0 10 10"
3718
+ refX="8"
3719
+ refY="5"
3720
+ markerWidth="6"
3721
+ markerHeight="6"
3722
+ orient="auto-start-reverse"
3723
+ >
3724
+ <path d="M 0 0 L 10 5 L 0 10 z" />
3725
+ </marker>
3726
+ </defs>
3727
+
3728
+ {processConnections.map((connection, index) => {
3729
+ const fromIndex = processItems.findIndex((item) => item.id === connection.from);
3730
+ const toIndex = processItems.findIndex((item) => item.id === connection.to);
3731
+ const from = processItems[fromIndex];
3732
+ const to = processItems[toIndex];
3733
+ if (!from || !to) return null;
3734
+
3735
+ const fromPoint = processNodePoint(from, fromIndex);
3736
+ const toPoint = processNodePoint(to, toIndex);
3737
+ const midX = (fromPoint.x + toPoint.x) / 2;
3738
+ const midY = (fromPoint.y + toPoint.y) / 2;
3739
+ const label = textFromNode(connection.label).trim();
3740
+
3741
+ return (
3742
+ <g
3743
+ key={`${connection.from}-${connection.to}-${index}`}
3744
+ className="eth-domain-process-designer__edge"
3745
+ data-status={connection.status}
3746
+ >
3747
+ <line
3748
+ x1={fromPoint.x}
3749
+ y1={fromPoint.y}
3750
+ x2={toPoint.x}
3751
+ y2={toPoint.y}
3752
+ markerEnd={`url(#${markerId})`}
3753
+ />
3754
+ {label ? (
3755
+ <text x={midX} y={midY - 10} textAnchor="middle">
3756
+ {label}
3757
+ </text>
3758
+ ) : null}
3759
+ </g>
3760
+ );
3761
+ })}
3762
+
3763
+ {processItems.map((item, index) => {
3764
+ const point = processNodePoint(item, index);
3765
+ const kind = processStepKind(item, index, processItems.length);
3766
+ const selected = activeItem?.id === item.id;
3767
+ const lines = graphLabelLines(item.label, item.id);
3768
+
3769
+ return (
3770
+ <g
3771
+ key={item.id}
3772
+ className="eth-domain-process-designer__node"
3773
+ data-kind={kind}
3774
+ data-status={item.status}
3775
+ data-selected={selected ? "true" : undefined}
3776
+ transform={`translate(${point.x}, ${point.y})`}
3777
+ role="button"
3778
+ tabIndex={0}
3779
+ aria-label={`${textFromNode(item.label) || item.id}, ${statusLabel(
3780
+ item.status ?? "queued"
3781
+ )}`}
3782
+ onClick={() => selectNode(item.id)}
3783
+ onKeyDown={(event) => {
3784
+ if (event.key === "Enter" || event.key === " ") {
3785
+ event.preventDefault();
3786
+ selectNode(item.id);
3787
+ }
3788
+ }}
3789
+ >
3790
+ {kind === "decision" ? (
3791
+ <polygon points="0,-44 78,0 0,44 -78,0" />
3792
+ ) : (
3793
+ <rect x="-72" y="-36" width="144" height="72" rx="0" />
3794
+ )}
3795
+ <circle
3796
+ className="eth-domain-process-designer__node-status"
3797
+ cx={kind === "decision" ? -44 : -52}
3798
+ cy={kind === "decision" ? -18 : -18}
3799
+ r="5"
3800
+ />
3801
+ <text textAnchor="middle">
3802
+ {lines.map((line, lineIndex) => (
3803
+ <tspan
3804
+ key={`${line}-${lineIndex}`}
3805
+ x="0"
3806
+ dy={
3807
+ lineIndex === 0
3808
+ ? lines.length > 1
3809
+ ? "-0.35em"
3810
+ : "0.35em"
3811
+ : "1.2em"
3812
+ }
3813
+ >
3814
+ {line}
3815
+ </tspan>
3816
+ ))}
3817
+ </text>
3818
+ </g>
3819
+ );
3820
+ })}
3821
+ </svg>
3822
+ </div>
3823
+ ) : (
3824
+ <div className="eth-domain-process-designer__empty">No process steps configured.</div>
3825
+ )}
3826
+ </section>
3827
+
3828
+ <aside className="eth-domain-process-designer__inspector" aria-labelledby={panelTitleId}>
3829
+ <div className="eth-domain-process-designer__panel-header">
3830
+ <span>Step properties</span>
3831
+ <h3 id={panelTitleId}>{activeItem ? activeItem.label : "No step selected"}</h3>
3832
+ </div>
3833
+ {activeItem ? (
3834
+ <>
3835
+ <StatusDot
3836
+ status={activeItem.status ?? "queued"}
3837
+ label={statusLabel(activeItem.status ?? "queued")}
3838
+ />
3839
+ {activeItem.description ? (
3840
+ <p className="eth-domain-process-designer__description">
3841
+ {activeItem.description}
3842
+ </p>
3843
+ ) : null}
3844
+ <dl className="eth-domain-process-designer__facts">
3845
+ <div>
3846
+ <dt>Owner</dt>
3847
+ <dd>{activeItem.owner ?? "Unassigned"}</dd>
3848
+ </div>
3849
+ <div>
3850
+ <dt>Lane</dt>
3851
+ <dd>{activeItem.lane ?? "Default lane"}</dd>
3852
+ </div>
3853
+ <div>
3854
+ <dt>SLA</dt>
3855
+ <dd>{activeItem.duration ?? activeItem.meta ?? "Not set"}</dd>
3856
+ </div>
3857
+ </dl>
3858
+ </>
3859
+ ) : (
3860
+ <p className="eth-domain-process-designer__description">
3861
+ No selected step.
3862
+ </p>
3863
+ )}
3864
+
3865
+ <section className="eth-domain-process-designer__validation" aria-label="Validation">
3866
+ <div className="eth-domain-process-designer__panel-header">
3867
+ <span>Validation</span>
3868
+ <strong>{validationLabel}</strong>
3869
+ </div>
3870
+ {validationIssues.length ? (
3871
+ <ul>
3872
+ {validationIssues.map((issue) => (
3873
+ <li key={issue.id} data-status={issue.status ?? "warning"}>
3874
+ <StatusDot
3875
+ status={issue.status ?? "warning"}
3876
+ label={statusLabel(issue.status ?? "warning")}
3877
+ />
3878
+ <div>
3879
+ {issue.target ? <strong>{issue.target}</strong> : null}
3880
+ <span>{issue.message}</span>
3881
+ </div>
3882
+ </li>
3883
+ ))}
3884
+ </ul>
3885
+ ) : (
3886
+ <p className="eth-domain-process-designer__empty-note">
3887
+ No validation issues detected.
3888
+ </p>
3889
+ )}
3890
+ </section>
3891
+ </aside>
3892
+ </div>
3893
+
3894
+ {children}
3895
+ </Surface>
3896
+ );
3897
+ }
3898
+
3899
+ export const SalesPipelineBoard = createSurfaceComponent("SalesPipelineBoard");
3900
+ export const ContractReviewPanel = createSurfaceComponent("ContractReviewPanel");
3901
+ export const RuleSimulationPanel = createSurfaceComponent("RuleSimulationPanel");
3902
+
3903
+ export type SalesPipelineBoardProps = SurfaceComponentProps;
3904
+ export type ContractReviewPanelProps = SurfaceComponentProps;
3905
+ export type RuleSimulationPanelProps = SurfaceComponentProps;
3906
+
3907
+ export const DomainWidgetsComponentNames = [
3908
+ "CRMContactPanel",
3909
+ "SalesPipelineBoard",
3910
+ "SupportTicketQueue",
3911
+ "KnowledgeBaseArticleViewer",
3912
+ "InventoryTable",
3913
+ "OrderManagementPanel",
3914
+ "InvoiceViewer",
3915
+ "ContractReviewPanel",
3916
+ "ComplianceChecklist",
3917
+ "RiskMatrix",
3918
+ "ApprovalWorkflowEditor",
3919
+ "DataImportWizard",
3920
+ "MappingConfigurationWizard",
3921
+ "ReportBuilder",
3922
+ "ProcessDesigner",
3923
+ "RuleSimulationPanel"
3924
+ ] as const;
3925
+ export type DomainWidgetsComponentName = (typeof DomainWidgetsComponentNames)[number];