@multiplatform.one/frappe-ui 7.67.0 → 7.69.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 (268) hide show
  1. package/dist/cjs/FileBrowser/index.cjs +15 -25
  2. package/dist/cjs/FileBrowser/index.native.js +15 -25
  3. package/dist/cjs/FileBrowser/index.native.js.map +1 -1
  4. package/dist/cjs/FormSidebar.cjs +37 -22
  5. package/dist/cjs/FormSidebar.native.js +41 -24
  6. package/dist/cjs/FormSidebar.native.js.map +1 -1
  7. package/dist/cjs/FrappeAssignment.cjs +0 -3
  8. package/dist/cjs/FrappeAssignment.native.js +0 -3
  9. package/dist/cjs/FrappeAssignment.native.js.map +1 -1
  10. package/dist/cjs/FrappeDashboard.cjs +0 -2
  11. package/dist/cjs/FrappeDashboard.native.js +0 -2
  12. package/dist/cjs/FrappeDashboard.native.js.map +1 -1
  13. package/dist/cjs/FrappeForm.cjs +6 -5
  14. package/dist/cjs/FrappeForm.native.js +6 -5
  15. package/dist/cjs/FrappeForm.native.js.map +1 -1
  16. package/dist/cjs/FrappeFormActions.cjs +20 -30
  17. package/dist/cjs/FrappeFormActions.native.js +23 -31
  18. package/dist/cjs/FrappeFormActions.native.js.map +1 -1
  19. package/dist/cjs/FrappeKanban.cjs +0 -1
  20. package/dist/cjs/FrappeKanban.native.js +0 -1
  21. package/dist/cjs/FrappeKanban.native.js.map +1 -1
  22. package/dist/cjs/FrappeLikedBy.cjs +1 -1
  23. package/dist/cjs/FrappeLikedBy.native.js +1 -1
  24. package/dist/cjs/FrappeLikedBy.native.js.map +1 -1
  25. package/dist/cjs/FrappeListView.cjs +0 -1
  26. package/dist/cjs/FrappeListView.native.js +0 -1
  27. package/dist/cjs/FrappeListView.native.js.map +1 -1
  28. package/dist/cjs/FrappeReportView.cjs +2 -4
  29. package/dist/cjs/FrappeReportView.native.js +2 -4
  30. package/dist/cjs/FrappeReportView.native.js.map +1 -1
  31. package/dist/cjs/desk/DeskShell.cjs +0 -1
  32. package/dist/cjs/desk/DeskShell.native.js +0 -1
  33. package/dist/cjs/desk/DeskShell.native.js.map +1 -1
  34. package/dist/cjs/fields/FileUpload.cjs +53 -58
  35. package/dist/cjs/fields/FileUpload.native.js +53 -56
  36. package/dist/cjs/fields/FileUpload.native.js.map +1 -1
  37. package/dist/cjs/list/ListControls.cjs +1 -3
  38. package/dist/cjs/list/ListControls.native.js +1 -3
  39. package/dist/cjs/list/ListControls.native.js.map +1 -1
  40. package/dist/cjs/list/ListControlsSheet.cjs +5 -15
  41. package/dist/cjs/list/ListControlsSheet.native.js +7 -17
  42. package/dist/cjs/list/ListControlsSheet.native.js.map +1 -1
  43. package/dist/cjs/list/ListSidebarToolbar.cjs +5 -20
  44. package/dist/cjs/list/ListSidebarToolbar.native.js +7 -22
  45. package/dist/cjs/list/ListSidebarToolbar.native.js.map +1 -1
  46. package/dist/cjs/list/RecordCard.cjs +2 -14
  47. package/dist/cjs/list/RecordCard.native.js +2 -10
  48. package/dist/cjs/list/RecordCard.native.js.map +1 -1
  49. package/dist/cjs/list/ShufflePanel.cjs +0 -2
  50. package/dist/cjs/list/ShufflePanel.native.js +0 -2
  51. package/dist/cjs/list/ShufflePanel.native.js.map +1 -1
  52. package/dist/cjs/list/listColumns.cjs +1 -1
  53. package/dist/cjs/list/listColumns.native.js +1 -1
  54. package/dist/cjs/list/listColumns.native.js.map +1 -1
  55. package/dist/cjs/record/RecordPage.cjs +4 -10
  56. package/dist/cjs/record/RecordPage.native.js +4 -10
  57. package/dist/cjs/record/RecordPage.native.js.map +1 -1
  58. package/dist/cjs/report/ReportViewToolbar.cjs +8 -10
  59. package/dist/cjs/report/ReportViewToolbar.native.js +8 -10
  60. package/dist/cjs/report/ReportViewToolbar.native.js.map +1 -1
  61. package/dist/esm/FileBrowser/index.mjs +16 -26
  62. package/dist/esm/FileBrowser/index.mjs.map +1 -1
  63. package/dist/esm/FileBrowser/index.native.js +16 -26
  64. package/dist/esm/FileBrowser/index.native.js.map +1 -1
  65. package/dist/esm/FormSidebar.mjs +41 -26
  66. package/dist/esm/FormSidebar.mjs.map +1 -1
  67. package/dist/esm/FormSidebar.native.js +45 -28
  68. package/dist/esm/FormSidebar.native.js.map +1 -1
  69. package/dist/esm/FrappeAssignment.mjs +0 -3
  70. package/dist/esm/FrappeAssignment.mjs.map +1 -1
  71. package/dist/esm/FrappeAssignment.native.js +0 -3
  72. package/dist/esm/FrappeAssignment.native.js.map +1 -1
  73. package/dist/esm/FrappeDashboard.mjs +0 -2
  74. package/dist/esm/FrappeDashboard.mjs.map +1 -1
  75. package/dist/esm/FrappeDashboard.native.js +0 -2
  76. package/dist/esm/FrappeDashboard.native.js.map +1 -1
  77. package/dist/esm/FrappeForm.mjs +7 -6
  78. package/dist/esm/FrappeForm.mjs.map +1 -1
  79. package/dist/esm/FrappeForm.native.js +7 -6
  80. package/dist/esm/FrappeForm.native.js.map +1 -1
  81. package/dist/esm/FrappeFormActions.mjs +22 -32
  82. package/dist/esm/FrappeFormActions.mjs.map +1 -1
  83. package/dist/esm/FrappeFormActions.native.js +25 -33
  84. package/dist/esm/FrappeFormActions.native.js.map +1 -1
  85. package/dist/esm/FrappeKanban.mjs +0 -1
  86. package/dist/esm/FrappeKanban.mjs.map +1 -1
  87. package/dist/esm/FrappeKanban.native.js +0 -1
  88. package/dist/esm/FrappeKanban.native.js.map +1 -1
  89. package/dist/esm/FrappeLikedBy.mjs +1 -1
  90. package/dist/esm/FrappeLikedBy.mjs.map +1 -1
  91. package/dist/esm/FrappeLikedBy.native.js +1 -1
  92. package/dist/esm/FrappeLikedBy.native.js.map +1 -1
  93. package/dist/esm/FrappeListView.mjs +0 -1
  94. package/dist/esm/FrappeListView.mjs.map +1 -1
  95. package/dist/esm/FrappeListView.native.js +0 -1
  96. package/dist/esm/FrappeListView.native.js.map +1 -1
  97. package/dist/esm/FrappeReportView.mjs +3 -5
  98. package/dist/esm/FrappeReportView.mjs.map +1 -1
  99. package/dist/esm/FrappeReportView.native.js +3 -5
  100. package/dist/esm/FrappeReportView.native.js.map +1 -1
  101. package/dist/esm/desk/DeskShell.mjs +0 -1
  102. package/dist/esm/desk/DeskShell.mjs.map +1 -1
  103. package/dist/esm/desk/DeskShell.native.js +0 -1
  104. package/dist/esm/desk/DeskShell.native.js.map +1 -1
  105. package/dist/esm/fields/FileUpload.mjs +57 -62
  106. package/dist/esm/fields/FileUpload.mjs.map +1 -1
  107. package/dist/esm/fields/FileUpload.native.js +57 -60
  108. package/dist/esm/fields/FileUpload.native.js.map +1 -1
  109. package/dist/esm/list/ListControls.mjs +1 -3
  110. package/dist/esm/list/ListControls.mjs.map +1 -1
  111. package/dist/esm/list/ListControls.native.js +1 -3
  112. package/dist/esm/list/ListControls.native.js.map +1 -1
  113. package/dist/esm/list/ListControlsSheet.mjs +6 -16
  114. package/dist/esm/list/ListControlsSheet.mjs.map +1 -1
  115. package/dist/esm/list/ListControlsSheet.native.js +8 -18
  116. package/dist/esm/list/ListControlsSheet.native.js.map +1 -1
  117. package/dist/esm/list/ListSidebarToolbar.mjs +6 -21
  118. package/dist/esm/list/ListSidebarToolbar.mjs.map +1 -1
  119. package/dist/esm/list/ListSidebarToolbar.native.js +8 -23
  120. package/dist/esm/list/ListSidebarToolbar.native.js.map +1 -1
  121. package/dist/esm/list/RecordCard.mjs +2 -14
  122. package/dist/esm/list/RecordCard.mjs.map +1 -1
  123. package/dist/esm/list/RecordCard.native.js +2 -10
  124. package/dist/esm/list/RecordCard.native.js.map +1 -1
  125. package/dist/esm/list/ShufflePanel.mjs +0 -2
  126. package/dist/esm/list/ShufflePanel.mjs.map +1 -1
  127. package/dist/esm/list/ShufflePanel.native.js +0 -2
  128. package/dist/esm/list/ShufflePanel.native.js.map +1 -1
  129. package/dist/esm/list/listColumns.mjs +1 -1
  130. package/dist/esm/list/listColumns.mjs.map +1 -1
  131. package/dist/esm/list/listColumns.native.js +1 -1
  132. package/dist/esm/list/listColumns.native.js.map +1 -1
  133. package/dist/esm/record/RecordPage.mjs +4 -10
  134. package/dist/esm/record/RecordPage.mjs.map +1 -1
  135. package/dist/esm/record/RecordPage.native.js +4 -10
  136. package/dist/esm/record/RecordPage.native.js.map +1 -1
  137. package/dist/esm/report/ReportViewToolbar.mjs +9 -11
  138. package/dist/esm/report/ReportViewToolbar.mjs.map +1 -1
  139. package/dist/esm/report/ReportViewToolbar.native.js +9 -11
  140. package/dist/esm/report/ReportViewToolbar.native.js.map +1 -1
  141. package/dist/jsx/FileBrowser/index.mjs +16 -26
  142. package/dist/jsx/FileBrowser/index.mjs.map +1 -1
  143. package/dist/jsx/FileBrowser/index.native.js +15 -25
  144. package/dist/jsx/FileBrowser/index.native.js.map +1 -1
  145. package/dist/jsx/FormSidebar.mjs +41 -26
  146. package/dist/jsx/FormSidebar.mjs.map +1 -1
  147. package/dist/jsx/FormSidebar.native.js +41 -24
  148. package/dist/jsx/FormSidebar.native.js.map +1 -1
  149. package/dist/jsx/FrappeAssignment.mjs +0 -3
  150. package/dist/jsx/FrappeAssignment.mjs.map +1 -1
  151. package/dist/jsx/FrappeAssignment.native.js +0 -3
  152. package/dist/jsx/FrappeAssignment.native.js.map +1 -1
  153. package/dist/jsx/FrappeDashboard.mjs +0 -2
  154. package/dist/jsx/FrappeDashboard.mjs.map +1 -1
  155. package/dist/jsx/FrappeDashboard.native.js +0 -2
  156. package/dist/jsx/FrappeDashboard.native.js.map +1 -1
  157. package/dist/jsx/FrappeForm.mjs +7 -6
  158. package/dist/jsx/FrappeForm.mjs.map +1 -1
  159. package/dist/jsx/FrappeForm.native.js +6 -5
  160. package/dist/jsx/FrappeForm.native.js.map +1 -1
  161. package/dist/jsx/FrappeFormActions.mjs +22 -32
  162. package/dist/jsx/FrappeFormActions.mjs.map +1 -1
  163. package/dist/jsx/FrappeFormActions.native.js +23 -31
  164. package/dist/jsx/FrappeFormActions.native.js.map +1 -1
  165. package/dist/jsx/FrappeKanban.mjs +0 -1
  166. package/dist/jsx/FrappeKanban.mjs.map +1 -1
  167. package/dist/jsx/FrappeKanban.native.js +0 -1
  168. package/dist/jsx/FrappeKanban.native.js.map +1 -1
  169. package/dist/jsx/FrappeLikedBy.mjs +1 -1
  170. package/dist/jsx/FrappeLikedBy.mjs.map +1 -1
  171. package/dist/jsx/FrappeLikedBy.native.js +1 -1
  172. package/dist/jsx/FrappeLikedBy.native.js.map +1 -1
  173. package/dist/jsx/FrappeListView.mjs +0 -1
  174. package/dist/jsx/FrappeListView.mjs.map +1 -1
  175. package/dist/jsx/FrappeListView.native.js +0 -1
  176. package/dist/jsx/FrappeListView.native.js.map +1 -1
  177. package/dist/jsx/FrappeReportView.mjs +3 -5
  178. package/dist/jsx/FrappeReportView.mjs.map +1 -1
  179. package/dist/jsx/FrappeReportView.native.js +2 -4
  180. package/dist/jsx/FrappeReportView.native.js.map +1 -1
  181. package/dist/jsx/desk/DeskShell.mjs +0 -1
  182. package/dist/jsx/desk/DeskShell.mjs.map +1 -1
  183. package/dist/jsx/desk/DeskShell.native.js +0 -1
  184. package/dist/jsx/desk/DeskShell.native.js.map +1 -1
  185. package/dist/jsx/fields/FileUpload.mjs +57 -62
  186. package/dist/jsx/fields/FileUpload.mjs.map +1 -1
  187. package/dist/jsx/fields/FileUpload.native.js +53 -56
  188. package/dist/jsx/fields/FileUpload.native.js.map +1 -1
  189. package/dist/jsx/list/ListControls.mjs +1 -3
  190. package/dist/jsx/list/ListControls.mjs.map +1 -1
  191. package/dist/jsx/list/ListControls.native.js +1 -3
  192. package/dist/jsx/list/ListControls.native.js.map +1 -1
  193. package/dist/jsx/list/ListControlsSheet.mjs +6 -16
  194. package/dist/jsx/list/ListControlsSheet.mjs.map +1 -1
  195. package/dist/jsx/list/ListControlsSheet.native.js +7 -17
  196. package/dist/jsx/list/ListControlsSheet.native.js.map +1 -1
  197. package/dist/jsx/list/ListSidebarToolbar.mjs +6 -21
  198. package/dist/jsx/list/ListSidebarToolbar.mjs.map +1 -1
  199. package/dist/jsx/list/ListSidebarToolbar.native.js +7 -22
  200. package/dist/jsx/list/ListSidebarToolbar.native.js.map +1 -1
  201. package/dist/jsx/list/RecordCard.mjs +2 -14
  202. package/dist/jsx/list/RecordCard.mjs.map +1 -1
  203. package/dist/jsx/list/RecordCard.native.js +2 -10
  204. package/dist/jsx/list/RecordCard.native.js.map +1 -1
  205. package/dist/jsx/list/ShufflePanel.mjs +0 -2
  206. package/dist/jsx/list/ShufflePanel.mjs.map +1 -1
  207. package/dist/jsx/list/ShufflePanel.native.js +0 -2
  208. package/dist/jsx/list/ShufflePanel.native.js.map +1 -1
  209. package/dist/jsx/list/listColumns.mjs +1 -1
  210. package/dist/jsx/list/listColumns.mjs.map +1 -1
  211. package/dist/jsx/list/listColumns.native.js +1 -1
  212. package/dist/jsx/list/listColumns.native.js.map +1 -1
  213. package/dist/jsx/record/RecordPage.mjs +4 -10
  214. package/dist/jsx/record/RecordPage.mjs.map +1 -1
  215. package/dist/jsx/record/RecordPage.native.js +4 -10
  216. package/dist/jsx/record/RecordPage.native.js.map +1 -1
  217. package/dist/jsx/report/ReportViewToolbar.mjs +9 -11
  218. package/dist/jsx/report/ReportViewToolbar.mjs.map +1 -1
  219. package/dist/jsx/report/ReportViewToolbar.native.js +8 -10
  220. package/dist/jsx/report/ReportViewToolbar.native.js.map +1 -1
  221. package/package.json +16 -16
  222. package/src/FileBrowser/index.tsx +11 -15
  223. package/src/FormSidebar.tsx +53 -33
  224. package/src/FrappeAssignment.tsx +0 -3
  225. package/src/FrappeDashboard.spec.tsx +7 -3
  226. package/src/FrappeDashboard.tsx +0 -2
  227. package/src/FrappeForm.connections.spec.tsx +2 -2
  228. package/src/FrappeForm.deskSemantics.spec.tsx +2 -2
  229. package/src/FrappeForm.lifecycle.spec.tsx +27 -27
  230. package/src/FrappeForm.tsx +11 -7
  231. package/src/FrappeFormActions.tsx +24 -38
  232. package/src/FrappeKanban.tsx +0 -1
  233. package/src/FrappeLikedBy.tsx +1 -1
  234. package/src/FrappeListView.spec.tsx +7 -3
  235. package/src/FrappeListView.tsx +0 -1
  236. package/src/FrappeReportView.tsx +3 -6
  237. package/src/desk/DeskShell.tsx +0 -1
  238. package/src/desk/deskDocumentRoutes.spec.tsx +7 -4
  239. package/src/fields/Date/typeParity.spec.tsx +6 -4
  240. package/src/fields/FileUpload.spec.tsx +20 -3
  241. package/src/fields/FileUpload.tsx +65 -69
  242. package/src/list/ListControls.tsx +1 -3
  243. package/src/list/ListControlsSheet.tsx +10 -12
  244. package/src/list/ListSidebarToolbar.tsx +7 -18
  245. package/src/list/RecordCard.native.tsx +2 -7
  246. package/src/list/RecordCard.tsx +2 -8
  247. package/src/list/ShufflePanel.tsx +0 -2
  248. package/src/list/listColumns.tsx +1 -1
  249. package/src/record/RecordPage.tsx +2 -10
  250. package/src/report/ReportViewToolbar.tsx +9 -11
  251. package/src/useFrappeForm.spec.tsx +1 -1
  252. package/types/FileBrowser/index.d.ts.map +1 -1
  253. package/types/FormSidebar.d.ts.map +1 -1
  254. package/types/FrappeAssignment.d.ts.map +1 -1
  255. package/types/FrappeDashboard.d.ts.map +1 -1
  256. package/types/FrappeFormActions.d.ts.map +1 -1
  257. package/types/FrappeKanban.d.ts.map +1 -1
  258. package/types/FrappeReportView.d.ts.map +1 -1
  259. package/types/desk/DeskShell.d.ts.map +1 -1
  260. package/types/fields/FileUpload.d.ts.map +1 -1
  261. package/types/list/ListControls.d.ts.map +1 -1
  262. package/types/list/ListControlsSheet.d.ts.map +1 -1
  263. package/types/list/ListSidebarToolbar.d.ts.map +1 -1
  264. package/types/list/RecordCard.d.ts.map +1 -1
  265. package/types/list/RecordCard.native.d.ts.map +1 -1
  266. package/types/list/ShufflePanel.d.ts.map +1 -1
  267. package/types/record/RecordPage.d.ts.map +1 -1
  268. package/types/report/ReportViewToolbar.d.ts.map +1 -1
