@puredesktop/puredesktop-ui-bridge 2.1.7 → 2.1.8
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/package.json +55 -1
- package/src/bridge/agentTypes.ts +21 -1
- package/src/bridge/collectionImagePaste.ts +19 -1
- package/src/bridge/context.mjs +32 -0
- package/src/bridge/documents.d.mts +76 -0
- package/src/bridge/documents.mjs +40 -0
- package/src/bridge/methods.d.mts +23 -0
- package/src/bridge/methods.mjs +23 -0
- package/src/bridge/pureRender/base.css +44 -0
- package/src/bridge/pureRender/document.ts +29 -2
- package/src/bridge/pureRender/index.ts +3 -0
- package/src/bridge/pureRender/normalize.ts +167 -0
- package/src/bridge/pureRender/sanitizeExportBody.test.ts +51 -0
- package/src/bridge/pureRender/sanitizeExportBody.ts +61 -0
- package/src/bridge/pureRender/styles.ts +125 -5
- package/src/bridge/pureRender/theme.test.ts +259 -0
- package/src/bridge/pureRender/theme.ts +19 -1
- package/src/bridge/pureRender/types.ts +33 -0
- package/src/bridge/pureRender/visualLayoutInspection.test.ts +26 -0
- package/src/bridge/pureRender/visualLayoutInspection.ts +75 -0
- package/src/bridge/react/useDocumentHotkeys.ts +41 -0
- package/src/bridge/react/useDocumentLifecycle.tsx +359 -0
- package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -0
- package/src/bridge/react/usePlatformAppSettings.tsx +104 -0
- package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -0
- package/src/bridge/react/usePlatformJsonStore.tsx +149 -0
- package/src/bridge/storage.d.mts +6 -6
- package/src/bridge/storage.test.ts +6 -6
- package/src/bridge/types.ts +7 -0
- package/src/components/agents/AgentDrawerPanel.tsx +2 -0
- package/src/components/agents/AgentMessageList.tsx +6 -1
- package/src/components/agents/AgentToolCallCard.tsx +20 -20
- package/src/components/agents/ContextPicker.tsx +239 -0
- package/src/components/agents/agentPanelStyles.ts +3 -2
- package/src/components/agents/agentTypes.ts +2 -0
- package/src/components/chrome/WorkspaceTabStrip.tsx +18 -1
- package/src/components/chrome/workspaceTabTypes.ts +2 -0
- package/src/components/common/containers/AppFrame.test.tsx +4 -4
- package/src/components/common/containers/AppFrame.tsx +10 -1
- package/src/components/common/containers/AppHeader.tsx +22 -0
- package/src/components/common/documents/DocumentHeaderActions.tsx +206 -0
- package/src/components/common/documents/DocumentSwitcher.tsx +1176 -0
- package/src/components/common/documents/index.ts +8 -0
- package/src/components/common/research/EvidenceDossier.tsx +1232 -150
- package/src/components/common/research/index.ts +2 -0
- package/src/components/print-design/PrintDesignPanel.test.tsx +430 -40
- package/src/components/print-design/PrintDesignPanel.tsx +1770 -207
- package/src/components/print-design/index.ts +2 -0
- package/src/components/print-design/previewContent.test.ts +32 -0
- package/src/components/print-design/previewContent.ts +31 -0
- package/src/context/buildContextDocument.test.ts +240 -0
- package/src/context/buildContextDocument.ts +309 -0
- package/src/context/contextAccess.ts +66 -0
- package/src/context/contextConfig.ts +72 -0
- package/src/context/contextDocument.test.ts +155 -0
- package/src/context/contextDocument.ts +300 -0
- package/src/context/contextSections.test.ts +88 -0
- package/src/context/contextSections.ts +84 -0
- package/src/context/htmlToContextMarkdown.test.ts +134 -0
- package/src/context/htmlToContextMarkdown.ts +369 -0
- package/src/theme/appAccents.test.ts +36 -35
- package/src/theme/appAccents.ts +87 -104
- package/src/theme/appIdentityCss.mjs +169 -231
- package/src/theme/appIdentityCss.ts +213 -233
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Check,
|
|
3
|
+
Download,
|
|
4
|
+
FileImage,
|
|
5
|
+
Play,
|
|
6
|
+
Square,
|
|
7
|
+
WandSparkles,
|
|
8
|
+
X,
|
|
9
|
+
} from 'lucide-react'
|
|
2
10
|
import type { CSSProperties, ReactNode } from 'react'
|
|
3
11
|
import { useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
4
12
|
import { styled } from 'styled-components'
|
|
@@ -65,6 +73,14 @@ export interface PrintDesignVisionTypesetting {
|
|
|
65
73
|
snapshotUrl?: string
|
|
66
74
|
snapshotAlt?: string
|
|
67
75
|
onSnapshotOpen?: () => void
|
|
76
|
+
/** Issue chips beside the latest page snapshot, flipped to fixed as patches land. */
|
|
77
|
+
snapshotIssues?: readonly PrintDesignVisionSnapshotIssue[]
|
|
78
|
+
/**
|
|
79
|
+
* Accumulating per-render snapshot strip: every checked page and fix
|
|
80
|
+
* attempt appends a card (image + issue chips) instead of replacing the
|
|
81
|
+
* previous one, so a run reads as scrollable progress.
|
|
82
|
+
*/
|
|
83
|
+
snapshotHistory?: readonly PrintDesignVisionSnapshotHistoryEntry[]
|
|
68
84
|
maxFixesPerIssue?: number
|
|
69
85
|
onMaxFixesPerIssueChange?: (value: number) => void
|
|
70
86
|
exportLabel?: string
|
|
@@ -72,6 +88,34 @@ export interface PrintDesignVisionTypesetting {
|
|
|
72
88
|
exportDisabled?: boolean
|
|
73
89
|
onToggle?: (enabled: boolean) => void | Promise<void>
|
|
74
90
|
onExport?: () => void
|
|
91
|
+
/** Stop a running typesetting loop; shown while progress.state is running. */
|
|
92
|
+
onStop?: () => void
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface PrintDesignLivePreview {
|
|
96
|
+
/** Post-layout Paged.js document HTML (no scripts run on display). */
|
|
97
|
+
html?: string
|
|
98
|
+
pageCount?: number
|
|
99
|
+
loading?: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface PrintDesignVisionSnapshotIssue {
|
|
103
|
+
id: string
|
|
104
|
+
label: string
|
|
105
|
+
state: 'open' | 'fixed'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface PrintDesignVisionSnapshotHistoryEntry {
|
|
109
|
+
id: string
|
|
110
|
+
url: string
|
|
111
|
+
/** e.g. "Page 2" or "Page 2 · fix 1" */
|
|
112
|
+
label: string
|
|
113
|
+
alt?: string
|
|
114
|
+
/** Empty/omitted issues render a single pass chip. */
|
|
115
|
+
issues?: readonly PrintDesignVisionSnapshotIssue[]
|
|
116
|
+
/** Inspector comment shown with the card (recommendation, fix summary). */
|
|
117
|
+
note?: string
|
|
118
|
+
onOpen?: () => void
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
export interface PrintDesignVisionProgress {
|
|
@@ -118,6 +162,13 @@ export interface PrintDesignVisionSnapshot {
|
|
|
118
162
|
onOpen?: () => void
|
|
119
163
|
}
|
|
120
164
|
|
|
165
|
+
/** Host-document excerpt shown in the live preview instead of sample copy. */
|
|
166
|
+
export interface PrintDesignPreviewDocument {
|
|
167
|
+
title?: string
|
|
168
|
+
byline?: string
|
|
169
|
+
paragraphs?: readonly string[]
|
|
170
|
+
}
|
|
171
|
+
|
|
121
172
|
export interface PrintDesignPanelProps {
|
|
122
173
|
presets?: readonly PrintDesignThemePreset[]
|
|
123
174
|
value?: PureRenderTheme
|
|
@@ -125,6 +176,17 @@ export interface PrintDesignPanelProps {
|
|
|
125
176
|
density?: PrintDesignPanelDensity
|
|
126
177
|
headerAccessory?: ReactNode
|
|
127
178
|
modes?: readonly PrintDesignMode[]
|
|
179
|
+
/**
|
|
180
|
+
* The document being exported, for the live preview. When present, the
|
|
181
|
+
* preview typesets THIS content (a "Sample" checkbox switches back to the
|
|
182
|
+
* built-in copy); when absent, the sample is used.
|
|
183
|
+
*/
|
|
184
|
+
previewDocument?: PrintDesignPreviewDocument
|
|
185
|
+
/**
|
|
186
|
+
* Real Paged.js live preview (post-layout HTML, all pages). When present
|
|
187
|
+
* it replaces the styled facsimile in the workspace well.
|
|
188
|
+
*/
|
|
189
|
+
livePreview?: PrintDesignLivePreview
|
|
128
190
|
actions?: readonly PrintDesignPanelAction[]
|
|
129
191
|
extractionCapability?: PrintDesignExtractionCapability
|
|
130
192
|
visionTypesetting?: PrintDesignVisionTypesetting
|
|
@@ -152,6 +214,12 @@ export interface PrintDesignPopoverProps {
|
|
|
152
214
|
const DEFAULT_PRESETS: readonly PrintDesignThemePreset[] =
|
|
153
215
|
PURE_RENDER_THEME_PRESETS
|
|
154
216
|
|
|
217
|
+
const SAMPLE_PREVIEW_PARAGRAPHS: readonly string[] = [
|
|
218
|
+
'Typography is the craft of endowing human language with a durable visual form. A page earns trust quietly: a steady measure, an even rhythm, ink that sits comfortably on paper.',
|
|
219
|
+
'The reader should notice none of it. Margins hold the text the way hands hold a book, and the leading you chose above sets the pace at which the eye walks each line — no faster, no slower.',
|
|
220
|
+
'Change a control and this page follows: display and body faces, the accent that marks structure, the alignment of this very paragraph.',
|
|
221
|
+
]
|
|
222
|
+
|
|
155
223
|
const DEFAULT_VISION_SAMPLE_REPORT = [
|
|
156
224
|
['sample', 'No export inspection has run yet.'],
|
|
157
225
|
[
|
|
@@ -210,11 +278,32 @@ function snapshotDisplayLabel(
|
|
|
210
278
|
const Panel = styled.section<{
|
|
211
279
|
$variant: PrintDesignPanelVariant
|
|
212
280
|
$density: PrintDesignPanelDensity
|
|
281
|
+
$mode: PrintDesignMode
|
|
213
282
|
}>`
|
|
214
283
|
--print-design-page-surface: var(--platform-colors-elevated, #fff);
|
|
215
284
|
--print-design-panel-surface: var(--platform-colors-surface, #ffffff);
|
|
216
285
|
--print-design-preview-paper: #fff;
|
|
217
286
|
--platform-panel-tab-surface: var(--print-design-page-surface);
|
|
287
|
+
/* Shared export component: identity is INHERITED from the host app via
|
|
288
|
+
--app-acc / --app-block (Writer citron, Manuscript olive, …). Never a
|
|
289
|
+
baked-in colour. --app-block is the deep desaturated variant for the one
|
|
290
|
+
large fill (the Export primary) so white text stays legible on
|
|
291
|
+
light-base hosts. */
|
|
292
|
+
--print-design-hue: var(--app-acc, var(--platform-colors-accent));
|
|
293
|
+
--print-design-hue-block: var(
|
|
294
|
+
--app-block,
|
|
295
|
+
var(--app-acc, var(--platform-colors-accent))
|
|
296
|
+
);
|
|
297
|
+
--print-design-mono: var(
|
|
298
|
+
--platform-typography-font-family-mono,
|
|
299
|
+
'JetBrains Mono',
|
|
300
|
+
ui-monospace,
|
|
301
|
+
monospace
|
|
302
|
+
);
|
|
303
|
+
--print-design-faint: var(
|
|
304
|
+
--ink-faint,
|
|
305
|
+
var(--platform-colors-text-secondary)
|
|
306
|
+
);
|
|
218
307
|
|
|
219
308
|
display: grid;
|
|
220
309
|
gap: ${({ $variant }) =>
|
|
@@ -230,7 +319,9 @@ const Panel = styled.section<{
|
|
|
230
319
|
$variant === 'workspace'
|
|
231
320
|
? `
|
|
232
321
|
flex: 1 1 auto;
|
|
233
|
-
|
|
322
|
+
/* Merged Typesetter: DesignMain (controls | well) plus a slim right
|
|
323
|
+
rail carrying the run summary/Activity ledger. */
|
|
324
|
+
grid-template-columns: minmax(0, 1fr) minmax(280px, 320px);
|
|
234
325
|
grid-template-rows: auto minmax(0, 1fr);
|
|
235
326
|
align-content: stretch;
|
|
236
327
|
align-items: stretch;
|
|
@@ -398,6 +489,8 @@ const WorkspaceSummaryText = styled.span`
|
|
|
398
489
|
max-width: 260px;
|
|
399
490
|
overflow: hidden;
|
|
400
491
|
text-overflow: ellipsis;
|
|
492
|
+
color: var(--print-design-faint);
|
|
493
|
+
font-family: var(--print-design-mono);
|
|
401
494
|
`
|
|
402
495
|
|
|
403
496
|
const WorkspaceSummarySwatches = styled.span`
|
|
@@ -424,11 +517,36 @@ const HeaderActionButton = styled(Button)`
|
|
|
424
517
|
white-space: nowrap;
|
|
425
518
|
`
|
|
426
519
|
|
|
520
|
+
/**
|
|
521
|
+
* The one identity-carrying primary in this component. Uses the host's
|
|
522
|
+
* --app-block (deep desaturated hue, white text) so it reads correctly on
|
|
523
|
+
* light-base hosts like Writer/Manuscript — never a hard-coded colour.
|
|
524
|
+
*/
|
|
525
|
+
const ExportPrimaryButton = styled(HeaderActionButton)`
|
|
526
|
+
&& {
|
|
527
|
+
border-color: var(--print-design-hue-block);
|
|
528
|
+
background: var(--print-design-hue-block);
|
|
529
|
+
color: #fff;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
&&:hover:not(:disabled) {
|
|
533
|
+
border-color: var(--print-design-hue-block);
|
|
534
|
+
background: color-mix(
|
|
535
|
+
in oklch,
|
|
536
|
+
var(--print-design-hue-block) 92%,
|
|
537
|
+
white 8%
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
`
|
|
541
|
+
|
|
427
542
|
const SectionTitle = styled.div`
|
|
428
|
-
color: var(--
|
|
429
|
-
font-
|
|
543
|
+
color: var(--print-design-faint);
|
|
544
|
+
font-family: var(--print-design-mono);
|
|
545
|
+
font-size: 10px;
|
|
430
546
|
font-weight: var(--platform-typography-font-weight-semibold);
|
|
547
|
+
letter-spacing: 0.08em;
|
|
431
548
|
line-height: 1.2;
|
|
549
|
+
text-transform: uppercase;
|
|
432
550
|
`
|
|
433
551
|
|
|
434
552
|
const DesignSection = styled.section<{ $variant: PrintDesignPanelVariant }>`
|
|
@@ -481,18 +599,15 @@ const DesignMain = styled.div<{
|
|
|
481
599
|
gap: var(--platform-spacing-sm);
|
|
482
600
|
background: var(--print-design-page-surface);
|
|
483
601
|
|
|
484
|
-
${({ $variant
|
|
602
|
+
${({ $variant }) =>
|
|
485
603
|
$variant === 'workspace'
|
|
486
604
|
? `
|
|
605
|
+
/* Merged view: slim controls column, the page well takes the rest. */
|
|
487
606
|
grid-column: 1 / 2;
|
|
488
607
|
grid-row: 2 / 3;
|
|
489
|
-
grid-template-columns:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
grid-template-rows: ${
|
|
493
|
-
$mode === 'vision' ? 'minmax(0, 1fr)' : 'minmax(0, 1fr) auto'
|
|
494
|
-
};
|
|
495
|
-
gap: ${$mode === 'vision' ? '0' : 'var(--platform-spacing-md)'};
|
|
608
|
+
grid-template-columns: minmax(320px, 390px) minmax(0, 1fr);
|
|
609
|
+
grid-template-rows: minmax(0, 1fr) auto;
|
|
610
|
+
gap: var(--platform-spacing-md);
|
|
496
611
|
min-height: 0;
|
|
497
612
|
overflow: hidden;
|
|
498
613
|
`
|
|
@@ -513,33 +628,18 @@ const WorkspaceControls = styled.div<{
|
|
|
513
628
|
? `
|
|
514
629
|
display: grid;
|
|
515
630
|
align-content: start;
|
|
516
|
-
gap: var(--platform-spacing-
|
|
631
|
+
gap: var(--platform-spacing-lg);
|
|
517
632
|
min-width: 0;
|
|
518
633
|
min-height: 0;
|
|
519
634
|
overflow: auto;
|
|
635
|
+
grid-column: 1 / 2;
|
|
636
|
+
grid-row: 1 / 2;
|
|
520
637
|
padding-right: var(--platform-spacing-md);
|
|
521
638
|
border-right: 1px solid var(--platform-colors-divider);
|
|
522
639
|
`
|
|
523
640
|
: `
|
|
524
641
|
display: contents;
|
|
525
642
|
`}
|
|
526
|
-
|
|
527
|
-
${({ $variant, $mode }) =>
|
|
528
|
-
$variant === 'workspace' && $mode !== 'vision'
|
|
529
|
-
? `
|
|
530
|
-
grid-column: 1 / 2;
|
|
531
|
-
grid-row: 1 / 2;
|
|
532
|
-
padding-right: var(--platform-spacing-md);
|
|
533
|
-
border-right: 0;
|
|
534
|
-
`
|
|
535
|
-
: ''}
|
|
536
|
-
|
|
537
|
-
${({ $variant, $mode }) =>
|
|
538
|
-
$variant === 'workspace' && $mode === 'vision'
|
|
539
|
-
? `
|
|
540
|
-
display: none;
|
|
541
|
-
`
|
|
542
|
-
: ''}
|
|
543
643
|
`
|
|
544
644
|
|
|
545
645
|
const WorkspaceFooter = styled.div<{
|
|
@@ -553,29 +653,14 @@ const WorkspaceFooter = styled.div<{
|
|
|
553
653
|
align-self: end;
|
|
554
654
|
gap: var(--platform-spacing-sm);
|
|
555
655
|
min-width: 0;
|
|
656
|
+
grid-column: 1 / 2;
|
|
657
|
+
grid-row: 2 / 3;
|
|
556
658
|
padding-right: var(--platform-spacing-md);
|
|
557
659
|
border-right: 1px solid var(--platform-colors-divider);
|
|
558
660
|
`
|
|
559
661
|
: `
|
|
560
662
|
display: contents;
|
|
561
663
|
`}
|
|
562
|
-
|
|
563
|
-
${({ $variant, $mode }) =>
|
|
564
|
-
$variant === 'workspace' && $mode === 'vision'
|
|
565
|
-
? `
|
|
566
|
-
display: none;
|
|
567
|
-
`
|
|
568
|
-
: ''}
|
|
569
|
-
|
|
570
|
-
${({ $variant, $mode }) =>
|
|
571
|
-
$variant === 'workspace' && $mode !== 'vision'
|
|
572
|
-
? `
|
|
573
|
-
grid-column: 1 / 2;
|
|
574
|
-
grid-row: 2 / 3;
|
|
575
|
-
padding-right: var(--platform-spacing-md);
|
|
576
|
-
border-right: 0;
|
|
577
|
-
`
|
|
578
|
-
: ''}
|
|
579
664
|
`
|
|
580
665
|
|
|
581
666
|
const WorkspaceCanvas = styled.div<{
|
|
@@ -586,7 +671,8 @@ const WorkspaceCanvas = styled.div<{
|
|
|
586
671
|
$variant === 'workspace'
|
|
587
672
|
? `
|
|
588
673
|
display: grid;
|
|
589
|
-
|
|
674
|
+
grid-template-rows: minmax(0, 1fr);
|
|
675
|
+
align-content: stretch;
|
|
590
676
|
gap: var(--platform-spacing-md);
|
|
591
677
|
min-width: 0;
|
|
592
678
|
min-height: 0;
|
|
@@ -598,22 +684,6 @@ const WorkspaceCanvas = styled.div<{
|
|
|
598
684
|
: `
|
|
599
685
|
display: contents;
|
|
600
686
|
`}
|
|
601
|
-
|
|
602
|
-
${({ $variant, $mode }) =>
|
|
603
|
-
$variant === 'workspace' && $mode !== 'vision'
|
|
604
|
-
? `
|
|
605
|
-
display: none;
|
|
606
|
-
`
|
|
607
|
-
: ''}
|
|
608
|
-
|
|
609
|
-
${({ $variant, $mode }) =>
|
|
610
|
-
$variant === 'workspace' && $mode === 'vision'
|
|
611
|
-
? `
|
|
612
|
-
grid-column: 1 / 2;
|
|
613
|
-
grid-row: 1 / 2;
|
|
614
|
-
padding-right: var(--platform-spacing-md);
|
|
615
|
-
`
|
|
616
|
-
: ''}
|
|
617
687
|
`
|
|
618
688
|
|
|
619
689
|
const DesignReadout = styled.aside<{ $variant: PrintDesignPanelVariant }>`
|
|
@@ -650,6 +720,157 @@ const DesignReadout = styled.aside<{ $variant: PrintDesignPanelVariant }>`
|
|
|
650
720
|
: ''}
|
|
651
721
|
`
|
|
652
722
|
|
|
723
|
+
/**
|
|
724
|
+
* Live typeset preview: a miniature page rendered from the ACTIVE theme —
|
|
725
|
+
* fonts, ink/paper/accent, lead, alignment — updating with every control.
|
|
726
|
+
* The stage sits on --well so the page floats like paper.
|
|
727
|
+
*/
|
|
728
|
+
const PreviewStage = styled.div`
|
|
729
|
+
container-type: inline-size;
|
|
730
|
+
display: grid;
|
|
731
|
+
align-content: start;
|
|
732
|
+
justify-items: center;
|
|
733
|
+
gap: var(--platform-spacing-sm);
|
|
734
|
+
width: 100%;
|
|
735
|
+
min-height: 0;
|
|
736
|
+
padding: var(--platform-spacing-md);
|
|
737
|
+
border-radius: var(--platform-radius-md);
|
|
738
|
+
background: var(--well, var(--platform-colors-surface-active, #eceef1));
|
|
739
|
+
overflow: auto;
|
|
740
|
+
`
|
|
741
|
+
|
|
742
|
+
const PreviewPage = styled.div<{
|
|
743
|
+
$paper: string
|
|
744
|
+
$ink: string
|
|
745
|
+
$bodyFont: string
|
|
746
|
+
$lead: number
|
|
747
|
+
$justified: boolean
|
|
748
|
+
}>`
|
|
749
|
+
/* Fill the preview column (the whole point of the pane) and size type
|
|
750
|
+
with the container so the page reads like a page at any width. NOTE:
|
|
751
|
+
padding is in cqw, not % — percentage padding resolves against the
|
|
752
|
+
CONTAINING BLOCK's width, which blew the margins up to stage-width
|
|
753
|
+
proportions. */
|
|
754
|
+
width: min(92cqw, 720px);
|
|
755
|
+
aspect-ratio: 1 / 1.414;
|
|
756
|
+
padding: 6.5cqw 6cqw;
|
|
757
|
+
background: ${({ $paper }) => $paper};
|
|
758
|
+
box-shadow:
|
|
759
|
+
0 1px 2px rgba(15, 18, 20, 0.14),
|
|
760
|
+
0 10px 28px rgba(15, 18, 20, 0.14);
|
|
761
|
+
color: ${({ $ink }) => $ink};
|
|
762
|
+
font-family: ${({ $bodyFont }) => $bodyFont};
|
|
763
|
+
font-size: clamp(10.5px, 1.7cqw, 16px);
|
|
764
|
+
line-height: ${({ $lead }) => $lead};
|
|
765
|
+
text-align: ${({ $justified }) => ($justified ? 'justify' : 'left')};
|
|
766
|
+
overflow: hidden;
|
|
767
|
+
user-select: none;
|
|
768
|
+
`
|
|
769
|
+
|
|
770
|
+
const PreviewKicker = styled.div<{ $accent: string; $labelFont: string }>`
|
|
771
|
+
margin-bottom: 0.9em;
|
|
772
|
+
color: ${({ $accent }) => $accent};
|
|
773
|
+
font-family: ${({ $labelFont }) => $labelFont};
|
|
774
|
+
font-size: 0.72em;
|
|
775
|
+
letter-spacing: 0.14em;
|
|
776
|
+
text-transform: uppercase;
|
|
777
|
+
`
|
|
778
|
+
|
|
779
|
+
const PreviewTitle = styled.div<{ $displayFont: string }>`
|
|
780
|
+
margin-bottom: 0.35em;
|
|
781
|
+
font-family: ${({ $displayFont }) => $displayFont};
|
|
782
|
+
font-size: 1.8em;
|
|
783
|
+
font-weight: 600;
|
|
784
|
+
line-height: 1.18;
|
|
785
|
+
text-align: left;
|
|
786
|
+
`
|
|
787
|
+
|
|
788
|
+
const PreviewByline = styled.div`
|
|
789
|
+
margin-bottom: 1.4em;
|
|
790
|
+
font-size: 0.82em;
|
|
791
|
+
opacity: 0.72;
|
|
792
|
+
text-align: left;
|
|
793
|
+
`
|
|
794
|
+
|
|
795
|
+
const PreviewRule = styled.div<{ $accent: string }>`
|
|
796
|
+
height: 1.5px;
|
|
797
|
+
margin-bottom: 1.4em;
|
|
798
|
+
background: ${({ $accent }) => $accent};
|
|
799
|
+
`
|
|
800
|
+
|
|
801
|
+
const PreviewParagraph = styled.p`
|
|
802
|
+
margin: 0 0 0.9em;
|
|
803
|
+
hyphens: auto;
|
|
804
|
+
`
|
|
805
|
+
|
|
806
|
+
const PreviewFolio = styled.div`
|
|
807
|
+
margin-top: 1.6em;
|
|
808
|
+
font-family: var(--print-design-mono);
|
|
809
|
+
font-size: 0.72em;
|
|
810
|
+
opacity: 0.55;
|
|
811
|
+
text-align: center;
|
|
812
|
+
`
|
|
813
|
+
|
|
814
|
+
/** Vision mode: the LIVE page snapshot from the Paged.js inspection pass. */
|
|
815
|
+
const PreviewSnapshotButton = styled.button`
|
|
816
|
+
display: block;
|
|
817
|
+
width: min(92cqw, 720px);
|
|
818
|
+
padding: 0;
|
|
819
|
+
border: 1px solid var(--platform-colors-border);
|
|
820
|
+
border-radius: 2px;
|
|
821
|
+
background: var(--print-design-preview-paper);
|
|
822
|
+
box-shadow:
|
|
823
|
+
0 1px 2px rgba(15, 18, 20, 0.14),
|
|
824
|
+
0 10px 28px rgba(15, 18, 20, 0.14);
|
|
825
|
+
cursor: pointer;
|
|
826
|
+
|
|
827
|
+
img {
|
|
828
|
+
display: block;
|
|
829
|
+
width: 100%;
|
|
830
|
+
height: auto;
|
|
831
|
+
}
|
|
832
|
+
`
|
|
833
|
+
|
|
834
|
+
const PreviewSampleToggle = styled.label`
|
|
835
|
+
display: inline-flex;
|
|
836
|
+
flex: 0 0 auto;
|
|
837
|
+
align-items: center;
|
|
838
|
+
gap: 5px;
|
|
839
|
+
color: var(--print-design-faint);
|
|
840
|
+
font-family: var(--print-design-mono);
|
|
841
|
+
font-size: 10px;
|
|
842
|
+
letter-spacing: 0.08em;
|
|
843
|
+
text-transform: uppercase;
|
|
844
|
+
cursor: pointer;
|
|
845
|
+
|
|
846
|
+
input {
|
|
847
|
+
margin: 0;
|
|
848
|
+
accent-color: var(--print-design-hue);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
&[data-disabled='true'] {
|
|
852
|
+
opacity: 0.5;
|
|
853
|
+
cursor: default;
|
|
854
|
+
}
|
|
855
|
+
`
|
|
856
|
+
|
|
857
|
+
const PreviewMeta = styled.div`
|
|
858
|
+
max-width: 100%;
|
|
859
|
+
overflow: hidden;
|
|
860
|
+
text-overflow: ellipsis;
|
|
861
|
+
white-space: nowrap;
|
|
862
|
+
color: var(--print-design-faint);
|
|
863
|
+
font-family: var(--print-design-mono);
|
|
864
|
+
font-size: var(--platform-typography-font-size-xs);
|
|
865
|
+
`
|
|
866
|
+
|
|
867
|
+
const ActivityScroll = styled.div<{ $tall?: boolean }>`
|
|
868
|
+
/* In vision mode the report IS the rail's main content — let it run tall. */
|
|
869
|
+
max-height: ${({ $tall }) => ($tall ? 'none' : '200px')};
|
|
870
|
+
min-height: 0;
|
|
871
|
+
overflow: auto;
|
|
872
|
+
`
|
|
873
|
+
|
|
653
874
|
const ModeTabs = styled(PanelTabStrip<PrintDesignMode>)`
|
|
654
875
|
grid-area: nav;
|
|
655
876
|
justify-self: start;
|
|
@@ -702,12 +923,14 @@ const PresetOption = styled.button<{
|
|
|
702
923
|
padding: ${({ $density }) =>
|
|
703
924
|
$density === 'compact' ? '3px 8px' : '5px 9px'};
|
|
704
925
|
border: 1px solid var(--platform-colors-border);
|
|
926
|
+
/* Selected preset carries the HOST hue (edge + underline), not the theme
|
|
927
|
+
accent — identity marks the choice, preset palettes stay content. */
|
|
705
928
|
border-color: ${({ $active }) =>
|
|
706
|
-
$active
|
|
707
|
-
? 'var(--platform-colors-accent, var(--platform-colors-border))'
|
|
708
|
-
: 'var(--platform-colors-border)'};
|
|
929
|
+
$active ? 'var(--print-design-hue)' : 'var(--platform-colors-border)'};
|
|
709
930
|
border-radius: var(--platform-radius-sm);
|
|
710
931
|
background: var(--print-design-panel-surface);
|
|
932
|
+
box-shadow: ${({ $active }) =>
|
|
933
|
+
$active ? 'inset 0 -2px 0 0 var(--print-design-hue)' : 'none'};
|
|
711
934
|
color: var(--platform-colors-text);
|
|
712
935
|
font: inherit;
|
|
713
936
|
font-size: var(--platform-typography-font-size-xs);
|
|
@@ -747,8 +970,9 @@ const ControlStack = styled.div`
|
|
|
747
970
|
|
|
748
971
|
const ReferenceGrid = styled.div<{ $variant: PrintDesignPanelVariant }>`
|
|
749
972
|
display: grid;
|
|
973
|
+
/* Workspace: the controls column is narrow — stack Image and Describe. */
|
|
750
974
|
grid-template-columns: ${({ $variant }) =>
|
|
751
|
-
$variant === 'popover'
|
|
975
|
+
$variant === 'popover'
|
|
752
976
|
? 'minmax(240px, 0.72fr) minmax(360px, 1.28fr)'
|
|
753
977
|
: 'minmax(0, 1fr)'};
|
|
754
978
|
gap: var(--platform-spacing-sm);
|
|
@@ -790,8 +1014,11 @@ const VisionPanel = styled.div<{ $variant: PrintDesignPanelVariant }>`
|
|
|
790
1014
|
$variant === 'workspace'
|
|
791
1015
|
? `
|
|
792
1016
|
grid-template-columns: minmax(0, 1fr);
|
|
793
|
-
|
|
1017
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
1018
|
+
align-content: stretch;
|
|
1019
|
+
align-items: stretch;
|
|
794
1020
|
gap: var(--platform-spacing-sm);
|
|
1021
|
+
min-height: 0;
|
|
795
1022
|
`
|
|
796
1023
|
: ''}
|
|
797
1024
|
`
|
|
@@ -812,11 +1039,11 @@ const VisionSettingsRail = styled.div<{ $variant: PrintDesignPanelVariant }>`
|
|
|
812
1039
|
`
|
|
813
1040
|
|
|
814
1041
|
const VisionEvidenceColumn = styled.div`
|
|
815
|
-
display:
|
|
1042
|
+
display: flex;
|
|
1043
|
+
flex-direction: column;
|
|
816
1044
|
gap: var(--platform-spacing-sm);
|
|
817
1045
|
min-width: 0;
|
|
818
1046
|
min-height: 0;
|
|
819
|
-
align-content: start;
|
|
820
1047
|
`
|
|
821
1048
|
|
|
822
1049
|
const VisionSettingsStrip = styled.div`
|
|
@@ -1226,16 +1453,17 @@ const ManualGrid = styled.div<{
|
|
|
1226
1453
|
$variant === 'popover'
|
|
1227
1454
|
? 'repeat(3, minmax(132px, 156px))'
|
|
1228
1455
|
: $variant === 'workspace'
|
|
1229
|
-
? 'repeat(
|
|
1456
|
+
? 'repeat(2, minmax(160px, 1fr))'
|
|
1230
1457
|
: $density === 'compact'
|
|
1231
1458
|
? 'repeat(2, minmax(0, 1fr))'
|
|
1232
1459
|
: 'repeat(3, minmax(0, 1fr))'};
|
|
1460
|
+
/* 8px rhythm between tune rows. */
|
|
1233
1461
|
gap: ${({ $density, $variant }) =>
|
|
1234
1462
|
$variant === 'popover'
|
|
1235
1463
|
? 'var(--platform-spacing-xs) var(--platform-spacing-sm)'
|
|
1236
1464
|
: $density === 'compact'
|
|
1237
1465
|
? 'var(--platform-spacing-xs)'
|
|
1238
|
-
: '
|
|
1466
|
+
: '8px 16px'};
|
|
1239
1467
|
justify-content: start;
|
|
1240
1468
|
max-width: ${({ $variant }) => ($variant === 'popover' ? '504px' : 'none')};
|
|
1241
1469
|
|
|
@@ -1254,10 +1482,13 @@ const ManualGrid = styled.div<{
|
|
|
1254
1482
|
const ManualField = styled.label<{ $span?: 'wide' }>`
|
|
1255
1483
|
display: grid;
|
|
1256
1484
|
min-width: 0;
|
|
1257
|
-
gap:
|
|
1258
|
-
color: var(--
|
|
1259
|
-
font-
|
|
1485
|
+
gap: 4px;
|
|
1486
|
+
color: var(--print-design-faint);
|
|
1487
|
+
font-family: var(--print-design-mono);
|
|
1488
|
+
font-size: 10px;
|
|
1489
|
+
letter-spacing: 0.08em;
|
|
1260
1490
|
line-height: 1.2;
|
|
1491
|
+
text-transform: uppercase;
|
|
1261
1492
|
|
|
1262
1493
|
${({ $span }) =>
|
|
1263
1494
|
$span === 'wide'
|
|
@@ -1271,7 +1502,7 @@ const ManualField = styled.label<{ $span?: 'wide' }>`
|
|
|
1271
1502
|
: ''}
|
|
1272
1503
|
`
|
|
1273
1504
|
|
|
1274
|
-
const ManualInput = styled.input
|
|
1505
|
+
const ManualInput = styled.input<{ $mono?: boolean }>`
|
|
1275
1506
|
width: 100%;
|
|
1276
1507
|
min-width: 0;
|
|
1277
1508
|
min-height: 28px;
|
|
@@ -1281,15 +1512,64 @@ const ManualInput = styled.input`
|
|
|
1281
1512
|
background: var(--print-design-page-surface);
|
|
1282
1513
|
color: var(--platform-colors-text);
|
|
1283
1514
|
font: inherit;
|
|
1515
|
+
font-family: ${({ $mono }) =>
|
|
1516
|
+
$mono ? 'var(--print-design-mono)' : 'inherit'};
|
|
1284
1517
|
font-size: var(--platform-typography-font-size-xs);
|
|
1518
|
+
letter-spacing: normal;
|
|
1285
1519
|
line-height: 1.2;
|
|
1520
|
+
text-transform: none;
|
|
1521
|
+
`
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Ink / Paper / Accent as SWATCH CHIPS + hex, not full-width colour slabs
|
|
1525
|
+
* (the fill-large mistake — a giant Accent bar read as identity; these are
|
|
1526
|
+
* the DOCUMENT's colours and stay user-set, just presented small). The chip
|
|
1527
|
+
* is bordered so a white Paper value still reads as a control, and the
|
|
1528
|
+
* invisible native input keeps the OS picker.
|
|
1529
|
+
*/
|
|
1530
|
+
const ColorChipsRow = styled.div`
|
|
1531
|
+
grid-column: 1 / -1;
|
|
1532
|
+
display: flex;
|
|
1533
|
+
flex-wrap: wrap;
|
|
1534
|
+
gap: 8px 16px;
|
|
1535
|
+
`
|
|
1536
|
+
|
|
1537
|
+
const ColorChipSwatch = styled.span<{ $color: string }>`
|
|
1538
|
+
position: relative;
|
|
1539
|
+
display: inline-block;
|
|
1540
|
+
flex: 0 0 auto;
|
|
1541
|
+
width: 28px;
|
|
1542
|
+
height: 28px;
|
|
1543
|
+
border: 1px solid var(--platform-colors-border);
|
|
1544
|
+
border-radius: var(--platform-radius-sm);
|
|
1545
|
+
background: ${({ $color }) => $color};
|
|
1546
|
+
cursor: pointer;
|
|
1286
1547
|
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1548
|
+
input {
|
|
1549
|
+
position: absolute;
|
|
1550
|
+
inset: 0;
|
|
1551
|
+
width: 100%;
|
|
1552
|
+
height: 100%;
|
|
1553
|
+
opacity: 0;
|
|
1554
|
+
cursor: pointer;
|
|
1290
1555
|
}
|
|
1291
1556
|
`
|
|
1292
1557
|
|
|
1558
|
+
const ColorChipValue = styled.span`
|
|
1559
|
+
align-self: center;
|
|
1560
|
+
color: var(--platform-colors-text);
|
|
1561
|
+
font-family: var(--print-design-mono);
|
|
1562
|
+
font-size: var(--platform-typography-font-size-xs);
|
|
1563
|
+
letter-spacing: normal;
|
|
1564
|
+
text-transform: lowercase;
|
|
1565
|
+
`
|
|
1566
|
+
|
|
1567
|
+
const ColorChipControl = styled.span`
|
|
1568
|
+
display: inline-flex;
|
|
1569
|
+
align-items: center;
|
|
1570
|
+
gap: 8px;
|
|
1571
|
+
`
|
|
1572
|
+
|
|
1293
1573
|
const ManualSelect = styled.select`
|
|
1294
1574
|
width: 100%;
|
|
1295
1575
|
min-width: 0;
|
|
@@ -1345,8 +1625,9 @@ const StatusText = styled.span`
|
|
|
1345
1625
|
overflow: hidden;
|
|
1346
1626
|
text-overflow: ellipsis;
|
|
1347
1627
|
white-space: nowrap;
|
|
1348
|
-
color: var(--
|
|
1349
|
-
font-
|
|
1628
|
+
color: var(--print-design-faint);
|
|
1629
|
+
font-family: var(--print-design-mono);
|
|
1630
|
+
font-size: var(--platform-typography-font-size-xs);
|
|
1350
1631
|
line-height: var(--platform-typography-line-height-base);
|
|
1351
1632
|
`
|
|
1352
1633
|
|
|
@@ -1418,53 +1699,225 @@ const DecisionType = styled.span<{ $type: string }>`
|
|
|
1418
1699
|
white-space: nowrap;
|
|
1419
1700
|
`
|
|
1420
1701
|
|
|
1421
|
-
const
|
|
1702
|
+
const VisionPagesScroll = styled.div`
|
|
1703
|
+
/* The grey well: same treatment as the app page surfaces — pages sit
|
|
1704
|
+
forward on a sunken ground so each snapshot reads as a PAGE. */
|
|
1705
|
+
display: flex;
|
|
1706
|
+
flex-direction: column;
|
|
1707
|
+
gap: var(--platform-spacing-xl, 32px);
|
|
1708
|
+
/* Fill the canvas column — no dead white band under the well. */
|
|
1709
|
+
flex: 1 1 auto;
|
|
1710
|
+
min-height: 0;
|
|
1711
|
+
overflow-y: auto;
|
|
1712
|
+
scroll-behavior: smooth;
|
|
1713
|
+
padding: var(--platform-spacing-lg, 24px);
|
|
1714
|
+
background: var(--well, var(--platform-colors-background, #ececec));
|
|
1715
|
+
border: 1px solid var(--platform-colors-border);
|
|
1716
|
+
border-radius: var(--platform-radius-sm);
|
|
1717
|
+
`
|
|
1718
|
+
|
|
1719
|
+
const VisionPageCard = styled.div`
|
|
1720
|
+
/* Page + its annotation rail travel as one centered unit — the notes sit
|
|
1721
|
+
right beside the page, not stranded at the far edge of the well. */
|
|
1722
|
+
display: flex;
|
|
1723
|
+
justify-content: center;
|
|
1724
|
+
align-items: flex-start;
|
|
1725
|
+
gap: var(--platform-spacing-md);
|
|
1422
1726
|
min-width: 0;
|
|
1423
|
-
|
|
1424
|
-
|
|
1727
|
+
`
|
|
1728
|
+
|
|
1729
|
+
const VisionPageImageButton = styled.button`
|
|
1730
|
+
display: flex;
|
|
1731
|
+
justify-content: center;
|
|
1732
|
+
max-width: 100%;
|
|
1733
|
+
padding: 0;
|
|
1734
|
+
border: none;
|
|
1735
|
+
background: transparent;
|
|
1736
|
+
cursor: zoom-in;
|
|
1737
|
+
|
|
1738
|
+
img {
|
|
1739
|
+
display: block;
|
|
1740
|
+
/* Fit the WHOLE page in the frame. */
|
|
1741
|
+
max-height: 60vh;
|
|
1742
|
+
max-width: 100%;
|
|
1743
|
+
width: auto;
|
|
1744
|
+
height: auto;
|
|
1745
|
+
background: #fff;
|
|
1746
|
+
box-shadow:
|
|
1747
|
+
0 1px 2px rgb(0 0 0 / 0.16),
|
|
1748
|
+
0 10px 28px rgb(0 0 0 / 0.14);
|
|
1749
|
+
}
|
|
1750
|
+
`
|
|
1751
|
+
|
|
1752
|
+
const VisionPageAnnotations = styled.div`
|
|
1753
|
+
/* White rail: the card's label, chips and inspector note on a raised
|
|
1754
|
+
surface beside the page. */
|
|
1755
|
+
display: flex;
|
|
1756
|
+
flex-direction: column;
|
|
1757
|
+
gap: 8px;
|
|
1758
|
+
flex: 0 0 240px;
|
|
1759
|
+
width: 240px;
|
|
1760
|
+
padding: 12px 14px;
|
|
1761
|
+
background: var(--platform-colors-elevated, #fff);
|
|
1762
|
+
border: 1px solid var(--platform-colors-border);
|
|
1763
|
+
border-radius: var(--platform-radius-sm);
|
|
1764
|
+
box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
|
|
1765
|
+
`
|
|
1766
|
+
|
|
1767
|
+
const VisionPageNote = styled.span`
|
|
1425
1768
|
color: var(--platform-colors-text-secondary);
|
|
1426
1769
|
font-size: var(--platform-typography-font-size-xs);
|
|
1427
|
-
line-height: 1.
|
|
1770
|
+
line-height: 1.5;
|
|
1428
1771
|
overflow-wrap: break-word;
|
|
1429
|
-
word-break: break-word;
|
|
1430
|
-
white-space: normal;
|
|
1431
1772
|
`
|
|
1432
1773
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1774
|
+
/**
|
|
1775
|
+
* Presentation chrome injected into the live preview document: the grey
|
|
1776
|
+
* well behind centered pages with a paper shadow — matching the snapshot
|
|
1777
|
+
* strip. The iframe cannot read the app's CSS variables, so the well
|
|
1778
|
+
* colour is a fixed fallback.
|
|
1779
|
+
*/
|
|
1780
|
+
const LIVE_PREVIEW_CHROME = `<style data-live-preview-chrome>
|
|
1781
|
+
html, body { background: #eceef1 !important; }
|
|
1782
|
+
.pagedjs_pages { display: flex; flex-direction: column; align-items: center; gap: 28px; padding: 28px 0; }
|
|
1783
|
+
.pagedjs_page { background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / 0.16), 0 10px 28px rgb(0 0 0 / 0.14); }
|
|
1784
|
+
</style>`
|
|
1785
|
+
|
|
1786
|
+
function livePreviewDocument(html: string): string {
|
|
1787
|
+
return html.includes('</head>')
|
|
1788
|
+
? html.replace('</head>', `${LIVE_PREVIEW_CHROME}</head>`)
|
|
1789
|
+
: `${LIVE_PREVIEW_CHROME}${html}`
|
|
1790
|
+
}
|
|
1439
1791
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1792
|
+
const LivePreviewFrame = styled.iframe`
|
|
1793
|
+
flex: 1 1 auto;
|
|
1794
|
+
width: 100%;
|
|
1795
|
+
min-height: 480px;
|
|
1796
|
+
border: 1px solid var(--platform-colors-border);
|
|
1797
|
+
border-radius: var(--platform-radius-sm);
|
|
1798
|
+
background: var(--well, var(--platform-colors-background, #ececec));
|
|
1448
1799
|
`
|
|
1449
1800
|
|
|
1450
|
-
const
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
&:hover {
|
|
1455
|
-
text-decoration: underline;
|
|
1456
|
-
}
|
|
1801
|
+
const WellViewSwitch = styled.div`
|
|
1802
|
+
display: flex;
|
|
1803
|
+
align-items: center;
|
|
1804
|
+
gap: 6px;
|
|
1457
1805
|
`
|
|
1458
1806
|
|
|
1459
|
-
const
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1807
|
+
const WellViewButton = styled.button<{ $active: boolean }>`
|
|
1808
|
+
font-family: var(--print-design-mono);
|
|
1809
|
+
font-size: 10px;
|
|
1810
|
+
letter-spacing: 0.06em;
|
|
1811
|
+
text-transform: uppercase;
|
|
1812
|
+
padding: 4px 10px;
|
|
1813
|
+
border-radius: var(--platform-radius-sm);
|
|
1814
|
+
border: 1px solid
|
|
1815
|
+
${({ $active }) =>
|
|
1816
|
+
$active ? 'var(--print-design-hue)' : 'var(--platform-colors-border)'};
|
|
1817
|
+
background: ${({ $active }) =>
|
|
1818
|
+
$active
|
|
1819
|
+
? 'color-mix(in srgb, var(--print-design-hue) 10%, transparent)'
|
|
1820
|
+
: 'transparent'};
|
|
1821
|
+
color: var(--platform-colors-text);
|
|
1822
|
+
cursor: pointer;
|
|
1465
1823
|
`
|
|
1466
1824
|
|
|
1467
|
-
|
|
1825
|
+
const PageZoomOverlay = styled.div`
|
|
1826
|
+
position: fixed;
|
|
1827
|
+
inset: 0;
|
|
1828
|
+
z-index: 60;
|
|
1829
|
+
display: flex;
|
|
1830
|
+
align-items: center;
|
|
1831
|
+
justify-content: center;
|
|
1832
|
+
background: rgb(15 17 21 / 0.62);
|
|
1833
|
+
cursor: zoom-out;
|
|
1834
|
+
`
|
|
1835
|
+
|
|
1836
|
+
const PageZoomImage = styled.img`
|
|
1837
|
+
max-width: 92vw;
|
|
1838
|
+
max-height: 92vh;
|
|
1839
|
+
width: auto;
|
|
1840
|
+
height: auto;
|
|
1841
|
+
background: #fff;
|
|
1842
|
+
box-shadow: 0 24px 80px rgb(0 0 0 / 0.45);
|
|
1843
|
+
`
|
|
1844
|
+
|
|
1845
|
+
const SnapshotHistoryLabel = styled.span`
|
|
1846
|
+
font-family: var(--print-design-mono);
|
|
1847
|
+
font-size: 10px;
|
|
1848
|
+
letter-spacing: 0.08em;
|
|
1849
|
+
text-transform: uppercase;
|
|
1850
|
+
color: var(--print-design-faint, var(--platform-colors-text-secondary));
|
|
1851
|
+
`
|
|
1852
|
+
|
|
1853
|
+
const SnapshotIssueList = styled.div`
|
|
1854
|
+
display: flex;
|
|
1855
|
+
flex-direction: column;
|
|
1856
|
+
gap: 4px;
|
|
1857
|
+
margin-top: 8px;
|
|
1858
|
+
`
|
|
1859
|
+
|
|
1860
|
+
const SnapshotIssueRow = styled.div`
|
|
1861
|
+
display: flex;
|
|
1862
|
+
align-items: center;
|
|
1863
|
+
gap: 8px;
|
|
1864
|
+
min-width: 0;
|
|
1865
|
+
`
|
|
1866
|
+
|
|
1867
|
+
const SnapshotIssueLabel = styled.span<{ $fixed: boolean }>`
|
|
1868
|
+
color: var(--platform-colors-text-secondary);
|
|
1869
|
+
font-size: var(--platform-typography-font-size-xs);
|
|
1870
|
+
text-decoration: ${({ $fixed }) => ($fixed ? 'line-through' : 'none')};
|
|
1871
|
+
opacity: ${({ $fixed }) => ($fixed ? 0.7 : 1)};
|
|
1872
|
+
`
|
|
1873
|
+
|
|
1874
|
+
const DecisionMessage = styled(Text)`
|
|
1875
|
+
min-width: 0;
|
|
1876
|
+
display: block;
|
|
1877
|
+
padding-top: 1px;
|
|
1878
|
+
color: var(--platform-colors-text-secondary);
|
|
1879
|
+
font-size: var(--platform-typography-font-size-xs);
|
|
1880
|
+
line-height: 1.55;
|
|
1881
|
+
overflow-wrap: break-word;
|
|
1882
|
+
word-break: break-word;
|
|
1883
|
+
white-space: normal;
|
|
1884
|
+
`
|
|
1885
|
+
|
|
1886
|
+
const DecisionColor = styled.span<{ $color: string }>`
|
|
1887
|
+
display: inline-flex;
|
|
1888
|
+
align-items: center;
|
|
1889
|
+
gap: 3px;
|
|
1890
|
+
margin: 0 2px;
|
|
1891
|
+
white-space: nowrap;
|
|
1892
|
+
|
|
1893
|
+
&::before {
|
|
1894
|
+
content: '';
|
|
1895
|
+
width: 0.75em;
|
|
1896
|
+
height: 0.75em;
|
|
1897
|
+
border: 1px solid var(--platform-colors-border);
|
|
1898
|
+
border-radius: 2px;
|
|
1899
|
+
background: ${({ $color }) => $color};
|
|
1900
|
+
}
|
|
1901
|
+
`
|
|
1902
|
+
|
|
1903
|
+
const DecisionLink = styled.a`
|
|
1904
|
+
color: var(--platform-colors-accent);
|
|
1905
|
+
text-decoration: none;
|
|
1906
|
+
|
|
1907
|
+
&:hover {
|
|
1908
|
+
text-decoration: underline;
|
|
1909
|
+
}
|
|
1910
|
+
`
|
|
1911
|
+
|
|
1912
|
+
const HiddenFile = styled.input`
|
|
1913
|
+
width: 1px;
|
|
1914
|
+
height: 1px;
|
|
1915
|
+
opacity: 0;
|
|
1916
|
+
position: absolute;
|
|
1917
|
+
pointer-events: none;
|
|
1918
|
+
`
|
|
1919
|
+
|
|
1920
|
+
export function PrintDesignPopover({
|
|
1468
1921
|
open,
|
|
1469
1922
|
children,
|
|
1470
1923
|
title = 'Design - PDF Typesetter',
|
|
@@ -1623,6 +2076,15 @@ function paperColorForTheme(theme: PureRenderTheme): string {
|
|
|
1623
2076
|
return theme.color.paper ?? theme.color.surface ?? '#FFFFFF'
|
|
1624
2077
|
}
|
|
1625
2078
|
|
|
2079
|
+
/** Numeric leading for the live preview page (type.lead is number | string). */
|
|
2080
|
+
function previewLeadValue(theme: PureRenderTheme): number {
|
|
2081
|
+
const lead =
|
|
2082
|
+
typeof theme.type.lead === 'number'
|
|
2083
|
+
? theme.type.lead
|
|
2084
|
+
: Number.parseFloat(theme.type.lead)
|
|
2085
|
+
return Number.isFinite(lead) && lead >= 1.1 && lead <= 2.4 ? lead : 1.5
|
|
2086
|
+
}
|
|
2087
|
+
|
|
1626
2088
|
function fontRoleForFamily(family: string | undefined): 'serif' | 'sans' {
|
|
1627
2089
|
return /sans|inter|arial|helvetica|system-ui/i.test(family ?? '')
|
|
1628
2090
|
? 'sans'
|
|
@@ -1846,10 +2308,15 @@ export function PrintDesignPanel({
|
|
|
1846
2308
|
actions = [],
|
|
1847
2309
|
extractionCapability,
|
|
1848
2310
|
visionTypesetting,
|
|
2311
|
+
previewDocument,
|
|
2312
|
+
livePreview,
|
|
1849
2313
|
onThemeChange,
|
|
1850
2314
|
onProfileExtractRequest,
|
|
1851
2315
|
}: PrintDesignPanelProps): React.ReactElement {
|
|
1852
2316
|
const imageInputId = useId()
|
|
2317
|
+
// Preview shows the host document by default; "Sample" opts back into the
|
|
2318
|
+
// built-in copy (and is forced when the host supplies no content).
|
|
2319
|
+
const [useSampleContent, setUseSampleContent] = useState(false)
|
|
1853
2320
|
const [mode, setMode] = useState<PrintDesignMode>('preset')
|
|
1854
2321
|
const [presetId, setPresetId] = useState(presets[0]?.id ?? 'default')
|
|
1855
2322
|
const [description, setDescription] = useState('')
|
|
@@ -1880,6 +2347,59 @@ export function PrintDesignPanel({
|
|
|
1880
2347
|
const [manualAlign, setManualAlign] = useState<'justified' | 'flush-left'>(
|
|
1881
2348
|
'justified',
|
|
1882
2349
|
)
|
|
2350
|
+
const [manualHeadingRule, setManualHeadingRule] = useState<
|
|
2351
|
+
'none' | 'underline'
|
|
2352
|
+
>('none')
|
|
2353
|
+
const [manualEyebrow, setManualEyebrow] = useState('')
|
|
2354
|
+
const [manualDropCap, setManualDropCap] = useState(false)
|
|
2355
|
+
const [manualColumns, setManualColumns] = useState<1 | 2>(1)
|
|
2356
|
+
const [manualMarginY, setManualMarginY] = useState('')
|
|
2357
|
+
const [manualMarginX, setManualMarginX] = useState('')
|
|
2358
|
+
const [manualTableRule, setManualTableRule] = useState<
|
|
2359
|
+
'hairline' | 'light' | 'medium'
|
|
2360
|
+
>('light')
|
|
2361
|
+
const [manualTableHeader, setManualTableHeader] = useState<
|
|
2362
|
+
'default' | 'plain' | 'shaded' | 'accent'
|
|
2363
|
+
>('default')
|
|
2364
|
+
const [manualTableDensity, setManualTableDensity] = useState<
|
|
2365
|
+
'airy' | 'regular' | 'compact'
|
|
2366
|
+
>('regular')
|
|
2367
|
+
const [manualTableGrid, setManualTableGrid] = useState(false)
|
|
2368
|
+
const [manualRunningHeader, setManualRunningHeader] = useState<
|
|
2369
|
+
'none' | 'title' | 'section' | 'custom'
|
|
2370
|
+
>('none')
|
|
2371
|
+
const [manualRunningHeaderText, setManualRunningHeaderText] = useState('')
|
|
2372
|
+
const [manualFolio, setManualFolio] = useState<'page' | 'none'>('page')
|
|
2373
|
+
const [manualFolioSlot, setManualFolioSlot] = useState<
|
|
2374
|
+
'bottom-center' | 'bottom-left' | 'bottom-right' | 'top-right'
|
|
2375
|
+
>('bottom-center')
|
|
2376
|
+
const [manualFolioStyle, setManualFolioStyle] = useState<
|
|
2377
|
+
'plain' | 'accent' | 'of-total'
|
|
2378
|
+
>('plain')
|
|
2379
|
+
const [manualCoverPage, setManualCoverPage] = useState(false)
|
|
2380
|
+
const [manualCoverBackground, setManualCoverBackground] = useState('')
|
|
2381
|
+
const [manualCoverAuthor, setManualCoverAuthor] = useState('')
|
|
2382
|
+
const [manualCoverDate, setManualCoverDate] = useState('')
|
|
2383
|
+
const [manualCoverImage, setManualCoverImage] = useState('')
|
|
2384
|
+
const [manualToc, setManualToc] = useState(false)
|
|
2385
|
+
const coverImageInputRef = useRef<HTMLInputElement | null>(null)
|
|
2386
|
+
// Workspace: the Reference (image/describe) inputs start collapsed.
|
|
2387
|
+
const [referenceOpen, setReferenceOpen] = useState(false)
|
|
2388
|
+
const [manualLogo, setManualLogo] = useState('')
|
|
2389
|
+
const [manualLogoSlot, setManualLogoSlot] = useState<
|
|
2390
|
+
'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
|
2391
|
+
>('top-right')
|
|
2392
|
+
// Which face the well shows once a run exists: the live preview or the
|
|
2393
|
+
// last run's annotated pages. Style edits flip back to live and mark the
|
|
2394
|
+
// pages stale until the next Run.
|
|
2395
|
+
const [wellView, setWellView] = useState<'live' | 'pages'>('pages')
|
|
2396
|
+
const [stripStale, setStripStale] = useState(false)
|
|
2397
|
+
// In-window zoom for page snapshots — no external viewer.
|
|
2398
|
+
const [zoomedPage, setZoomedPage] = useState<{
|
|
2399
|
+
url: string
|
|
2400
|
+
alt: string
|
|
2401
|
+
} | null>(null)
|
|
2402
|
+
const logoFileInputRef = useRef<HTMLInputElement | null>(null)
|
|
1883
2403
|
const [decisions, setDecisions] = useState<readonly [string, string][]>([])
|
|
1884
2404
|
const [status, setStatus] = useState<string>(
|
|
1885
2405
|
onProfileExtractRequest ? 'Ready' : 'Preset/manual only',
|
|
@@ -1898,9 +2418,15 @@ export function PrintDesignPanel({
|
|
|
1898
2418
|
}, [modes])
|
|
1899
2419
|
|
|
1900
2420
|
useEffect(() => {
|
|
2421
|
+
// Workspace is ONE merged view — pin mode to 'preset' so no stale tab
|
|
2422
|
+
// state (or host default) can hide the design controls.
|
|
2423
|
+
if (variant === 'workspace') {
|
|
2424
|
+
if (mode !== 'preset') setMode('preset')
|
|
2425
|
+
return
|
|
2426
|
+
}
|
|
1901
2427
|
if (modeOptions.some(option => option.value === mode)) return
|
|
1902
2428
|
setMode(modeOptions[0]?.value ?? 'preset')
|
|
1903
|
-
}, [mode, modeOptions])
|
|
2429
|
+
}, [mode, modeOptions, variant])
|
|
1904
2430
|
|
|
1905
2431
|
useEffect(() => {
|
|
1906
2432
|
if (!value) return
|
|
@@ -1940,8 +2466,61 @@ export function PrintDesignPanel({
|
|
|
1940
2466
|
setManualMeasure(activeTheme.type.measure)
|
|
1941
2467
|
setManualLead(String(activeTheme.type.lead))
|
|
1942
2468
|
setManualAlign(manualAlignForTheme(activeTheme.setting?.align))
|
|
2469
|
+
setManualHeadingRule(
|
|
2470
|
+
activeTheme.setting?.headingRule === 'underline' ? 'underline' : 'none',
|
|
2471
|
+
)
|
|
2472
|
+
setManualEyebrow(activeTheme.setting?.eyebrowText ?? '')
|
|
2473
|
+
setManualDropCap(activeTheme.setting?.dropCap === true)
|
|
2474
|
+
setManualColumns(activeTheme.page?.columns === 2 ? 2 : 1)
|
|
2475
|
+
const marginParts = activeTheme.page?.margin?.trim().split(/\s+/) ?? []
|
|
2476
|
+
setManualMarginY(marginParts[0] ?? '')
|
|
2477
|
+
setManualMarginX(marginParts[1] ?? marginParts[0] ?? '')
|
|
2478
|
+
const tableRules = activeTheme.profile_rules?.table
|
|
2479
|
+
setManualTableRule(
|
|
2480
|
+
tableRules?.rule_weight === 'hairline' ||
|
|
2481
|
+
tableRules?.rule_weight === 'medium'
|
|
2482
|
+
? tableRules.rule_weight
|
|
2483
|
+
: 'light',
|
|
2484
|
+
)
|
|
2485
|
+
setManualTableHeader(
|
|
2486
|
+
tableRules?.header_treatment === 'plain' ||
|
|
2487
|
+
tableRules?.header_treatment === 'shaded' ||
|
|
2488
|
+
tableRules?.header_treatment === 'accent'
|
|
2489
|
+
? tableRules.header_treatment
|
|
2490
|
+
: 'default',
|
|
2491
|
+
)
|
|
2492
|
+
setManualTableDensity(
|
|
2493
|
+
tableRules?.density === 'airy'
|
|
2494
|
+
? 'airy'
|
|
2495
|
+
: tableRules?.density === 'dense' || tableRules?.density === 'compact'
|
|
2496
|
+
? 'compact'
|
|
2497
|
+
: 'regular',
|
|
2498
|
+
)
|
|
2499
|
+
setManualTableGrid(tableRules?.grid === 'lines')
|
|
2500
|
+
setManualRunningHeader(activeTheme.setting?.runningHeader ?? 'none')
|
|
2501
|
+
setManualRunningHeaderText(activeTheme.setting?.runningHeaderText ?? '')
|
|
2502
|
+
setManualFolio(activeTheme.setting?.folio === 'none' ? 'none' : 'page')
|
|
2503
|
+
setManualFolioSlot(activeTheme.setting?.folioSlot ?? 'bottom-center')
|
|
2504
|
+
setManualFolioStyle(activeTheme.setting?.folioStyle ?? 'plain')
|
|
2505
|
+
setManualCoverPage(activeTheme.setting?.coverPage === true)
|
|
2506
|
+
setManualCoverBackground(activeTheme.setting?.coverBackground ?? '')
|
|
2507
|
+
setManualCoverAuthor(activeTheme.setting?.coverAuthor ?? '')
|
|
2508
|
+
setManualCoverDate(activeTheme.setting?.coverDate ?? '')
|
|
2509
|
+
setManualCoverImage(activeTheme.setting?.coverImageDataUri ?? '')
|
|
2510
|
+
setManualToc(activeTheme.setting?.toc === true)
|
|
2511
|
+
setManualLogo(activeTheme.setting?.logoDataUri ?? '')
|
|
2512
|
+
setManualLogoSlot(activeTheme.setting?.logoSlot ?? 'top-right')
|
|
1943
2513
|
}, [activeTheme])
|
|
1944
2514
|
|
|
2515
|
+
useEffect(() => {
|
|
2516
|
+
if (!zoomedPage) return
|
|
2517
|
+
const onKey = (event: KeyboardEvent): void => {
|
|
2518
|
+
if (event.key === 'Escape') setZoomedPage(null)
|
|
2519
|
+
}
|
|
2520
|
+
window.addEventListener('keydown', onKey)
|
|
2521
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
2522
|
+
}, [zoomedPage])
|
|
2523
|
+
|
|
1945
2524
|
function extractionUnavailableReason(
|
|
1946
2525
|
targetMode: PrintDesignMode,
|
|
1947
2526
|
): string | undefined {
|
|
@@ -1979,7 +2558,10 @@ export function PrintDesignPanel({
|
|
|
1979
2558
|
? `${images.length} images`
|
|
1980
2559
|
: images[0]?.name ?? 'Choose image'
|
|
1981
2560
|
const imageButtonLabel = images.length > 0 ? 'Replace image' : 'Choose image'
|
|
1982
|
-
|
|
2561
|
+
// The merged workspace view has no tabs — design controls ALWAYS show
|
|
2562
|
+
// there, whatever mode state was left behind (e.g. 'vision' from before
|
|
2563
|
+
// the merge, preserved across HMR or by a host).
|
|
2564
|
+
const unifiedDesignMode = mode === 'preset' || variant === 'workspace'
|
|
1983
2565
|
const rawVisionReport = visionTypesetting?.report?.length
|
|
1984
2566
|
? visionTypesetting.report
|
|
1985
2567
|
: []
|
|
@@ -1998,6 +2580,66 @@ export function PrintDesignPanel({
|
|
|
1998
2580
|
? [...liveReadoutEntries].reverse()
|
|
1999
2581
|
: liveReadoutEntries
|
|
2000
2582
|
const visionEvidence = visionTypesetting?.evidence ?? []
|
|
2583
|
+
// The vision canvas owns ALL information — there is no right rail in
|
|
2584
|
+
// vision mode (the report lives in Design mode's Activity section).
|
|
2585
|
+
const snapshotHistory =
|
|
2586
|
+
mode === 'vision' || variant === 'workspace'
|
|
2587
|
+
? visionTypesetting?.snapshotHistory ?? []
|
|
2588
|
+
: []
|
|
2589
|
+
// New cards append at the bottom — keep the strip pinned to the latest
|
|
2590
|
+
// render so a running sweep reads as progress.
|
|
2591
|
+
const snapshotHistoryRef = useRef<HTMLDivElement | null>(null)
|
|
2592
|
+
useEffect(() => {
|
|
2593
|
+
const node = snapshotHistoryRef.current
|
|
2594
|
+
if (!node) return
|
|
2595
|
+
if (typeof node.scrollTo === 'function') {
|
|
2596
|
+
node.scrollTo({ top: node.scrollHeight, behavior: 'smooth' })
|
|
2597
|
+
} else {
|
|
2598
|
+
node.scrollTop = node.scrollHeight
|
|
2599
|
+
}
|
|
2600
|
+
}, [snapshotHistory.length])
|
|
2601
|
+
// Bidirectional contract: new run output pulls the well back to the
|
|
2602
|
+
// pages view and clears staleness; a cleared history resets it.
|
|
2603
|
+
const prevHistoryLengthRef = useRef(0)
|
|
2604
|
+
useEffect(() => {
|
|
2605
|
+
const prev = prevHistoryLengthRef.current
|
|
2606
|
+
prevHistoryLengthRef.current = snapshotHistory.length
|
|
2607
|
+
if (snapshotHistory.length === 0) {
|
|
2608
|
+
setStripStale(false)
|
|
2609
|
+
return
|
|
2610
|
+
}
|
|
2611
|
+
if (snapshotHistory.length > prev) {
|
|
2612
|
+
setWellView('pages')
|
|
2613
|
+
setStripStale(false)
|
|
2614
|
+
}
|
|
2615
|
+
}, [snapshotHistory.length])
|
|
2616
|
+
// Style edits make the last run's snapshots stale — show the live
|
|
2617
|
+
// preview until the user runs typesetting again.
|
|
2618
|
+
const markDesignEdited = (): void => {
|
|
2619
|
+
if (prevHistoryLengthRef.current > 0) {
|
|
2620
|
+
setStripStale(true)
|
|
2621
|
+
setWellView('live')
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
const hasHostPreviewContent = Boolean(
|
|
2625
|
+
previewDocument?.paragraphs?.length || previewDocument?.title?.trim(),
|
|
2626
|
+
)
|
|
2627
|
+
const previewUsesSample = useSampleContent || !hasHostPreviewContent
|
|
2628
|
+
const previewContent = previewUsesSample
|
|
2629
|
+
? {
|
|
2630
|
+
title: 'The Shape of a Page',
|
|
2631
|
+
byline: `A. Reader · Typeset with ${activeTheme.name}`,
|
|
2632
|
+
paragraphs: SAMPLE_PREVIEW_PARAGRAPHS,
|
|
2633
|
+
}
|
|
2634
|
+
: {
|
|
2635
|
+
title: previewDocument?.title?.trim() || 'Untitled document',
|
|
2636
|
+
byline:
|
|
2637
|
+
previewDocument?.byline?.trim() ||
|
|
2638
|
+
`Typeset with ${activeTheme.name}`,
|
|
2639
|
+
paragraphs: previewDocument?.paragraphs?.length
|
|
2640
|
+
? previewDocument.paragraphs.slice(0, 4)
|
|
2641
|
+
: SAMPLE_PREVIEW_PARAGRAPHS,
|
|
2642
|
+
}
|
|
2001
2643
|
const imageSectionEnabled = !modes || modes.includes('image')
|
|
2002
2644
|
const descriptionSectionEnabled = !modes || modes.includes('description')
|
|
2003
2645
|
const showPresets = unifiedDesignMode
|
|
@@ -2013,24 +2655,40 @@ export function PrintDesignPanel({
|
|
|
2013
2655
|
action => /export/i.test(action.id) || /export\s+pdf/i.test(action.label),
|
|
2014
2656
|
)
|
|
2015
2657
|
const secondaryActions = actions.filter(action => action !== exportAction)
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
const headerExportDisabled =
|
|
2021
|
-
|
|
2022
|
-
exportAction?.busy ||
|
|
2023
|
-
(routeExportThroughVision &&
|
|
2024
|
-
(visionTypesetting?.exportDisabled || visionTypesetting?.exportBusy))
|
|
2025
|
-
const headerExportBusy = routeExportThroughVision
|
|
2026
|
-
? visionTypesetting?.exportBusy
|
|
2027
|
-
: exportAction?.busy
|
|
2658
|
+
// Export ALWAYS just exports the PDF. The vision inspection loop is its
|
|
2659
|
+
// own explicit action — a separate Run button shown only in the vision
|
|
2660
|
+
// tab (pressing Export there previously ran the whole loop, and there was
|
|
2661
|
+
// no way to simply export).
|
|
2662
|
+
const headerExportDisabled = exportAction?.disabled || exportAction?.busy
|
|
2663
|
+
const headerExportBusy = exportAction?.busy
|
|
2028
2664
|
const headerExportLabel =
|
|
2029
2665
|
headerExportBusy && exportAction
|
|
2030
2666
|
? exportAction.busyLabel ?? exportAction.label
|
|
2031
|
-
:
|
|
2032
|
-
|
|
2033
|
-
|
|
2667
|
+
: exportAction?.label
|
|
2668
|
+
const visionRunning = visionTypesetting?.progress?.state === 'running'
|
|
2669
|
+
// While a loop runs, the Run button becomes STOP (the sweep walks every
|
|
2670
|
+
// page until end-of-document or the user halts it).
|
|
2671
|
+
const visionRunAction =
|
|
2672
|
+
(mode === 'vision' || variant === 'workspace') &&
|
|
2673
|
+
visionTypesetting?.enabled === true &&
|
|
2674
|
+
visionTypesetting.onExport !== undefined
|
|
2675
|
+
? visionRunning && visionTypesetting.onStop
|
|
2676
|
+
? {
|
|
2677
|
+
label: 'Stop',
|
|
2678
|
+
stop: true as const,
|
|
2679
|
+
disabled: false,
|
|
2680
|
+
onClick: visionTypesetting.onStop,
|
|
2681
|
+
}
|
|
2682
|
+
: {
|
|
2683
|
+
label: visionRunning ? 'Running…' : 'Run typesetting',
|
|
2684
|
+
stop: false as const,
|
|
2685
|
+
disabled:
|
|
2686
|
+
visionRunning ||
|
|
2687
|
+
visionTypesetting.exportBusy === true ||
|
|
2688
|
+
visionTypesetting.exportDisabled === true,
|
|
2689
|
+
onClick: visionTypesetting.onExport,
|
|
2690
|
+
}
|
|
2691
|
+
: null
|
|
2034
2692
|
const showModeActionButton =
|
|
2035
2693
|
mode !== 'preset' &&
|
|
2036
2694
|
mode !== 'manual' &&
|
|
@@ -2043,6 +2701,7 @@ export function PrintDesignPanel({
|
|
|
2043
2701
|
setDecisions([['apply', `${preset.label} preset selected`]])
|
|
2044
2702
|
onThemeChange?.(preset.theme)
|
|
2045
2703
|
setStatus('Ready')
|
|
2704
|
+
markDesignEdited()
|
|
2046
2705
|
}
|
|
2047
2706
|
}
|
|
2048
2707
|
|
|
@@ -2125,6 +2784,29 @@ export function PrintDesignPanel({
|
|
|
2125
2784
|
measure: string
|
|
2126
2785
|
lead: string
|
|
2127
2786
|
align: 'justified' | 'flush-left'
|
|
2787
|
+
headingRule: 'none' | 'underline'
|
|
2788
|
+
eyebrowText: string
|
|
2789
|
+
dropCap: boolean
|
|
2790
|
+
columns: 1 | 2
|
|
2791
|
+
marginY: string
|
|
2792
|
+
marginX: string
|
|
2793
|
+
tableRule: 'hairline' | 'light' | 'medium'
|
|
2794
|
+
tableHeader: 'default' | 'plain' | 'shaded' | 'accent'
|
|
2795
|
+
tableDensity: 'airy' | 'regular' | 'compact'
|
|
2796
|
+
tableGrid: boolean
|
|
2797
|
+
runningHeader: 'none' | 'title' | 'section' | 'custom'
|
|
2798
|
+
runningHeaderText: string
|
|
2799
|
+
folio: 'page' | 'none'
|
|
2800
|
+
folioSlot: 'bottom-center' | 'bottom-left' | 'bottom-right' | 'top-right'
|
|
2801
|
+
folioStyle: 'plain' | 'accent' | 'of-total'
|
|
2802
|
+
coverPage: boolean
|
|
2803
|
+
coverBackground: string
|
|
2804
|
+
coverAuthor: string
|
|
2805
|
+
coverDate: string
|
|
2806
|
+
coverImageDataUri: string
|
|
2807
|
+
toc: boolean
|
|
2808
|
+
logoDataUri: string
|
|
2809
|
+
logoSlot: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
|
2128
2810
|
}> = {},
|
|
2129
2811
|
): void {
|
|
2130
2812
|
const ink = overrides.ink ?? manualInk
|
|
@@ -2135,6 +2817,38 @@ export function PrintDesignPanel({
|
|
|
2135
2817
|
const labelRole = overrides.labelFont ?? manualLabelFont
|
|
2136
2818
|
const measure = overrides.measure ?? manualMeasure
|
|
2137
2819
|
const align = overrides.align ?? manualAlign
|
|
2820
|
+
const headingRule = overrides.headingRule ?? manualHeadingRule
|
|
2821
|
+
const eyebrowText = overrides.eyebrowText ?? manualEyebrow
|
|
2822
|
+
const dropCap = overrides.dropCap ?? manualDropCap
|
|
2823
|
+
const columns = overrides.columns ?? manualColumns
|
|
2824
|
+
const marginY = (overrides.marginY ?? manualMarginY).trim()
|
|
2825
|
+
const marginX = (overrides.marginX ?? manualMarginX).trim()
|
|
2826
|
+
const pageMargin = marginY
|
|
2827
|
+
? marginX && marginX !== marginY
|
|
2828
|
+
? `${marginY} ${marginX}`
|
|
2829
|
+
: marginY
|
|
2830
|
+
: undefined
|
|
2831
|
+
const tableRule = overrides.tableRule ?? manualTableRule
|
|
2832
|
+
const tableHeader = overrides.tableHeader ?? manualTableHeader
|
|
2833
|
+
const tableDensity = overrides.tableDensity ?? manualTableDensity
|
|
2834
|
+
const tableGrid = overrides.tableGrid ?? manualTableGrid
|
|
2835
|
+
const runningHeader = overrides.runningHeader ?? manualRunningHeader
|
|
2836
|
+
const runningHeaderText =
|
|
2837
|
+
overrides.runningHeaderText ?? manualRunningHeaderText
|
|
2838
|
+
const folio = overrides.folio ?? manualFolio
|
|
2839
|
+
const folioSlot = overrides.folioSlot ?? manualFolioSlot
|
|
2840
|
+
const folioStyle = overrides.folioStyle ?? manualFolioStyle
|
|
2841
|
+
const coverPage = overrides.coverPage ?? manualCoverPage
|
|
2842
|
+
const requestedToc = overrides.toc ?? manualToc
|
|
2843
|
+
const coverBackground = overrides.coverBackground ?? manualCoverBackground
|
|
2844
|
+
const coverAuthor = overrides.coverAuthor ?? manualCoverAuthor
|
|
2845
|
+
const coverDate = overrides.coverDate ?? manualCoverDate
|
|
2846
|
+
const coverImageDataUri =
|
|
2847
|
+
overrides.coverImageDataUri ?? manualCoverImage
|
|
2848
|
+
// Contents implies a cover.
|
|
2849
|
+
const toc = coverPage ? requestedToc : false
|
|
2850
|
+
const logoDataUri = overrides.logoDataUri ?? manualLogo
|
|
2851
|
+
const logoSlot = overrides.logoSlot ?? manualLogoSlot
|
|
2138
2852
|
const leadText = overrides.lead ?? manualLead
|
|
2139
2853
|
const lead = Number.parseFloat(leadText)
|
|
2140
2854
|
const nextLead = Number.isFinite(lead) && lead >= 1.2 ? lead : 1.5
|
|
@@ -2165,10 +2879,31 @@ export function PrintDesignPanel({
|
|
|
2165
2879
|
},
|
|
2166
2880
|
},
|
|
2167
2881
|
rhythm: `${(10.6 * nextLead).toFixed(1)}pt`,
|
|
2882
|
+
page: {
|
|
2883
|
+
...activeTheme.page,
|
|
2884
|
+
columns,
|
|
2885
|
+
margin: pageMargin,
|
|
2886
|
+
},
|
|
2168
2887
|
setting: {
|
|
2169
2888
|
...activeTheme.setting,
|
|
2170
2889
|
align,
|
|
2171
2890
|
hyphenate: align === 'justified',
|
|
2891
|
+
headingRule,
|
|
2892
|
+
eyebrowText: eyebrowText.trim() || undefined,
|
|
2893
|
+
dropCap,
|
|
2894
|
+
runningHeader,
|
|
2895
|
+
runningHeaderText: runningHeaderText.trim() || undefined,
|
|
2896
|
+
folio,
|
|
2897
|
+
folioSlot,
|
|
2898
|
+
folioStyle,
|
|
2899
|
+
coverPage,
|
|
2900
|
+
coverBackground: coverBackground.trim() || undefined,
|
|
2901
|
+
coverAuthor: coverAuthor.trim() || undefined,
|
|
2902
|
+
coverDate: coverDate.trim() || undefined,
|
|
2903
|
+
coverImageDataUri: coverImageDataUri.trim() || undefined,
|
|
2904
|
+
toc,
|
|
2905
|
+
logoDataUri: logoDataUri.trim() || undefined,
|
|
2906
|
+
logoSlot,
|
|
2172
2907
|
},
|
|
2173
2908
|
profile_rules: {
|
|
2174
2909
|
...activeTheme.profile_rules,
|
|
@@ -2176,6 +2911,19 @@ export function PrintDesignPanel({
|
|
|
2176
2911
|
...activeTheme.profile_rules?.caption,
|
|
2177
2912
|
label_style: labelRole === 'sans' ? 'small-caps' : 'plain',
|
|
2178
2913
|
},
|
|
2914
|
+
table: {
|
|
2915
|
+
...activeTheme.profile_rules?.table,
|
|
2916
|
+
rule_weight: tableRule,
|
|
2917
|
+
density:
|
|
2918
|
+
tableDensity === 'airy'
|
|
2919
|
+
? 'airy'
|
|
2920
|
+
: tableDensity === 'compact'
|
|
2921
|
+
? 'dense'
|
|
2922
|
+
: 'balanced',
|
|
2923
|
+
header_treatment:
|
|
2924
|
+
tableHeader === 'default' ? undefined : tableHeader,
|
|
2925
|
+
grid: tableGrid ? 'lines' : 'none',
|
|
2926
|
+
},
|
|
2179
2927
|
},
|
|
2180
2928
|
enforced_rules: [
|
|
2181
2929
|
...(activeTheme.enforced_rules ?? []),
|
|
@@ -2190,6 +2938,7 @@ export function PrintDesignPanel({
|
|
|
2190
2938
|
])
|
|
2191
2939
|
onThemeChange?.(theme)
|
|
2192
2940
|
setStatus('Ready')
|
|
2941
|
+
markDesignEdited()
|
|
2193
2942
|
}
|
|
2194
2943
|
|
|
2195
2944
|
async function chooseImages(files: readonly File[]): Promise<void> {
|
|
@@ -2241,10 +2990,50 @@ export function PrintDesignPanel({
|
|
|
2241
2990
|
}
|
|
2242
2991
|
}
|
|
2243
2992
|
|
|
2993
|
+
// Live theme facsimile — shown in the popover readout AND as the merged
|
|
2994
|
+
// workspace well's idle state (until a run fills it with real pages).
|
|
2995
|
+
const facsimilePreview = (
|
|
2996
|
+
<PreviewStage aria-label="Live typeset preview">
|
|
2997
|
+
<PreviewPage
|
|
2998
|
+
$paper={paperColorForTheme(activeTheme)}
|
|
2999
|
+
$ink={activeTheme.color.ink}
|
|
3000
|
+
$bodyFont={activeTheme.type.fonts.serif}
|
|
3001
|
+
$lead={previewLeadValue(activeTheme)}
|
|
3002
|
+
$justified={activeTheme.setting?.align !== 'flush-left'}
|
|
3003
|
+
>
|
|
3004
|
+
<PreviewKicker
|
|
3005
|
+
$accent={activeTheme.color.accent}
|
|
3006
|
+
$labelFont={activeTheme.type.fonts.sans}
|
|
3007
|
+
>
|
|
3008
|
+
{previewUsesSample
|
|
3009
|
+
? activeTheme.setting?.eyebrowText?.trim() || 'Research Article'
|
|
3010
|
+
: activeTheme.setting?.eyebrowText?.trim() || 'Your document'}
|
|
3011
|
+
</PreviewKicker>
|
|
3012
|
+
<PreviewTitle
|
|
3013
|
+
$displayFont={
|
|
3014
|
+
activeTheme.type.fonts.display ?? activeTheme.type.fonts.serif
|
|
3015
|
+
}
|
|
3016
|
+
>
|
|
3017
|
+
{previewContent.title}
|
|
3018
|
+
</PreviewTitle>
|
|
3019
|
+
<PreviewByline>{previewContent.byline}</PreviewByline>
|
|
3020
|
+
<PreviewRule $accent={activeTheme.color.accent} />
|
|
3021
|
+
{previewContent.paragraphs.map((paragraph, index) => (
|
|
3022
|
+
<PreviewParagraph key={index}>{paragraph}</PreviewParagraph>
|
|
3023
|
+
))}
|
|
3024
|
+
<PreviewFolio>· 1 ·</PreviewFolio>
|
|
3025
|
+
</PreviewPage>
|
|
3026
|
+
<PreviewMeta title={activeDesignSummary}>
|
|
3027
|
+
{activeDesignSummary}
|
|
3028
|
+
</PreviewMeta>
|
|
3029
|
+
</PreviewStage>
|
|
3030
|
+
)
|
|
3031
|
+
|
|
2244
3032
|
return (
|
|
2245
3033
|
<Panel
|
|
2246
3034
|
$variant={variant}
|
|
2247
3035
|
$density={panelDensity}
|
|
3036
|
+
$mode={mode}
|
|
2248
3037
|
data-density={panelDensity}
|
|
2249
3038
|
data-panel-variant={variant}
|
|
2250
3039
|
aria-label="Print design"
|
|
@@ -2265,36 +3054,59 @@ export function PrintDesignPanel({
|
|
|
2265
3054
|
</WorkspaceSummary>
|
|
2266
3055
|
</WorkspaceIdentity>
|
|
2267
3056
|
) : null}
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
3057
|
+
{variant !== 'workspace' ? (
|
|
3058
|
+
// Workspace is ONE merged Typesetter view — no Design/vision tabs.
|
|
3059
|
+
<ModeTabs
|
|
3060
|
+
items={modeOptions.map(option => ({
|
|
3061
|
+
id: option.value,
|
|
3062
|
+
label: option.label,
|
|
3063
|
+
}))}
|
|
3064
|
+
activeId={mode}
|
|
3065
|
+
onSelect={setMode}
|
|
3066
|
+
data-mode-count={modeOptions.length}
|
|
3067
|
+
aria-label="Print design source"
|
|
3068
|
+
/>
|
|
3069
|
+
) : null}
|
|
3070
|
+
{exportAction || headerAccessory || visionRunAction ? (
|
|
2279
3071
|
<HeaderActions>
|
|
2280
3072
|
{headerAccessory}
|
|
2281
|
-
{
|
|
3073
|
+
{visionRunAction ? (
|
|
2282
3074
|
<HeaderActionButton
|
|
2283
3075
|
type="button"
|
|
2284
|
-
variant=
|
|
2285
|
-
disabled={
|
|
2286
|
-
onClick={() =>
|
|
2287
|
-
if (routeExportThroughVision) {
|
|
2288
|
-
visionTypesetting?.onExport?.()
|
|
2289
|
-
} else {
|
|
2290
|
-
exportAction.onClick()
|
|
2291
|
-
}
|
|
2292
|
-
}}
|
|
3076
|
+
variant="subtle"
|
|
3077
|
+
disabled={visionRunAction.disabled}
|
|
3078
|
+
onClick={() => visionRunAction.onClick?.()}
|
|
2293
3079
|
>
|
|
2294
|
-
|
|
2295
|
-
|
|
3080
|
+
<PlatformIcon
|
|
3081
|
+
icon={visionRunAction.stop ? Square : Play}
|
|
3082
|
+
size={14}
|
|
3083
|
+
/>
|
|
3084
|
+
{visionRunAction.label}
|
|
2296
3085
|
</HeaderActionButton>
|
|
2297
3086
|
) : null}
|
|
3087
|
+
{exportAction ? (
|
|
3088
|
+
(exportAction.variant ?? 'primary') === 'primary' ? (
|
|
3089
|
+
<ExportPrimaryButton
|
|
3090
|
+
type="button"
|
|
3091
|
+
variant="primary"
|
|
3092
|
+
disabled={headerExportDisabled}
|
|
3093
|
+
onClick={() => exportAction.onClick()}
|
|
3094
|
+
>
|
|
3095
|
+
{exportAction.icon}
|
|
3096
|
+
{headerExportLabel}
|
|
3097
|
+
</ExportPrimaryButton>
|
|
3098
|
+
) : (
|
|
3099
|
+
<HeaderActionButton
|
|
3100
|
+
type="button"
|
|
3101
|
+
variant={exportAction.variant}
|
|
3102
|
+
disabled={headerExportDisabled}
|
|
3103
|
+
onClick={() => exportAction.onClick()}
|
|
3104
|
+
>
|
|
3105
|
+
{exportAction.icon}
|
|
3106
|
+
{headerExportLabel}
|
|
3107
|
+
</HeaderActionButton>
|
|
3108
|
+
)
|
|
3109
|
+
) : null}
|
|
2298
3110
|
</HeaderActions>
|
|
2299
3111
|
) : null}
|
|
2300
3112
|
</HeaderRow>
|
|
@@ -2305,7 +3117,18 @@ export function PrintDesignPanel({
|
|
|
2305
3117
|
<DesignSection $variant={variant}>
|
|
2306
3118
|
<SectionHeader>
|
|
2307
3119
|
<SectionTitle>Reference</SectionTitle>
|
|
3120
|
+
{variant === 'workspace' ? (
|
|
3121
|
+
// Collapsed by default: the image/describe extraction
|
|
3122
|
+
// inputs are occasional-use and ate half the column.
|
|
3123
|
+
<ReferenceButton
|
|
3124
|
+
type="button"
|
|
3125
|
+
onClick={() => setReferenceOpen(open => !open)}
|
|
3126
|
+
>
|
|
3127
|
+
{referenceOpen ? 'Hide' : 'Show'}
|
|
3128
|
+
</ReferenceButton>
|
|
3129
|
+
) : null}
|
|
2308
3130
|
</SectionHeader>
|
|
3131
|
+
{variant !== 'workspace' || referenceOpen ? (
|
|
2309
3132
|
<ReferenceGrid $variant={variant}>
|
|
2310
3133
|
{(unifiedDesignMode && imageSectionEnabled) ||
|
|
2311
3134
|
mode === 'image' ? (
|
|
@@ -2377,6 +3200,7 @@ export function PrintDesignPanel({
|
|
|
2377
3200
|
</ReferencePane>
|
|
2378
3201
|
) : null}
|
|
2379
3202
|
</ReferenceGrid>
|
|
3203
|
+
) : null}
|
|
2380
3204
|
</DesignSection>
|
|
2381
3205
|
) : null}
|
|
2382
3206
|
|
|
@@ -2384,7 +3208,35 @@ export function PrintDesignPanel({
|
|
|
2384
3208
|
<DesignSection $variant={variant}>
|
|
2385
3209
|
<SectionHeader>
|
|
2386
3210
|
<SectionTitle>Presets</SectionTitle>
|
|
3211
|
+
{variant === 'workspace' ? (
|
|
3212
|
+
<PresetPreview aria-hidden="true">
|
|
3213
|
+
<Swatch $color={activeTheme.color.ink} />
|
|
3214
|
+
<Swatch $color={activeTheme.color.accent} />
|
|
3215
|
+
<Swatch $color={paperColorForTheme(activeTheme)} />
|
|
3216
|
+
</PresetPreview>
|
|
3217
|
+
) : null}
|
|
2387
3218
|
</SectionHeader>
|
|
3219
|
+
{variant === 'workspace' ? (
|
|
3220
|
+
// Compact dropdown — the full list ate half the column.
|
|
3221
|
+
<ManualSelect
|
|
3222
|
+
aria-label="Print theme presets"
|
|
3223
|
+
value={
|
|
3224
|
+
presets.some(preset => preset.id === presetId)
|
|
3225
|
+
? presetId
|
|
3226
|
+
: ''
|
|
3227
|
+
}
|
|
3228
|
+
onChange={event => applyPreset(event.currentTarget.value)}
|
|
3229
|
+
>
|
|
3230
|
+
{presets.some(preset => preset.id === presetId) ? null : (
|
|
3231
|
+
<option value="">Custom theme</option>
|
|
3232
|
+
)}
|
|
3233
|
+
{presets.map(preset => (
|
|
3234
|
+
<option key={preset.id} value={preset.id}>
|
|
3235
|
+
{preset.label}
|
|
3236
|
+
</option>
|
|
3237
|
+
))}
|
|
3238
|
+
</ManualSelect>
|
|
3239
|
+
) : (
|
|
2388
3240
|
<PresetList $variant={variant} aria-label="Print theme presets">
|
|
2389
3241
|
{presets.map(preset => (
|
|
2390
3242
|
<PresetOption
|
|
@@ -2415,6 +3267,7 @@ export function PrintDesignPanel({
|
|
|
2415
3267
|
</PresetOption>
|
|
2416
3268
|
))}
|
|
2417
3269
|
</PresetList>
|
|
3270
|
+
)}
|
|
2418
3271
|
</DesignSection>
|
|
2419
3272
|
) : null}
|
|
2420
3273
|
|
|
@@ -2437,42 +3290,62 @@ export function PrintDesignPanel({
|
|
|
2437
3290
|
$variant={variant}
|
|
2438
3291
|
aria-label="Manual theme controls"
|
|
2439
3292
|
>
|
|
2440
|
-
<
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
3293
|
+
<ColorChipsRow>
|
|
3294
|
+
<ManualField>
|
|
3295
|
+
Ink
|
|
3296
|
+
<ColorChipControl>
|
|
3297
|
+
<ColorChipSwatch $color={manualInk}>
|
|
3298
|
+
<input
|
|
3299
|
+
type="color"
|
|
3300
|
+
aria-label="Ink color"
|
|
3301
|
+
value={manualInk}
|
|
3302
|
+
onChange={event => {
|
|
3303
|
+
const ink = event.currentTarget.value
|
|
3304
|
+
setManualInk(ink)
|
|
3305
|
+
applyManualTheme({ ink })
|
|
3306
|
+
}}
|
|
3307
|
+
/>
|
|
3308
|
+
</ColorChipSwatch>
|
|
3309
|
+
<ColorChipValue>{manualInk}</ColorChipValue>
|
|
3310
|
+
</ColorChipControl>
|
|
3311
|
+
</ManualField>
|
|
3312
|
+
<ManualField>
|
|
3313
|
+
Paper
|
|
3314
|
+
<ColorChipControl>
|
|
3315
|
+
<ColorChipSwatch $color={manualPaper}>
|
|
3316
|
+
<input
|
|
3317
|
+
type="color"
|
|
3318
|
+
aria-label="Paper color"
|
|
3319
|
+
value={manualPaper}
|
|
3320
|
+
onChange={event => {
|
|
3321
|
+
const paper = event.currentTarget.value
|
|
3322
|
+
setManualPaper(paper)
|
|
3323
|
+
applyManualTheme({ paper })
|
|
3324
|
+
}}
|
|
3325
|
+
/>
|
|
3326
|
+
</ColorChipSwatch>
|
|
3327
|
+
<ColorChipValue>{manualPaper}</ColorChipValue>
|
|
3328
|
+
</ColorChipControl>
|
|
3329
|
+
</ManualField>
|
|
3330
|
+
<ManualField>
|
|
3331
|
+
Accent
|
|
3332
|
+
<ColorChipControl>
|
|
3333
|
+
<ColorChipSwatch $color={manualAccent}>
|
|
3334
|
+
<input
|
|
3335
|
+
type="color"
|
|
3336
|
+
aria-label="Accent color"
|
|
3337
|
+
value={manualAccent}
|
|
3338
|
+
onChange={event => {
|
|
3339
|
+
const accent = event.currentTarget.value
|
|
3340
|
+
setManualAccent(accent)
|
|
3341
|
+
applyManualTheme({ accent })
|
|
3342
|
+
}}
|
|
3343
|
+
/>
|
|
3344
|
+
</ColorChipSwatch>
|
|
3345
|
+
<ColorChipValue>{manualAccent}</ColorChipValue>
|
|
3346
|
+
</ColorChipControl>
|
|
3347
|
+
</ManualField>
|
|
3348
|
+
</ColorChipsRow>
|
|
2476
3349
|
<ManualField>
|
|
2477
3350
|
Display
|
|
2478
3351
|
<ManualSelect
|
|
@@ -2524,6 +3397,7 @@ export function PrintDesignPanel({
|
|
|
2524
3397
|
<ManualField>
|
|
2525
3398
|
Measure
|
|
2526
3399
|
<ManualInput
|
|
3400
|
+
$mono
|
|
2527
3401
|
value={manualMeasure}
|
|
2528
3402
|
onChange={event => {
|
|
2529
3403
|
const measure = event.currentTarget.value
|
|
@@ -2535,6 +3409,7 @@ export function PrintDesignPanel({
|
|
|
2535
3409
|
<ManualField>
|
|
2536
3410
|
Lead
|
|
2537
3411
|
<ManualInput
|
|
3412
|
+
$mono
|
|
2538
3413
|
value={manualLead}
|
|
2539
3414
|
onChange={event => {
|
|
2540
3415
|
const lead = event.currentTarget.value
|
|
@@ -2559,13 +3434,495 @@ export function PrintDesignPanel({
|
|
|
2559
3434
|
<option value="flush-left">Flush left</option>
|
|
2560
3435
|
</ManualSelect>
|
|
2561
3436
|
</ManualField>
|
|
3437
|
+
<ManualField>
|
|
3438
|
+
Heading rule
|
|
3439
|
+
<ManualSelect
|
|
3440
|
+
value={manualHeadingRule}
|
|
3441
|
+
onChange={event => {
|
|
3442
|
+
const headingRule = event.currentTarget.value as
|
|
3443
|
+
| 'none'
|
|
3444
|
+
| 'underline'
|
|
3445
|
+
setManualHeadingRule(headingRule)
|
|
3446
|
+
applyManualTheme({ headingRule })
|
|
3447
|
+
}}
|
|
3448
|
+
>
|
|
3449
|
+
<option value="none">None</option>
|
|
3450
|
+
<option value="underline">Accent underline</option>
|
|
3451
|
+
</ManualSelect>
|
|
3452
|
+
</ManualField>
|
|
3453
|
+
<ManualField>
|
|
3454
|
+
Drop cap
|
|
3455
|
+
<ManualSelect
|
|
3456
|
+
value={manualDropCap ? 'on' : 'off'}
|
|
3457
|
+
onChange={event => {
|
|
3458
|
+
const dropCap = event.currentTarget.value === 'on'
|
|
3459
|
+
setManualDropCap(dropCap)
|
|
3460
|
+
applyManualTheme({ dropCap })
|
|
3461
|
+
}}
|
|
3462
|
+
>
|
|
3463
|
+
<option value="off">Off</option>
|
|
3464
|
+
<option value="on">Accent letter</option>
|
|
3465
|
+
</ManualSelect>
|
|
3466
|
+
</ManualField>
|
|
3467
|
+
<ManualField>
|
|
3468
|
+
Eyebrow
|
|
3469
|
+
<ManualInput
|
|
3470
|
+
value={manualEyebrow}
|
|
3471
|
+
placeholder="e.g. Essay · July 2026"
|
|
3472
|
+
onChange={event => {
|
|
3473
|
+
const eyebrowText = event.currentTarget.value
|
|
3474
|
+
setManualEyebrow(eyebrowText)
|
|
3475
|
+
applyManualTheme({ eyebrowText })
|
|
3476
|
+
}}
|
|
3477
|
+
/>
|
|
3478
|
+
</ManualField>
|
|
3479
|
+
<ManualField>
|
|
3480
|
+
Margins · top/bottom
|
|
3481
|
+
<ManualInput
|
|
3482
|
+
$mono
|
|
3483
|
+
value={manualMarginY}
|
|
3484
|
+
placeholder="e.g. 20mm"
|
|
3485
|
+
onChange={event => {
|
|
3486
|
+
const marginY = event.currentTarget.value
|
|
3487
|
+
setManualMarginY(marginY)
|
|
3488
|
+
applyManualTheme({ marginY })
|
|
3489
|
+
}}
|
|
3490
|
+
/>
|
|
3491
|
+
</ManualField>
|
|
3492
|
+
<ManualField>
|
|
3493
|
+
Margins · sides
|
|
3494
|
+
<ManualInput
|
|
3495
|
+
$mono
|
|
3496
|
+
value={manualMarginX}
|
|
3497
|
+
placeholder="e.g. 18mm"
|
|
3498
|
+
onChange={event => {
|
|
3499
|
+
const marginX = event.currentTarget.value
|
|
3500
|
+
setManualMarginX(marginX)
|
|
3501
|
+
applyManualTheme({ marginX })
|
|
3502
|
+
}}
|
|
3503
|
+
/>
|
|
3504
|
+
</ManualField>
|
|
3505
|
+
<ManualField>
|
|
3506
|
+
Columns
|
|
3507
|
+
<ManualSelect
|
|
3508
|
+
value={String(manualColumns)}
|
|
3509
|
+
onChange={event => {
|
|
3510
|
+
const columns =
|
|
3511
|
+
event.currentTarget.value === '2' ? 2 : (1 as const)
|
|
3512
|
+
setManualColumns(columns)
|
|
3513
|
+
applyManualTheme({ columns })
|
|
3514
|
+
}}
|
|
3515
|
+
>
|
|
3516
|
+
<option value="1">Single column</option>
|
|
3517
|
+
<option value="2">Two columns</option>
|
|
3518
|
+
</ManualSelect>
|
|
3519
|
+
</ManualField>
|
|
3520
|
+
<ManualField>
|
|
3521
|
+
Cover
|
|
3522
|
+
<ManualSelect
|
|
3523
|
+
value={manualCoverPage ? 'on' : 'off'}
|
|
3524
|
+
onChange={event => {
|
|
3525
|
+
const coverPage = event.currentTarget.value === 'on'
|
|
3526
|
+
setManualCoverPage(coverPage)
|
|
3527
|
+
applyManualTheme({ coverPage })
|
|
3528
|
+
}}
|
|
3529
|
+
>
|
|
3530
|
+
<option value="off">Off</option>
|
|
3531
|
+
<option value="on">Cover page</option>
|
|
3532
|
+
</ManualSelect>
|
|
3533
|
+
</ManualField>
|
|
3534
|
+
{manualCoverPage ? (
|
|
3535
|
+
<>
|
|
3536
|
+
<ManualField>
|
|
3537
|
+
Cover background
|
|
3538
|
+
<ManualInput
|
|
3539
|
+
$mono
|
|
3540
|
+
value={manualCoverBackground}
|
|
3541
|
+
placeholder="e.g. #10241E"
|
|
3542
|
+
onChange={event => {
|
|
3543
|
+
const coverBackground = event.currentTarget.value
|
|
3544
|
+
setManualCoverBackground(coverBackground)
|
|
3545
|
+
applyManualTheme({ coverBackground })
|
|
3546
|
+
}}
|
|
3547
|
+
/>
|
|
3548
|
+
</ManualField>
|
|
3549
|
+
<ManualField>
|
|
3550
|
+
Cover author
|
|
3551
|
+
<ManualInput
|
|
3552
|
+
value={manualCoverAuthor}
|
|
3553
|
+
placeholder="e.g. A. Hyde"
|
|
3554
|
+
onChange={event => {
|
|
3555
|
+
const coverAuthor = event.currentTarget.value
|
|
3556
|
+
setManualCoverAuthor(coverAuthor)
|
|
3557
|
+
applyManualTheme({ coverAuthor })
|
|
3558
|
+
}}
|
|
3559
|
+
/>
|
|
3560
|
+
</ManualField>
|
|
3561
|
+
<ManualField>
|
|
3562
|
+
Cover date
|
|
3563
|
+
<ManualInput
|
|
3564
|
+
value={manualCoverDate}
|
|
3565
|
+
placeholder="e.g. July 2026"
|
|
3566
|
+
onChange={event => {
|
|
3567
|
+
const coverDate = event.currentTarget.value
|
|
3568
|
+
setManualCoverDate(coverDate)
|
|
3569
|
+
applyManualTheme({ coverDate })
|
|
3570
|
+
}}
|
|
3571
|
+
/>
|
|
3572
|
+
</ManualField>
|
|
3573
|
+
<ManualField>
|
|
3574
|
+
Cover image
|
|
3575
|
+
<ReferenceButton
|
|
3576
|
+
type="button"
|
|
3577
|
+
onClick={() => coverImageInputRef.current?.click()}
|
|
3578
|
+
>
|
|
3579
|
+
{manualCoverImage ? 'Replace image' : 'Choose image'}
|
|
3580
|
+
</ReferenceButton>
|
|
3581
|
+
{manualCoverImage ? (
|
|
3582
|
+
<ReferenceButton
|
|
3583
|
+
type="button"
|
|
3584
|
+
onClick={() => {
|
|
3585
|
+
setManualCoverImage('')
|
|
3586
|
+
applyManualTheme({ coverImageDataUri: '' })
|
|
3587
|
+
}}
|
|
3588
|
+
>
|
|
3589
|
+
Remove image
|
|
3590
|
+
</ReferenceButton>
|
|
3591
|
+
) : null}
|
|
3592
|
+
<HiddenFile
|
|
3593
|
+
ref={coverImageInputRef}
|
|
3594
|
+
type="file"
|
|
3595
|
+
accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
|
3596
|
+
aria-label="Choose cover image"
|
|
3597
|
+
onChange={event => {
|
|
3598
|
+
const file = event.currentTarget.files?.[0]
|
|
3599
|
+
event.currentTarget.value = ''
|
|
3600
|
+
if (!file) return
|
|
3601
|
+
const reader = new FileReader()
|
|
3602
|
+
reader.onload = () => {
|
|
3603
|
+
const coverImageDataUri =
|
|
3604
|
+
typeof reader.result === 'string'
|
|
3605
|
+
? reader.result
|
|
3606
|
+
: ''
|
|
3607
|
+
if (!coverImageDataUri) return
|
|
3608
|
+
setManualCoverImage(coverImageDataUri)
|
|
3609
|
+
applyManualTheme({ coverImageDataUri })
|
|
3610
|
+
}
|
|
3611
|
+
reader.readAsDataURL(file)
|
|
3612
|
+
}}
|
|
3613
|
+
/>
|
|
3614
|
+
</ManualField>
|
|
3615
|
+
</>
|
|
3616
|
+
) : null}
|
|
3617
|
+
{manualCoverPage ? (
|
|
3618
|
+
// Contents implies a cover — no toc without one.
|
|
3619
|
+
<ManualField>
|
|
3620
|
+
Contents
|
|
3621
|
+
<ManualSelect
|
|
3622
|
+
value={manualToc ? 'on' : 'off'}
|
|
3623
|
+
onChange={event => {
|
|
3624
|
+
const toc = event.currentTarget.value === 'on'
|
|
3625
|
+
setManualToc(toc)
|
|
3626
|
+
applyManualTheme({ toc })
|
|
3627
|
+
}}
|
|
3628
|
+
>
|
|
3629
|
+
<option value="off">Off</option>
|
|
3630
|
+
<option value="on">Generated contents</option>
|
|
3631
|
+
</ManualSelect>
|
|
3632
|
+
</ManualField>
|
|
3633
|
+
) : null}
|
|
3634
|
+
<ManualField>
|
|
3635
|
+
Running header
|
|
3636
|
+
<ManualSelect
|
|
3637
|
+
value={manualRunningHeader}
|
|
3638
|
+
onChange={event => {
|
|
3639
|
+
const runningHeader = event.currentTarget.value as
|
|
3640
|
+
| 'none'
|
|
3641
|
+
| 'title'
|
|
3642
|
+
| 'section'
|
|
3643
|
+
| 'custom'
|
|
3644
|
+
setManualRunningHeader(runningHeader)
|
|
3645
|
+
applyManualTheme({ runningHeader })
|
|
3646
|
+
}}
|
|
3647
|
+
>
|
|
3648
|
+
<option value="none">None</option>
|
|
3649
|
+
<option value="title">Document title</option>
|
|
3650
|
+
<option value="section">Section titles</option>
|
|
3651
|
+
<option value="custom">Custom text</option>
|
|
3652
|
+
</ManualSelect>
|
|
3653
|
+
</ManualField>
|
|
3654
|
+
{manualRunningHeader === 'custom' ? (
|
|
3655
|
+
<ManualField>
|
|
3656
|
+
Header text
|
|
3657
|
+
<ManualInput
|
|
3658
|
+
value={manualRunningHeaderText}
|
|
3659
|
+
placeholder="e.g. Draft · Confidential"
|
|
3660
|
+
onChange={event => {
|
|
3661
|
+
const runningHeaderText = event.currentTarget.value
|
|
3662
|
+
setManualRunningHeaderText(runningHeaderText)
|
|
3663
|
+
applyManualTheme({ runningHeaderText })
|
|
3664
|
+
}}
|
|
3665
|
+
/>
|
|
3666
|
+
</ManualField>
|
|
3667
|
+
) : null}
|
|
3668
|
+
<ManualField>
|
|
3669
|
+
Footer
|
|
3670
|
+
<ManualSelect
|
|
3671
|
+
value={manualFolio === 'none' ? 'none' : manualFolioStyle}
|
|
3672
|
+
onChange={event => {
|
|
3673
|
+
const value = event.currentTarget.value
|
|
3674
|
+
if (value === 'none') {
|
|
3675
|
+
setManualFolio('none')
|
|
3676
|
+
applyManualTheme({ folio: 'none' })
|
|
3677
|
+
return
|
|
3678
|
+
}
|
|
3679
|
+
const folioStyle = value as
|
|
3680
|
+
| 'plain'
|
|
3681
|
+
| 'accent'
|
|
3682
|
+
| 'of-total'
|
|
3683
|
+
setManualFolio('page')
|
|
3684
|
+
setManualFolioStyle(folioStyle)
|
|
3685
|
+
applyManualTheme({ folio: 'page', folioStyle })
|
|
3686
|
+
}}
|
|
3687
|
+
>
|
|
3688
|
+
<option value="plain">Page number</option>
|
|
3689
|
+
<option value="accent">Accent number</option>
|
|
3690
|
+
<option value="of-total">Page X / Y</option>
|
|
3691
|
+
<option value="none">None</option>
|
|
3692
|
+
</ManualSelect>
|
|
3693
|
+
</ManualField>
|
|
3694
|
+
{manualFolio !== 'none' ? (
|
|
3695
|
+
<ManualField>
|
|
3696
|
+
Footer slot
|
|
3697
|
+
<ManualSelect
|
|
3698
|
+
value={manualFolioSlot}
|
|
3699
|
+
onChange={event => {
|
|
3700
|
+
const folioSlot = event.currentTarget.value as
|
|
3701
|
+
| 'bottom-center'
|
|
3702
|
+
| 'bottom-left'
|
|
3703
|
+
| 'bottom-right'
|
|
3704
|
+
| 'top-right'
|
|
3705
|
+
setManualFolioSlot(folioSlot)
|
|
3706
|
+
applyManualTheme({ folioSlot })
|
|
3707
|
+
}}
|
|
3708
|
+
>
|
|
3709
|
+
<option value="bottom-center">Bottom · center</option>
|
|
3710
|
+
<option value="bottom-left">Bottom · left</option>
|
|
3711
|
+
<option value="bottom-right">Bottom · right</option>
|
|
3712
|
+
<option value="top-right">Top · right</option>
|
|
3713
|
+
</ManualSelect>
|
|
3714
|
+
</ManualField>
|
|
3715
|
+
) : null}
|
|
3716
|
+
<ManualField>
|
|
3717
|
+
Tables
|
|
3718
|
+
<ManualSelect
|
|
3719
|
+
value={
|
|
3720
|
+
!manualTableGrid &&
|
|
3721
|
+
manualTableRule === 'hairline' &&
|
|
3722
|
+
manualTableHeader === 'default' &&
|
|
3723
|
+
manualTableDensity === 'regular'
|
|
3724
|
+
? 'quiet'
|
|
3725
|
+
: !manualTableGrid &&
|
|
3726
|
+
manualTableRule === 'light' &&
|
|
3727
|
+
manualTableHeader === 'plain' &&
|
|
3728
|
+
manualTableDensity === 'regular'
|
|
3729
|
+
? 'ruled'
|
|
3730
|
+
: manualTableGrid &&
|
|
3731
|
+
manualTableRule === 'light' &&
|
|
3732
|
+
manualTableHeader === 'shaded' &&
|
|
3733
|
+
manualTableDensity === 'compact'
|
|
3734
|
+
? 'grid'
|
|
3735
|
+
: !manualTableGrid &&
|
|
3736
|
+
manualTableRule === 'medium' &&
|
|
3737
|
+
manualTableHeader === 'accent' &&
|
|
3738
|
+
manualTableDensity === 'airy'
|
|
3739
|
+
? 'ledger'
|
|
3740
|
+
: 'custom'
|
|
3741
|
+
}
|
|
3742
|
+
onChange={event => {
|
|
3743
|
+
const presetsByName: Record<
|
|
3744
|
+
string,
|
|
3745
|
+
{
|
|
3746
|
+
tableRule: 'hairline' | 'light' | 'medium'
|
|
3747
|
+
tableHeader: 'default' | 'plain' | 'shaded' | 'accent'
|
|
3748
|
+
tableDensity: 'airy' | 'regular' | 'compact'
|
|
3749
|
+
tableGrid: boolean
|
|
3750
|
+
}
|
|
3751
|
+
> = {
|
|
3752
|
+
quiet: {
|
|
3753
|
+
tableRule: 'hairline',
|
|
3754
|
+
tableHeader: 'default',
|
|
3755
|
+
tableDensity: 'regular',
|
|
3756
|
+
tableGrid: false,
|
|
3757
|
+
},
|
|
3758
|
+
ruled: {
|
|
3759
|
+
tableRule: 'light',
|
|
3760
|
+
tableHeader: 'plain',
|
|
3761
|
+
tableDensity: 'regular',
|
|
3762
|
+
tableGrid: false,
|
|
3763
|
+
},
|
|
3764
|
+
grid: {
|
|
3765
|
+
tableRule: 'light',
|
|
3766
|
+
tableHeader: 'shaded',
|
|
3767
|
+
tableDensity: 'compact',
|
|
3768
|
+
tableGrid: true,
|
|
3769
|
+
},
|
|
3770
|
+
ledger: {
|
|
3771
|
+
tableRule: 'medium',
|
|
3772
|
+
tableHeader: 'accent',
|
|
3773
|
+
tableDensity: 'airy',
|
|
3774
|
+
tableGrid: false,
|
|
3775
|
+
},
|
|
3776
|
+
}
|
|
3777
|
+
const preset = presetsByName[event.currentTarget.value]
|
|
3778
|
+
if (!preset) return
|
|
3779
|
+
setManualTableRule(preset.tableRule)
|
|
3780
|
+
setManualTableHeader(preset.tableHeader)
|
|
3781
|
+
setManualTableDensity(preset.tableDensity)
|
|
3782
|
+
setManualTableGrid(preset.tableGrid)
|
|
3783
|
+
applyManualTheme(preset)
|
|
3784
|
+
}}
|
|
3785
|
+
>
|
|
3786
|
+
<option value="custom">Custom</option>
|
|
3787
|
+
<option value="quiet">Quiet (hairlines)</option>
|
|
3788
|
+
<option value="ruled">Ruled</option>
|
|
3789
|
+
<option value="grid">Bordered grid</option>
|
|
3790
|
+
<option value="ledger">Ledger (accent header)</option>
|
|
3791
|
+
</ManualSelect>
|
|
3792
|
+
</ManualField>
|
|
3793
|
+
<ManualField>
|
|
3794
|
+
Table borders
|
|
3795
|
+
<ManualSelect
|
|
3796
|
+
value={manualTableGrid ? 'grid' : manualTableRule}
|
|
3797
|
+
onChange={event => {
|
|
3798
|
+
const value = event.currentTarget.value
|
|
3799
|
+
if (value === 'grid') {
|
|
3800
|
+
setManualTableGrid(true)
|
|
3801
|
+
applyManualTheme({ tableGrid: true })
|
|
3802
|
+
return
|
|
3803
|
+
}
|
|
3804
|
+
const tableRule = value as
|
|
3805
|
+
| 'hairline'
|
|
3806
|
+
| 'light'
|
|
3807
|
+
| 'medium'
|
|
3808
|
+
setManualTableGrid(false)
|
|
3809
|
+
setManualTableRule(tableRule)
|
|
3810
|
+
applyManualTheme({ tableGrid: false, tableRule })
|
|
3811
|
+
}}
|
|
3812
|
+
>
|
|
3813
|
+
<option value="hairline">Hairline rules</option>
|
|
3814
|
+
<option value="light">Light rules</option>
|
|
3815
|
+
<option value="medium">Medium rules</option>
|
|
3816
|
+
<option value="grid">Full grid</option>
|
|
3817
|
+
</ManualSelect>
|
|
3818
|
+
</ManualField>
|
|
3819
|
+
<ManualField>
|
|
3820
|
+
Header row
|
|
3821
|
+
<ManualSelect
|
|
3822
|
+
value={manualTableHeader}
|
|
3823
|
+
onChange={event => {
|
|
3824
|
+
const tableHeader = event.currentTarget.value as
|
|
3825
|
+
| 'default'
|
|
3826
|
+
| 'plain'
|
|
3827
|
+
| 'shaded'
|
|
3828
|
+
| 'accent'
|
|
3829
|
+
setManualTableHeader(tableHeader)
|
|
3830
|
+
applyManualTheme({ tableHeader })
|
|
3831
|
+
}}
|
|
3832
|
+
>
|
|
3833
|
+
<option value="default">Small caps</option>
|
|
3834
|
+
<option value="plain">Plain</option>
|
|
3835
|
+
<option value="shaded">Shaded</option>
|
|
3836
|
+
<option value="accent">Accent fill</option>
|
|
3837
|
+
</ManualSelect>
|
|
3838
|
+
</ManualField>
|
|
3839
|
+
<ManualField>
|
|
3840
|
+
Table density
|
|
3841
|
+
<ManualSelect
|
|
3842
|
+
value={manualTableDensity}
|
|
3843
|
+
onChange={event => {
|
|
3844
|
+
const tableDensity = event.currentTarget.value as
|
|
3845
|
+
| 'airy'
|
|
3846
|
+
| 'regular'
|
|
3847
|
+
| 'compact'
|
|
3848
|
+
setManualTableDensity(tableDensity)
|
|
3849
|
+
applyManualTheme({ tableDensity })
|
|
3850
|
+
}}
|
|
3851
|
+
>
|
|
3852
|
+
<option value="airy">Airy</option>
|
|
3853
|
+
<option value="regular">Regular</option>
|
|
3854
|
+
<option value="compact">Compact</option>
|
|
3855
|
+
</ManualSelect>
|
|
3856
|
+
</ManualField>
|
|
3857
|
+
<ManualField>
|
|
3858
|
+
Logo
|
|
3859
|
+
<ReferenceButton
|
|
3860
|
+
type="button"
|
|
3861
|
+
onClick={() => logoFileInputRef.current?.click()}
|
|
3862
|
+
>
|
|
3863
|
+
{manualLogo ? 'Replace logo' : 'Choose logo'}
|
|
3864
|
+
</ReferenceButton>
|
|
3865
|
+
<HiddenFile
|
|
3866
|
+
ref={logoFileInputRef}
|
|
3867
|
+
type="file"
|
|
3868
|
+
accept="image/png,image/jpeg,image/webp,image/svg+xml"
|
|
3869
|
+
aria-label="Choose logo image"
|
|
3870
|
+
onChange={event => {
|
|
3871
|
+
const file = event.currentTarget.files?.[0]
|
|
3872
|
+
event.currentTarget.value = ''
|
|
3873
|
+
if (!file) return
|
|
3874
|
+
const reader = new FileReader()
|
|
3875
|
+
reader.onload = () => {
|
|
3876
|
+
const logoDataUri =
|
|
3877
|
+
typeof reader.result === 'string'
|
|
3878
|
+
? reader.result
|
|
3879
|
+
: ''
|
|
3880
|
+
if (!logoDataUri) return
|
|
3881
|
+
setManualLogo(logoDataUri)
|
|
3882
|
+
applyManualTheme({ logoDataUri })
|
|
3883
|
+
}
|
|
3884
|
+
reader.readAsDataURL(file)
|
|
3885
|
+
}}
|
|
3886
|
+
/>
|
|
3887
|
+
</ManualField>
|
|
3888
|
+
{manualLogo ? (
|
|
3889
|
+
<ManualField>
|
|
3890
|
+
Logo slot
|
|
3891
|
+
<ManualSelect
|
|
3892
|
+
value={manualLogoSlot}
|
|
3893
|
+
onChange={event => {
|
|
3894
|
+
const logoSlot = event.currentTarget.value as
|
|
3895
|
+
| 'top-left'
|
|
3896
|
+
| 'top-right'
|
|
3897
|
+
| 'bottom-left'
|
|
3898
|
+
| 'bottom-right'
|
|
3899
|
+
setManualLogoSlot(logoSlot)
|
|
3900
|
+
applyManualTheme({ logoSlot })
|
|
3901
|
+
}}
|
|
3902
|
+
>
|
|
3903
|
+
<option value="top-left">Header · left</option>
|
|
3904
|
+
<option value="top-right">Header · right</option>
|
|
3905
|
+
<option value="bottom-left">Footer · left</option>
|
|
3906
|
+
<option value="bottom-right">Footer · right</option>
|
|
3907
|
+
</ManualSelect>
|
|
3908
|
+
<ReferenceButton
|
|
3909
|
+
type="button"
|
|
3910
|
+
onClick={() => {
|
|
3911
|
+
setManualLogo('')
|
|
3912
|
+
applyManualTheme({ logoDataUri: '' })
|
|
3913
|
+
}}
|
|
3914
|
+
>
|
|
3915
|
+
Remove logo
|
|
3916
|
+
</ReferenceButton>
|
|
3917
|
+
</ManualField>
|
|
3918
|
+
) : null}
|
|
2562
3919
|
</ManualGrid>
|
|
2563
3920
|
</DesignSection>
|
|
2564
3921
|
) : null}
|
|
2565
3922
|
</WorkspaceControls>
|
|
2566
3923
|
|
|
2567
3924
|
<WorkspaceCanvas $variant={variant} $mode={mode}>
|
|
2568
|
-
{mode === 'vision' ? (
|
|
3925
|
+
{mode === 'vision' || variant === 'workspace' ? (
|
|
2569
3926
|
<VisionPanel
|
|
2570
3927
|
$variant={variant}
|
|
2571
3928
|
aria-label="Beta vision typesetting"
|
|
@@ -2644,6 +4001,143 @@ export function PrintDesignPanel({
|
|
|
2644
4001
|
</VisionProgressLabel>
|
|
2645
4002
|
</VisionProgressStrip>
|
|
2646
4003
|
) : null}
|
|
4004
|
+
{variant === 'workspace' && snapshotHistory.length > 0 ? (
|
|
4005
|
+
// Bidirectional views: the last run's annotated pages vs
|
|
4006
|
+
// the live preview. Style edits auto-flip to live and
|
|
4007
|
+
// mark the pages stale until the next Run.
|
|
4008
|
+
<WellViewSwitch aria-label="Well view">
|
|
4009
|
+
<WellViewButton
|
|
4010
|
+
type="button"
|
|
4011
|
+
$active={wellView === 'live'}
|
|
4012
|
+
onClick={() => setWellView('live')}
|
|
4013
|
+
>
|
|
4014
|
+
Live preview
|
|
4015
|
+
</WellViewButton>
|
|
4016
|
+
<WellViewButton
|
|
4017
|
+
type="button"
|
|
4018
|
+
$active={wellView === 'pages'}
|
|
4019
|
+
onClick={() => setWellView('pages')}
|
|
4020
|
+
>
|
|
4021
|
+
{stripStale ? 'Run pages · stale' : 'Run pages'}
|
|
4022
|
+
</WellViewButton>
|
|
4023
|
+
</WellViewSwitch>
|
|
4024
|
+
) : null}
|
|
4025
|
+
{snapshotHistory.length > 0 &&
|
|
4026
|
+
(variant !== 'workspace' || wellView === 'pages') ? (
|
|
4027
|
+
// The document, page by page: every checked page and fix
|
|
4028
|
+
// attempt appends an annotated card (screenshot + issue /
|
|
4029
|
+
// fixed chips + inspector comment). Auto-follows the
|
|
4030
|
+
// newest card while the sweep runs; scrollable after.
|
|
4031
|
+
<VisionPagesScroll
|
|
4032
|
+
ref={snapshotHistoryRef}
|
|
4033
|
+
aria-label="Page snapshot history"
|
|
4034
|
+
>
|
|
4035
|
+
{snapshotHistory.map(entry => (
|
|
4036
|
+
<VisionPageCard key={entry.id}>
|
|
4037
|
+
<VisionPageImageButton
|
|
4038
|
+
type="button"
|
|
4039
|
+
aria-label={entry.alt ?? entry.label}
|
|
4040
|
+
onClick={() =>
|
|
4041
|
+
setZoomedPage({
|
|
4042
|
+
url: entry.url,
|
|
4043
|
+
alt: entry.alt ?? entry.label,
|
|
4044
|
+
})
|
|
4045
|
+
}
|
|
4046
|
+
>
|
|
4047
|
+
<img src={entry.url} alt={entry.alt ?? entry.label} />
|
|
4048
|
+
</VisionPageImageButton>
|
|
4049
|
+
<VisionPageAnnotations>
|
|
4050
|
+
<SnapshotHistoryLabel>
|
|
4051
|
+
{entry.label}
|
|
4052
|
+
</SnapshotHistoryLabel>
|
|
4053
|
+
<SnapshotIssueList
|
|
4054
|
+
aria-label={`${entry.label} issues`}
|
|
4055
|
+
>
|
|
4056
|
+
{entry.issues?.length ? (
|
|
4057
|
+
entry.issues.map(issue => (
|
|
4058
|
+
<SnapshotIssueRow key={issue.id}>
|
|
4059
|
+
<DecisionType
|
|
4060
|
+
$type={
|
|
4061
|
+
issue.state === 'fixed' ? 'fix' : 'issue'
|
|
4062
|
+
}
|
|
4063
|
+
>
|
|
4064
|
+
{issue.state === 'fixed'
|
|
4065
|
+
? 'fixed'
|
|
4066
|
+
: 'issue'}
|
|
4067
|
+
</DecisionType>
|
|
4068
|
+
<SnapshotIssueLabel
|
|
4069
|
+
$fixed={issue.state === 'fixed'}
|
|
4070
|
+
>
|
|
4071
|
+
{issue.label}
|
|
4072
|
+
</SnapshotIssueLabel>
|
|
4073
|
+
</SnapshotIssueRow>
|
|
4074
|
+
))
|
|
4075
|
+
) : (
|
|
4076
|
+
<SnapshotIssueRow>
|
|
4077
|
+
<DecisionType $type="pass">pass</DecisionType>
|
|
4078
|
+
<SnapshotIssueLabel $fixed={false}>
|
|
4079
|
+
no layout issues
|
|
4080
|
+
</SnapshotIssueLabel>
|
|
4081
|
+
</SnapshotIssueRow>
|
|
4082
|
+
)}
|
|
4083
|
+
</SnapshotIssueList>
|
|
4084
|
+
{entry.note ? (
|
|
4085
|
+
<VisionPageNote>{entry.note}</VisionPageNote>
|
|
4086
|
+
) : null}
|
|
4087
|
+
</VisionPageAnnotations>
|
|
4088
|
+
</VisionPageCard>
|
|
4089
|
+
))}
|
|
4090
|
+
</VisionPagesScroll>
|
|
4091
|
+
) : variant === 'workspace' &&
|
|
4092
|
+
(snapshotHistory.length > 0 ||
|
|
4093
|
+
(visionEvidence.length === 0 &&
|
|
4094
|
+
!visionTypesetting?.snapshotUrl)) ? (
|
|
4095
|
+
// Merged view, live face: the REAL Paged.js preview when
|
|
4096
|
+
// the host supplies one (all pages, scrollable, adopted
|
|
4097
|
+
// fixes included); the styled facsimile until then.
|
|
4098
|
+
<>
|
|
4099
|
+
<SectionHeader>
|
|
4100
|
+
<SectionTitle>
|
|
4101
|
+
{livePreview?.html
|
|
4102
|
+
? `Live preview${
|
|
4103
|
+
livePreview.pageCount
|
|
4104
|
+
? ` · ${livePreview.pageCount} page(s)`
|
|
4105
|
+
: ''
|
|
4106
|
+
}`
|
|
4107
|
+
: 'Preview'}
|
|
4108
|
+
</SectionTitle>
|
|
4109
|
+
{livePreview?.html ? null : (
|
|
4110
|
+
<PreviewSampleToggle
|
|
4111
|
+
data-disabled={!hasHostPreviewContent}
|
|
4112
|
+
>
|
|
4113
|
+
<input
|
|
4114
|
+
type="checkbox"
|
|
4115
|
+
checked={previewUsesSample}
|
|
4116
|
+
disabled={!hasHostPreviewContent}
|
|
4117
|
+
onChange={event =>
|
|
4118
|
+
setUseSampleContent(event.currentTarget.checked)
|
|
4119
|
+
}
|
|
4120
|
+
/>
|
|
4121
|
+
Sample
|
|
4122
|
+
</PreviewSampleToggle>
|
|
4123
|
+
)}
|
|
4124
|
+
<StatusText title={displayedStatus}>
|
|
4125
|
+
{livePreview?.loading
|
|
4126
|
+
? 'Laying out pages…'
|
|
4127
|
+
: displayedStatus}
|
|
4128
|
+
</StatusText>
|
|
4129
|
+
</SectionHeader>
|
|
4130
|
+
{livePreview?.html ? (
|
|
4131
|
+
<LivePreviewFrame
|
|
4132
|
+
sandbox=""
|
|
4133
|
+
srcDoc={livePreviewDocument(livePreview.html)}
|
|
4134
|
+
title="Live paged preview"
|
|
4135
|
+
/>
|
|
4136
|
+
) : (
|
|
4137
|
+
facsimilePreview
|
|
4138
|
+
)}
|
|
4139
|
+
</>
|
|
4140
|
+
) : (
|
|
2647
4141
|
<VisionSnapshotGrid $hasEvidence={visionEvidence.length > 0}>
|
|
2648
4142
|
<VisionSnapshotFrame>
|
|
2649
4143
|
<VisionSnapshotLabel>
|
|
@@ -2685,10 +4179,17 @@ export function PrintDesignPanel({
|
|
|
2685
4179
|
))}
|
|
2686
4180
|
</VisionEvidenceList>
|
|
2687
4181
|
) : visionTypesetting?.snapshotUrl ? (
|
|
2688
|
-
|
|
4182
|
+
<>
|
|
2689
4183
|
<VisionSnapshotButton
|
|
2690
4184
|
type="button"
|
|
2691
|
-
onClick={
|
|
4185
|
+
onClick={() =>
|
|
4186
|
+
setZoomedPage({
|
|
4187
|
+
url: visionTypesetting.snapshotUrl!,
|
|
4188
|
+
alt:
|
|
4189
|
+
visionTypesetting.snapshotAlt ??
|
|
4190
|
+
'Visual typesetting page snapshot',
|
|
4191
|
+
})
|
|
4192
|
+
}
|
|
2692
4193
|
aria-label="Open page snapshot preview"
|
|
2693
4194
|
>
|
|
2694
4195
|
<VisionSnapshotImage
|
|
@@ -2699,15 +4200,27 @@ export function PrintDesignPanel({
|
|
|
2699
4200
|
}
|
|
2700
4201
|
/>
|
|
2701
4202
|
</VisionSnapshotButton>
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
4203
|
+
{visionTypesetting.snapshotIssues?.length ? (
|
|
4204
|
+
<SnapshotIssueList aria-label="Latest page issues">
|
|
4205
|
+
{visionTypesetting.snapshotIssues.map(issue => (
|
|
4206
|
+
<SnapshotIssueRow key={issue.id}>
|
|
4207
|
+
<DecisionType
|
|
4208
|
+
$type={
|
|
4209
|
+
issue.state === 'fixed' ? 'fix' : 'issue'
|
|
4210
|
+
}
|
|
4211
|
+
>
|
|
4212
|
+
{issue.state === 'fixed' ? 'fixed' : 'issue'}
|
|
4213
|
+
</DecisionType>
|
|
4214
|
+
<SnapshotIssueLabel
|
|
4215
|
+
$fixed={issue.state === 'fixed'}
|
|
4216
|
+
>
|
|
4217
|
+
{issue.label}
|
|
4218
|
+
</SnapshotIssueLabel>
|
|
4219
|
+
</SnapshotIssueRow>
|
|
4220
|
+
))}
|
|
4221
|
+
</SnapshotIssueList>
|
|
4222
|
+
) : null}
|
|
4223
|
+
</>
|
|
2711
4224
|
) : (
|
|
2712
4225
|
<VisionSnapshotPage aria-label="Sample PDF page snapshot">
|
|
2713
4226
|
<VisionPageHeading />
|
|
@@ -2740,6 +4253,7 @@ export function PrintDesignPanel({
|
|
|
2740
4253
|
</VisionSnapshotCopy>
|
|
2741
4254
|
)}
|
|
2742
4255
|
</VisionSnapshotGrid>
|
|
4256
|
+
)}
|
|
2743
4257
|
</VisionEvidenceColumn>
|
|
2744
4258
|
</VisionPanel>
|
|
2745
4259
|
) : null}
|
|
@@ -2827,21 +4341,70 @@ export function PrintDesignPanel({
|
|
|
2827
4341
|
</WorkspaceFooter>
|
|
2828
4342
|
</DesignMain>
|
|
2829
4343
|
|
|
2830
|
-
{variant
|
|
4344
|
+
{variant === 'workspace' ? (
|
|
4345
|
+
// Merged view's right rail: the run summary / design ledger.
|
|
2831
4346
|
<DesignReadout $variant={variant} aria-label="Live design readout">
|
|
2832
4347
|
<DesignSection $variant={variant}>
|
|
2833
4348
|
<SectionHeader>
|
|
2834
|
-
<SectionTitle>
|
|
4349
|
+
<SectionTitle>Activity</SectionTitle>
|
|
2835
4350
|
<StatusText title={displayedStatus}>{displayedStatus}</StatusText>
|
|
2836
4351
|
</SectionHeader>
|
|
2837
4352
|
{displayedReadoutEntries.length > 0 ? (
|
|
2838
|
-
<
|
|
4353
|
+
<ActivityScroll $tall>
|
|
4354
|
+
<DecisionReadout entries={displayedReadoutEntries} />
|
|
4355
|
+
</ActivityScroll>
|
|
4356
|
+
) : (
|
|
4357
|
+
<StatusText>No design decisions yet.</StatusText>
|
|
4358
|
+
)}
|
|
4359
|
+
</DesignSection>
|
|
4360
|
+
</DesignReadout>
|
|
4361
|
+
) : null}
|
|
4362
|
+
{variant === 'popover' && mode !== 'vision' ? (
|
|
4363
|
+
<DesignReadout $variant={variant} aria-label="Live design readout">
|
|
4364
|
+
<DesignSection $variant={variant}>
|
|
4365
|
+
<SectionHeader>
|
|
4366
|
+
<SectionTitle>Preview</SectionTitle>
|
|
4367
|
+
<PreviewSampleToggle data-disabled={!hasHostPreviewContent}>
|
|
4368
|
+
<input
|
|
4369
|
+
type="checkbox"
|
|
4370
|
+
checked={previewUsesSample}
|
|
4371
|
+
disabled={!hasHostPreviewContent}
|
|
4372
|
+
onChange={event =>
|
|
4373
|
+
setUseSampleContent(event.currentTarget.checked)
|
|
4374
|
+
}
|
|
4375
|
+
/>
|
|
4376
|
+
Sample
|
|
4377
|
+
</PreviewSampleToggle>
|
|
4378
|
+
<StatusText title={displayedStatus}>
|
|
4379
|
+
{displayedStatus}
|
|
4380
|
+
</StatusText>
|
|
4381
|
+
</SectionHeader>
|
|
4382
|
+
{facsimilePreview}
|
|
4383
|
+
</DesignSection>
|
|
4384
|
+
<DesignSection $variant={variant}>
|
|
4385
|
+
<SectionHeader>
|
|
4386
|
+
<SectionTitle>Activity</SectionTitle>
|
|
4387
|
+
</SectionHeader>
|
|
4388
|
+
{displayedReadoutEntries.length > 0 ? (
|
|
4389
|
+
<ActivityScroll $tall={false}>
|
|
4390
|
+
<DecisionReadout entries={displayedReadoutEntries} />
|
|
4391
|
+
</ActivityScroll>
|
|
2839
4392
|
) : (
|
|
2840
4393
|
<StatusText>No design decisions yet.</StatusText>
|
|
2841
4394
|
)}
|
|
2842
4395
|
</DesignSection>
|
|
2843
4396
|
</DesignReadout>
|
|
2844
4397
|
) : null}
|
|
4398
|
+
{zoomedPage ? (
|
|
4399
|
+
// In-window zoom: click anywhere (or Esc) to dismiss.
|
|
4400
|
+
<PageZoomOverlay
|
|
4401
|
+
role="dialog"
|
|
4402
|
+
aria-label="Page snapshot zoom"
|
|
4403
|
+
onClick={() => setZoomedPage(null)}
|
|
4404
|
+
>
|
|
4405
|
+
<PageZoomImage src={zoomedPage.url} alt={zoomedPage.alt} />
|
|
4406
|
+
</PageZoomOverlay>
|
|
4407
|
+
) : null}
|
|
2845
4408
|
</Panel>
|
|
2846
4409
|
)
|
|
2847
4410
|
}
|