@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.
Files changed (64) hide show
  1. package/package.json +55 -1
  2. package/src/bridge/agentTypes.ts +21 -1
  3. package/src/bridge/collectionImagePaste.ts +19 -1
  4. package/src/bridge/context.mjs +32 -0
  5. package/src/bridge/documents.d.mts +76 -0
  6. package/src/bridge/documents.mjs +40 -0
  7. package/src/bridge/methods.d.mts +23 -0
  8. package/src/bridge/methods.mjs +23 -0
  9. package/src/bridge/pureRender/base.css +44 -0
  10. package/src/bridge/pureRender/document.ts +29 -2
  11. package/src/bridge/pureRender/index.ts +3 -0
  12. package/src/bridge/pureRender/normalize.ts +167 -0
  13. package/src/bridge/pureRender/sanitizeExportBody.test.ts +51 -0
  14. package/src/bridge/pureRender/sanitizeExportBody.ts +61 -0
  15. package/src/bridge/pureRender/styles.ts +125 -5
  16. package/src/bridge/pureRender/theme.test.ts +259 -0
  17. package/src/bridge/pureRender/theme.ts +19 -1
  18. package/src/bridge/pureRender/types.ts +33 -0
  19. package/src/bridge/pureRender/visualLayoutInspection.test.ts +26 -0
  20. package/src/bridge/pureRender/visualLayoutInspection.ts +75 -0
  21. package/src/bridge/react/useDocumentHotkeys.ts +41 -0
  22. package/src/bridge/react/useDocumentLifecycle.tsx +359 -0
  23. package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -0
  24. package/src/bridge/react/usePlatformAppSettings.tsx +104 -0
  25. package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -0
  26. package/src/bridge/react/usePlatformJsonStore.tsx +149 -0
  27. package/src/bridge/storage.d.mts +6 -6
  28. package/src/bridge/storage.test.ts +6 -6
  29. package/src/bridge/types.ts +7 -0
  30. package/src/components/agents/AgentDrawerPanel.tsx +2 -0
  31. package/src/components/agents/AgentMessageList.tsx +6 -1
  32. package/src/components/agents/AgentToolCallCard.tsx +20 -20
  33. package/src/components/agents/ContextPicker.tsx +239 -0
  34. package/src/components/agents/agentPanelStyles.ts +3 -2
  35. package/src/components/agents/agentTypes.ts +2 -0
  36. package/src/components/chrome/WorkspaceTabStrip.tsx +18 -1
  37. package/src/components/chrome/workspaceTabTypes.ts +2 -0
  38. package/src/components/common/containers/AppFrame.test.tsx +4 -4
  39. package/src/components/common/containers/AppFrame.tsx +10 -1
  40. package/src/components/common/containers/AppHeader.tsx +22 -0
  41. package/src/components/common/documents/DocumentHeaderActions.tsx +206 -0
  42. package/src/components/common/documents/DocumentSwitcher.tsx +1176 -0
  43. package/src/components/common/documents/index.ts +8 -0
  44. package/src/components/common/research/EvidenceDossier.tsx +1232 -150
  45. package/src/components/common/research/index.ts +2 -0
  46. package/src/components/print-design/PrintDesignPanel.test.tsx +430 -40
  47. package/src/components/print-design/PrintDesignPanel.tsx +1770 -207
  48. package/src/components/print-design/index.ts +2 -0
  49. package/src/components/print-design/previewContent.test.ts +32 -0
  50. package/src/components/print-design/previewContent.ts +31 -0
  51. package/src/context/buildContextDocument.test.ts +240 -0
  52. package/src/context/buildContextDocument.ts +309 -0
  53. package/src/context/contextAccess.ts +66 -0
  54. package/src/context/contextConfig.ts +72 -0
  55. package/src/context/contextDocument.test.ts +155 -0
  56. package/src/context/contextDocument.ts +300 -0
  57. package/src/context/contextSections.test.ts +88 -0
  58. package/src/context/contextSections.ts +84 -0
  59. package/src/context/htmlToContextMarkdown.test.ts +134 -0
  60. package/src/context/htmlToContextMarkdown.ts +369 -0
  61. package/src/theme/appAccents.test.ts +36 -35
  62. package/src/theme/appAccents.ts +87 -104
  63. package/src/theme/appIdentityCss.mjs +169 -231
  64. package/src/theme/appIdentityCss.ts +213 -233
@@ -8,6 +8,11 @@ export interface AppHeaderProps {
8
8
  manifest: PlatformAppManifest
9
9
  /** Optional slot for the app's tools — the header earns its row. */
10
10
  actions?: React.ReactNode
11
+ /**
12
+ * Convention: the open document's file name, shown beside the wordmark
13
+ * (like any document-based app's window title).
14
+ */
15
+ documentName?: React.ReactNode
11
16
  }
12
17
 