@@ -206,7 +206,6 @@ function LiveDashboardContent<TDoc extends { name: string; doctype: string }>({
206
206
  `loading` (not bare `disabled`) is the derived busy state. */}
207
207
  <Button
208
208
  size="$2"
209
- circular
210
209
  icon={ArrowsClockwiseIcon}
211
210
  onPress={onRefresh}
212
211
  loading={isRefreshing || isLiveLoading}
@@ -328,7 +327,6 @@ export function FrappeDashboard<TDoc extends { name: string; doctype: string }>(
328
327
  {/* LC-54: house Button, own-box floor (see the live-path refresh). */}
329
328
  <Button
330
329
  size="$2"
331
- circular
332
330
  icon={ArrowsClockwiseIcon}
333
331
  onPress={bumpRefresh}
334
332
  loading={isRefreshing}
@@ -128,13 +128,13 @@ describe("FrappeForm connections, schema mode", () => {
128
128
 
129
129
  it("a new record has no connections yet", async () => {
130
130
  renderForm({});
131
- await waitFor(() => expect(screen.getByText("Save")).toBeTruthy());
131
+ await waitFor(() => expect(screen.getByLabelText("Save")).toBeTruthy());
132
132
  expect(screen.queryByText("Connections")).toBeNull();
133
133
  });
134
134
 
135
135
  it("showConnections={false} leaves the tab out", async () => {
136
136
  renderForm({ name: "bulbasaur", showConnections: false });
137
- await waitFor(() => expect(screen.getByText("Save")).toBeTruthy());
137
+ await waitFor(() => expect(screen.getByLabelText("Save")).toBeTruthy());
138
138
  expect(screen.queryByText("Connections")).toBeNull();
139
139
  });
140
140
  });
@@ -192,7 +192,7 @@ describe("FrappeForm resolves a doctype through the desk form engine", () => {
192
192
  it("save refuses an empty field that mandatory_depends_on made required", async () => {
193
193
  renderForm({ name: "LOAN-APPROVED", roles: ["Loan Officer"] });
194
194
  await expectProbe("status", "val=Approved");
195
- fireEvent.click(screen.getByText("Save"));
195
+ fireEvent.click(screen.getByLabelText("Save"));
196
196
  await expectProbe("reason", "err=Reason is required");
197
197
  });
198
198
 
@@ -276,7 +276,7 @@ describe("FrappeForm resolves a doctype through the desk form engine", () => {
276
276
  renderForm();
277
277
  await screen.findByTestId("probe-status");
278
278
  expect(screen.getByText("Name")).toBeTruthy();
279
- fireEvent.click(screen.getByText("Save"));
279
+ fireEvent.click(screen.getByLabelText("Save"));
280
280
  await waitFor(() => {
281
281
  expect(screen.getByText("Name is required")).toBeTruthy();
282
282
  });
@@ -132,7 +132,7 @@ describe("FrappeForm runs the desk docstatus lifecycle", () => {
132
132
  renderForm(fixtures, { name: "LOAN-DRAFT" });
133
133
  await expectProbe("employee_name", "val=Ada Lovelace");
134
134
  await expectStatus("draft");
135
- expect(screen.queryByText("Save")).toBeNull();
135
+ expect(screen.queryByLabelText("Save")).toBeNull();
136
136
 
137
137
  fireEvent.click(screen.getByText("Submit"));
138
138
  await waitFor(() => expect(confirm).toHaveBeenCalledWith("Permanently Submit LOAN-DRAFT?"));
@@ -172,7 +172,7 @@ describe("FrappeForm runs the desk docstatus lifecycle", () => {
172
172
 
173
173
  fireEvent.click(screen.getByTestId("set-remarks"));
174
174
  await expectStatus("notSaved");
175
- expect(screen.getByText("Save")).toBeTruthy();
175
+ expect(screen.getByLabelText("Save")).toBeTruthy();
176
176
  expect(screen.queryByText("Submit")).toBeNull();
177
177
  });
178
178
 
@@ -185,8 +185,8 @@ describe("FrappeForm runs the desk docstatus lifecycle", () => {
185
185
  onAmend: vi.fn(),
186
186
  });
187
187
  await expectStatus("submitted");
188
- expect(screen.queryByText("Delete")).toBeNull();
189
- expect(screen.getByText("Close")).toBeTruthy();
188
+ expect(screen.queryByLabelText("Delete")).toBeNull();
189
+ expect(screen.getByLabelText("Close")).toBeTruthy();
190
190
  expect(screen.getByTestId("frappe-form-cancel-document").textContent).toContain(
191
191
  "Cancel Document",
192
192
  );
@@ -196,7 +196,7 @@ describe("FrappeForm runs the desk docstatus lifecycle", () => {
196
196
  await expectStatus("cancelled");
197
197
  await waitFor(async () => expect(await docstatusOf(fixtures, "LOAN-SUBMITTED")).toBe(2));
198
198
  expect(screen.getByText("Amend")).toBeTruthy();
199
- expect(screen.getByText("Delete")).toBeTruthy();
199
+ expect(screen.getByLabelText("Delete")).toBeTruthy();
200
200
  });
201
201
 
202
202
  it("an allow_on_submit edit turns Cancel into Update, which keeps the document submitted", async () => {
@@ -205,10 +205,10 @@ describe("FrappeForm runs the desk docstatus lifecycle", () => {
205
205
  await expectStatus("submitted");
206
206
 
207
207
  fireEvent.click(screen.getByTestId("set-remarks"));
208
- await waitFor(() => expect(screen.getByText("Update")).toBeTruthy());
208
+ await waitFor(() => expect(screen.getByLabelText("Update")).toBeTruthy());
209
209
  expect(screen.queryByTestId("frappe-form-cancel-document")).toBeNull();
210
210
 
211
- fireEvent.click(screen.getByText("Update"));
211
+ fireEvent.click(screen.getByLabelText("Update"));
212
212
  await waitFor(async () =>
213
213
  expect((await fixtures.getDoc("Asset Loan", "LOAN-SUBMITTED"))?.remarks).toBe("edited"),
214
214
  );
@@ -255,7 +255,7 @@ describe("FrappeForm runs the desk docstatus lifecycle", () => {
255
255
  await expectStatus("notSaved");
256
256
  await expectProbe("employee_name", "val=Katherine Johnson");
257
257
  await expectProbe("serial_no", "ro=false");
258
- fireEvent.click(screen.getByText("Save"));
258
+ fireEvent.click(screen.getByLabelText("Save"));
259
259
  await waitFor(async () => {
260
260
  const amendments = await fixtures.getList("Asset Loan", {
261
261
  filters: { amended_from: "LOAN-CANCELLED" },
@@ -279,7 +279,7 @@ describe("FrappeForm runs the desk docstatus lifecycle", () => {
279
279
  renderForm(corpus(), { name: "LOAN-SUBMITTED", roles: ["Loan Officer"], onCancel: vi.fn() });
280
280
  await expectStatus("submitted");
281
281
  expect(screen.queryByTestId("frappe-form-cancel-document")).toBeNull();
282
- expect(screen.getByText("Cancel")).toBeTruthy();
282
+ expect(screen.getByLabelText("Cancel")).toBeTruthy();
283
283
  cleanup();
284
284
 
285
285
  renderForm(corpus(), { name: "LOAN-CANCELLED", roles: ["Loan Officer"], onAmend: vi.fn() });
@@ -289,7 +289,7 @@ describe("FrappeForm runs the desk docstatus lifecycle", () => {
289
289
 
290
290
  renderForm(corpus(), { name: "LOAN-DRAFT", roles: ["Loan Clerk"] });
291
291
  await expectStatus("draft");
292
- expect(screen.getByText("Save")).toBeTruthy();
292
+ expect(screen.getByLabelText("Save")).toBeTruthy();
293
293
  expect(screen.queryByText("Submit")).toBeNull();
294
294
  });
295
295
 
@@ -305,7 +305,7 @@ describe("FrappeForm runs the desk docstatus lifecycle", () => {
305
305
  renderForm(corpus(), { name: "LOAN-SUBMITTED", roles: ["Auditor"], onCancel: vi.fn() });
306
306
  await expectStatus("submitted");
307
307
  expect(screen.queryByTestId("frappe-form-cancel-document")).toBeNull();
308
- expect(screen.getByText("Cancel")).toBeTruthy();
308
+ expect(screen.getByLabelText("Cancel")).toBeTruthy();
309
309
  cleanup();
310
310
 
311
311
  renderForm(corpus(), { name: "LOAN-CANCELLED", roles: ["Auditor"], onAmend: vi.fn() });
@@ -326,11 +326,11 @@ describe("a read-only record view keeps desk's lifecycle beside Edit", () => {
326
326
  const onEdit = vi.fn();
327
327
  renderForm(fixtures, { name: "LOAN-DRAFT", onEdit, onCancel: vi.fn(), onDelete: vi.fn() });
328
328
  await expectStatus("draft");
329
- expect(screen.queryByText("Save")).toBeNull();
330
- expect(screen.queryByText("Cancel")).toBeNull();
331
- expect(screen.getByText("Delete")).toBeTruthy();
329
+ expect(screen.queryByLabelText("Save")).toBeNull();
330
+ expect(screen.queryByLabelText("Cancel")).toBeNull();
331
+ expect(screen.getByLabelText("Delete")).toBeTruthy();
332
332
 
333
- fireEvent.click(screen.getByText("Edit"));
333
+ fireEvent.click(screen.getByLabelText("Edit"));
334
334
  expect(onEdit).toHaveBeenCalledTimes(1);
335
335
 
336
336
  fireEvent.click(screen.getByText("Submit"));
@@ -338,30 +338,30 @@ describe("a read-only record view keeps desk's lifecycle beside Edit", () => {
338
338
  await waitFor(async () => expect(await docstatusOf(fixtures, "LOAN-DRAFT")).toBe(1));
339
339
  await expectStatus("submitted");
340
340
  expect(screen.getByText("Cancel Document")).toBeTruthy();
341
- expect(screen.getByText("Edit")).toBeTruthy();
342
- expect(screen.queryByText("Delete")).toBeNull();
341
+ expect(screen.getByLabelText("Edit")).toBeTruthy();
342
+ expect(screen.queryByLabelText("Delete")).toBeNull();
343
343
  });
344
344
 
345
345
  it("a submitted document offers Cancel Document, and its allow_on_submit field keeps Edit", async () => {
346
346
  const fixtures = corpus();
347
347
  renderForm(fixtures, { name: "LOAN-SUBMITTED", onEdit: vi.fn(), onCancel: vi.fn() });
348
348
  await expectStatus("submitted");
349
- expect(screen.getByText("Edit")).toBeTruthy();
350
- expect(screen.queryByText("Cancel")).toBeNull();
351
- expect(screen.queryByText("Close")).toBeNull();
349
+ expect(screen.getByLabelText("Edit")).toBeTruthy();
350
+ expect(screen.queryByLabelText("Cancel")).toBeNull();
351
+ expect(screen.queryByLabelText("Close")).toBeNull();
352
352
 
353
353
  fireEvent.click(screen.getByText("Cancel Document"));
354
354
  await waitFor(() => expect(confirm).toHaveBeenCalledWith("Permanently Cancel LOAN-SUBMITTED?"));
355
355
  await waitFor(async () => expect(await docstatusOf(fixtures, "LOAN-SUBMITTED")).toBe(2));
356
356
  await expectStatus("cancelled");
357
- expect(screen.queryByText("Edit")).toBeNull();
357
+ expect(screen.queryByLabelText("Edit")).toBeNull();
358
358
  });
359
359
 
360
360
  it("a cancelled document offers Amend and no Edit", async () => {
361
361
  const onAmend = vi.fn();
362
362
  renderForm(corpus(), { name: "LOAN-CANCELLED", onEdit: vi.fn(), onAmend });
363
363
  await expectStatus("cancelled");
364
- expect(screen.queryByText("Edit")).toBeNull();
364
+ expect(screen.queryByLabelText("Edit")).toBeNull();
365
365
 
366
366
  fireEvent.click(screen.getByText("Amend"));
367
367
  await waitFor(() => expect(onAmend).toHaveBeenCalled());
@@ -374,7 +374,7 @@ describe("a submittable doctype's routed pages", () => {
374
374
  const pushed = renderPage(corpus(), "/loans/LOAN-DRAFT");
375
375
  await expectStatus("draft");
376
376
  expect(screen.getByText("Submit")).toBeTruthy();
377
- expect(screen.queryByText("Save")).toBeNull();
377
+ expect(screen.queryByLabelText("Save")).toBeNull();
378
378
 
379
379
  // LC-145: Submit is the bar's one action, so Edit rides the "…" menu.
380
380
  fireEvent.click(screen.getByTestId("frappe-form-menu"));
@@ -386,14 +386,14 @@ describe("a submittable doctype's routed pages", () => {
386
386
  const pushed = renderPage(corpus(), "/loans/LOAN-DRAFT/edit");
387
387
  await expectStatus("draft");
388
388
  expect(screen.getByText("Submit")).toBeTruthy();
389
- expect(screen.queryByText("Edit")).toBeNull();
389
+ expect(screen.queryByLabelText("Edit")).toBeNull();
390
390
 
391
391
  fireEvent.click(screen.getByTestId("set-remarks"));
392
392
  await expectStatus("notSaved");
393
- expect(screen.getByText("Save")).toBeTruthy();
393
+ expect(screen.getByLabelText("Save")).toBeTruthy();
394
394
  expect(screen.queryByText("Submit")).toBeNull();
395
395
 
396
- fireEvent.click(screen.getAllByText("Cancel")[0]);
396
+ fireEvent.click(screen.getAllByLabelText(/^(Cancel|Back to .+ list)$/)[0]!);
397
397
  expect(pushed).toEqual(["/loans/LOAN-DRAFT"]);
398
398
  });
399
399
 
@@ -449,7 +449,7 @@ describe("the leave button stays live while a write is in flight (LC-30)", () =>
449
449
  const onCancel = vi.fn();
450
450
  renderForm(fixtures, { name: "LOAN-SUBMITTED", onCancel });
451
451
  await expectStatus("submitted");
452
- expect(screen.getByTestId("frappe-form-close").textContent).toContain("Close");
452
+ expect(screen.getByTestId("frappe-form-close").getAttribute("aria-label")).toBe("Close");
453
453
 
454
454
  fireEvent.click(screen.getByTestId("frappe-form-cancel-document"));
455
455
  await waitFor(() =>
@@ -23,7 +23,7 @@ import type React from "react";
23
23
  import { useEffect, useMemo, useState } from "react";
24
24
  import type { TFunction } from "i18next";
25
25
  import { useTranslation } from "react-i18next";
26
- import { Button, Input, formCommonColors } from "@multiplatform.one/forms";
26
+ import { IconButton, Input, formCommonColors, standardActions } from "@multiplatform.one/forms";
27
27
  import { Text as TamaguiText, XStack, YStack } from "tamagui";
28
28
  import { hairlineWidth, useResolvedKnobs } from "@multiplatform.one/theme";
29
29
  import { useFrappeConfig } from "./FrappeProvider";
@@ -677,13 +677,17 @@ function FrappeFormView<TDoc extends { name: string; doctype: string }>({
677
677
  <YStack flex={1} backgroundColor="$background" testID="frappe-form-route-editor">
678
678
  <XStack alignItems="center" gap="$2" paddingHorizontal="$2" paddingVertical="$1">
679
679
  <XStack flex={1} flexBasis={0} justifyContent="flex-start">
680
- <Button
681
- chromeless
680
+ <IconButton
681
+ icon={standardActions.cancel.icon}
682
+ edge="start"
683
+ data-mp-action="cancel"
682
684
  onPress={onCancel ? handleCancel : routePage.onBack}
683
- aria-label={onCancel ? undefined : t("Back to {{doctype}} list", { doctype })}
684
- >
685
- {t("Cancel")}
686
- </Button>
685
+ aria-label={
686
+ onCancel
687
+ ? t(standardActions.cancel.label)
688
+ : t("Back to {{doctype}} list", { doctype })
689
+ }
690
+ />
687
691
  </XStack>
688
692
  <Text
689
693
  flexShrink={1}
@@ -1,4 +1,5 @@
1
1
  import {
2
+ ActionButton,
2
3
  Chip,
3
4
  DropdownMenu,
4
5
  PageMenuButton,
@@ -10,7 +11,6 @@ import { useResolvedKnobs } from "@multiplatform.one/theme";
10
11
  import {
11
12
  ArrowCounterClockwiseIcon,
12
13
  CheckCircleIcon,
13
- FloppyDiskIcon,
14
14
  PencilSimpleIcon,
15
15
  ProhibitIcon,
16
16
  TrashIcon,
@@ -122,33 +122,27 @@ export function FrappeFormActions({
122
122
  )}
123
123
 
124
124
  {onDelete && (
125
- <Button
126
- outlined
125
+ <ActionButton
126
+ kind="delete"
127
127
  error
128
- icon={TrashIcon}
129
128
  loading={busy}
130
129
  disabled={disabled}
131
130
  disabledReason={disabled ? readOnlyReason : undefined}
132
131
  disabledReasonPlacement="inline"
133
132
  onPress={onDelete}
134
- >
135
- {t("Delete")}
136
- </Button>
133
+ />
137
134
  )}
138
135
 
139
136
  {onClose && (
140
- <Button
141
- outlined={!footer}
142
- icon={footer ? undefined : XIcon}
137
+ <ActionButton
138
+ // A submitted document's own Cancel sits beside this button.
139
+ kind={action === "cancel" ? "close" : "cancel"}
143
140
  disabled={closeDisabled}
144
141
  disabledReason={closeDisabled ? readOnlyReason : undefined}
145
142
  disabledReasonPlacement="inline"
146
143
  onPress={onClose}
147
144
  testID="frappe-form-close"
148
- >
149
- {/* A submitted document's own Cancel sits beside this button. */}
150
- {action === "cancel" ? t("Close") : t("Cancel")}
151
- </Button>
145
+ />
152
146
  )}
153
147
 
154
148
  {action === "cancel" && (
@@ -169,18 +163,14 @@ export function FrappeFormActions({
169
163
  )}
170
164
 
171
165
  {onEdit && (
172
- <Button
173
- accent={action !== "submit"}
174
- outlined={action === "submit"}
175
- icon={PencilSimpleIcon}
166
+ <ActionButton
167
+ kind="edit"
176
168
  disabled={disabled}
177
169
  disabledReason={disabled ? readOnlyReason : undefined}
178
170
  disabledReasonPlacement="inline"
179
171
  onPress={onEdit}
180
172
  testID="frappe-form-edit"
181
- >
182
- {t("Edit")}
183
- </Button>
173
+ />
184
174
  )}
185
175
 
186
176
  {action === "amend" && onAmend && (
@@ -208,6 +198,7 @@ export function FrappeFormActions({
208
198
 
209
199
  {action === "submit" && (
210
200
  <Button
201
+ // action-word-escape: Frappe's docstatus Submit, a lifecycle step a check would hide
211
202
  accent
212
203
  icon={CheckCircleIcon}
213
204
  flex={footer ? 1 : undefined}
@@ -223,19 +214,16 @@ export function FrappeFormActions({
223
214
  )}
224
215
 
225
216
  {(action === "save" || action === "update") && (
226
- <Button
227
- accent
228
- icon={footer ? undefined : FloppyDiskIcon}
229
- flex={footer ? 1 : undefined}
217
+ <ActionButton
218
+ kind="save"
219
+ label={action === "update" ? t("Update") : undefined}
230
220
  loading={busy}
231
221
  disabled={disabled}
232
222
  disabledReason={disabled ? readOnlyReason : undefined}
233
223
  disabledReasonPlacement="inline"
234
224
  onPress={onSave}
235
225
  testID="frappe-form-save"
236
- >
237
- {busy ? t("Saving...") : action === "update" ? t("Update") : t("Save")}
238
- </Button>
226
+ />
239
227
  )}
240
228
  </XStack>
241
229
  );
@@ -269,19 +257,19 @@ export function FrappeFormHeaderActions({
269
257
 
270
258
  const primary =
271
259
  action === "save" || action === "update" ? (
272
- <Button
273
- accent
260
+ <ActionButton
261
+ kind="save"
262
+ label={action === "update" ? t("Update") : undefined}
274
263
  loading={busy}
275
264
  disabled={disabled}
276
265
  disabledReason={disabled ? readOnlyReason : undefined}
277
266
  disabledReasonPlacement="inline"
278
267
  onPress={onSave}
279
268
  testID="frappe-form-save"
280
- >
281
- {busy ? t("Saving...") : action === "update" ? t("Update") : t("Save")}
282
- </Button>
269
+ />
283
270
  ) : action === "submit" ? (
284
271
  <Button
272
+ // action-word-escape: Frappe's docstatus Submit, a lifecycle step a check would hide
285
273
  accent
286
274
  loading={busy}
287
275
  disabled={disabled}
@@ -312,16 +300,14 @@ export function FrappeFormHeaderActions({
312
300
  {t("Amend")}
313
301
  </Button>
314
302
  ) : onEdit ? (
315
- <Button
316
- accent
303
+ <ActionButton
304
+ kind="edit"
317
305
  disabled={disabled}
318
306
  disabledReason={disabled ? readOnlyReason : undefined}
319
307
  disabledReasonPlacement="inline"
320
308
  onPress={onEdit}
321
309
  testID="frappe-form-edit"
322
- >
323
- {t("Edit")}
324
- </Button>
310
+ />
325
311
  ) : null;
326
312
 
327
313
  const lifecyclePrimary =
@@ -266,7 +266,6 @@ export function FrappeKanban<TDoc extends { name: string; doctype: string }>({
266
266
  {canQuickAdd && column.id !== "__unassigned__" && (
267
267
  <Button
268
268
  size="$2"
269
- circular
270
269
  chromeless
271
270
  icon={PlusIcon}
272
271
  aria-label={t("Add card to {{column}}", { column: column.title })}
@@ -281,10 +281,10 @@ function FrappeLikedByBody({
281
281
  <XStack alignItems="center" gap="$2">
282
282
  <Button
283
283
  size="$2"
284
- circular
285
284
  chromeless
286
285
  disabled={!canToggle}
287
286
  aria-pressed={liked}
287
+ selected={false}
288
288
  aria-label={liked ? t("Unlike") : t("Like")}
289
289
  onPress={handleToggle}
290
290
  icon={
@@ -130,10 +130,14 @@ describe("FrappeListView refresh press floor (LC-54 amendment)", () => {
130
130
  expect(refresh.className).not.toMatch(negativeMarginAtom);
131
131
  const token = refresh.getAttribute("data-size-token") as SizeTokens;
132
132
  const px = Number(getVariableValue(getSize(token)));
133
- expect(refresh.className).toContain(`_mih-${px}px`);
134
- expect(refresh.className).toContain(`_miw-${px}px`);
133
+ // LC-176: the bare glyph sits in a square box at its control height, inside
134
+ // the token box, and slop restores the 44pt press floor.
135
+ const box = Number(/(?:^| )_mih-(\d+)px/.exec(refresh.className)?.[1]);
136
+ expect(refresh.className).toContain(`_miw-${box}px`);
137
+ expect(box).toBeGreaterThan(0);
138
+ expect(box).toBeLessThanOrEqual(px);
135
139
  const slop = Number(refresh.getAttribute("data-mp-press-slop") ?? 0);
136
- expect(px + slop * 2).toBeGreaterThanOrEqual(44);
140
+ expect(box + slop * 2).toBeGreaterThanOrEqual(44);
137
141
  });
138
142
 
139
143
  it("hover feedback rides the button's own in-slot box (house channel)", async () => {
@@ -1202,7 +1202,6 @@ function FrappeListViewBody<TDoc extends { name: string; doctype: string }>({
1202
1202
  feedback stays inside its own layout slot. */}
1203
1203
  <Button
1204
1204
  size="$2"
1205
- circular
1206
1205
  chromeless
1207
1206
  icon={ArrowsClockwiseIcon}
1208
1207
  onPress={handleRefresh}
@@ -40,7 +40,7 @@
40
40
  * hidden where the web download pipeline does not exist.
41
41
  */
42
42
  import { AsyncBoundary, EmptyState } from "@multiplatform.one/components";
43
- import { Button, DatePicker, Spinner, formCommonColors } from "@multiplatform.one/forms";
43
+ import { DatePicker, IconButton, Spinner, formCommonColors } from "@multiplatform.one/forms";
44
44
  import {
45
45
  DataTable,
46
46
  DataTableExport,
@@ -477,14 +477,11 @@ export function FrappeReportView({
477
477
  </XStack>
478
478
  )}
479
479
  {refreshShown && (
480
- <Button
481
- size="$2"
480
+ <IconButton
482
481
  icon={ArrowClockwiseIcon}
483
482
  onPress={refresh}
484
483
  aria-label={t("Refresh report")}
485
- >
486
- {t("Refresh")}
487
- </Button>
484
+ />
488
485
  )}
489
486
  {exportEnabled && rows.length > 0 && (
490
487
  <DataTableExport
@@ -157,7 +157,6 @@ export function DeskShell({
157
157
  const toggleButton = showMobileToggle ? (
158
158
  <Button
159
159
  chromeless
160
- circular
161
160
  icon={mobileSidebarOpen ? XIcon : ListIcon}
162
161
  aria-label={t("Toggle sidebar")}
163
162
  aria-expanded={mobileSidebarOpen}
@@ -21,9 +21,12 @@ vi.mock("../FrappeListView", () => ({
21
21
  }));
22
22
  vi.mock("../FrappeForm", () => ({
23
23
  FrappeForm: ({ onSave, name }: { onSave: (doc: { name: string }) => void; name?: string }) => (
24
- <button onClick={() => onSave({ name: state.name })} data-testid="form" data-name={name}>
25
- Save
26
- </button>
24
+ <button
25
+ aria-label="Save"
26
+ onClick={() => onSave({ name: state.name })}
27
+ data-testid="form"
28
+ data-name={name}
29
+ />
27
30
  ),
28
31
  }));
29
32
  vi.mock("@multiplatform.one/components", () => ({
@@ -59,7 +62,7 @@ describe.each(["A/B", "A?B", "A#B", "A B", "Pokémon", "A%2FB"])("desk document
59
62
  state.name = name;
60
63
  const navigate = vi.fn();
61
64
  render(<DeskFormPage doctype="todo" onNavigate={navigate} />);
62
- fireEvent.click(screen.getByText("Save"));
65
+ fireEvent.click(screen.getByLabelText("Save"));
63
66
  expect(navigate).toHaveBeenCalledWith(path);
64
67
  });
65
68
  });
@@ -119,10 +119,12 @@ async function pressText(root: ParentNode, text: string) {
119
119
  let target: Element | null = null;
120
120
  await waitFor(
121
121
  () => {
122
- const node = Array.from(root.querySelectorAll("*")).find(
123
- (el) => el.children.length === 0 && norm(el.textContent ?? "") === norm(text),
124
- );
125
- if (!node) throw new Error(`no element with text "${text}"`);
122
+ // LC-260: Next and Done are icon buttons, named by their label.
123
+ const node =
124
+ Array.from(root.querySelectorAll("*")).find(
125
+ (el) => el.children.length === 0 && norm(el.textContent ?? "") === norm(text),
126
+ ) ?? root.querySelector(`[aria-label="${text}"]`);
127
+ if (!node) throw new Error(`no element with text or label "${text}"`);
126
128
  target = node;
127
129
  },
128
130
  { timeout: 30_000 },
@@ -123,7 +123,7 @@ describe("FileUpload size gate", () => {
123
123
  * `aria-label` still fails. Recorded on MPO-123.
124
124
  */
125
125
  const buttonLabelled = (container: HTMLElement, prefix: string) =>
126
- Array.from(container.querySelectorAll("button")).find((b) =>
126
+ Array.from(container.querySelectorAll('button, [role="button"]')).find((b) =>
127
127
  (b.getAttribute("aria-label") ?? "").startsWith(prefix),
128
128
  );
129
129
 
@@ -226,8 +226,9 @@ describe("FileUpload transfer", () => {
226
226
  /^Retry \S/,
227
227
  );
228
228
  expect(remove?.getAttribute("aria-label")).toMatch(/^Remove \S/);
229
- // The visible text stays the short verb; the file name lives on the label.
230
- expect(buttonLabelled(result.container, "Retry ")?.textContent).toContain("Retry");
229
+ // LC-260: the button shows only its glyph; the name lives on the label.
230
+ expect(buttonLabelled(result.container, "Retry ")?.textContent).toBe("");
231
+ expect(buttonLabelled(result.container, "Retry ")?.querySelector("svg")).toBeTruthy();
231
232
 
232
233
  await act(async () => {
233
234
  fireEvent.click(buttonLabelled(result.container, "Retry ") as Element);
@@ -292,6 +293,22 @@ describe("FileUpload with a value", () => {
292
293
  expect(onChange).toHaveBeenCalledWith("");
293
294
  });
294
295
 
296
+ it("an image's Replace and Remove are glyphs in one media capsule over it (LC-260, LC-176)", () => {
297
+ const result = renderWithProviders(
298
+ <FileUpload label="Photo" value="/files/sprite.png" isImage onChange={() => {}} />,
299
+ );
300
+ const actions = result.container.querySelector('[data-file-upload="preview-actions"]');
301
+ expect(actions).toBeTruthy();
302
+ const capsule = actions!.querySelector('[data-mp-icon-button-group="media"]');
303
+ expect(capsule).toBeTruthy();
304
+ const buttons = Array.from(capsule!.querySelectorAll("[data-mp-action]"));
305
+ expect(buttons.map((b) => b.getAttribute("aria-label"))).toEqual(["Replace", "Remove"]);
306
+ for (const button of buttons) {
307
+ expect(button.getAttribute("data-mp-icon-button")).not.toBe("media");
308
+ expect(button.textContent).toBe("");
309
+ }
310
+ });
311
+
295
312
  it("readOnly hides Replace and Remove — the value is visible, not editable", () => {
296
313
  const result = renderWithProviders(
297
314
  <FileUpload label="Attachment" value="/files/report.pdf" readOnly onChange={() => {}} />,