@puredesktop/puredesktop-ui-bridge 2.1.7 → 2.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/package.json +89 -5
  2. package/src/agents/mcpServerPresets.test.ts +105 -0
  3. package/src/agents/mcpServerPresets.ts +380 -0
  4. package/src/agents/runtime/index.ts +7 -0
  5. package/src/agents/runtime/mapAgentSnapshot.ts +11 -8
  6. package/src/agents/runtime/pendingAsks.test.ts +117 -0
  7. package/src/agents/runtime/pendingAsks.ts +110 -0
  8. package/src/bridge/agentScopeDefaults.ts +65 -0
  9. package/src/bridge/agentTypes.ts +57 -3
  10. package/src/bridge/agents.d.mts +6 -0
  11. package/src/bridge/agents.mjs +10 -0
  12. package/src/bridge/agents.ts +4 -0
  13. package/src/bridge/collectionImagePaste.ts +19 -1
  14. package/src/bridge/context.mjs +32 -0
  15. package/src/bridge/credentials.d.mts +62 -0
  16. package/src/bridge/credentials.mjs +53 -0
  17. package/src/bridge/documents.d.mts +76 -0
  18. package/src/bridge/documents.mjs +40 -0
  19. package/src/bridge/events.d.mts +1 -0
  20. package/src/bridge/events.mjs +1 -0
  21. package/src/bridge/googleAuth.ts +7 -152
  22. package/src/bridge/mcpClient.d.mts +74 -0
  23. package/src/bridge/mcpClient.mjs +79 -0
  24. package/src/bridge/methods.d.mts +45 -0
  25. package/src/bridge/methods.mjs +46 -0
  26. package/src/bridge/notifications.d.mts +12 -0
  27. package/src/bridge/notifications.mjs +36 -0
  28. package/src/bridge/operations.d.mts +44 -0
  29. package/src/bridge/operations.mjs +40 -0
  30. package/src/bridge/pureRender/base.css +44 -0
  31. package/src/bridge/pureRender/document.ts +29 -2
  32. package/src/bridge/pureRender/index.ts +3 -0
  33. package/src/bridge/pureRender/normalize.ts +167 -0
  34. package/src/bridge/pureRender/sanitizeExportBody.test.ts +51 -0
  35. package/src/bridge/pureRender/sanitizeExportBody.ts +61 -0
  36. package/src/bridge/pureRender/styles.ts +125 -5
  37. package/src/bridge/pureRender/theme.test.ts +259 -0
  38. package/src/bridge/pureRender/theme.ts +19 -1
  39. package/src/bridge/pureRender/types.ts +33 -0
  40. package/src/bridge/pureRender/visualLayoutInspection.test.ts +26 -0
  41. package/src/bridge/pureRender/visualLayoutInspection.ts +75 -0
  42. package/src/bridge/react/useDocumentHotkeys.ts +41 -0
  43. package/src/bridge/react/useDocumentLifecycle.tsx +359 -0
  44. package/src/bridge/react/usePlatformAgentSessionDrive.test.tsx +29 -10
  45. package/src/bridge/react/usePlatformAgentSessionDrive.tsx +10 -81
  46. package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -0
  47. package/src/bridge/react/usePlatformAppSettings.tsx +104 -0
  48. package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -0
  49. package/src/bridge/react/usePlatformJsonStore.tsx +149 -0
  50. package/src/bridge/react/usePlatformOperations.tsx +75 -0
  51. package/src/bridge/storage.d.mts +23 -10
  52. package/src/bridge/storage.mjs +6 -3
  53. package/src/bridge/storage.test.ts +6 -6
  54. package/src/bridge/types.ts +7 -0
  55. package/src/components/agents/AgentComposer.tsx +10 -6
  56. package/src/components/agents/AgentDrawerPanel.test.tsx +2 -205
  57. package/src/components/agents/AgentDrawerPanel.tsx +23 -62
  58. package/src/components/agents/AgentMessageBubble.tsx +59 -33
  59. package/src/components/agents/AgentMessageList.tsx +4 -4
  60. package/src/components/agents/AgentQuestionPromptList.tsx +34 -212
  61. package/src/components/agents/AgentToolApprovalActions.tsx +13 -11
  62. package/src/components/agents/AgentToolCallCard.tsx +20 -20
  63. package/src/components/agents/AgentToolPendingList.tsx +6 -6
  64. package/src/components/agents/AgentToolPendingReview.tsx +6 -6
  65. package/src/components/agents/ContextPicker.tsx +239 -0
  66. package/src/components/agents/QuestionRequestList.tsx +278 -0
  67. package/src/components/agents/agentPanelStyles.ts +8 -31
  68. package/src/components/agents/agentTypes.ts +25 -41
  69. package/src/components/agents/index.ts +4 -0
  70. package/src/components/assets/asset-library/sidebar/AssetLibraryList.tsx +10 -4
  71. package/src/components/chrome/WorkspaceTabStrip.tsx +22 -4
  72. package/src/components/chrome/workspaceTabTypes.ts +2 -0
  73. package/src/components/common/chat/ChatBox.tsx +9 -9
  74. package/src/components/common/chat/ChatModelMenu.tsx +34 -4
  75. package/src/components/common/chat/ChatThread.tsx +23 -7
  76. package/src/components/common/connections/ProviderConnection.test.tsx +208 -0
  77. package/src/components/common/connections/ProviderConnection.tsx +277 -0
  78. package/src/components/common/connections/index.ts +5 -0
  79. package/src/components/common/containers/AppFrame.test.tsx +4 -4
  80. package/src/components/common/containers/AppFrame.tsx +26 -10
  81. package/src/components/common/containers/AppHeader.tsx +37 -15
  82. package/src/components/common/documents/DocumentHeaderActions.tsx +206 -0
  83. package/src/components/common/documents/DocumentSwitcher.tsx +1181 -0
  84. package/src/components/common/documents/index.ts +8 -0
  85. package/src/components/common/dropdown/MenuDropdownItem.tsx +14 -2
  86. package/src/components/common/dropdown/MenuPortal.tsx +3 -1
  87. package/src/components/common/dropdown/menuViewportPosition.test.ts +20 -10
  88. package/src/components/common/dropdown/menuViewportPosition.ts +35 -27
  89. package/src/components/common/overlays/Modal.tsx +13 -2
  90. package/src/components/common/research/EvidenceDossier.tsx +1232 -150
  91. package/src/components/common/research/index.ts +2 -0
  92. package/src/components/editor/EditorLinkPromptDialog.tsx +1 -1
  93. package/src/components/print-design/PrintDesignPanel.test.tsx +430 -40
  94. package/src/components/print-design/PrintDesignPanel.tsx +1770 -207
  95. package/src/components/print-design/index.ts +2 -0
  96. package/src/components/print-design/previewContent.test.ts +32 -0
  97. package/src/components/print-design/previewContent.ts +31 -0
  98. package/src/context/buildContextDocument.test.ts +240 -0
  99. package/src/context/buildContextDocument.ts +309 -0
  100. package/src/context/contextAccess.ts +66 -0
  101. package/src/context/contextConfig.ts +72 -0
  102. package/src/context/contextDocument.test.ts +155 -0
  103. package/src/context/contextDocument.ts +300 -0
  104. package/src/context/contextSections.test.ts +88 -0
  105. package/src/context/contextSections.ts +84 -0
  106. package/src/context/htmlToContextMarkdown.test.ts +134 -0
  107. package/src/context/htmlToContextMarkdown.ts +369 -0
  108. package/src/ics/generateIcs.test.ts +153 -0
  109. package/src/ics/generateIcs.ts +197 -0
  110. package/src/index.ts +3 -0
  111. package/src/net/httpRetry.test.ts +117 -0
  112. package/src/net/httpRetry.ts +111 -0
  113. package/src/theme/appAccents.test.ts +36 -35
  114. package/src/theme/appAccents.ts +93 -104
  115. package/src/theme/appIdentityCss.mjs +178 -229
  116. package/src/theme/appIdentityCss.ts +222 -231
  117. package/src/theme/themes/dark.ts +3 -1
  118. package/src/theme/themes/light.ts +3 -1
  119. package/src/bridge/googleProviderConfig.ts +0 -73
