@multiplatform.one/frappe-ui 7.66.0 → 7.67.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 (290) hide show
  1. package/dist/cjs/FormSidebar.cjs +95 -194
  2. package/dist/cjs/FormSidebar.native.js +107 -217
  3. package/dist/cjs/FormSidebar.native.js.map +1 -1
  4. package/dist/cjs/FrappeConnectionIndicator.cjs +5 -24
  5. package/dist/cjs/FrappeConnectionIndicator.native.js +5 -24
  6. package/dist/cjs/FrappeConnectionIndicator.native.js.map +1 -1
  7. package/dist/cjs/FrappeImageView.cjs +46 -65
  8. package/dist/cjs/FrappeImageView.native.js +46 -68
  9. package/dist/cjs/FrappeImageView.native.js.map +1 -1
  10. package/dist/cjs/FrappeListView.cjs +24 -67
  11. package/dist/cjs/FrappeListView.native.js +25 -71
  12. package/dist/cjs/FrappeListView.native.js.map +1 -1
  13. package/dist/cjs/FrappeRecordView.cjs +4 -0
  14. package/dist/cjs/FrappeRecordView.native.js +4 -0
  15. package/dist/cjs/FrappeRecordView.native.js.map +1 -1
  16. package/dist/cjs/FrappeReportBuilder.cjs +21 -18
  17. package/dist/cjs/FrappeReportBuilder.native.js +21 -18
  18. package/dist/cjs/FrappeReportBuilder.native.js.map +1 -1
  19. package/dist/cjs/SyncErrorNotice.cjs +8 -36
  20. package/dist/cjs/SyncErrorNotice.native.js +8 -36
  21. package/dist/cjs/SyncErrorNotice.native.js.map +1 -1
  22. package/dist/cjs/desk/DeskFormSidebar.cjs +1 -6
  23. package/dist/cjs/desk/DeskFormSidebar.native.js +1 -6
  24. package/dist/cjs/desk/DeskFormSidebar.native.js.map +1 -1
  25. package/dist/cjs/desk/DeskShell.native.js.map +1 -1
  26. package/dist/cjs/fields/Date/catalog.native.js +0 -1
  27. package/dist/cjs/fields/Date/catalog.native.js.map +1 -1
  28. package/dist/cjs/fields/Date/index.native.js +4 -363
  29. package/dist/cjs/fields/Date/index.native.js.map +1 -1
  30. package/dist/cjs/insights/InsightsFacetTags.cjs +16 -92
  31. package/dist/cjs/insights/InsightsFacetTags.native.js +15 -97
  32. package/dist/cjs/insights/InsightsFacetTags.native.js.map +1 -1
  33. package/dist/cjs/list/FacetBadge.cjs +4 -28
  34. package/dist/cjs/list/FacetBadge.native.js +4 -28
  35. package/dist/cjs/list/FacetBadge.native.js.map +1 -1
  36. package/dist/cjs/list/FacetRail.cjs +37 -128
  37. package/dist/cjs/list/FacetRail.native.js +32 -130
  38. package/dist/cjs/list/FacetRail.native.js.map +1 -1
  39. package/dist/cjs/list/RecordCard.cjs +107 -127
  40. package/dist/cjs/list/RecordCard.native.js +86 -96
  41. package/dist/cjs/list/RecordCard.native.js.map +1 -1
  42. package/dist/cjs/list/ShufflePanel.cjs +57 -36
  43. package/dist/cjs/list/ShufflePanel.native.js +64 -39
  44. package/dist/cjs/list/ShufflePanel.native.js.map +1 -1
  45. package/dist/cjs/record/RecordHero.cjs +7 -85
  46. package/dist/cjs/record/RecordHero.native.js +7 -85
  47. package/dist/cjs/record/RecordHero.native.js.map +1 -1
  48. package/dist/cjs/record/RecordPage.cjs +1 -0
  49. package/dist/cjs/record/RecordPage.native.js +1 -0
  50. package/dist/cjs/record/RecordPage.native.js.map +1 -1
  51. package/dist/cjs/report/ReportViewToolbar.cjs +70 -39
  52. package/dist/cjs/report/ReportViewToolbar.native.js +73 -40
  53. package/dist/cjs/report/ReportViewToolbar.native.js.map +1 -1
  54. package/dist/esm/FormSidebar.mjs +98 -197
  55. package/dist/esm/FormSidebar.mjs.map +1 -1
  56. package/dist/esm/FormSidebar.native.js +110 -220
  57. package/dist/esm/FormSidebar.native.js.map +1 -1
  58. package/dist/esm/FrappeConnectionIndicator.mjs +7 -26
  59. package/dist/esm/FrappeConnectionIndicator.mjs.map +1 -1
  60. package/dist/esm/FrappeConnectionIndicator.native.js +7 -26
  61. package/dist/esm/FrappeConnectionIndicator.native.js.map +1 -1
  62. package/dist/esm/FrappeImageView.mjs +48 -67
  63. package/dist/esm/FrappeImageView.mjs.map +1 -1
  64. package/dist/esm/FrappeImageView.native.js +48 -70
  65. package/dist/esm/FrappeImageView.native.js.map +1 -1
  66. package/dist/esm/FrappeListView.mjs +25 -68
  67. package/dist/esm/FrappeListView.mjs.map +1 -1
  68. package/dist/esm/FrappeListView.native.js +26 -72
  69. package/dist/esm/FrappeListView.native.js.map +1 -1
  70. package/dist/esm/FrappeRecordView.mjs +4 -0
  71. package/dist/esm/FrappeRecordView.mjs.map +1 -1
  72. package/dist/esm/FrappeRecordView.native.js +4 -0
  73. package/dist/esm/FrappeRecordView.native.js.map +1 -1
  74. package/dist/esm/FrappeReportBuilder.mjs +21 -18
  75. package/dist/esm/FrappeReportBuilder.mjs.map +1 -1
  76. package/dist/esm/FrappeReportBuilder.native.js +21 -18
  77. package/dist/esm/FrappeReportBuilder.native.js.map +1 -1
  78. package/dist/esm/SyncErrorNotice.mjs +10 -38
  79. package/dist/esm/SyncErrorNotice.mjs.map +1 -1
  80. package/dist/esm/SyncErrorNotice.native.js +10 -38
  81. package/dist/esm/SyncErrorNotice.native.js.map +1 -1
  82. package/dist/esm/desk/DeskFormSidebar.mjs +1 -6
  83. package/dist/esm/desk/DeskFormSidebar.mjs.map +1 -1
  84. package/dist/esm/desk/DeskFormSidebar.native.js +1 -6
  85. package/dist/esm/desk/DeskFormSidebar.native.js.map +1 -1
  86. package/dist/esm/desk/DeskShell.mjs.map +1 -1
  87. package/dist/esm/desk/DeskShell.native.js.map +1 -1
  88. package/dist/esm/fields/Date/catalog.mjs.map +1 -1
  89. package/dist/esm/fields/Date/catalog.native.js +0 -1
  90. package/dist/esm/fields/Date/catalog.native.js.map +1 -1
  91. package/dist/esm/fields/Date/index.native.js +2 -361
  92. package/dist/esm/fields/Date/index.native.js.map +1 -1
  93. package/dist/esm/insights/InsightsFacetTags.mjs +18 -94
  94. package/dist/esm/insights/InsightsFacetTags.mjs.map +1 -1
  95. package/dist/esm/insights/InsightsFacetTags.native.js +17 -99
  96. package/dist/esm/insights/InsightsFacetTags.native.js.map +1 -1
  97. package/dist/esm/list/FacetBadge.mjs +5 -29
  98. package/dist/esm/list/FacetBadge.mjs.map +1 -1
  99. package/dist/esm/list/FacetBadge.native.js +5 -29
  100. package/dist/esm/list/FacetBadge.native.js.map +1 -1
  101. package/dist/esm/list/FacetRail.mjs +40 -131
  102. package/dist/esm/list/FacetRail.mjs.map +1 -1
  103. package/dist/esm/list/FacetRail.native.js +35 -133
  104. package/dist/esm/list/FacetRail.native.js.map +1 -1
  105. package/dist/esm/list/RecordCard.mjs +107 -127
  106. package/dist/esm/list/RecordCard.mjs.map +1 -1
  107. package/dist/esm/list/RecordCard.native.js +86 -96
  108. package/dist/esm/list/RecordCard.native.js.map +1 -1
  109. package/dist/esm/list/ShufflePanel.mjs +59 -38
  110. package/dist/esm/list/ShufflePanel.mjs.map +1 -1
  111. package/dist/esm/list/ShufflePanel.native.js +66 -41
  112. package/dist/esm/list/ShufflePanel.native.js.map +1 -1
  113. package/dist/esm/record/RecordHero.mjs +8 -86
  114. package/dist/esm/record/RecordHero.mjs.map +1 -1
  115. package/dist/esm/record/RecordHero.native.js +8 -86
  116. package/dist/esm/record/RecordHero.native.js.map +1 -1
  117. package/dist/esm/record/RecordPage.mjs +2 -1
  118. package/dist/esm/record/RecordPage.mjs.map +1 -1
  119. package/dist/esm/record/RecordPage.native.js +2 -1
  120. package/dist/esm/record/RecordPage.native.js.map +1 -1
  121. package/dist/esm/report/ReportViewToolbar.mjs +72 -41
  122. package/dist/esm/report/ReportViewToolbar.mjs.map +1 -1
  123. package/dist/esm/report/ReportViewToolbar.native.js +75 -42
  124. package/dist/esm/report/ReportViewToolbar.native.js.map +1 -1
  125. package/dist/jsx/FormSidebar.mjs +98 -197
  126. package/dist/jsx/FormSidebar.mjs.map +1 -1
  127. package/dist/jsx/FormSidebar.native.js +107 -217
  128. package/dist/jsx/FormSidebar.native.js.map +1 -1
  129. package/dist/jsx/FrappeConnectionIndicator.mjs +7 -26
  130. package/dist/jsx/FrappeConnectionIndicator.mjs.map +1 -1
  131. package/dist/jsx/FrappeConnectionIndicator.native.js +5 -24
  132. package/dist/jsx/FrappeConnectionIndicator.native.js.map +1 -1
  133. package/dist/jsx/FrappeImageView.mjs +48 -67
  134. package/dist/jsx/FrappeImageView.mjs.map +1 -1
  135. package/dist/jsx/FrappeImageView.native.js +46 -68
  136. package/dist/jsx/FrappeImageView.native.js.map +1 -1
  137. package/dist/jsx/FrappeListView.mjs +25 -68
  138. package/dist/jsx/FrappeListView.mjs.map +1 -1
  139. package/dist/jsx/FrappeListView.native.js +25 -71
  140. package/dist/jsx/FrappeListView.native.js.map +1 -1
  141. package/dist/jsx/FrappeRecordView.mjs +4 -0
  142. package/dist/jsx/FrappeRecordView.mjs.map +1 -1
  143. package/dist/jsx/FrappeRecordView.native.js +4 -0
  144. package/dist/jsx/FrappeRecordView.native.js.map +1 -1
  145. package/dist/jsx/FrappeReportBuilder.mjs +21 -18
  146. package/dist/jsx/FrappeReportBuilder.mjs.map +1 -1
  147. package/dist/jsx/FrappeReportBuilder.native.js +21 -18
  148. package/dist/jsx/FrappeReportBuilder.native.js.map +1 -1
  149. package/dist/jsx/SyncErrorNotice.mjs +10 -38
  150. package/dist/jsx/SyncErrorNotice.mjs.map +1 -1
  151. package/dist/jsx/SyncErrorNotice.native.js +8 -36
  152. package/dist/jsx/SyncErrorNotice.native.js.map +1 -1
  153. package/dist/jsx/desk/DeskFormSidebar.mjs +1 -6
  154. package/dist/jsx/desk/DeskFormSidebar.mjs.map +1 -1
  155. package/dist/jsx/desk/DeskFormSidebar.native.js +1 -6
  156. package/dist/jsx/desk/DeskFormSidebar.native.js.map +1 -1
  157. package/dist/jsx/desk/DeskShell.mjs.map +1 -1
  158. package/dist/jsx/desk/DeskShell.native.js.map +1 -1
  159. package/dist/jsx/fields/Date/catalog.mjs.map +1 -1
  160. package/dist/jsx/fields/Date/catalog.native.js +0 -1
  161. package/dist/jsx/fields/Date/catalog.native.js.map +1 -1
  162. package/dist/jsx/fields/Date/index.native.js +4 -363
  163. package/dist/jsx/fields/Date/index.native.js.map +1 -1
  164. package/dist/jsx/insights/InsightsFacetTags.mjs +18 -94
  165. package/dist/jsx/insights/InsightsFacetTags.mjs.map +1 -1
  166. package/dist/jsx/insights/InsightsFacetTags.native.js +15 -97
  167. package/dist/jsx/insights/InsightsFacetTags.native.js.map +1 -1
  168. package/dist/jsx/list/FacetBadge.mjs +5 -29
  169. package/dist/jsx/list/FacetBadge.mjs.map +1 -1
  170. package/dist/jsx/list/FacetBadge.native.js +4 -28
  171. package/dist/jsx/list/FacetBadge.native.js.map +1 -1
  172. package/dist/jsx/list/FacetRail.mjs +40 -131
  173. package/dist/jsx/list/FacetRail.mjs.map +1 -1
  174. package/dist/jsx/list/FacetRail.native.js +32 -130
  175. package/dist/jsx/list/FacetRail.native.js.map +1 -1
  176. package/dist/jsx/list/RecordCard.mjs +107 -127
  177. package/dist/jsx/list/RecordCard.mjs.map +1 -1
  178. package/dist/jsx/list/RecordCard.native.js +86 -96
  179. package/dist/jsx/list/RecordCard.native.js.map +1 -1
  180. package/dist/jsx/list/ShufflePanel.mjs +59 -38
  181. package/dist/jsx/list/ShufflePanel.mjs.map +1 -1
  182. package/dist/jsx/list/ShufflePanel.native.js +64 -39
  183. package/dist/jsx/list/ShufflePanel.native.js.map +1 -1
  184. package/dist/jsx/record/RecordHero.mjs +8 -86
  185. package/dist/jsx/record/RecordHero.mjs.map +1 -1
  186. package/dist/jsx/record/RecordHero.native.js +7 -85
  187. package/dist/jsx/record/RecordHero.native.js.map +1 -1
  188. package/dist/jsx/record/RecordPage.mjs +2 -1
  189. package/dist/jsx/record/RecordPage.mjs.map +1 -1
  190. package/dist/jsx/record/RecordPage.native.js +1 -0
  191. package/dist/jsx/record/RecordPage.native.js.map +1 -1
  192. package/dist/jsx/report/ReportViewToolbar.mjs +72 -41
  193. package/dist/jsx/report/ReportViewToolbar.mjs.map +1 -1
  194. package/dist/jsx/report/ReportViewToolbar.native.js +73 -40
  195. package/dist/jsx/report/ReportViewToolbar.native.js.map +1 -1
  196. package/package.json +16 -16
  197. package/src/FormSidebar.stories.tsx +1 -1
  198. package/src/FormSidebar.tsx +128 -253
  199. package/src/FrappeConnectionIndicator.tsx +6 -28
  200. package/src/FrappeImageView.stories.tsx +1 -6
  201. package/src/FrappeImageView.tsx +52 -66
  202. package/src/FrappeListView.controls.spec.tsx +3 -2
  203. package/src/FrappeListView.tsx +23 -55
  204. package/src/FrappeRecordView.tsx +16 -3
  205. package/src/FrappeReportBuilder.tsx +21 -19
  206. package/src/SyncErrorNotice.spec.tsx +7 -12
  207. package/src/SyncErrorNotice.stories.tsx +2 -4
  208. package/src/SyncErrorNotice.tsx +13 -41
  209. package/src/desk/DeskFormSidebar.tsx +1 -2
  210. package/src/desk/DeskShell.tsx +1 -0
  211. package/src/fields/Date/catalog.tsx +6 -7
  212. package/src/fields/Date/nativeIsCatalog.spec.tsx +26 -0
  213. package/src/fields/Date/typeParity.spec.tsx +20 -98
  214. package/src/frappeRemainder.designLaw.spec.ts +8 -7
  215. package/src/insights/InsightsFacetTags.tsx +21 -102
  216. package/src/list/FacetBadge.tsx +7 -29
  217. package/src/list/FacetRail.spec.tsx +11 -10
  218. package/src/list/FacetRail.tsx +49 -161
  219. package/src/list/RecordCard.native.tsx +63 -64
  220. package/src/list/RecordCard.spec.tsx +8 -5
  221. package/src/list/RecordCard.tsx +116 -129
  222. package/src/list/ShufflePanel.tsx +65 -38
  223. package/src/record/RecordHero.tsx +11 -51
  224. package/src/record/RecordPage.tsx +6 -1
  225. package/src/report/ReportViewToolbar.spec.tsx +45 -3
  226. package/src/report/ReportViewToolbar.tsx +109 -56
  227. package/types/FormSidebar.d.ts +13 -3
  228. package/types/FormSidebar.d.ts.map +1 -1
  229. package/types/FrappeConnectionIndicator.d.ts +2 -1
  230. package/types/FrappeConnectionIndicator.d.ts.map +1 -1
  231. package/types/FrappeImageView.d.ts.map +1 -1
  232. package/types/FrappeListView.d.ts.map +1 -1
  233. package/types/FrappeRecordView.d.ts.map +1 -1
  234. package/types/FrappeReportBuilder.d.ts.map +1 -1
  235. package/types/SyncErrorNotice.d.ts +4 -6
  236. package/types/SyncErrorNotice.d.ts.map +1 -1
  237. package/types/desk/DeskFormSidebar.d.ts.map +1 -1
  238. package/types/desk/DeskShell.d.ts.map +1 -1
  239. package/types/fields/Date/catalog.d.ts +5 -4
  240. package/types/fields/Date/catalog.d.ts.map +1 -1
  241. package/types/insights/InsightsFacetTags.d.ts +4 -1
  242. package/types/insights/InsightsFacetTags.d.ts.map +1 -1
  243. package/types/list/FacetBadge.d.ts +3 -3
  244. package/types/list/FacetBadge.d.ts.map +1 -1
  245. package/types/list/FacetRail.d.ts +7 -5
  246. package/types/list/FacetRail.d.ts.map +1 -1
  247. package/types/list/RecordCard.d.ts +6 -6
  248. package/types/list/RecordCard.d.ts.map +1 -1
  249. package/types/list/RecordCard.native.d.ts +4 -3
  250. package/types/list/RecordCard.native.d.ts.map +1 -1
  251. package/types/list/ShufflePanel.d.ts +8 -3
  252. package/types/list/ShufflePanel.d.ts.map +1 -1
  253. package/types/record/RecordHero.d.ts +5 -1
  254. package/types/record/RecordHero.d.ts.map +1 -1
  255. package/types/record/RecordPage.d.ts.map +1 -1
  256. package/types/report/ReportViewToolbar.d.ts.map +1 -1
  257. package/dist/cjs/fields/Date/dateTimePickerLoader.cjs +0 -38
  258. package/dist/cjs/fields/Date/dateTimePickerLoader.native.js +0 -42
  259. package/dist/cjs/fields/Date/dateTimePickerLoader.native.js.map +0 -1
  260. package/dist/cjs/fields/Date/sheetKeyboard.cjs +0 -54
  261. package/dist/cjs/fields/Date/sheetKeyboard.native.js +0 -57
  262. package/dist/cjs/fields/Date/sheetKeyboard.native.js.map +0 -1
  263. package/dist/esm/fields/Date/dateTimePickerLoader.mjs +0 -13
  264. package/dist/esm/fields/Date/dateTimePickerLoader.mjs.map +0 -1
  265. package/dist/esm/fields/Date/dateTimePickerLoader.native.js +0 -14
  266. package/dist/esm/fields/Date/dateTimePickerLoader.native.js.map +0 -1
  267. package/dist/esm/fields/Date/sheetKeyboard.mjs +0 -21
  268. package/dist/esm/fields/Date/sheetKeyboard.mjs.map +0 -1
  269. package/dist/esm/fields/Date/sheetKeyboard.native.js +0 -21
  270. package/dist/esm/fields/Date/sheetKeyboard.native.js.map +0 -1
  271. package/dist/jsx/fields/Date/dateTimePickerLoader.mjs +0 -13
  272. package/dist/jsx/fields/Date/dateTimePickerLoader.mjs.map +0 -1
  273. package/dist/jsx/fields/Date/dateTimePickerLoader.native.js +0 -42
  274. package/dist/jsx/fields/Date/dateTimePickerLoader.native.js.map +0 -1
  275. package/dist/jsx/fields/Date/sheetKeyboard.mjs +0 -21
  276. package/dist/jsx/fields/Date/sheetKeyboard.mjs.map +0 -1
  277. package/dist/jsx/fields/Date/sheetKeyboard.native.js +0 -57
  278. package/dist/jsx/fields/Date/sheetKeyboard.native.js.map +0 -1
  279. package/src/fields/Date/dateTimePickerLoader.ts +0 -25
  280. package/src/fields/Date/index.native.tsx +0 -445
  281. package/src/fields/Date/nativeFallback.spec.tsx +0 -21
  282. package/src/fields/Date/nativePresentation.spec.tsx +0 -278
  283. package/src/fields/Date/sheetKeyboard.spec.ts +0 -60
  284. package/src/fields/Date/sheetKeyboard.ts +0 -45
  285. package/types/fields/Date/dateTimePickerLoader.d.ts +0 -11
  286. package/types/fields/Date/dateTimePickerLoader.d.ts.map +0 -1
  287. package/types/fields/Date/index.native.d.ts +0 -14
  288. package/types/fields/Date/index.native.d.ts.map +0 -1
  289. package/types/fields/Date/sheetKeyboard.d.ts +0 -20
  290. package/types/fields/Date/sheetKeyboard.d.ts.map +0 -1
