@parallelworks/ui 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +197 -17
  2. package/dist/ai/agent/toolStrings.d.ts +2 -0
  3. package/dist/ai/agent/toolStrings.js +20 -18
  4. package/dist/ai/ai.css +17 -3
  5. package/dist/ai/components/AllocationSelector.js +5 -1
  6. package/dist/ai/components/AttachmentManager.js +51 -42
  7. package/dist/ai/components/AttachmentUpload.d.ts +4 -2
  8. package/dist/ai/components/AttachmentUpload.js +9 -9
  9. package/dist/ai/components/BlockedGroupBanner.js +8 -14
  10. package/dist/ai/components/BranchNavigator.js +11 -12
  11. package/dist/ai/components/ChatEmptyState.js +12 -19
  12. package/dist/ai/components/ChatInput.d.ts +4 -3
  13. package/dist/ai/components/ChatInput.js +27 -13
  14. package/dist/ai/components/ChatMessage.js +45 -121
  15. package/dist/ai/components/ChatMessageList.d.ts +5 -2
  16. package/dist/ai/components/ChatMessageList.js +89 -51
  17. package/dist/ai/components/ChatSidebar.js +11 -7
  18. package/dist/ai/components/ChatThread.js +1 -13
  19. package/dist/ai/components/DragOverlay.js +3 -3
  20. package/dist/ai/components/MessageAttachments.d.ts +10 -0
  21. package/dist/ai/components/MessageAttachments.js +78 -0
  22. package/dist/ai/components/ModelSelector.js +3 -3
  23. package/dist/ai/components/PastedText.js +2 -2
  24. package/dist/ai/components/ProviderIssueBanner.js +19 -23
  25. package/dist/ai/components/ShareDialog.js +2 -2
  26. package/dist/ai/components/agent/ActivityLine.d.ts +0 -2
  27. package/dist/ai/components/agent/ActivityLine.js +8 -51
  28. package/dist/ai/components/agent/AgentMessageParts.js +114 -53
  29. package/dist/ai/core/ChatProvider.d.ts +16 -5
  30. package/dist/ai/core/ChatProvider.js +41 -8
  31. package/dist/ai/core/config.js +4 -3
  32. package/dist/ai/index.d.ts +1 -1
  33. package/dist/ai/index.js +3 -3
  34. package/dist/ai/strings.d.ts +29 -3
  35. package/dist/ai/strings.js +80 -26
  36. package/dist/ai/types.d.ts +4 -0
  37. package/dist/ai/ui/Notice.d.ts +2 -2
  38. package/dist/ai/ui/Notice.js +29 -62
  39. package/dist/ai/ui/sidebar.d.ts +5 -3
  40. package/dist/ai/ui/sidebar.js +22 -10
  41. package/dist/components/ActionMenu.d.ts +1 -0
  42. package/dist/components/ActionMenu.js +6 -1
  43. package/dist/components/AppLink.d.ts +26 -0
  44. package/dist/components/AppLink.js +51 -0
  45. package/dist/components/Avatar.d.ts +2 -1
  46. package/dist/components/Avatar.js +4 -2
  47. package/dist/components/Button.d.ts +15 -5
  48. package/dist/components/Button.js +47 -40
  49. package/dist/components/Callout.d.ts +31 -7
  50. package/dist/components/Callout.js +50 -19
  51. package/dist/components/Card.d.ts +11 -6
  52. package/dist/components/Card.js +14 -8
  53. package/dist/components/Checkbox.js +2 -1
  54. package/dist/components/ColorInput.js +5 -3
  55. package/dist/components/Combobox.js +49 -19
  56. package/dist/components/ConfirmModal.d.ts +19 -8
  57. package/dist/components/ConfirmModal.js +31 -53
  58. package/dist/components/CopyButton.d.ts +38 -0
  59. package/dist/components/CopyButton.js +68 -0
  60. package/dist/components/CreateModal.js +118 -111
  61. package/dist/components/DatePicker.js +12 -8
  62. package/dist/components/DescriptionList.d.ts +25 -5
  63. package/dist/components/DescriptionList.js +56 -22
  64. package/dist/components/Disclosure.js +1 -1
  65. package/dist/components/Drawer.js +10 -16
  66. package/dist/components/Field.d.ts +16 -5
  67. package/dist/components/Field.js +63 -8
  68. package/dist/components/IconButton.d.ts +14 -5
  69. package/dist/components/IconButton.js +25 -14
  70. package/dist/components/InfoTip.d.ts +12 -1
  71. package/dist/components/InfoTip.js +13 -2
  72. package/dist/components/Input.d.ts +14 -8
  73. package/dist/components/Input.js +46 -31
  74. package/dist/components/Loader.js +1 -1
  75. package/dist/components/MarkdownImpl.d.ts +1 -0
  76. package/dist/components/MarkdownImpl.js +1 -0
  77. package/dist/components/MenuEntries.d.ts +4 -0
  78. package/dist/components/MenuEntries.js +11 -12
  79. package/dist/components/Meter.js +5 -4
  80. package/dist/components/Modal.d.ts +28 -4
  81. package/dist/components/Modal.js +37 -14
  82. package/dist/components/Overview.d.ts +20 -12
  83. package/dist/components/Overview.js +14 -9
  84. package/dist/components/PageHeader.d.ts +47 -65
  85. package/dist/components/PageHeader.js +275 -153
  86. package/dist/components/PageHeaderAction.d.ts +67 -0
  87. package/dist/components/PageHeaderAction.js +107 -0
  88. package/dist/components/Pagination.js +6 -16
  89. package/dist/components/Popover.d.ts +4 -1
  90. package/dist/components/Popover.js +5 -3
  91. package/dist/components/Provider.d.ts +79 -19
  92. package/dist/components/Provider.js +140 -130
  93. package/dist/components/Radio.js +2 -1
  94. package/dist/components/RefreshButton.js +7 -8
  95. package/dist/components/RequiredMark.js +1 -1
  96. package/dist/components/Section.js +1 -1
  97. package/dist/components/Select.d.ts +4 -9
  98. package/dist/components/Select.js +24 -11
  99. package/dist/components/SettingModeOption.d.ts +1 -1
  100. package/dist/components/SettingModeOption.js +7 -1
  101. package/dist/components/SettingsCard.d.ts +5 -3
  102. package/dist/components/SettingsCard.js +27 -20
  103. package/dist/components/Slider.js +1 -1
  104. package/dist/components/StatusBadge.js +2 -2
  105. package/dist/components/Switch.js +5 -4
  106. package/dist/components/Table.d.ts +30 -20
  107. package/dist/components/Table.js +50 -73
  108. package/dist/components/Tabs.d.ts +3 -7
  109. package/dist/components/Tabs.js +6 -10
  110. package/dist/components/ThemeEditor.js +18 -8
  111. package/dist/components/Toast.d.ts +11 -3
  112. package/dist/components/Toast.js +45 -20
  113. package/dist/components/buttonClasses.js +1 -1
  114. package/dist/components/comboboxItems.d.ts +4 -0
  115. package/dist/components/comboboxItems.js +6 -1
  116. package/dist/components/dismiss.d.ts +12 -0
  117. package/dist/components/dismiss.js +19 -0
  118. package/dist/components/fieldBox.d.ts +4 -0
  119. package/dist/components/fieldBox.js +7 -3
  120. package/dist/components/floating.d.ts +3 -3
  121. package/dist/components/floating.js +1 -1
  122. package/dist/components/focus.d.ts +5 -0
  123. package/dist/components/focus.js +21 -1
  124. package/dist/components/markdownCodeBreaks.js +3 -4
  125. package/dist/components/pageHeaderStore.d.ts +41 -0
  126. package/dist/components/pageHeaderStore.js +67 -0
  127. package/dist/components/pathBreaks.d.ts +8 -0
  128. package/dist/components/pathBreaks.js +28 -0
  129. package/dist/components/segmented.js +1 -1
  130. package/dist/components/shortcutKey.d.ts +2 -1
  131. package/dist/components/shortcutKey.js +19 -2
  132. package/dist/components/splitter.js +1 -1
  133. package/dist/components/useCssIsDark.js +40 -18
  134. package/dist/components/useTheme.js +9 -10
  135. package/dist/dev.d.ts +9 -0
  136. package/dist/dev.js +14 -0
  137. package/dist/editor/Editor.d.ts +3 -2
  138. package/dist/editor/Editor.js +24 -5
  139. package/dist/editor/EditorField.d.ts +2 -1
  140. package/dist/editor/EditorField.js +11 -29
  141. package/dist/editor/FormCodePanel.d.ts +7 -3
  142. package/dist/editor/FormCodePanel.js +14 -7
  143. package/dist/editor/Monaco.d.ts +22 -6
  144. package/dist/editor/Monaco.js +177 -113
  145. package/dist/editor/editorModel.d.ts +1 -1
  146. package/dist/editor/editorModel.js +1 -1
  147. package/dist/editor/index.d.ts +6 -5
  148. package/dist/editor/index.js +5 -4
  149. package/dist/editor/languages.d.ts +24 -0
  150. package/dist/editor/languages.js +31 -0
  151. package/dist/editor/monacoFeatures.d.ts +7 -0
  152. package/dist/editor/monacoFeatures.js +19 -0
  153. package/dist/editor/themes.d.ts +8 -5
  154. package/dist/editor/themes.js +65 -32
  155. package/dist/editor/workers.d.ts +3 -3
  156. package/dist/editor/workers.js +8 -4
  157. package/dist/editor/yaml.d.ts +6 -2
  158. package/dist/editor/yaml.js +11 -2
  159. package/dist/editor/yamlLanguage.js +55 -38
  160. package/dist/file-explorer/FileExplorer.d.ts +18 -4
  161. package/dist/file-explorer/FileExplorer.js +204 -142
  162. package/dist/file-explorer/FileExplorerContext.js +1 -1
  163. package/dist/file-explorer/FilePreview/StreamedBadge.js +3 -1
  164. package/dist/file-explorer/FilePreview/Viewer.js +14 -10
  165. package/dist/file-explorer/FilePreview/index.js +4 -4
  166. package/dist/file-explorer/FileRow.d.ts +6 -2
  167. package/dist/file-explorer/FileRow.js +35 -17
  168. package/dist/file-explorer/FileRowsWindow.d.ts +2 -1
  169. package/dist/file-explorer/FileRowsWindow.js +36 -4
  170. package/dist/file-explorer/FileTable.d.ts +3 -1
  171. package/dist/file-explorer/FileTable.js +6 -3
  172. package/dist/file-explorer/TreeView.d.ts +5 -1
  173. package/dist/file-explorer/TreeView.js +86 -39
  174. package/dist/file-explorer/helpers.d.ts +2 -1
  175. package/dist/file-explorer/helpers.js +24 -23
  176. package/dist/file-explorer/lib/utils.d.ts +3 -0
  177. package/dist/file-explorer/lib/utils.js +6 -1
  178. package/dist/file-explorer/toasts.js +27 -19
  179. package/dist/file-explorer/treeRows.d.ts +6 -0
  180. package/dist/file-explorer/treeRows.js +19 -1
  181. package/dist/file-explorer/useNearEnd.js +3 -6
  182. package/dist/form/Fields/CheckboxGroupField.js +12 -5
  183. package/dist/form/Fields/DurationField.js +1 -1
  184. package/dist/form/Fields/LabelField.js +1 -1
  185. package/dist/form/Fields/ObjectField.d.ts +1 -1
  186. package/dist/form/Fields/ObjectField.js +3 -2
  187. package/dist/form/Fields/StringField.js +16 -29
  188. package/dist/form/Fields/TextAreaField.js +3 -4
  189. package/dist/form/Form.js +3 -8
  190. package/dist/form/Wizard/WizardNavigation.js +0 -3
  191. package/dist/form/Wizard/utils.js +2 -1
  192. package/dist/form/fieldFrame.d.ts +1 -1
  193. package/dist/form/fieldFrame.js +2 -2
  194. package/dist/form/types/fieldComponentTypes.d.ts +2 -1
  195. package/dist/graph/AnnotationBanner.d.ts +1 -2
  196. package/dist/graph/AnnotationBanner.js +30 -32
  197. package/dist/graph/Connectors.js +4 -1
  198. package/dist/graph/DependencyGraph.js +2 -2
  199. package/dist/graph/GraphEditor.js +11 -9
  200. package/dist/graph/GraphEditorDialogs.js +1 -1
  201. package/dist/graph/GraphNode.js +7 -4
  202. package/dist/graph/InputDialog.js +1 -1
  203. package/dist/graph/InputsEditor.js +14 -11
  204. package/dist/graph/MatrixGroup.js +9 -3
  205. package/dist/graph/Reveal.js +18 -10
  206. package/dist/graph/ShortcutsButton.d.ts +11 -2
  207. package/dist/graph/ShortcutsButton.js +14 -3
  208. package/dist/graph/TreeView.js +35 -18
  209. package/dist/graph/WorkflowSettingsDialog.js +1 -1
  210. package/dist/graph/editorFields.js +1 -1
  211. package/dist/graph/editorRows.js +16 -10
  212. package/dist/graph/editorStrings.d.ts +7 -0
  213. package/dist/graph/editorStrings.js +8 -1
  214. package/dist/graph/inputRefs.js +1 -1
  215. package/dist/graph/settingsViews.js +11 -18
  216. package/dist/graph/stepWith.js +1 -1
  217. package/dist/icons.d.ts +2 -0
  218. package/dist/icons.js +4 -2
  219. package/dist/index.d.ts +12 -12
  220. package/dist/index.js +16 -16
  221. package/dist/list/ActionToolbar.d.ts +42 -6
  222. package/dist/list/ActionToolbar.js +298 -101
  223. package/dist/list/ListPage.d.ts +15 -23
  224. package/dist/list/ListPage.js +42 -43
  225. package/dist/list/ListTable.d.ts +37 -7
  226. package/dist/list/ListTable.js +52 -12
  227. package/dist/list/ListViewControls.js +1 -1
  228. package/dist/list/UserHoverCard.js +7 -15
  229. package/dist/list/index.d.ts +2 -2
  230. package/dist/list/index.js +2 -2
  231. package/dist/list/listSearch.js +12 -10
  232. package/dist/list/listView.d.ts +10 -1
  233. package/dist/list/listView.js +15 -1
  234. package/dist/logviewer/LogViewer.d.ts +4 -2
  235. package/dist/logviewer/LogViewer.js +87 -69
  236. package/dist/logviewer/logviewer.css +3 -2
  237. package/dist/reducedMotion.d.ts +5 -0
  238. package/dist/reducedMotion.js +20 -0
  239. package/dist/safeUrl.d.ts +1 -1
  240. package/dist/safeUrl.js +4 -2
  241. package/dist/styles.css +1 -1
  242. package/package.json +5 -2
  243. package/dist/components/Breadcrumbs.d.ts +0 -37
  244. package/dist/components/Breadcrumbs.js +0 -90
  245. package/dist/components/CopyToClipboard.d.ts +0 -17
  246. package/dist/components/CopyToClipboard.js +0 -66
  247. package/dist/components/Sort.d.ts +0 -9
  248. package/dist/components/Sort.js +0 -42
  249. package/dist/file-explorer/FilePreview/CodePreviewEditor.d.ts +0 -6
  250. package/dist/file-explorer/FilePreview/CodePreviewEditor.js +0 -64