@@ -1,7 +1,9 @@
1
1
  export {
2
2
  EvidenceDossier,
3
+ dossierMarkdown,
3
4
  type EvidenceDossierProps,
4
5
  type EvidenceFinding,
5
6
  type EvidenceSource,
6
7
  type EvidenceStrength,
8
+ type EvidenceTermNote,
7
9
  } from './EvidenceDossier.js'
@@ -19,7 +19,7 @@ export function EditorLinkPromptDialog({
19
19
  }
20
20
 
21
21
  return (
22
- <Modal open={open} onClose={onCancel} title="Insert link" size="md">
22
+ <Modal open={open} onClose={onCancel} title="Insert link" size="xs">
23
23
  <StyledBody>
24
24
  <StyledLinkInput
25
25
  autoFocus
@@ -228,13 +228,13 @@ describe('PrintDesignPanel', () => {
228
228
 
229
229
  expect(container.textContent).toContain('Sample page snapshot')
230
230
  expect(container.textContent).toContain('Visual preflight sample')
231
- expect(container.textContent).toContain('No export inspection has run yet.')
232
- expect(container.textContent).toContain(
233
- 'The next beta export will render with Paged.js and capture a real page snapshot.',
234
- )
235
231
  expect(
236
232
  container.querySelector('[aria-label="Sample PDF page snapshot"]'),
237
233
  ).not.toBeNull()
234
+ // No run has finished — the rail (final run report) stays hidden.
235
+ expect(
236
+ container.querySelector('[aria-label="Live design readout"]'),
237
+ ).toBeNull()
238
238
  })
239
239
 
240
240
  it('shows a provided live beta vision snapshot instead of the sample page', () => {
@@ -261,7 +261,6 @@ describe('PrintDesignPanel', () => {
261
261
  expect(
262
262
  container.querySelector('[aria-label="Sample PDF page snapshot"]'),
263
263
  ).toBeNull()
264
- expect(container.textContent).toContain('Rendered page 2 inspected.')
265
264
  })
266
265
 
267
266
  it('shows multiple beta vision evidence snapshots with annotations', () => {
@@ -434,29 +433,38 @@ describe('PrintDesignPanel', () => {
434
433
  />,
435
434
  )
436
435
 
437
- click(buttonByText(container, 'Beta vision typesetting'))
438
-
436
+ // ONE merged Typesetter view: no Design/vision tabs, controls at left,
437
+ // vision canvas as the well — all visible at once.
439
438
  expect(
440
439
  container.querySelector('[data-panel-variant="workspace"]'),
441
440
  ).not.toBeNull()
441
+ expect(
442
+ Array.from(container.querySelectorAll('button')).some(button =>
443
+ button.textContent?.includes('Beta vision typesetting'),
444
+ ),
445
+ ).toBe(false)
442
446
  expect(container.textContent).toContain('Design - PDF Typesetter')
443
- expect(container.textContent).toContain('Clean · Justified · 34rem')
447
+ expect(container.textContent).toContain('Clean · Justified · 38rem')
444
448
  expect(container.textContent).toContain('In flight')
445
449
  expect(container.textContent).toContain('Pass 2 · checking Fix 1')
446
450
  expect(container.textContent).toContain('Beta settings')
447
451
  expect(container.textContent).toContain('Use beta vision typesetting')
448
452
  expect(container.textContent).toContain('Max fixes per issue')
449
453
  expect(container.textContent).toContain('Baseline page 2')
450
- expect(container.textContent).toContain('Live readout')
451
- expect(container.textContent).toContain('FIX')
452
- expect(container.textContent).toContain('ISSUE')
453
- expect(container.textContent).toContain('PDF')
454
+ expect(container.textContent).toContain('Presets')
455
+ // The right rail carries the run summary / Activity ledger.
456
+ const readout = container.querySelector(
457
+ '[aria-label="Live design readout"]',
458
+ )
459
+ expect(readout).not.toBeNull()
460
+ expect(readout?.textContent).toContain('Activity')
461
+ expect(readout?.textContent).toContain('Issue 1: stranded heading')
454
462
  expect(document.head.textContent).not.toMatch(
455
463
  /grid-template-columns:\s*minmax\(190px,\s*240px\)\s*minmax\(0,\s*1fr\)/,
456
464
  )
457
465
  })
458
466
 
459
- it('keeps the workspace readout visible when switching away from vision', () => {
467
+ it('shows the run ledger in the controls-column Activity section', () => {
460
468
  const { container } = render(
461
469
  <PrintDesignPanel
462
470
  variant="workspace"
@@ -491,18 +499,12 @@ describe('PrintDesignPanel', () => {
491
499
  />,
492
500
  )
493
501
 
494
- click(buttonByText(container, 'Beta vision typesetting'))
495
-
502
+ // Evidence in the well and the ledger in the left column Activity —
503
+ // simultaneously, no tab switching.
496
504
  expect(container.textContent).toContain('Baseline page 2')
497
-
498
- click(buttonByText(container, 'Design'))
499
-
500
- const readout = container.querySelector(
501
- '[aria-label="Live design readout"]',
502
- )
503
- expect(readout?.textContent).toContain('Issue 1: stranded heading')
504
- expect(readout?.textContent).toContain('baseline.pdf')
505
- expect(container.textContent).not.toContain('Baseline page 2')
505
+ expect(container.textContent).toContain('Issue 1: stranded heading')
506
+ expect(container.textContent).toContain('baseline.pdf')
507
+ expect(container.textContent).toContain('Activity')
506
508
  })
507
509
 
508
510
  it('groups workspace readout entries by visual pass', () => {
@@ -521,21 +523,18 @@ describe('PrintDesignPanel', () => {
521
523
  />,
522
524
  )
523
525
 
524
- click(buttonByText(container, 'Beta vision typesetting'))
525
-
526
- const readout = container.querySelector(
527
- '[aria-label="Live design readout"]',
528
- )
526
+ // Merged view: the grouped ledger renders in the controls-column
527
+ // Activity section — no tabs, no readout aside.
529
528
  expect(
530
- readout?.querySelector('[aria-label="Pass 2 event group"]'),
529
+ container.querySelector('[aria-label="Pass 2 event group"]'),
531
530
  ).not.toBeNull()
532
531
  expect(
533
- readout?.querySelector('[aria-label="Pass 1 event group"]'),
532
+ container.querySelector('[aria-label="Pass 1 event group"]'),
534
533
  ).not.toBeNull()
535
- expect(readout?.textContent).toContain('CHECK')
536
- expect(readout?.textContent).toContain('FIX')
537
- expect(readout?.textContent).toContain('ISSUE')
538
- expect(readout?.textContent).toContain('baseline.pdf')
534
+ expect(container.textContent).toContain('CHECK')
535
+ expect(container.textContent).toContain('FIX')
536
+ expect(container.textContent).toContain('ISSUE')
537
+ expect(container.textContent).toContain('baseline.pdf')
539
538
  })
540
539
 
541
540
  it('opens passed fix comparisons from evidence only when available', () => {
@@ -618,7 +617,6 @@ describe('PrintDesignPanel', () => {
618
617
 
619
618
  click(buttonByText(container, 'Beta vision typesetting'))
620
619
 
621
- expect(container.textContent).toContain('FIX')
622
620
  expect(container.textContent).toContain('View fix comparison')
623
621
  expect(
624
622
  Array.from(container.querySelectorAll('button')).filter(button =>
@@ -791,8 +789,11 @@ describe('PrintDesignPanel', () => {
791
789
  )
792
790
  changeInput(textInputs[0]!, '32rem')
793
791
  changeInput(textInputs[1]!, '1.6')
792
+ // Font roles, align, flourishes (heading rule, drop cap), page
793
+ // furniture (columns, running header, footer + slot) and Cover.
794
+ // (Contents only appears once Cover is on.)
794
795
  const selects = Array.from(container.querySelectorAll('select'))
795
- expect(selects).toHaveLength(4)
796
+ expect(selects).toHaveLength(15)
796
797
  changeInput(selects[0]!, 'sans')
797
798
  changeInput(selects[1]!, 'serif')
798
799
  changeInput(selects[2]!, 'sans')
@@ -1272,7 +1273,10 @@ describe('PrintDesignPanel', () => {
1272
1273
  expect(onRenderPdf).toHaveBeenCalledOnce()
1273
1274
  })
1274
1275
 
1275
- it('routes persistent export through beta vision when enabled', () => {
1276
+ it('never routes Export PDF through the vision loop, even when enabled', () => {
1277
+ // Export is always a plain export; the loop has its own Run button in
1278
+ // the vision tab (previously Export silently ran the whole inspection
1279
+ // loop and there was no way to just export).
1276
1280
  const onRenderPdf = vi.fn()
1277
1281
  const onVisionExport = vi.fn()
1278
1282
  const { container } = render(
@@ -1295,8 +1299,8 @@ describe('PrintDesignPanel', () => {
1295
1299
 
1296
1300
  click(buttonByText(container, 'Export PDF'))
1297
1301
 
1298
- expect(onVisionExport).toHaveBeenCalledOnce()
1299
- expect(onRenderPdf).not.toHaveBeenCalled()
1302
+ expect(onRenderPdf).toHaveBeenCalledOnce()
1303
+ expect(onVisionExport).not.toHaveBeenCalled()
1300
1304
  })
1301
1305
 
1302
1306
  it('keeps beta vision report entries in the persistent live readout', () => {
@@ -1445,4 +1449,390 @@ describe('PrintDesignPanel', () => {
1445
1449
  expect(container.textContent).toContain('#111111')
1446
1450
  expect(container.textContent).toContain('#335f8f')
1447
1451
  })
1452
+
1453
+ it('inherits the host identity instead of baking in a colour (issue spec)', () => {
1454
+ render(<PrintDesignPanel variant="workspace" />)
1455
+ const css = document.head.textContent ?? ''
1456
+ // Export primary + markers resolve through the host's --app-acc/--app-block
1457
+ // (whitespace-tolerant: styled-components may collapse the declarations).
1458
+ expect(css).toMatch(/--print-design-hue:\s*var\(\s*--app-acc/)
1459
+ expect(css).toMatch(/--app-block/)
1460
+ // Selected preset carries the host-hue edge/underline.
1461
+ expect(css).toMatch(/inset 0 -2px 0 0 var\(--print-design-hue\)/)
1462
+ })
1463
+
1464
+ it('presents Ink/Paper/Accent as swatch chips with hex, not full-width bars', () => {
1465
+ const { container } = render(
1466
+ <PrintDesignPanel variant="workspace" modes={['preset', 'manual']} />,
1467
+ )
1468
+ const pickers = Array.from(
1469
+ container.querySelectorAll<HTMLInputElement>('input[type="color"]'),
1470
+ )
1471
+ expect(pickers).toHaveLength(3)
1472
+ // Each picker is embedded in a 28px chip, and the hex value is visible.
1473
+ for (const picker of pickers) {
1474
+ expect(picker.closest('span')).not.toBeNull()
1475
+ }
1476
+ expect(container.textContent).toMatch(/#[0-9a-fA-F]{6}/)
1477
+ })
1478
+
1479
+ it('previews the host document by default with a Sample opt-out', () => {
1480
+ const { container } = render(
1481
+ <PrintDesignPanel
1482
+ variant="workspace"
1483
+ previewDocument={{
1484
+ title: 'Emergent Workflows',
1485
+ byline: 'A. Hyde',
1486
+ paragraphs: ['A first paragraph from the actual manuscript body.'],
1487
+ }}
1488
+ />,
1489
+ )
1490
+ expect(container.textContent).toContain('Emergent Workflows')
1491
+ expect(container.textContent).toContain(
1492
+ 'A first paragraph from the actual manuscript body.',
1493
+ )
1494
+ expect(container.textContent).not.toContain('The Shape of a Page')
1495
+
1496
+ // The Sample toggle lives in the well header (the first checkbox in the
1497
+ // merged view is the beta vision toggle).
1498
+ const toggle = container.querySelector<HTMLInputElement>(
1499
+ 'label[data-disabled] input[type="checkbox"]',
1500
+ )
1501
+ expect(toggle?.disabled).toBe(false)
1502
+ act(() => {
1503
+ toggle?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
1504
+ })
1505
+ expect(container.textContent).toContain('The Shape of a Page')
1506
+ })
1507
+
1508
+ it('forces the sample when the host supplies no preview content', () => {
1509
+ const { container } = render(<PrintDesignPanel variant="workspace" />)
1510
+ expect(container.textContent).toContain('The Shape of a Page')
1511
+ const toggle = container.querySelector<HTMLInputElement>(
1512
+ 'label[data-disabled] input[type="checkbox"]',
1513
+ )
1514
+ expect(toggle?.disabled).toBe(true)
1515
+ })
1516
+
1517
+ it('shows the vision page snapshot in the preview pane in vision mode', () => {
1518
+ const { container } = render(
1519
+ <PrintDesignPanel
1520
+ variant="workspace"
1521
+ visionTypesetting={{
1522
+ enabled: true,
1523
+ snapshotUrl: 'file:///tmp/page-1.png',
1524
+ snapshotAlt: 'Rendered page 1',
1525
+ onToggle: vi.fn(),
1526
+ }}
1527
+ />,
1528
+ )
1529
+ const snapshot = container.querySelector(
1530
+ 'img[alt="Rendered page 1"]',
1531
+ )
1532
+ expect(snapshot?.getAttribute('src')).toBe('file:///tmp/page-1.png')
1533
+ // The theme facsimile yields to the real snapshot in vision mode.
1534
+ expect(container.textContent).not.toContain('The Shape of a Page')
1535
+ })
1536
+
1537
+ it('separates Run (vision loop) from Export PDF (plain export)', () => {
1538
+ const onRun = vi.fn()
1539
+ const onPlainExport = vi.fn()
1540
+ const { container } = render(
1541
+ <PrintDesignPanel
1542
+ variant="workspace"
1543
+ actions={[
1544
+ {
1545
+ id: 'export-pdf',
1546
+ label: 'Export PDF',
1547
+ variant: 'primary',
1548
+ onClick: onPlainExport,
1549
+ },
1550
+ ]}
1551
+ visionTypesetting={{
1552
+ enabled: true,
1553
+ onToggle: vi.fn(),
1554
+ onExport: onRun,
1555
+ }}
1556
+ />,
1557
+ )
1558
+
1559
+ // Merged view: Run and Export sit side by side; Export stays a plain
1560
+ // export and Run starts the loop.
1561
+ click(buttonByText(container, 'Export PDF'))
1562
+ expect(onPlainExport).toHaveBeenCalledOnce()
1563
+ expect(onRun).not.toHaveBeenCalled()
1564
+
1565
+ click(buttonByText(container, 'Run typesetting'))
1566
+ expect(onRun).toHaveBeenCalledOnce()
1567
+ })
1568
+
1569
+ it('swaps Run for Stop while the vision loop is running', () => {
1570
+ const onRun = vi.fn()
1571
+ const onStop = vi.fn()
1572
+ const { container } = render(
1573
+ <PrintDesignPanel
1574
+ variant="workspace"
1575
+ visionTypesetting={{
1576
+ enabled: true,
1577
+ onToggle: vi.fn(),
1578
+ onExport: onRun,
1579
+ onStop,
1580
+ progress: { state: 'running', label: 'Page 2 of 9 · inspecting' },
1581
+ }}
1582
+ />,
1583
+ )
1584
+ // While running, the Run slot becomes a Stop control — always enabled.
1585
+ expect(
1586
+ Array.from(container.querySelectorAll('button')).some(button =>
1587
+ button.textContent?.includes('Run typesetting'),
1588
+ ),
1589
+ ).toBe(false)
1590
+ const stop = buttonByText(container, 'Stop')
1591
+ expect(stop.hasAttribute('disabled')).toBe(false)
1592
+ click(stop)
1593
+ expect(onStop).toHaveBeenCalledOnce()
1594
+ expect(onRun).not.toHaveBeenCalled()
1595
+ })
1596
+
1597
+ it('shows open/fixed issue chips beside the latest page snapshot', () => {
1598
+ const { container } = render(
1599
+ <PrintDesignPanel
1600
+ variant="workspace"
1601
+ visionTypesetting={{
1602
+ enabled: true,
1603
+ onToggle: vi.fn(),
1604
+ snapshotUrl: 'file:///tmp/page-3.png',
1605
+ snapshotAlt: 'Page 3 of 9',
1606
+ snapshotIssues: [
1607
+ { id: 'stranded-heading', label: 'stranded heading', state: 'fixed' },
1608
+ { id: 'sparse-sample-page', label: 'sparse sample page', state: 'open' },
1609
+ ],
1610
+ }}
1611
+ />,
1612
+ )
1613
+ const list = container.querySelector('[aria-label="Latest page issues"]')
1614
+ expect(list).not.toBeNull()
1615
+ expect(list?.textContent).toContain('fixed')
1616
+ expect(list?.textContent).toContain('stranded heading')
1617
+ expect(list?.textContent).toContain('issue')
1618
+ expect(list?.textContent).toContain('sparse sample page')
1619
+ })
1620
+
1621
+ it('accumulates page snapshots as a scrollable strip with per-card chips', () => {
1622
+ const { container } = render(
1623
+ <PrintDesignPanel
1624
+ variant="workspace"
1625
+ visionTypesetting={{
1626
+ enabled: true,
1627
+ onToggle: vi.fn(),
1628
+ snapshotHistory: [
1629
+ {
1630
+ id: 'p1-check',
1631
+ url: 'file:///tmp/page-1.png',
1632
+ label: 'Page 1',
1633
+ issues: [],
1634
+ },
1635
+ {
1636
+ id: 'p2-check',
1637
+ url: 'file:///tmp/page-2.png',
1638
+ label: 'Page 2',
1639
+ issues: [
1640
+ { id: 'stranded-heading', label: 'stranded heading', state: 'open' },
1641
+ ],
1642
+ },
1643
+ {
1644
+ id: 'p2-fix1',
1645
+ url: 'file:///tmp/page-2-fix.png',
1646
+ label: 'Page 2 · fix 1',
1647
+ issues: [
1648
+ { id: 'stranded-heading', label: 'stranded heading', state: 'fixed' },
1649
+ ],
1650
+ },
1651
+ ],
1652
+ }}
1653
+ />,
1654
+ )
1655
+ const strip = container.querySelector('[aria-label="Page snapshot history"]')
1656
+ expect(strip).not.toBeNull()
1657
+ const images = Array.from(strip?.querySelectorAll('img') ?? [])
1658
+ expect(images.map(img => img.getAttribute('src'))).toEqual([
1659
+ 'file:///tmp/page-1.png',
1660
+ 'file:///tmp/page-2.png',
1661
+ 'file:///tmp/page-2-fix.png',
1662
+ ])
1663
+ // Clean page → pass chip; the fix card flips its chip to fixed.
1664
+ expect(strip?.textContent).toContain('no layout issues')
1665
+ expect(strip?.textContent).toContain('Page 2 · fix 1')
1666
+ expect(strip?.textContent).toContain('fixed')
1667
+ })
1668
+
1669
+ it('zooms a page snapshot in-window instead of opening a viewer', () => {
1670
+ const onOpen = vi.fn()
1671
+ const { container } = render(
1672
+ <PrintDesignPanel
1673
+ variant="workspace"
1674
+ visionTypesetting={{
1675
+ enabled: true,
1676
+ onToggle: vi.fn(),
1677
+ snapshotHistory: [
1678
+ {
1679
+ id: 'p1-check',
1680
+ url: 'file:///tmp/page-1.png',
1681
+ label: 'Page 1',
1682
+ issues: [],
1683
+ onOpen,
1684
+ },
1685
+ ],
1686
+ }}
1687
+ />,
1688
+ )
1689
+ const card = container.querySelector<HTMLButtonElement>(
1690
+ 'button[aria-label="Page 1"]',
1691
+ )
1692
+ expect(card).not.toBeNull()
1693
+ if (!card) throw new Error('page card missing')
1694
+ click(card)
1695
+
1696
+ const zoom = container.querySelector('[aria-label="Page snapshot zoom"]')
1697
+ expect(zoom).not.toBeNull()
1698
+ expect(zoom?.querySelector('img')?.getAttribute('src')).toBe(
1699
+ 'file:///tmp/page-1.png',
1700
+ )
1701
+ // No external viewer involved.
1702
+ expect(onOpen).not.toHaveBeenCalled()
1703
+
1704
+ act(() => {
1705
+ zoom?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
1706
+ })
1707
+ expect(
1708
+ container.querySelector('[aria-label="Page snapshot zoom"]'),
1709
+ ).toBeNull()
1710
+ })
1711
+
1712
+ it('applies flourish controls to the theme', () => {
1713
+ const onThemeChange = vi.fn()
1714
+ const { container } = render(
1715
+ <PrintDesignPanel variant="workspace" onThemeChange={onThemeChange} />,
1716
+ )
1717
+ const headingRule = Array.from(
1718
+ container.querySelectorAll<HTMLSelectElement>('select'),
1719
+ ).find(select =>
1720
+ Array.from(select.options).some(
1721
+ option => option.value === 'underline',
1722
+ ),
1723
+ )
1724
+ expect(headingRule).not.toBeNull()
1725
+ if (!headingRule) throw new Error('heading rule select missing')
1726
+ changeInput(headingRule, 'underline')
1727
+ const theme = onThemeChange.mock.calls.at(-1)?.[0]
1728
+ expect(theme?.setting?.headingRule).toBe('underline')
1729
+ })
1730
+
1731
+ it('flips to live preview and marks run pages stale on style edits', () => {
1732
+ const { container } = render(
1733
+ <PrintDesignPanel
1734
+ variant="workspace"
1735
+ onThemeChange={vi.fn()}
1736
+ visionTypesetting={{
1737
+ enabled: true,
1738
+ onToggle: vi.fn(),
1739
+ snapshotHistory: [
1740
+ {
1741
+ id: 'p1-check',
1742
+ url: 'file:///tmp/page-1.png',
1743
+ label: 'Page 1',
1744
+ issues: [],
1745
+ },
1746
+ ],
1747
+ }}
1748
+ />,
1749
+ )
1750
+ // With run output present, the pages view shows by default.
1751
+ expect(
1752
+ container.querySelector('[aria-label="Page snapshot history"]'),
1753
+ ).not.toBeNull()
1754
+
1755
+ // Change a style control → live preview returns, pages marked stale.
1756
+ const headingRule = Array.from(
1757
+ container.querySelectorAll<HTMLSelectElement>('select'),
1758
+ ).find(select =>
1759
+ Array.from(select.options).some(option => option.value === 'underline'),
1760
+ )
1761
+ if (!headingRule) throw new Error('heading rule select missing')
1762
+ changeInput(headingRule, 'underline')
1763
+
1764
+ expect(
1765
+ container.querySelector('[aria-label="Page snapshot history"]'),
1766
+ ).toBeNull()
1767
+ expect(container.textContent).toContain('Run pages · stale')
1768
+ expect(container.textContent).toContain('The Shape of a Page')
1769
+
1770
+ // The user can flip back to the (stale) pages manually.
1771
+ click(buttonByText(container, 'Run pages · stale'))
1772
+ expect(
1773
+ container.querySelector('[aria-label="Page snapshot history"]'),
1774
+ ).not.toBeNull()
1775
+ })
1776
+
1777
+ it('shows the real Paged.js live preview in the well when supplied', () => {
1778
+ const { container } = render(
1779
+ <PrintDesignPanel
1780
+ variant="workspace"
1781
+ livePreview={{
1782
+ html: '<html><body><div class="pagedjs_page">Page one</div></body></html>',
1783
+ pageCount: 3,
1784
+ }}
1785
+ />,
1786
+ )
1787
+ const frame = container.querySelector<HTMLIFrameElement>(
1788
+ 'iframe[title="Live paged preview"]',
1789
+ )
1790
+ expect(frame).not.toBeNull()
1791
+ expect(frame?.getAttribute('srcdoc')).toContain('pagedjs_page')
1792
+ // The grey well + paper shadow ride along inside the preview document.
1793
+ expect(frame?.getAttribute('srcdoc')).toContain('data-live-preview-chrome')
1794
+ expect(container.textContent).toContain('Live preview · 3 page(s)')
1795
+ // The facsimile (and its Sample toggle) yields to the real preview.
1796
+ expect(container.textContent).not.toContain('The Shape of a Page')
1797
+ })
1798
+
1799
+ it('selects presets from a dropdown in the merged workspace view', () => {
1800
+ const onThemeChange = vi.fn()
1801
+ const { container } = render(
1802
+ <PrintDesignPanel variant="workspace" onThemeChange={onThemeChange} />,
1803
+ )
1804
+ const presetSelect = container.querySelector<HTMLSelectElement>(
1805
+ 'select[aria-label="Print theme presets"]',
1806
+ )
1807
+ expect(presetSelect).not.toBeNull()
1808
+ if (!presetSelect) throw new Error('preset select missing')
1809
+ const journal = Array.from(presetSelect.options).find(option =>
1810
+ option.textContent?.includes('Journal Article'),
1811
+ )
1812
+ expect(journal).toBeDefined()
1813
+ changeInput(presetSelect, journal!.value)
1814
+ expect(onThemeChange).toHaveBeenCalled()
1815
+ })
1816
+
1817
+ it('lets the host set max fixes per issue in the vision tab', () => {
1818
+ const onMaxFixesPerIssueChange = vi.fn()
1819
+ const { container } = render(
1820
+ <PrintDesignPanel
1821
+ variant="workspace"
1822
+ visionTypesetting={{
1823
+ enabled: true,
1824
+ onToggle: vi.fn(),
1825
+ maxFixesPerIssue: 3,
1826
+ onMaxFixesPerIssueChange,
1827
+ }}
1828
+ />,
1829
+ )
1830
+ const input = container.querySelector<HTMLInputElement>(
1831
+ 'input[type="number"]',
1832
+ )
1833
+ expect(input?.value).toBe('3')
1834
+ if (!input) throw new Error('max fixes input missing')
1835
+ changeInput(input, '5')
1836
+ expect(onMaxFixesPerIssueChange).toHaveBeenCalledWith(5)
1837
+ })
1448
1838
  })