@@ -65,12 +65,12 @@ function isModifiedPress(event?: PressLikeEvent): boolean {
65
65
  }
66
66
 
67
67
  /**
68
- * A record as a tile: the image well is the card (tinted by the record's
69
- * facet, its values stickered on it, its code stamped in the corner) with
70
- * the title and a muted subtitle under it. A routed card is a stretched
71
- * link with the heart as its sibling button (LC-74), so a first-paint click
72
- * on the heart never follows the link; modified clicks keep the browser's
73
- * new-tab semantics.
68
+ * A record as an MPO media `Card` (LC-175): the image well, tinted by the
69
+ * record's facet, is the card's full-bleed media, with the title, code,
70
+ * muted subtitle and facet chips in the body under it. A routed card is a
71
+ * stretched link in the card's overlay with the heart as its sibling button
72
+ * above it (LC-74), so a first-paint click on the heart never follows the
73
+ * link; modified clicks keep the browser's new-tab semantics.
74
74
  */
75
75
  export function RecordCard({
76
76
  title,
@@ -109,21 +109,86 @@ export function RecordCard({
109
109
  favorite?.toggle();
110
110
  };
111
111
 
112
- const passThrough = href ? "box-none" : undefined;
113
- const inert = href ? "none" : undefined;
112
+ const heart = favorite ? (
113
+ <XStack
114
+ position="absolute"
115
+ top="$1"
116
+ right="$1"
117
+ zIndex={2}
118
+ {...(isWeb
119
+ ? {
120
+ onClickCapture: (event: { preventDefault: () => void }) => {
121
+ event.preventDefault();
122
+ },
123
+ }
124
+ : undefined)}
125
+ >
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 }}
135
+ onPress={handleToggleFavorite}
136
+ render="button"
137
+ type="button"
138
+ col={(favorite.active ? "$red9" : "$color11") as never}
139
+ icon={<HeartIcon size={20} weight={favorite.active ? "fill" : "regular"} />}
140
+ aria-label={
141
+ favorite.active
142
+ ? t("Remove {{name}} from favorites", { name: title })
143
+ : t("Add {{name}} to favorites", { name: title })
144
+ }
145
+ aria-pressed={favorite.active}
146
+ testID={favoriteTestID}
147
+ />
148
+ </XStack>
149
+ ) : null;
114
150
 
115
151
  return (
116
152
  <Card
117
153
  testID={testID}
118
- position="relative"
119
- {...knobProps.cardSurface}
120
- // Full-bleed media: the card has no inset of its own and its radius
121
- // clips the image well; the body below carries the inset.
122
- padding={0}
123
- gap={0}
124
- overflow="hidden"
125
- data-record-card-bleed=""
126
- elevation={knobProps.elevation}
154
+ media={
155
+ <YStack position="relative">
156
+ <ImageWell
157
+ sources={images}
158
+ alt={title}
159
+ tint={tint}
160
+ inset={0.04}
161
+ borderRadius={0}
162
+ lazy={lazy}
163
+ {...({ "data-record-card-media": "" } as Record<string, unknown>)}
164
+ />
165
+ {heart}
166
+ </YStack>
167
+ }
168
+ overlay={
169
+ href ? (
170
+ <Anchor
171
+ href={href as never}
172
+ position="absolute"
173
+ top={0}
174
+ left={0}
175
+ right={0}
176
+ bottom={0}
177
+ textDecorationLine="none"
178
+ outlineWidth={0}
179
+ aria-label={title}
180
+ onPress={handlePress}
181
+ {...(isWeb
182
+ ? {
183
+ onFocus: () => {
184
+ if (wasKeyboardFocus()) setAnchorKeyboardFocus(true);
185
+ },
186
+ onBlur: () => setAnchorKeyboardFocus(false),
187
+ }
188
+ : undefined)}
189
+ />
190
+ ) : undefined
191
+ }
127
192
  cursor="pointer"
128
193
  {...transitionProps(knobProps.motion.control, [
129
194
  "boxShadow",
@@ -146,119 +211,41 @@ export function RecordCard({
146
211
  focusVisibleStyle: focusRing,
147
212
  })}
148
213
  >
149
- {href ? (
150
- <Anchor
151
- href={href as never}
152
- position="absolute"
153
- top={0}
154
- left={0}
155
- right={0}
156
- bottom={0}
157
- zIndex={0}
158
- textDecorationLine="none"
159
- outlineWidth={0}
160
- aria-label={title}
161
- onPress={handlePress}
162
- {...(isWeb
163
- ? {
164
- onFocus: () => {
165
- if (wasKeyboardFocus()) setAnchorKeyboardFocus(true);
166
- },
167
- onBlur: () => setAnchorKeyboardFocus(false),
168
- }
169
- : undefined)}
170
- />
214
+ <XStack gap="$2" alignItems="baseline">
215
+ <Text
216
+ {...knobProps.heading}
217
+ fontWeight="700"
218
+ fontSize="$4"
219
+ numberOfLines={1}
220
+ color="$color12"
221
+ flexShrink={1}
222
+ >
223
+ {title}
224
+ </Text>
225
+ {code ? (
226
+ <Text
227
+ fontSize="$2"
228
+ fontWeight="700"
229
+ color="$color10"
230
+ flexShrink={0}
231
+ {...({ "data-record-card-code": "" } as Record<string, unknown>)}
232
+ >
233
+ {code}
234
+ </Text>
235
+ ) : null}
236
+ </XStack>
237
+ {subtitle ? (
238
+ <Text {...knobProps.body} fontSize="$2" color="$color10" numberOfLines={1}>
239
+ {subtitle}
240
+ </Text>
241
+ ) : null}
242
+ {badges.length > 0 ? (
243
+ <XStack gap="$1.5" flexWrap="wrap">
244
+ {badges.map((badge) => (
245
+ <FacetBadge key={badge.value} label={badge.label} tint={badge.tint} size="$3" />
246
+ ))}
247
+ </XStack>
171
248
  ) : null}
172
- <YStack gap={0} zIndex={1} pointerEvents={passThrough}>
173
- <YStack position="relative" pointerEvents={passThrough}>
174
- <ImageWell
175
- sources={images}
176
- alt={title}
177
- tint={tint}
178
- inset={0.04}
179
- borderRadius={0}
180
- lazy={lazy}
181
- pointerEvents={inert}
182
- {...({ "data-record-card-media": "" } as Record<string, unknown>)}
183
- />
184
- {favorite ? (
185
- <XStack
186
- position="absolute"
187
- top="$1"
188
- right="$1"
189
- zIndex={2}
190
- pointerEvents="auto"
191
- {...(isWeb
192
- ? {
193
- onClickCapture: (event: { preventDefault: () => void }) => {
194
- event.preventDefault();
195
- },
196
- }
197
- : undefined)}
198
- >
199
- <Button
200
- circular
201
- chromeless
202
- minWidth={knobProps.sizeToken}
203
- minHeight={knobProps.sizeToken}
204
- {...transitionProps(knobProps.motion.control)}
205
- backgroundColor="$background08"
206
- hoverStyle={{ backgroundColor: "$background", scale: 1.08 }}
207
- pressStyle={{ backgroundColor: "$background", scale: 0.92 }}
208
- onPress={handleToggleFavorite}
209
- render="button"
210
- type="button"
211
- col={(favorite.active ? "$red9" : "$color11") as never}
212
- icon={<HeartIcon size={20} weight={favorite.active ? "fill" : "regular"} />}
213
- aria-label={
214
- favorite.active
215
- ? t("Remove {{name}} from favorites", { name: title })
216
- : t("Add {{name}} to favorites", { name: title })
217
- }
218
- aria-pressed={favorite.active}
219
- testID={favoriteTestID}
220
- />
221
- </XStack>
222
- ) : null}
223
- </YStack>
224
- <YStack {...knobProps.panelPadding} gap="$1.5" pointerEvents={inert}>
225
- <XStack gap="$2" alignItems="baseline">
226
- <Text
227
- {...knobProps.heading}
228
- fontWeight="700"
229
- fontSize="$4"
230
- numberOfLines={1}
231
- color="$color12"
232
- flexShrink={1}
233
- >
234
- {title}
235
- </Text>
236
- {code ? (
237
- <Text
238
- fontSize="$2"
239
- fontWeight="700"
240
- color="$color10"
241
- flexShrink={0}
242
- {...({ "data-record-card-code": "" } as Record<string, unknown>)}
243
- >
244
- {code}
245
- </Text>
246
- ) : null}
247
- </XStack>
248
- {subtitle ? (
249
- <Text {...knobProps.body} fontSize="$2" color="$color10" numberOfLines={1}>
250
- {subtitle}
251
- </Text>
252
- ) : null}
253
- {badges.length > 0 ? (
254
- <XStack gap="$1.5" flexWrap="wrap">
255
- {badges.map((badge) => (
256
- <FacetBadge key={badge.value} label={badge.label} tint={badge.tint} size="$3" />
257
- ))}
258
- </XStack>
259
- ) : null}
260
- </YStack>
261
- </YStack>
262
249
  </Card>
263
250
  );
264
251
  }
@@ -1,6 +1,14 @@
1
- import { Button, Wheel, XStack, YStack } from "@multiplatform.one/components";
1
+ import {
2
+ Button,
3
+ Tooltip,
4
+ Wheel,
5
+ XStack,
6
+ YStack,
7
+ useImageGridGeometry,
8
+ type ImageGridProps,
9
+ } from "@multiplatform.one/components";
2
10
  import { useResolvedKnobs } from "@multiplatform.one/theme";
3
- import { ArrowCounterClockwiseIcon, ArrowRightIcon } from "@phosphor-icons/react";
11
+ import { ArrowRightIcon, ShuffleIcon } from "@phosphor-icons/react";
4
12
  import type React from "react";
5
13
  import { useMemo, useState } from "react";
6
14
  import { useTranslation } from "react-i18next";
@@ -16,11 +24,11 @@ export interface ShufflePanelProps<TDoc extends { name?: string }> {
16
24
  /** Draws the current pick beside the wheel. */
17
25
  renderPick: (doc: TDoc) => React.ReactNode;
18
26
  onGo: (doc: TDoc) => void;
27
+ /** The card grid below, so the pick takes exactly one of its tracks. */
28
+ grid?: Pick<ImageGridProps<unknown>, "columns" | "tileSize">;
19
29
  testID?: string;
20
30
  }
21
31
 
22
- const pickWidth = "$12";
23
-
24
32
  function randomOtherIndex(current: number, length: number): number {
25
33
  if (length < 2) return Math.max(0, current);
26
34
  const offset = 1 + Math.floor(Math.random() * (length - 1));
@@ -28,8 +36,10 @@ function randomOtherIndex(current: number, length: number): number {
28
36
  }
29
37
 
30
38
  /**
31
- * Pick a random record: a wheel of record labels beside the current pick.
32
- * Drag or flick the wheel, or Spin for a random jump, then Go opens it.
39
+ * Pick a random record: a wheel of record labels with Shuffle and Open as
40
+ * icon buttons (LC-260), beside the current pick drawn at the card grid's
41
+ * own tile width. The panel paints no card of its own, so the pick card never
42
+ * sits inside another (LC-169).
33
43
  */
34
44
  export function ShufflePanel<TDoc extends { name?: string }>({
35
45
  id,
@@ -38,10 +48,12 @@ export function ShufflePanel<TDoc extends { name?: string }>({
38
48
  itemLabel,
39
49
  renderPick,
40
50
  onGo,
51
+ grid,
41
52
  testID,
42
53
  }: ShufflePanelProps<TDoc>) {
43
54
  const { t } = useTranslation();
44
55
  const { knobProps } = useResolvedKnobs();
56
+ const { columns, gutter } = useImageGridGeometry(grid);
45
57
  const named = useMemo(() => rows.filter((row) => row.name), [rows]);
46
58
  const items = useMemo(
47
59
  () => named.map((row) => ({ value: row.name as string, label: itemLabel(row) })),
@@ -59,49 +71,64 @@ export function ShufflePanel<TDoc extends { name?: string }>({
59
71
  const current = named.findIndex((row) => row.name === pick.name);
60
72
  setSelected(named[randomOtherIndex(current, named.length)]?.name ?? "");
61
73
  };
74
+ const pickLabel = itemLabel(pick);
75
+ const tracks = Math.max(2, columns);
62
76
 
63
77
  return (
64
78
  <XStack
65
79
  id={id}
66
80
  role="region"
67
81
  aria-label={label}
68
- {...knobProps.surface}
69
- {...knobProps.borderRadius}
70
- {...knobProps.panelPadding}
71
- {...knobProps.gap}
82
+ gap={gutter}
72
83
  alignItems="center"
73
- flexWrap="wrap"
74
84
  testID={testID}
75
85
  >
76
- <Wheel
77
- items={items}
78
- value={pick.name as string}
79
- onChange={(next) => setSelected(String(next))}
80
- visibleItems={3}
81
- loop
82
- aria-label={label}
83
- />
84
- <YStack width={pickWidth} flexShrink={0}>
86
+ {/* The controls span every track but the pick's, with the gutters
87
+ between them, so the pick lands on one tile's exact width. */}
88
+ <YStack
89
+ flexGrow={tracks - 1}
90
+ flexShrink={1}
91
+ flexBasis={gutter * (tracks - 2)}
92
+ minWidth={0}
93
+ alignItems="center"
94
+ {...knobProps.gap}
95
+ >
96
+ <Wheel
97
+ items={items}
98
+ value={pick.name as string}
99
+ onChange={(next) => setSelected(String(next))}
100
+ visibleItems={3}
101
+ loop
102
+ aria-label={label}
103
+ />
104
+ <XStack alignItems="center" gap="$2">
105
+ <Tooltip content={t("Shuffle")}>
106
+ <Button
107
+ chromeless
108
+ accentInk
109
+ circular
110
+ icon={ShuffleIcon}
111
+ aria-label={t("Shuffle")}
112
+ onPress={spin}
113
+ testID={testID ? `${testID}-spin` : undefined}
114
+ />
115
+ </Tooltip>
116
+ <Tooltip content={t("Open {{name}}", { name: pickLabel })}>
117
+ <Button
118
+ chromeless
119
+ accentInk
120
+ circular
121
+ icon={ArrowRightIcon}
122
+ aria-label={t("Open {{name}}", { name: pickLabel })}
123
+ onPress={() => onGo(pick)}
124
+ testID={testID ? `${testID}-go` : undefined}
125
+ />
126
+ </Tooltip>
127
+ </XStack>
128
+ </YStack>
129
+ <YStack flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0}>
85
130
  {renderPick(pick)}
86
131
  </YStack>
87
- <XStack {...knobProps.gap} alignItems="center" flexShrink={0}>
88
- <Button
89
- outlined
90
- onPress={spin}
91
- icon={<ArrowCounterClockwiseIcon size={16} />}
92
- testID={testID ? `${testID}-spin` : undefined}
93
- >
94
- {t("Spin")}
95
- </Button>
96
- <Button
97
- theme="accent"
98
- onPress={() => onGo(pick)}
99
- iconAfter={<ArrowRightIcon size={16} />}
100
- testID={testID ? `${testID}-go` : undefined}
101
- >
102
- {t("Go")}
103
- </Button>
104
- </XStack>
105
132
  </XStack>
106
133
  );
107
134
  }
@@ -1,5 +1,5 @@
1
1
  import {
2
- Skeleton,
2
+ AsyncSkeleton,
3
3
  TamaguiSizableText,
4
4
  Text,
5
5
  XStack,
@@ -8,7 +8,6 @@ import {
8
8
  } from "@multiplatform.one/components";
9
9
  import { sizeRecipeEscape, useResolvedKnobs } from "@multiplatform.one/theme";
10
10
  import type { ReactNode } from "react";
11
- import { useTranslation } from "react-i18next";
12
11
 
13
12
  /**
14
13
  * The record hero's geometry, shared by the loaded hero and its skeleton so
@@ -92,56 +91,17 @@ export function RecordVitals({ tiles }: { tiles: readonly RecordVitalTile[] }) {
92
91
  );
93
92
  }
94
93
 
95
- const bone = (px: number) =>
96
- sizeRecipeEscape(px, "loading bones sized to the record page they stand in for");
97
-
98
- /** The hero and tab strip's loading twin, on the same geometry. */
94
+ /**
95
+ * The hero and tab strip's loading twin: the MPO detail skeleton on the
96
+ * record hero's own geometry, so the page does not jump when the record
97
+ * lands (LC-20, LC-175).
98
+ */
99
99
  export function RecordSkeleton({ testID }: { testID?: string }) {
100
- const { t } = useTranslation();
101
- const { knobProps } = useResolvedKnobs();
102
100
  return (
103
- <YStack testID={testID} data-async-skeleton="record" aria-busy aria-label={t("Loading")}>
104
- <RecordHeroFrame {...{ "data-skeleton-part": "art" }}>
105
- <YStack alignItems="center">
106
- <Skeleton
107
- variant="rounded"
108
- width={bone(recordHeroArtHeight)}
109
- height={bone(recordHeroArtHeight)}
110
- />
111
- </YStack>
112
- </RecordHeroFrame>
113
- <RecordHeroIdentityColumn {...{ "data-skeleton-part": "identity" }}>
114
- <XStack gap="$2">
115
- <Skeleton width={bone(40)} height={bone(20)} />
116
- <Skeleton variant="rounded" width={bone(56)} height={bone(22)} />
117
- <Skeleton variant="rounded" width={bone(56)} height={bone(22)} />
118
- </XStack>
119
- <Skeleton width={bone(220)} height={bone(40)} />
120
- <Skeleton width={bone(140)} height={bone(18)} />
121
- <XStack columnGap={knobProps.gapLg.gap} paddingTop={knobProps.gap.gap}>
122
- {[0, 1, 2].map((tile) => (
123
- <YStack key={tile} gap="$1">
124
- <Skeleton width={bone(56)} height={bone(14)} />
125
- <Skeleton width={bone(48)} height={bone(22)} />
126
- </YStack>
127
- ))}
128
- </XStack>
129
- </RecordHeroIdentityColumn>
130
- <YStack {...knobProps.gapLg} paddingTop={knobProps.panelPadding.padding}>
131
- <XStack
132
- {...knobProps.gapLg}
133
- {...knobProps.panelPadding}
134
- paddingVertical={0}
135
- {...{ "data-skeleton-part": "tabs" }}
136
- >
137
- {[96, 64, 88].map((width, tab) => (
138
- <Skeleton key={tab} width={bone(width)} height={bone(40)} />
139
- ))}
140
- </XStack>
141
- <YStack {...knobProps.panelPadding} paddingVertical={0}>
142
- <Skeleton.Text lines={4} />
143
- </YStack>
144
- </YStack>
145
- </YStack>
101
+ <AsyncSkeleton
102
+ layout="detail"
103
+ hero={{ height: recordHeroStageHeight, art: recordHeroArtHeight }}
104
+ testID={testID}
105
+ />
146
106
  );
147
107
  }
@@ -15,7 +15,11 @@ import {
15
15
  type DropdownMenuEntry,
16
16
  } from "@multiplatform.one/components";
17
17
  import { Button } from "@multiplatform.one/forms";
18
- import { formatAbsoluteDateTime, useResolvedKnobs } from "@multiplatform.one/theme";
18
+ import {
19
+ DETAIL_READABLE_WIDTH,
20
+ formatAbsoluteDateTime,
21
+ useResolvedKnobs,
22
+ } from "@multiplatform.one/theme";
19
23
  import { DotsThreeOutlineIcon, HeartIcon, PencilSimpleIcon } from "@phosphor-icons/react";
20
24
  import type { ComponentType, ReactNode } from "react";
21
25
  import { useTranslation } from "react-i18next";
@@ -332,6 +336,7 @@ export function RecordPage<TDoc extends { name: string; doctype: string }>({
332
336
  <Screen
333
337
  noPadding
334
338
  gap={0}
339
+ maxWidth={DETAIL_READABLE_WIDTH}
335
340
  topBar={
336
341
  <RecordTopBar
337
342
  listName={listName}
@@ -11,8 +11,10 @@
11
11
  */
12
12
  import { renderWithProviders } from "@multiplatform.one/test-utils";
13
13
  import { fireEvent, waitFor } from "@testing-library/react";
14
+ import i18next from "i18next";
14
15
  import React from "react";
15
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
+ import { initReactI18next } from "react-i18next";
17
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
16
18
  import type { ReportColumnDef } from "./reportViewModel";
17
19
  import { ReportViewToolbar as ReportToolbar } from "./ReportViewToolbar";
18
20
 
@@ -32,7 +34,7 @@ const columns: ReportColumnDef[] = [
32
34
  },
33
35
  ];
34
36
 
35
- function renderToolbar() {
37
+ function renderToolbar(overrides: Partial<React.ComponentProps<typeof ReportToolbar>> = {}) {
36
38
  return renderWithProviders(
37
39
  <ReportToolbar
38
40
  columns={columns}
@@ -50,10 +52,19 @@ function renderToolbar() {
50
52
  onSaveReport={vi.fn()}
51
53
  onDeleteReport={vi.fn()}
52
54
  onExport={vi.fn()}
55
+ {...overrides}
53
56
  />,
54
57
  );
55
58
  }
56
59
 
60
+ beforeAll(async () => {
61
+ await i18next.use(initReactI18next).init({
62
+ lng: "en",
63
+ interpolation: { escapeValue: false },
64
+ resources: { en: { translation: {} } },
65
+ });
66
+ });
67
+
57
68
  let errorSpy: ReturnType<typeof vi.spyOn> | null = null;
58
69
  beforeEach(() => {
59
70
  // Strict local spy: the shared test setup filters known leak warnings
@@ -75,7 +86,7 @@ describe("ReportToolbar Save As popover (LC-46 DOM prop hygiene)", () => {
75
86
  it("opens the Save As popover without leaking non-DOM props", async () => {
76
87
  const result = renderToolbar();
77
88
 
78
- const saveAs = result.findTextElement("Save As…");
89
+ const saveAs = result.getButton("Save report");
79
90
  expect(saveAs).toBeDefined();
80
91
  fireEvent.click(saveAs as Element);
81
92
 
@@ -88,3 +99,34 @@ describe("ReportToolbar Save As popover (LC-46 DOM prop hygiene)", () => {
88
99
  expect(document.querySelector("[elevate]")).toBeNull();
89
100
  });
90
101
  });
102
+
103
+ describe("ReportToolbar actions are icons, delete is direct (LC-260, LC-261, LC-262)", () => {
104
+ it("names icon-only actions in their labels and counts columns without a dot join", () => {
105
+ const result = renderToolbar({ onClearColumnFilters: vi.fn(), columnFilterCount: 2 });
106
+ expect(result.getButton("Export CSV")?.textContent?.trim()).toBe("");
107
+ expect(result.getButton("Save report")?.textContent?.trim()).toBe("");
108
+ const columnsTrigger = result.getButton("Columns, 2 of 2");
109
+ expect(columnsTrigger?.textContent).toContain("2/2");
110
+ expect(document.body.textContent).not.toContain("·");
111
+ });
112
+
113
+ it("deletes the selected saved report from a trash button, after a confirm", async () => {
114
+ const onDeleteReport = vi.fn();
115
+ const result = renderToolbar({
116
+ savedReports: [{ name: "RPT-1", reportName: "Open moves" }],
117
+ currentReport: "RPT-1",
118
+ onDeleteReport,
119
+ });
120
+ fireEvent.click(result.getButton("Delete “Open moves”") as Element);
121
+ expect(onDeleteReport).not.toHaveBeenCalled();
122
+ const confirm = await waitFor(() => {
123
+ const button = Array.from(document.querySelectorAll('button, [role="button"]')).find(
124
+ (node) => node.textContent?.trim() === "Delete",
125
+ );
126
+ if (!button) throw new Error("no confirm");
127
+ return button;
128
+ });
129
+ fireEvent.click(confirm);
130
+ expect(onDeleteReport).toHaveBeenCalledWith("RPT-1");
131
+ });
132
+ });