package/README.md CHANGED
@@ -11,14 +11,34 @@ so an application ships only what it imports:
11
11
  | `./list` | list pages, tables, row menus and list controls |
12
12
  | `./form` | `DynamicForm` and its field registry |
13
13
  | `./graph` | job dependency graph and tree view, and the visual workflow editor: the graph, the run form as an inputs editor, and their dialogs |
14
- | `./editor` | Monaco editor (`monaco-editor` is an optional peer); YAML validation, completion and hover run in a bundled worker |
14
+ | `./editor` | Monaco editor (`monaco-editor` is an optional peer), loaded on first mount with the languages it needs; YAML validation, completion and hover run in a bundled worker |
15
15
  | `./logviewer` | log viewer with ANSI color and search |
16
16
  | `./file-explorer` | object storage explorer over a host-supplied provider |
17
17
  | `./ai`, `./ai/openai`, `./ai/mock` | chat interface and adapters |
18
18
  | `./theme.css`, `./base.css`, `./fonts.css`, `./styles.css`, `./logviewer.css`, `./ai.css` | stylesheets |
19
19
 
20
20
  Hosts bind strings, navigation, data hooks and the workflow engine through
21
- `UIProvider`, which also shows the package's toasts.
21
+ `UIProvider`, which also shows the package's toasts:
22
+
23
+ ```tsx
24
+ <UIProvider
25
+ strings={{ common: { save: 'Speichern' }, fileExplorer: { preview: { open: 'Öffnen' } } }}
26
+ navigation={{ goTo: navigate }}
27
+ data={{ useRunFile, usePersistedState }}
28
+ slots={{ link: RouterLink }}
29
+ engine={() => import('./engine').then((m) => m.engine)}
30
+ >
31
+ {app}
32
+ </UIProvider>
33
+ ```
34
+
35
+ Every string has an English default, and `strings` overrides any of them at
36
+ any depth. Each concern has its own context, compared by value, so a host may
37
+ pass object literals: a render that passes equal ones re-renders nothing, and
38
+ new `data` re-renders only what reads data. `navigation` is read when the
39
+ package navigates and never re-renders anything. Define slot components and
40
+ string functions outside a component: one made each render is a new value, and
41
+ a new slot component remounts what it renders.
22
42
 