13
18
  /**
@@ -28,6 +33,21 @@ const StyledHeader = styled.header`
28
33
  background: var(--pure-chrome-wordmark-bar, #fbfbfc);
29
34
  `
30
35
 
36
+ const DocumentName = styled.span`
37
+ overflow: hidden;
38
+ max-width: 36ch;
39
+ color: var(--platform-colors-text-muted, rgb(46 51 56 / 62%));
40
+ font-size: 12px;
41
+ text-overflow: ellipsis;
42
+ white-space: nowrap;
43
+
44
+ &::before {
45
+ content: '·';
46
+ margin-right: var(--platform-spacing-sm, 8px);
47
+ color: var(--platform-colors-text-muted, rgb(46 51 56 / 38%));
48
+ }
49
+ `
50
+
31
51
  const ActionSlot = styled.div`
32
52
  display: flex;
33
53
  align-items: center;
@@ -49,6 +69,7 @@ export const APP_HEADER_ACTIONS_PORTAL_ID = 'purescience-app-header-actions'
49
69
  export function AppHeader({
50
70
  manifest,
51
71
  actions,
72
+ documentName,
52
73
  }: AppHeaderProps): React.ReactElement {
53
74
  return (
54
75
  <StyledHeader>
@@ -57,6 +78,7 @@ export function AppHeader({
57
78
  size="sm"
58
79
  accentName
59
80
  />
81
+ {documentName ? <DocumentName>{documentName}</DocumentName> : null}
60
82
  <ActionSlot id={APP_HEADER_ACTIONS_PORTAL_ID}>{actions}</ActionSlot>
61
83
  </StyledHeader>
62
84
  )
@@ -0,0 +1,206 @@
1
+ import { useCallback, type ReactElement } from 'react'
2
+ import { styled } from 'styled-components'
3
+ import { MenuButtonDropdown } from '../dropdown/MenuButtonDropdown.js'
4
+ import type { MenuActionItem, MenuEntry } from '../dropdown/menuTypes.js'
5
+ import type { DocumentLifecycle } from '../../../bridge/react/useDocumentLifecycle.js'
6
+ import { bridge } from '../../../bridge/client.mjs'
7
+ import { PLATFORM_BRIDGE_METHODS } from '../../../bridge/methods.mjs'
8
+ import { suggestPlatformDocumentLocation } from '../../../bridge/documents.mjs'
9
+
10
+ export interface DocumentHeaderActionsProps {
11
+ lifecycle: DocumentLifecycle
12
+ /** Current document title, used when a draft is named-and-filed. */
13
+ title: string
14
+ /** App's own menu entries, appended after the shared document actions. */
15
+ menuItems?: MenuEntry[]
16
+ onSelectMenuItem?: (item: MenuActionItem) => void
17
+ /** Open the app's DocumentSwitcher (also reachable via ⌘O). */
18
+ onOpenSwitcher?: () => void
19
+ /** Explicit save handler for filed documents; defaults to flush(). */
20
+ onSave?: () => void
21
+ saveLabel?: string
22
+ menuLabel?: string
23
+ }
24
+
25
+ function savedLabel(savedAt: string | null): string {
26
+ if (!savedAt) return 'Saved'
27
+ return `Saved ${new Date(savedAt).toLocaleTimeString([], {
28
+ hour: '2-digit',
29
+ minute: '2-digit',
30
+ })}`
31
+ }
32
+
33
+ /**
34
+ * The shared document lifecycle cluster for an app's existing header row
35
+ * (AppFrame `headerActions`): quiet autosave status · filing-location chip ·
36
+ * a dynamic Save / "Name & file" action · the app's menu with the shared
37
+ * document actions merged in. One component, no extra bars.
38
+ */
39
+ export function DocumentHeaderActions({
40
+ lifecycle,
41
+ title,
42
+ menuItems = [],
43
+ onSelectMenuItem,
44
+ onOpenSwitcher,
45
+ onSave,
46
+ saveLabel = 'Save',
47
+ menuLabel = 'Options',
48
+ }: DocumentHeaderActionsProps): ReactElement {
49
+ const { doc } = lifecycle
50
+
51
+ // The location chip already says "Drafts" — the status only reports save
52
+ // state, never duplicates location.
53
+ const statusText = doc.error
54
+ ? doc.error
55
+ : doc.saving
56
+ ? 'Saving…'
57
+ : doc.status === 'none'
58
+ ? ''
59
+ : savedLabel(doc.savedAt)
60
+
61
+ const locationLabel =
62
+ doc.status === 'draft'
63
+ ? 'Drafts'
64
+ : doc.status === 'filed'
65
+ ? doc.path?.split('/').slice(-2, -1)[0] ?? ''
66
+ : ''
67
+
68
+ const moveDocument = useCallback(async () => {
69
+ const suggestion = await suggestPlatformDocumentLocation().catch(() => null)
70
+ const picked = await bridge.call<{ path: string | null }>(
71
+ PLATFORM_BRIDGE_METHODS.DIALOG_OPEN_FOLDER,
72
+ [
73
+ {
74
+ title: 'Choose a folder for this document',
75
+ defaultPath: suggestion?.dir,
76
+ },
77
+ ],
78
+ )
79
+ if (!picked?.path) return
80
+ await lifecycle.promote(title || 'Untitled', picked.path)
81
+ }, [lifecycle, title])
82
+
83
+ const documentMenuItems: MenuEntry[] = [
84
+ ...(onOpenSwitcher
85
+ ? [{ id: 'document-open', label: 'Open…', shortcut: '⌘O' }]
86
+ : []),
87
+ { id: 'document-move', label: 'Move…', disabled: !doc.path },
88
+ { id: 'document-duplicate', label: 'Duplicate', disabled: !doc.path },
89
+ {
90
+ id: 'document-reveal-files',
91
+ label: 'Reveal in PureFiles',
92
+ disabled: !doc.path,
93
+ },
94
+ {
95
+ id: 'document-reveal-finder',
96
+ label: 'Find in Finder',
97
+ disabled: !doc.path,
98
+ },
99
+ ]
100
+ const mergedItems: MenuEntry[] = menuItems.length
101
+ ? [...documentMenuItems, { type: 'divider' }, ...menuItems]
102
+ : documentMenuItems
103
+
104
+ const handleSelect = useCallback(
105
+ (item: MenuActionItem) => {
106
+ if (item.id === 'document-open') onOpenSwitcher?.()
107
+ else if (item.id === 'document-move') void moveDocument()
108
+ else if (item.id === 'document-duplicate') void lifecycle.duplicate()
109
+ else if (item.id === 'document-reveal-files') void lifecycle.openInFiles()
110
+ else if (item.id === 'document-reveal-finder')
111
+ void lifecycle.revealInFinder()
112
+ else onSelectMenuItem?.(item)
113
+ },
114
+ [lifecycle, moveDocument, onOpenSwitcher, onSelectMenuItem],
115
+ )
116
+
117
+ const handleSave = useCallback(() => {
118
+ if (doc.status === 'draft') {
119
+ void lifecycle.promote(title || 'Untitled')
120
+ return
121
+ }
122
+ if (onSave) onSave()
123
+ else void lifecycle.flush()
124
+ }, [doc.status, lifecycle, onSave, title])
125
+
126
+ return (
127
+ <Cluster role="group" aria-label="Document">
128
+ {statusText ? (
129
+ <StatusText $error={Boolean(doc.error)} aria-live="polite">
130
+ {statusText}
131
+ </StatusText>
132
+ ) : null}
133
+ {locationLabel ? (
134
+ <LocationChip
135
+ type="button"
136
+ title={doc.path ?? undefined}
137
+ onClick={() => void moveDocument()}
138
+ >
139
+ {locationLabel}
140
+ </LocationChip>
141
+ ) : null}
142
+ <SaveAction type="button" onClick={handleSave}>
143
+ {doc.status === 'draft' ? 'Name & file' : saveLabel}
144
+ </SaveAction>
145
+ <MenuButtonDropdown
146
+ label={menuLabel}
147
+ items={mergedItems}
148
+ variant="subtle"
149
+ size="sm"
150
+ onSelectItem={handleSelect}
151
+ />
152
+ </Cluster>
153
+ )
154
+ }
155
+
156
+ //#region styled-components
157
+
158
+ const Cluster = styled.div`
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 10px;
162
+ `
163
+
164
+ const StatusText = styled.span<{ $error?: boolean }>`
165
+ color: ${props =>
166
+ props.$error
167
+ ? 'var(--platform-colors-danger, #b3261e)'
168
+ : 'var(--platform-colors-text-muted, rgb(46 51 56 / 60%))'};
169
+ font-size: 11.5px;
170
+ white-space: nowrap;
171
+ `
172
+
173
+ const LocationChip = styled.button`
174
+ padding: 1px 9px;
175
+ border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 16%));
176
+ border-radius: 999px;
177
+ background: transparent;
178
+ color: var(--platform-colors-text-muted, rgb(46 51 56 / 66%));
179
+ font-family: var(--platform-typography-font-family-mono, monospace);
180
+ font-size: 10.5px;
181
+ letter-spacing: 0.03em;
182
+ cursor: pointer;
183
+
184
+ &:hover {
185
+ border-color: var(--platform-colors-accent, #0067b8);
186
+ color: var(--platform-colors-text, #1c1a17);
187
+ }
188
+ `
189
+
190
+ const SaveAction = styled.button`
191
+ padding: 0;
192
+ border: 0;
193
+ background: transparent;
194
+ color: var(--platform-colors-text-muted, rgb(46 51 56 / 72%));
195
+ font-size: 12px;
196
+ font-weight: 700;
197
+ letter-spacing: 0.06em;
198
+ text-transform: uppercase;
199
+ cursor: pointer;
200
+
201
+ &:hover {
202
+ color: var(--platform-colors-text, #1c1a17);
203
+ }
204
+ `
205
+
206
+ //#endregion