@multiplatform.one/frappe-ui 7.67.0 → 7.68.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 +34 -22
  5. package/dist/cjs/FormSidebar.native.js +38 -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 +50 -58
  35. package/dist/cjs/fields/FileUpload.native.js +50 -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 +37 -25
  66. package/dist/esm/FormSidebar.mjs.map +1 -1
  67. package/dist/esm/FormSidebar.native.js +41 -27
  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 +53 -61
  106. package/dist/esm/fields/FileUpload.mjs.map +1 -1
  107. package/dist/esm/fields/FileUpload.native.js +53 -59
  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 +37 -25
  146. package/dist/jsx/FormSidebar.mjs.map +1 -1
  147. package/dist/jsx/FormSidebar.native.js +38 -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 +53 -61
  186. package/dist/jsx/fields/FileUpload.mjs.map +1 -1
  187. package/dist/jsx/fields/FileUpload.native.js +50 -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 +52 -32
  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 +18 -3
  241. package/src/fields/FileUpload.tsx +64 -68
  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
@@ -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,20 @@ describe("FileUpload with a value", () => {
292
293
  expect(onChange).toHaveBeenCalledWith("");
293
294
  });
294
295
 
296
+ it("an image's Replace and Remove are glyphs on the media plate 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 buttons = Array.from(actions!.querySelectorAll("[data-mp-action]"));
303
+ expect(buttons.map((b) => b.getAttribute("aria-label"))).toEqual(["Replace", "Remove"]);
304
+ for (const button of buttons) {
305
+ expect(button.getAttribute("data-mp-icon-button")).toBe("media");
306
+ expect(button.textContent).toBe("");
307
+ }
308
+ });
309
+
295
310
  it("readOnly hides Replace and Remove — the value is visible, not editable", () => {
296
311
  const result = renderWithProviders(
297
312
  <FileUpload label="Attachment" value="/files/report.pdf" readOnly onChange={() => {}} />,
@@ -1,13 +1,13 @@
1
1
  import { useFrappeCollection } from "@multiplatform.one/frappe";
2
2
  import { Skeleton } from "@multiplatform.one/forms";
3
- import { FileIcon, UploadIcon, XIcon } from "@phosphor-icons/react";
3
+ import { FileIcon, UploadIcon } from "@phosphor-icons/react";
4
4
  import { sizeRecipeForToken, useResolvedKnobs, sizeTokenStep } from "@multiplatform.one/theme";
5
5
  import type React from "react";
6
6
  import { useCallback, useEffect, useRef, useState } from "react";
7
7
  import { useTranslation } from "react-i18next";
8
8
  import { Button, Image, Label, Text as TamaguiText, XStack, YStack } from "tamagui";
9
9
  import { useFrappeConfig } from "../FrappeProvider";
10
- import { Text } from "@multiplatform.one/components";
10
+ import { Text, ActionButton } from "@multiplatform.one/components";
11
11
 
12
12
  export type FrappeFileUploadHandler = (
13
13
  file: File,
@@ -291,6 +291,28 @@ export function FileUpload({
291
291
  );
292
292
  }
293
293
 
294
+ // LC-260 ICONS-OVER-WORDS: Replace and Remove are their symbols, floating
295
+ // over an image on the LC-176 media plate.
296
+ const fileActions =
297
+ !readOnly && !transfer
298
+ ? (overMedia: boolean) => (
299
+ <>
300
+ <ActionButton
301
+ kind="replace"
302
+ overMedia={overMedia}
303
+ disabled={isLocked}
304
+ onPress={handleClick}
305
+ />
306
+ <ActionButton
307
+ kind="remove"
308
+ overMedia={overMedia}
309
+ disabled={isLocked}
310
+ onPress={handleRemove}
311
+ />
312
+ </>
313
+ )
314
+ : undefined;
315
+
294
316
  return (
295
317
  <YStack {...knobProps.gap}>
296
318
  {label && (
@@ -325,17 +347,12 @@ export function FileUpload({
325
347
  </Text>
326
348
  </YStack>
327
349
  {transfer.status === "uploading" ? (
328
- <Button
329
- size="$2"
330
- variant="outlined"
331
- icon={XIcon}
332
- aria-label={t("Cancel {{name}}", { name: transfer.file.name })}
350
+ <ActionButton
351
+ kind="cancel"
352
+ label={t("Cancel {{name}}", { name: transfer.file.name })}
333
353
  disabled={isLocked}
334
354
  onPress={handleCancel}
335
- {...knobProps.borderRadius}
336
- >
337
- {t("Cancel")}
338
- </Button>
355
+ />
339
356
  ) : null}
340
357
  </XStack>
341
358
  {transfer.status === "uploading" ? (
@@ -364,27 +381,18 @@ export function FileUpload({
364
381
  {transfer.action}
365
382
  </Text>
366
383
  <XStack {...knobProps.gap}>
367
- <Button
368
- size="$2"
369
- variant="outlined"
370
- aria-label={t("Retry {{name}}", { name: transfer.file.name })}
384
+ <ActionButton
385
+ kind="retry"
386
+ label={t("Retry {{name}}", { name: transfer.file.name })}
371
387
  disabled={isLocked}
372
388
  onPress={handleRetry}
373
- {...knobProps.borderRadius}
374
- >
375
- {t("Retry")}
376
- </Button>
377
- <Button
378
- size="$2"
379
- variant="outlined"
380
- icon={XIcon}
381
- aria-label={t("Remove {{name}}", { name: transfer.file.name })}
389
+ />
390
+ <ActionButton
391
+ kind="remove"
392
+ label={t("Remove {{name}}", { name: transfer.file.name })}
382
393
  disabled={isLocked}
383
394
  onPress={handleRemoveFailed}
384
- {...knobProps.borderRadius}
385
- >
386
- {t("Remove")}
387
- </Button>
395
+ />
388
396
  </XStack>
389
397
  </YStack>
390
398
  ) : null}
@@ -404,8 +412,8 @@ export function FileUpload({
404
412
  ) : null}
405
413
 
406
414
  {value ? (
407
- <YStack {...knobProps.gap}>
408
- {isImage ? (
415
+ isImage ? (
416
+ <YStack position="relative" alignSelf="flex-start">
409
417
  <Image
410
418
  // src (string), not source: the RN source object leaks onto the
411
419
  // DOM <img> as source="[object Object]" on web
@@ -416,45 +424,33 @@ export function FileUpload({
416
424
  {...knobProps.borderRadius}
417
425
  objectFit="cover"
418
426
  />
419
- ) : (
420
- <XStack
421
- gap="$2"
422
- alignItems="center"
423
- {...knobProps.panelPadding}
424
- {...{ backgroundColor: "$color2" }}
425
- >
426
- <FileIcon size={20} />
427
- <Text flex={1} numberOfLines={1}>
428
- {value.split("/").pop() || value}
429
- </Text>
430
- </XStack>
431
- )}
432
-
433
- {!readOnly && !transfer ? (
434
- <XStack {...knobProps.gap}>
435
- <Button
436
- size="$2"
437
- variant="outlined"
438
- icon={UploadIcon}
439
- disabled={isLocked}
440
- onPress={handleClick}
441
- {...knobProps.borderRadius}
427
+ {fileActions ? (
428
+ <XStack
429
+ data-file-upload="preview-actions"
430
+ position="absolute"
431
+ top="$2"
432
+ right="$2"
433
+ zIndex={1}
434
+ {...knobProps.gap}
442
435
  >
443
- {t("Replace")}
444
- </Button>
445
- <Button
446
- size="$2"
447
- variant="outlined"
448
- icon={XIcon}
449
- disabled={isLocked}
450
- onPress={handleRemove}
451
- {...knobProps.borderRadius}
452
- >
453
- {t("Remove")}
454
- </Button>
455
- </XStack>
456
- ) : null}
457
- </YStack>
436
+ {fileActions(true)}
437
+ </XStack>
438
+ ) : null}
439
+ </YStack>
440
+ ) : (
441
+ <XStack
442
+ gap="$2"
443
+ alignItems="center"
444
+ {...knobProps.panelPadding}
445
+ {...{ backgroundColor: "$color2" }}
446
+ >
447
+ <FileIcon size={20} />
448
+ <Text flex={1} numberOfLines={1}>
449
+ {value.split("/").pop() || value}
450
+ </Text>
451
+ {fileActions?.(false)}
452
+ </XStack>
453
+ )
458
454
  ) : null}
459
455
 
460
456
  {helperText && !error && !transfer ? (
@@ -208,9 +208,7 @@ export function ListControls({
208
208
  {shuffle ? (
209
209
  <Tooltip content={t("Open a random record")}>
210
210
  <Button
211
- outlined={!shuffle.open}
212
- theme={shuffle.open ? "accent" : undefined}
213
- circular
211
+ selected={shuffle.open}
214
212
  onPress={shuffle.toggle}
215
213
  icon={<DiceFiveIcon size={16} />}
216
214
  aria-label={t("Surprise me")}
@@ -7,6 +7,7 @@ import {
7
7
  XStack,
8
8
  YStack,
9
9
  isWeb,
10
+ OverlayHeader,
10
11
  } from "@multiplatform.one/components";
11
12
  import { Button, SearchInput, Select, Switch } from "@multiplatform.one/forms";
12
13
  import {
@@ -180,7 +181,6 @@ export function ListControlsSheet({
180
181
  <XStack gap="$2" alignItems="center" flexShrink={0}>
181
182
  {shuffle ? (
182
183
  <Button
183
- circular
184
184
  chromeless
185
185
  onPress={shuffle.toggle}
186
186
  icon={DiceFiveIcon}
@@ -212,17 +212,15 @@ export function ListControlsSheet({
212
212
  onOpenChange={setOpen}
213
213
  scrollable
214
214
  header={
215
- <XStack alignItems="center" gap="$2">
216
- <Text fontSize="$6" fontWeight="700" color="$color12">
217
- {t("Filters", "Filters")}
218
- </Text>
219
- <Button chromeless onPress={reset} testID={`${testID}-reset`}>
220
- {t("Reset")}
221
- </Button>
222
- <Button chromeless onPress={() => setOpen(false)}>
223
- {t("Done")}
224
- </Button>
225
- </XStack>
215
+ <OverlayHeader
216
+ title={t("Filters", "Filters")}
217
+ actions={
218
+ <Button chromeless onPress={reset} testID={`${testID}-reset`}>
219
+ {t("Reset")}
220
+ </Button>
221
+ }
222
+ onConfirm={() => setOpen(false)}
223
+ />
226
224
  }
227
225
  >
228
226
  <YStack gap="$5" paddingBottom="$4" testID={`${testID}-filters-sheet`}>
@@ -2,11 +2,11 @@ import {
2
2
  Badge,
3
3
  DropdownMenu,
4
4
  SheetModal,
5
- Text,
6
5
  Tooltip,
7
6
  XStack,
8
7
  YStack,
9
8
  type DropdownMenuEntry,
9
+ OverlayHeader,
10
10
  } from "@multiplatform.one/components";
11
11
  import { Button, SearchInput, Select } from "@multiplatform.one/forms";
12
12
  import { useResolvedKnobs } from "@multiplatform.one/theme";
@@ -107,9 +107,7 @@ export function ListSidebarToolbar({
107
107
  >
108
108
  <Tooltip content={t("Open a random record")}>
109
109
  <Button
110
- outlined={!shuffle.open}
111
- theme={shuffle.open ? "accent" : undefined}
112
- circular
110
+ selected={shuffle.open}
113
111
  onPress={shuffle.toggle}
114
112
  icon={<DiceFiveIcon size={16} />}
115
113
  aria-label={t("Surprise me")}
@@ -145,8 +143,6 @@ export function ListSidebarToolbar({
145
143
  {menuItems.length > 0 ? (
146
144
  <DropdownMenu items={[...menuItems]} placement="bottom-end">
147
145
  <Button
148
- outlined
149
- circular
150
146
  icon={<DotsThreeIcon size={20} weight="bold" />}
151
147
  aria-label={t("More actions")}
152
148
  testID={`${testID}-more`}
@@ -162,18 +158,11 @@ export function ListSidebarToolbar({
162
158
  scrollable
163
159
  snapPoint={85}
164
160
  header={
165
- <XStack alignItems="center" {...knobProps.gap}>
166
- <Text {...knobProps.heading} fontWeight="700" role="heading" aria-level={2}>
167
- {t("Filters")}
168
- </Text>
169
- <Button
170
- theme="accent"
171
- onPress={() => setSheetOpen(false)}
172
- testID={`${testID}-filters-done`}
173
- >
174
- {t("Done")}
175
- </Button>
176
- </XStack>
161
+ <OverlayHeader
162
+ title={t("Filters")}
163
+ onConfirm={() => setSheetOpen(false)}
164
+ testID={`${testID}-filters`}
165
+ />
177
166
  }
178
167
  >
179
168
  {sheetOpen ? (
@@ -53,13 +53,8 @@ export function RecordCard({
53
53
  {favorite ? (
54
54
  <XStack position="absolute" top="$1" right="$1" zIndex={2}>
55
55
  <Button
56
- circular
57
- chromeless
58
- minWidth={knobProps.sizeToken}
59
- minHeight={knobProps.sizeToken}
60
- {...transitionProps(knobProps.motion.control)}
61
- backgroundColor="$background08"
62
- pressStyle={{ backgroundColor: "$background", scale: 0.92 }}
56
+ plate="media"
57
+ selected={false}
63
58
  onPress={handleToggleFavorite}
64
59
  col={(favorite.active ? "$red9" : "$color11") as never}
65
60
  icon={
@@ -124,14 +124,8 @@ export function RecordCard({
124
124
  : undefined)}
125
125
  >
126
126
  <Button
127
- circular
128
- chromeless
129
- minWidth={knobProps.sizeToken}
130
- minHeight={knobProps.sizeToken}
131
- {...transitionProps(knobProps.motion.control)}
132
- backgroundColor="$background08"
133
- hoverStyle={{ backgroundColor: "$background", scale: 1.08 }}
134
- pressStyle={{ backgroundColor: "$background", scale: 0.92 }}
127
+ plate="media"
128
+ selected={false}
135
129
  onPress={handleToggleFavorite}
136
130
  render="button"
137
131
  type="button"