23
43
  ## Which component?
24
44
 
@@ -27,7 +47,8 @@ Each need has one component. Reach for it before styling markup by hand.
27
47
  | Need | Use |
28
48
  | --- | --- |
29
49
  | An action | `Button`; `IconButton` for an icon alone, named by its tooltip |
30
- | A link or another component's trigger that looks like a button | `buttonClasses()` |
50
+ | Navigation, in any form | the component's `href` (see [Links](#links)); `AppLink` for a link of your own |
51
+ | Another component's trigger that looks like a button | `buttonClasses()` |
31
52
  | A link out of the app | `ExternalLink` |
32
53
  | Work under way | `Loader`; `Skeleton` in a `LoadingRegion` for the shape of content still loading |
33
54
  | Nothing to show yet, or nothing matches | `EmptyState` |
@@ -35,7 +56,7 @@ Each need has one component. Reach for it before styling markup by hand.
35
56
  | A number, a date, a color | `NumberInput`, `DatePicker`, `ColorInput` |
36
57
  | A one-time code, such as a sign-in check | `OneTimeCodeInput` |
37
58
  | A labeled value nobody edits, among fields | `FieldValue` in a `Field` |
38
- | A record's details, such as in a drawer or a card: names and values on their own | `DescriptionList` of `DescriptionListItem`s; `copy` on an ID, `mono` on a path, `variant="card"` to frame them |
59
+ | A record's details, such as in a drawer or a card: names and values on their own | `DescriptionList` of `DescriptionListItem`s; `copy` on an ID, `mono` on a path, `variant="card"` to frame them; titled lists in a `DescriptionListGroup` to share one label column |
39
60
  | An error beside a control outside a `Field` | `ErrorText` |
40
61
  | A dense field in a toolbar or a table cell | any field control with `size="sm"`; a width in `className` wins, and `cx` merges classes the same way |
41
62
  | On or off | `Switch`; `Checkbox` in a list of things to tick, several of which may be on |
@@ -45,15 +66,18 @@ Each need has one component. Reach for it before styling markup by hand.
45
66
  | A value from a list | `Select`, or `Combobox` when the list needs filtering |
46
67
  | Free text with suggestions, or tags | `Autocomplete`; `Combobox` with `multiple creatable` |
47
68
  | A range | `Slider` |
69
+ | A page's breadcrumbs, title and commands | `PageHeader`, shown by the shell's `PageHeaderBar` (see [Page header](#page-header)) |
70
+ | An action in the page header | `PageHeaderAction`; `PageHeaderAddButton` for "+ Add" or "+ Create"; `PageHeaderMenu` for the "…" menu |
48
71
  | A titled part of a page or form | `Section` |
49
72
  | More detail under a line | `Disclosure` |
50
73
  | Views of one thing | `Tabs` |
51
74
  | A list of actions | `ActionMenu`; `useContextMenu` for a right-click or a row's menu |
52
75
  | A panel of controls anchored to a trigger | `Popover` |
53
- | A hint for a control | `Tooltip`; `InfoTip` for help everyone must reach |
76
+ | A hint for a control | `Tooltip`; a `Field`'s `help` for more about a field, which its control reads and which opens as Tab reaches it; `InfoTip` for help about anything else |
54
77
  | A preview of what a link leads to | `HoverCard` |
55
- | A task that blocks the page | `Modal`; `ConfirmModal` to confirm one action |
78
+ | A task that blocks the page | `Modal`, laid out with `title`, `description` and `footer` as a `Drawer` is; `ConfirmModal` to confirm one action; `CreateModal` to name and create one thing |
56
79
  | A panel beside the page | `Drawer` |
80
+ | A message set into the page: guidance, a warning, an error | `Callout`; `variant="bar"` across a surface under its header |
57
81
  | A passing notice, or the progress of work in the background | `toast` |
58
82
  | How full something is, how far a task has come | `Meter`, `ProgressBar` |
59
83
  | Pages of a long list | `Pagination` |
@@ -61,9 +85,12 @@ Each need has one component. Reach for it before styling markup by hand.
61
85
  | A person | `Avatar` |
62
86
  | A status in words | `StatusBadge`, or `StatusDot` beside text that says it |
63
87
  | A run's, job's or step's status beside its name | `RunStatusIcon`; `StatusBadge` with `runStatusTone` to say it in words |
64
- | A copy action | `useCopy`, or `CopyToClipboard` for its button |
88
+ | A copy action | `CopyButton`; `CopyCodeBlock` for a command or snippet; `useCopy` for a menu row or a control of your own |
65
89
  | A live relative time or duration | `RelativeTime`, `Elapsed` |
66
90
  | Rendered Markdown | `Markdown` |
91
+ | Code or YAML to read or edit | `Editor` from `./editor`; `EditorField` as a form field, `FormCodePanel` for a form's values as JSON |
92
+ | A run's jobs, steps, logs and annotations | `DependencyGraph` from `./graph`, as a graph or a list |
93
+ | A workflow's jobs drawn from its YAML, or edited by hand | `DependencyGraphPreview` from `./graph`, editable with an `editor`; `InputsFormEditor` for its run form |
67
94
  | A value that survives a reload | `usePersistedState` |
68
95
  | Choosing the app's colors | `ThemeEditor`, applied with `useApplyTheme` |
69
96
 
@@ -102,6 +129,71 @@ Some pairs look alike and are split on purpose:
102
129
  A `Combobox` filters as you type, for a list long enough to search, and is the
103
130
  one that takes several values, new ones, or free text (`Autocomplete`).
104
131
 
132
+ ## Page header
133
+
134
+ A page declares its header in one place, and the shell shows it:
135
+
136
+ ```tsx
137
+ // The shell, once: the provider above the bar and the routes.
138
+ <PageHeaderProvider>
139
+ <Sidebar />
140
+ <div className="flex min-w-0 flex-1 flex-col">
141
+ <PageHeaderBar compact={fullBleed} persistent={isMobile} />
142
+ <main><Outlet /></main>
143
+ </div>
144
+ </PageHeaderProvider>
145
+
146
+ // A page.
147
+ <PageHeader
148
+ breadcrumbs={[{ label: 'Clusters', href: '/clusters' }, { label: cluster?.name ?? '' }]}
149
+ titleActions={
150
+ <>
151
+ <PageHeaderAction icon={EditIcon} iconOnly label="Rename" onClick={rename} />
152
+ <PageHeaderMenu items={menu} />
153
+ </>
154
+ }
155
+ actions={
156
+ <PageHeaderAction icon={SaveIcon} label="Save" onClick={save} disabled={!dirty} disabledReason="No changes" />
157
+ }
158
+ />
159
+ ```
160
+
161
+ - **`breadcrumbs`** is the whole trail, ending with the page itself, which the
162
+ bar makes the page's one `h1` (`aria-current="page"`). Don't render another
163
+ `h1`. An empty label, such as a name still loading, shows a placeholder. The
164
+ trail is a list in a `nav` landmark named by the strings' `breadcrumb`. A
165
+ trail too wide for its bar folds its ancestors into a "…" menu of links
166
+ (named by `showFullPath`), keeping the parent when it fits beside the page's
167
+ name; the name truncates last, with the full name on hover.
168
+ - **`titleActions`** sit beside the title, for actions on the page's subject.
169
+ **`actions`** sit at the end of the bar, for the page's commands: a detail
170
+ page's Start, Stop or Terminal go here, not in a row of buttons below it.
171
+ `ActionToolbar` (from `./list`) is for the controls of a panel on the page,
172
+ such as a log's follow and refresh: its buttons are outlined, and `primary`
173
+ fills the one, if any, the panel leads to.
174
+ **`toolbar`** is a row under the bar, such as tabs or a time range.
175
+ - **`hidden`** takes the bar away while the page is mounted, for a page that
176
+ fills the window.
177
+
178
+ The actions render in the page's React tree, so they keep its context and
179
+ state, and show in the bar through portals; they come before the page's content
180
+ in focus order. Any component may render a `PageHeader`, such as a form adding
181
+ its Save button: the bar shows the actions of all of them, outermost first,
182
+ and the trail of the last one rendered that has one. A layout renders its own
183
+ before its `Outlet`, so a page under it that names a trail replaces the
184
+ layout's, and a page that doesn't keeps it. Changing pages swaps the header
185
+ before paint, and a page's actions go when it unmounts.
186
+
187
+ `PageHeaderBar` renders nothing while the page declares nothing, unless it is
188
+ `persistent`, as on a narrow screen where it holds the `navToggle` slot. The
189
+ `breadcrumbPin` slot renders after the trail. Without a `PageHeaderProvider`, a
190
+ `PageHeader` renders its bar in place, for a page with no shell.
191
+
192
+ The header's actions share one control: a link (`href`, with `external` for a
193
+ new tab), a button (`onClick`, with `pressed` for a toggle) or a menu (`menu`).
194
+ `destructive` takes the error color. A `disabled` one stays focusable and says
195
+ its `disabledReason` in its tooltip and name.
196
+
105
197
  ## Menu and row actions
106
198
 
107
199
  A `MenuAction`, and the `RowAction` and `ToolbarAction` built on it, does
@@ -317,13 +409,43 @@ Every pressable thing has one look, from `buttonClasses({ variant, size, iconOnl
317
409
 
318
410
  Sizes are `xs`, `sm` and `md`. `Button` and `IconButton` apply it. An
319
411
  `IconButton` is named by its tooltip, and takes `pressed` for a toggle and
320
- `dot` for an indicator. Pass `buttonClasses()` to a host link or to another
321
- component's trigger rather than styling a button by hand. A pressed toggle
412
+ `dot` for an indicator. Either takes `href` to be a link in the same look. Pass
413
+ `buttonClasses()` to another component's trigger rather than styling a button
414
+ by hand. A pressed toggle
322
415
  (`aria-pressed`) and an open popup's trigger read as selected.
323
416
 
324
417
  `Loader` is the one spinner. Inside a button, or beside text that already
325
418
  says what is happening, use `<Loader inline decorative />`.
326
419
 
420
+ ## Links
421
+
422
+ Navigation is a link, never a button whose `onClick` navigates: a link opens
423
+ in a new tab with a middle or modified click, can be copied, shows its URL on
424
+ hover, and reads as a link. Every component that can navigate takes `href`,
425
+ with `reloadDocument` for a path the host's router doesn't serve and `target`
426
+ where it opens elsewhere, and renders it through `AppLink`:
427
+
428
+ | Surface | Navigates with |
429
+ | --- | --- |
430
+ | `Button`, `IconButton`, `Card` | `href` |
431
+ | Menu rows, `RowAction`s (a list row's menu and pinned buttons), `ToolbarAction`s | `href` on the action, with `target: '_blank'` for a new tab |
432
+ | `PageHeaderAction`, `PageHeaderAddButton`, `PageHeader` breadcrumbs | `href`; `external` opens a new tab |
433
+ | `Tabs` that are pages, `Pagination` | `href` on the item, `pageHref` |
434
+ | A list row | `ListRow` `href` with a `NameCell` of the same `href`: the name's link covers the row |
435
+ | A toast's action | `action: { label, href }` |
436
+ | `SidebarRailItem` | `href`; `onSelect` then runs only on a plain click |
437
+ | `FileExplorer` rows, tree, trail and "Open folder" | `pathHref(path)`; `onPathChange` is then optional, and the explorer's own moves go to `navigation.goTo` |
438
+ | `EmptyState`, `SettingsCard` actions | a `Button` with `href` |
439
+
440
+ `AppLink` routes in-app URLs through the provider's `link` slot, so a host
441
+ binds its router's `Link` there once. That link receives the whole URL, search
442
+ and hash included, and routes it client-side on a plain click, leaving
443
+ modified and middle clicks to the browser. `AppLink` passes every href through
444
+ `safeUrl`, gives a `reloadDocument` or another origin a plain anchor, and
445
+ renders a disabled link with no href and `aria-disabled`, still focusable for
446
+ its tooltip. Work a link's `onClick` does beside navigating, such as closing a
447
+ drawer, belongs to a plain click only: check `isPlainClick(event)`.
448
+
327
449
  ## Toasts
328
450
 
329
451
  `toast` shows a notice from anywhere, a component or not. `UIProvider` renders
@@ -358,6 +480,7 @@ root:
358
480
  | ------------------- | ----------------------------------------------------------------------- |
359
481
  | `--app-inset-top` | The height of a fixed top bar. Drawers and toasts start below it. |
360
482
  | `--app-inset-start` | The width of a fixed sidebar on the start side. Toasts center over the page beside it, and a drawer at its widest leaves that strip, and never less than 2.5rem. Update it when the sidebar collapses. |
483
+ | `--table-header-top` | How far below the top of its scrolling box a table's sticky header row stops (`Table stickyHeader`, which list tables use). Set it on the scrolling box, or an ancestor, when something else sticks at that box's top, such as a page header in a page that scrolls as a whole. Inside `ListPage` the rows scroll in their own box below the header and toolbar, so it stays 0. |
361
484
 
362
485
  ```ts
363
486
  document.documentElement.style.setProperty('--app-inset-top', '56px')
@@ -413,6 +536,9 @@ meaning across the apps.
413
536
  record. `RemoveUserIcon` takes someone out of a group or project, while
414
537
  `DisableUserIcon` turns off their account. `ChangesRequestedIcon` asks for
415
538
  changes to something sent, while `FlaggedIcon` marks an item for attention.
539
+ `ProjectIcon` is a project, while `FolderIcon` is a folder of files.
540
+ `LibraryIcon` is a collection of write-ups to browse, while `NotebookIcon`
541
+ is one notebook.
416
542
  `PendingIcon` is a requirement not met yet and `NotRunningIcon` a run not
417
543
  started; they share the empty circle, so keep them out of the same list.
418
544
  - **Adding an icon:** pick the [Lucide glyph](https://lucide.dev/icons) and add
@@ -477,36 +603,84 @@ configureEditorYaml([
477
603
  ])
478
604
  ```
479
605
 
480
- Without them, those views still edit, but check nothing against the schema.
606
+ Without them, those views still edit, but check nothing against the schema. Registering a schema
607
+ under a `uri` already registered replaces it, and `configureEditorYaml` returns a function that
608
+ removes what it added.
609
+
610
+ `Editor` is controlled like an input: pass `value` and `onChange`, and hand back the text it sent.
611
+ Text that comes back unchanged leaves the cursor alone, and a reset from outside keeps the cursor
612
+ and scroll where they were. Each editor has a model of its own unless it is given a `path`, and it
613
+ is named `UIStrings.common.codeEditor` unless given an `ariaLabel`.
614
+
615
+ Monaco comes from the host's own `monaco-editor` through its bundler, never from a CDN, and loads
616
+ when the first editor mounts, so it stays out of the host's first bundle. Its workers are files in
617
+ the package that the host's bundler copies, started with `new Worker(url)`: a CSP needs
618
+ `worker-src 'self'` and no `blob:`. The YAML language server starts with the first YAML model.
619
+
620
+ The editor colors every language Monaco has, Bash among them (as shell), and fetches each
621
+ language's coloring only when an editor shows it. It validates and formats JSON. The TypeScript,
622
+ CSS and HTML language services are left out, as each runs in a worker of megabytes; their text is
623
+ still colored. `addEditorLanguages` adds a language service and its workers, by the label Monaco
624
+ asks for, or a language of the host's own. It loads with the next editor to mount; call it once
625
+ at startup. With Vite:
626
+
627
+ ```ts
628
+ import { addEditorLanguages } from '@parallelworks/ui/editor'
629
+
630
+ const tsWorker = () =>
631
+ new Worker(new URL('monaco-editor/language/typescript/ts.worker.js', import.meta.url), {
632
+ type: 'module',
633
+ })
634
+ // The TypeScript service checks JavaScript too, and asks for a worker under each label.
635
+ addEditorLanguages({
636
+ load: () => import('monaco-editor/languages/features/typescript/register'),
637
+ workers: { typescript: tsWorker, javascript: tsWorker },
638
+ })
639
+ ```
640
+
641
+ `readOnly` makes it a view of code to read, such as a file's contents: the text stays selectable,
642
+ copyable and reachable by keyboard and is announced read-only, while the cursor, current line and
643
+ quick fixes are hidden, the view ends at the last line, and the wheel scrolls the page once the text
644
+ has scrolled to its end. Any of these can be set otherwise with Monaco's own options. `disabled`
645
+ greys the text out as well, as a disabled field does.
481
646
 
482
647
  ## AI chat
483
648
 
484
649
  `@parallelworks/ui/ai` is a chat interface (thread, composer, streaming,
485
650
  sidebar, model selector, attachments, sharing, branching) decoupled from any
486
651
  backend through a `ChatAdapter`. State lives in `ChatProvider` and is read
487
- with `useChat`:
652
+ with `useChat`; the reply being streamed is read with `useChatStream`, apart
653
+ from the rest, so only what draws the reply re-renders for each token:
488
654
 
489
655
  ```tsx
490
- import { ChatProvider, ChatLayout, ChatThread } from '@parallelworks/ui/ai'
656
+ import { ChatEmptyState, ChatLayout, ChatProvider, ChatThread } from '@parallelworks/ui/ai'
491
657
  import { createMockChatAdapter } from '@parallelworks/ui/ai/mock'
492
658
 
493
- export function Chat() {
659
+ export function Chat({ conversationId }: { conversationId: string | null }) {
494
660
  return (
495
661
  <ChatProvider
496
662
  adapter={createMockChatAdapter()}
497
663
  currentUser={{ id: 'u1', username: 'you', name: 'You' }}
498
- navigation={{ toConversation: id => {}, toNewChat: () => {}, toAttachments: () => {} }}
664
+ navigation={{ toConversation: id => {}, toNewChat: () => {} }}
665
+ activeConversationId={conversationId}
499
666
  >
500
667
  <ChatLayout>
501
- <ChatThread />
668
+ {conversationId ? <ChatThread conversationId={conversationId} /> : <ChatEmptyState />}
502
669
  </ChatLayout>
503
670
  </ChatProvider>
504
671
  )
505
672
  }
506
673
  ```
507
674
 
675
+ The chat's links (each conversation, a new chat, attachments, and the
676
+ `extraLinks` pages) render through the config's `LinkComponent`, which gets a
677
+ `ChatLinkTarget`; without one they go to `/chat/:id`, `/chat` and
678
+ `/chat/attachments` through `UIProvider`'s link slot. Navigation in the chat is
679
+ these links, so `navigation` handles only the moves that aren't clicks, such as
680
+ keyboard shortcuts.
681
+
508
682
  `ChatLayout sidebarMode="auto"` shows the conversation list as a drawer over
509
- the thread when the chat is narrower than `drawerBelowPx` (768 by default),
683
+ the thread when the chat is narrower than `drawerBelowPx` (`DRAWER_BELOW_PX`, 896, by default),
510
684
  measured on the chat's own box; `"drawer"` always does, and `"inline"` (the
511
685
  default) never does. The reader's toggle drives the list unless the host
512
686
  passes `sidebar` (`'expanded' | 'collapsed' | 'hidden'`) and `drawerOpen` to
@@ -533,6 +707,12 @@ animations) and streamdown's classes next to the theme contract:
533
707
 
534
708
  The prebuilt `@parallelworks/ui/styles.css` already includes both.
535
709
 
710
+ Math needs nothing from the host: `Markdown` imports KaTeX's stylesheet
711
+ itself, from the `katex` package, so the host's bundler loads it and its fonts
712
+ with the markdown renderer's lazy chunk. The bundler must accept CSS imported
713
+ from JavaScript, as the editor's Monaco also requires. Inline math takes `$$`
714
+ within a sentence; a single `$` stays text, so prices are never read as math.
715
+
536
716
  ## Form state
537
717
 
538
718
  `DynamicForm` keeps its values in react-hook-form, a peer dependency, so the
@@ -634,7 +814,7 @@ controls. A label that wraps to two lines sits at the foot of a label row the
634
814
  fields in that row share, so its control stays level with its neighbors'.
635
815
  Set `align: rows` when every immediate child is a
636
816
  section. Adjacent sections then share a header row and one row per direct child.
637
- A description, help text, or validation message increases the shared row height,
817
+ A description or validation message increases the shared row height,
638
818
  so the following rows stay aligned. A nested grid counts as one child row.
639
819
  The grid's gap controls shared row spacing. Hidden children retain their row slot
640
820
  while a neighboring section uses it; empty sections disappear. When the configured
@@ -34,4 +34,6 @@ export interface ToolStrings {
34
34
  sentence: (clauses: string[]) => string;
35
35
  };
36
36
  }
37
+ /** A count and its noun in English: "1 file", "1,024 files". */
38
+ export declare const countOf: (n: number, one: string, other: string) => string;
37
39
  export declare const defaultToolStrings: ToolStrings;
@@ -1,30 +1,32 @@
1
1
  //#region src/ai/agent/toolStrings.ts
2
- var count = (n, one, many) => `${n.toLocaleString()} ${n === 1 ? one : many}`;
2
+ var englishPlural = new Intl.PluralRules("en");
3
+ /** A count and its noun in English: "1 file", "1,024 files". */
4
+ var countOf = (n, one, other) => `${n.toLocaleString()} ${englishPlural.select(n) === "one" ? one : other}`;
3
5
  var defaultToolStrings = {
4
6
  noOutput: "(no output)",
5
- firstLine: (line, moreLines) => moreLines > 0 ? `${line} (+${count(moreLines, "line", "lines")})` : line,
7
+ firstLine: (line, moreLines) => moreLines > 0 ? `${line} (+${countOf(moreLines, "line", "lines")})` : line,
6
8
  emptyFile: "Empty file",
7
9
  readImage: "Read image",
8
- readLines: (lines, more) => more > 0 ? `Read ${count(lines, "line", "lines")} (${more.toLocaleString()} more)` : `Read ${count(lines, "line", "lines")}`,
10
+ readLines: (lines, more) => more > 0 ? `Read ${countOf(lines, "line", "lines")} (${more.toLocaleString()} more)` : `Read ${countOf(lines, "line", "lines")}`,
9
11
  noMatches: "No matches",
10
- foundMatches: (matches, files) => `Found ${count(matches, "match", "matches")} in ${count(files, "file", "files")}`,
11
- foundFiles: (files) => `Found ${count(files, "file", "files")}`,
12
+ foundMatches: (matches, files) => `Found ${countOf(matches, "match", "matches")} in ${countOf(files, "file", "files")}`,
13
+ foundFiles: (files) => `Found ${countOf(files, "file", "files")}`,
12
14
  noFilesMatched: "No files matched",
13
15
  truncated: (summary) => `${summary} (truncated)`,
14
- exitCode: (code, lines) => lines > 0 ? `Exit ${code} · ${count(lines, "line", "lines")}` : `Exit ${code}`,
15
- httpStatus: (status, lines) => `HTTP ${status} · ${count(lines, "line", "lines")}`,
16
+ exitCode: (code, lines) => lines > 0 ? `Exit ${code} · ${countOf(lines, "line", "lines")}` : `Exit ${code}`,
17
+ httpStatus: (status, lines) => `HTTP ${status} · ${countOf(lines, "line", "lines")}`,
16
18
  outputTruncated: (summary) => `${summary} (output truncated)`,
17
- edited: (added, removed) => added > 0 && removed > 0 ? `Added ${count(added, "line", "lines")}, removed ${count(removed, "line", "lines")}` : added > 0 ? `Added ${count(added, "line", "lines")}` : removed > 0 ? `Removed ${count(removed, "line", "lines")}` : "",
18
- wrote: (lines, path) => path ? `Wrote ${count(lines, "line", "lines")} to ${path}` : `Wrote ${count(lines, "line", "lines")}`,
19
+ edited: (added, removed) => added > 0 && removed > 0 ? `Added ${countOf(added, "line", "lines")}, removed ${countOf(removed, "line", "lines")}` : added > 0 ? `Added ${countOf(added, "line", "lines")}` : removed > 0 ? `Removed ${countOf(removed, "line", "lines")}` : "",
20
+ wrote: (lines, path) => path ? `Wrote ${countOf(lines, "line", "lines")} to ${path}` : `Wrote ${countOf(lines, "line", "lines")}`,
19
21
  rollup: {
20
- searched: (n) => `searched for ${count(n, "pattern", "patterns")}`,
21
- read: (n) => `read ${count(n, "file", "files")}`,
22
- ranCommands: (n) => `ran ${count(n, "shell command", "shell commands")}`,
23
- fetched: (n) => `fetched ${count(n, "URL", "URLs")}`,
24
- loadedSkills: (n) => `loaded ${count(n, "skill", "skills")}`,
25
- sentMessages: (n) => `sent ${count(n, "message", "messages")}`,
26
- calledMcpTools: (n) => `called ${count(n, "MCP tool", "MCP tools")}`,
27
- ranTools: (n) => `ran ${count(n, "tool", "tools")}`,
22
+ searched: (n) => `searched for ${countOf(n, "pattern", "patterns")}`,
23
+ read: (n) => `read ${countOf(n, "file", "files")}`,
24
+ ranCommands: (n) => `ran ${countOf(n, "shell command", "shell commands")}`,
25
+ fetched: (n) => `fetched ${countOf(n, "URL", "URLs")}`,
26
+ loadedSkills: (n) => `loaded ${countOf(n, "skill", "skills")}`,
27
+ sentMessages: (n) => `sent ${countOf(n, "message", "messages")}`,
28
+ calledMcpTools: (n) => `called ${countOf(n, "MCP tool", "MCP tools")}`,
29
+ ranTools: (n) => `ran ${countOf(n, "tool", "tools")}`,
28
30
  sentence: (clauses) => {
29
31
  const s = clauses.join(", ");
30
32
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -32,4 +34,4 @@ var defaultToolStrings = {
32
34
  }
33
35
  };
34
36
  //#endregion
35
- export { defaultToolStrings };
37
+ export { countOf, defaultToolStrings };
package/dist/ai/ai.css CHANGED
@@ -71,13 +71,15 @@
71
71
  color: inherit;
72
72
  }
73
73
 
74
- /* KaTeX overrides */
75
- .katex-display {
74
+ /* KaTeX overrides. Its own sheet loads with the markdown renderer, after
75
+ * this one, so these need the extra class to win. A wide display formula
76
+ * scrolls instead of widening the answer. */
77
+ .markdown .katex-display {
76
78
  margin: 1em 0;
77
79
  overflow-x: auto;
78
80
  overflow-y: hidden;
79
81
  }
80
- .katex {
82
+ .markdown .katex {
81
83
  font-size: 1.1em;
82
84
  }
83
85
 
@@ -338,6 +340,18 @@
338
340
  color: transparent;
339
341
  }
340
342
 
343
+ /* The shimmer sweeps forever, so under reduced motion it holds still: the
344
+ * text drawn plainly in the accent, the color the sweep passes through. */
345
+ @media (prefers-reduced-motion: reduce) {
346
+ .animate-shimmer {
347
+ animation: none;
348
+ }
349
+ .text-shimmer {
350
+ background-image: none;
351
+ color: var(--theme-accent, #06354f);
352
+ }
353
+ }
354
+
341
355
  /* Entrance animation for chat messages (CSS replacement for a JS motion lib).
342
356
  * Only `from` is defined so elements settle at their natural styles (queued
343
357
  * messages render at reduced opacity). */
@@ -8,7 +8,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
8
8
  import { useEffect } from "react";
9
9
  import useSWR from "swr";
10
10
  //#region src/ai/components/AllocationSelector.tsx
11
- var money = (amount) => `$${amount.toFixed(2)}`;
11
+ var dollars = new Intl.NumberFormat(void 0, {
12
+ style: "currency",
13
+ currency: "USD"
14
+ });
15
+ var money = (amount) => dollars.format(amount);
12
16
  /** Which allocation an organization model bills, with its spend so far. Shows
13
17
  * only for an organization model, and picks the first allocation for it. */
14
18
  function AllocationSelector() {
@@ -1,10 +1,11 @@
1
1
  import { cx } from "../../cx.js";
2
2
  import { DownloadIcon, FileIcon, TrashIcon } from "../../icons.js";
3
3
  import { safeUrl } from "../../safeUrl.js";
4
- import { EmptyState } from "../../components/EmptyState.js";
5
4
  import { Loader } from "../../components/Loader.js";
6
5
  import { toast } from "../../components/Toast.js";
6
+ import { Tooltip } from "../../components/Tooltip.js";
7
7
  import { useStrings } from "../../components/Provider.js";
8
+ import { EmptyState } from "../../components/EmptyState.js";
8
9
  import { formatBytes } from "../../format.js";
9
10
  import { RelativeTime } from "../../components/relativeTime.js";
10
11
  import { useChatConfig } from "../core/config.js";
@@ -14,16 +15,6 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
15
  import { useCallback, useMemo, useState } from "react";
15
16
  import useSWRInfinite from "swr/infinite";
16
17
  //#region src/ai/components/AttachmentManager.tsx
17
- function triggerDownload(url, filename) {
18
- const href = safeUrl(url);
19
- if (!href) return;
20
- const a = document.createElement("a");
21
- a.href = href;
22
- a.download = filename;
23
- document.body.appendChild(a);
24
- a.click();
25
- document.body.removeChild(a);
26
- }
27
18
  function AttachmentManager() {
28
19
  const { adapter } = useChat();
29
20
  const t = useChatConfig().strings.attachmentManager;
@@ -76,54 +67,72 @@ function AttachmentManager() {
76
67
  }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
77
68
  className: "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4",
78
69
  children: attachments.map((att) => {
79
- const downloadUrl = attachmentsAdapter.downloadUrl(att.id);
70
+ const downloadUrl = safeUrl(attachmentsAdapter.downloadUrl(att.id));
71
+ const name = att.filename || t.unnamedFile;
80
72
  const isDeleting = deleting === att.id;
81
73
  const thumbnail = thumbnails[att.id];
82
74
  const showThumbnail = att.contentType?.startsWith("image/") && thumbnail !== "broken";
83
- const openProps = attachmentsAdapter.onOpen ? {
84
- onClick: () => attachmentsAdapter.onOpen?.(att),
85
- role: "button",
86
- style: { cursor: "pointer" }
87
- } : {};
75
+ const preview = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FileIcon, {
76
+ "aria-hidden": "true",
77
+ className: "w-8 h-8 theme-muted-text"
78
+ }), showThumbnail && /* @__PURE__ */ jsx("img", {
79
+ src: downloadUrl,
80
+ alt: "",
81
+ className: cx("absolute inset-0 w-full h-full object-cover", thumbnail !== "loaded" && "opacity-0"),
82
+ loading: "lazy",
83
+ onLoad: () => markThumbnail(att.id, "loaded"),
84
+ onError: () => markThumbnail(att.id, "broken")
85
+ })] });
86
+ const previewClasses = "relative aspect-square w-full bg-(--theme-muted-panel-bg) flex items-center justify-center";
88
87
  return /* @__PURE__ */ jsxs("div", {
89
88
  className: cx("rounded-lg border theme-border overflow-hidden", isDeleting && "opacity-50 pointer-events-none"),
90
- children: [/* @__PURE__ */ jsxs("div", {
91
- className: "relative aspect-square bg-(--theme-muted-panel-bg) flex items-center justify-center",
92
- ...openProps,
93
- children: [/* @__PURE__ */ jsx(FileIcon, { className: "w-8 h-8 theme-muted-text" }), showThumbnail && /* @__PURE__ */ jsx("img", {
94
- src: downloadUrl,
95
- alt: att.filename || "Image",
96
- className: cx("absolute inset-0 w-full h-full object-cover", thumbnail !== "loaded" && "opacity-0"),
97
- loading: "lazy",
98
- onLoad: () => markThumbnail(att.id, "loaded"),
99
- onError: () => markThumbnail(att.id, "broken")
100
- })]
89
+ children: [attachmentsAdapter.onOpen ? /* @__PURE__ */ jsx("button", {
90
+ type: "button",
91
+ onClick: () => attachmentsAdapter.onOpen?.(att),
92
+ "aria-label": t.open(name),
93
+ className: cx(previewClasses, "cursor-pointer"),
94
+ children: preview
95
+ }) : /* @__PURE__ */ jsx("div", {
96
+ className: previewClasses,
97
+ children: preview
101
98
  }), /* @__PURE__ */ jsxs("div", {
102
99
  className: "p-3 text-xs bg-(--theme-panel-bg) flex items-center justify-between gap-1",
103
100
  children: [/* @__PURE__ */ jsxs("div", {
104
101
  className: "min-w-0 flex-1",
105
102
  children: [/* @__PURE__ */ jsx("p", {
106
103
  className: "truncate font-medium text-(--theme-app)",
107
- children: att.filename || "File"
104
+ children: name
108
105
  }), /* @__PURE__ */ jsxs("p", {
109
106
  className: "truncate theme-muted-text",
110
107
  children: [att.size ? formatBytes(att.size) : "", att.uploadedAt && /* @__PURE__ */ jsxs(Fragment, { children: [att.size ? " · " : "", /* @__PURE__ */ jsx(RelativeTime, { value: att.uploadedAt })] })]
111
108
  })]
112
109
  }), /* @__PURE__ */ jsxs("div", {
113
110
  className: "flex items-center gap-0.5 flex-shrink-0",
114
- children: [/* @__PURE__ */ jsx("button", {
115
- type: "button",
116
- onClick: () => triggerDownload(downloadUrl, att.filename || "File"),
117
- className: "p-1.5 rounded-lg hover:theme-hover theme-muted-text transition-colors",
118
- title: common.download,
119
- children: /* @__PURE__ */ jsx(DownloadIcon, { className: "w-4 h-4" })
120
- }), /* @__PURE__ */ jsx("button", {
121
- type: "button",
122
- onClick: () => handleDelete(att.id),
123
- disabled: isDeleting,
124
- className: "p-1.5 rounded-lg hover:bg-red-500/15 theme-muted-text hover:text-red-600 transition-colors",
125
- title: common.delete,
126
- children: /* @__PURE__ */ jsx(TrashIcon, { className: "w-4 h-4" })
111
+ children: [/* @__PURE__ */ jsx(Tooltip, {
112
+ content: common.download,
113
+ children: /* @__PURE__ */ jsx("a", {
114
+ href: downloadUrl,
115
+ download: att.filename || true,
116
+ "aria-label": t.download(name),
117
+ className: "p-1.5 rounded-lg hover:theme-hover theme-muted-text transition-colors",
118
+ children: /* @__PURE__ */ jsx(DownloadIcon, {
119
+ "aria-hidden": "true",
120
+ className: "w-4 h-4"
121
+ })
122
+ })
123
+ }), /* @__PURE__ */ jsx(Tooltip, {
124
+ content: common.delete,
125
+ children: /* @__PURE__ */ jsx("button", {
126
+ type: "button",
127
+ onClick: () => handleDelete(att.id),
128
+ disabled: isDeleting,
129
+ "aria-label": t.remove(name),
130
+ className: "p-1.5 rounded-lg hover:bg-(--theme-error-muted) theme-muted-text hover:text-(--theme-error-text) transition-colors",
131
+ children: /* @__PURE__ */ jsx(TrashIcon, {
132
+ "aria-hidden": "true",
133
+ className: "w-4 h-4"
134
+ })
135
+ })
127
136
  })]
128
137
  })]
129
138
  })]
@@ -1,3 +1,4 @@
1
+ import { type Ref } from 'react';
1
2
  export interface AttachmentUploadHandle {
2
3
  addFiles: (files: FileList | File[]) => void;
3
4
  }
@@ -19,6 +20,7 @@ interface AttachmentUploadProps {
19
20
  disabled?: boolean;
20
21
  maxFiles?: number;
21
22
  maxFileSize?: number;
23
+ ref?: Ref<AttachmentUploadHandle> | undefined;
22
24
  }
23
- declare const AttachmentUpload: import("react").ForwardRefExoticComponent<AttachmentUploadProps & import("react").RefAttributes<AttachmentUploadHandle>>;
24
- export default AttachmentUpload;
25
+ export default function AttachmentUpload({ conversationId, attachments, onAttachmentsChange, disabled, maxFiles, maxFileSize, ref, }: AttachmentUploadProps): import("react").JSX.Element;
26
+ export {};