@12-apps/ui 6.1.0 → 6.3.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 (194) hide show
  1. package/components-catalog.md +8 -0
  2. package/dist/{chunk-35VGGLOC.js → chunk-CEKPLUQV.js} +2 -2
  3. package/dist/{chunk-35VGGLOC.js.map → chunk-CEKPLUQV.js.map} +1 -1
  4. package/dist/{chunk-62XQWGXU.js → chunk-MXPXKORR.js} +19 -17
  5. package/dist/chunk-MXPXKORR.js.map +1 -0
  6. package/dist/{chunk-7XLEWBKL.js → chunk-N554AFDB.js} +7 -10
  7. package/dist/chunk-N554AFDB.js.map +1 -0
  8. package/dist/{chunk-JDO7W4D4.js → chunk-R5ZQZSEQ.js} +2 -20
  9. package/dist/{chunk-JDO7W4D4.js.map → chunk-R5ZQZSEQ.js.map} +1 -1
  10. package/dist/{chunk-R4TYXKDD.js → chunk-TPQRIPCN.js} +52 -48
  11. package/dist/chunk-TPQRIPCN.js.map +1 -0
  12. package/dist/copy.js +1 -0
  13. package/dist/copy.js.map +1 -0
  14. package/dist/data-display/CardKit.js +277 -0
  15. package/dist/data-display/CardKit.js.map +1 -0
  16. package/dist/data-display/DataViews.js +664 -616
  17. package/dist/data-display/DataViews.js.map +1 -1
  18. package/dist/data-display/SectionOnboarding.js +3 -3
  19. package/dist/data-display/SectionOnboarding.js.map +1 -1
  20. package/dist/feedback/ConfirmAction.js +3 -5
  21. package/dist/form/CategorySelect.js +1 -1
  22. package/dist/form/CepField.js +1 -1
  23. package/dist/form/total-form.js +8 -4
  24. package/dist/form/total-form.js.map +1 -1
  25. package/dist/layout/ContentToolbar.js +1 -1
  26. package/dist/layout/TableFilter.js +1 -1
  27. package/dist/pt-BR.js +222 -0
  28. package/dist/pt-BR.js.map +1 -0
  29. package/dist/types/components/data-display/CardKit/CardKebab.d.ts +23 -0
  30. package/dist/types/components/data-display/CardKit/CardKebab.d.ts.map +1 -0
  31. package/dist/types/components/data-display/CardKit/CardKit.types.d.ts +55 -0
  32. package/dist/types/components/data-display/CardKit/CardKit.types.d.ts.map +1 -0
  33. package/dist/types/components/data-display/CardKit/card-actions-context.d.ts +51 -0
  34. package/dist/types/components/data-display/CardKit/card-actions-context.d.ts.map +1 -0
  35. package/dist/types/components/data-display/CardKit/index.d.ts +15 -0
  36. package/dist/types/components/data-display/CardKit/index.d.ts.map +1 -0
  37. package/dist/types/components/data-display/CardKit/list-card-parts.d.ts +60 -0
  38. package/dist/types/components/data-display/CardKit/list-card-parts.d.ts.map +1 -0
  39. package/dist/types/components/data-display/CardKit/row-actions-to-menu.d.ts +17 -0
  40. package/dist/types/components/data-display/CardKit/row-actions-to-menu.d.ts.map +1 -0
  41. package/dist/types/components/data-display/CardKit/use-remove-confirm.d.ts +65 -0
  42. package/dist/types/components/data-display/CardKit/use-remove-confirm.d.ts.map +1 -0
  43. package/dist/types/components/data-display/CardKit/use-row-confirm.d.ts +60 -0
  44. package/dist/types/components/data-display/CardKit/use-row-confirm.d.ts.map +1 -0
  45. package/dist/types/components/data-display/DataViews/DataViewsBoard.d.ts.map +1 -1
  46. package/dist/types/components/data-display/DataViews/DeleteViewDialog.d.ts.map +1 -1
  47. package/dist/types/components/data-display/DataViews/ManageViewsDialog.d.ts +4 -1
  48. package/dist/types/components/data-display/DataViews/ManageViewsDialog.d.ts.map +1 -1
  49. package/dist/types/components/data-display/DataViews/SaveViewModal.d.ts +0 -5
  50. package/dist/types/components/data-display/DataViews/SaveViewModal.d.ts.map +1 -1
  51. package/dist/types/components/data-display/DataViews/base-card.d.ts.map +1 -1
  52. package/dist/types/components/data-display/DataViews/base-list-card-gutters.d.ts.map +1 -1
  53. package/dist/types/components/data-display/DataViews/data-views-bar-controls.d.ts.map +1 -1
  54. package/dist/types/components/data-display/DataViews/data-views-category-pill.d.ts.map +1 -1
  55. package/dist/types/components/data-display/DataViews/data-views-columns-tab.d.ts.map +1 -1
  56. package/dist/types/components/data-display/DataViews/data-views-copy-context.d.ts +17 -0
  57. package/dist/types/components/data-display/DataViews/data-views-copy-context.d.ts.map +1 -0
  58. package/dist/types/components/data-display/DataViews/data-views-copy.d.ts +205 -0
  59. package/dist/types/components/data-display/DataViews/data-views-copy.d.ts.map +1 -0
  60. package/dist/types/components/data-display/DataViews/data-views-display-panel.d.ts.map +1 -1
  61. package/dist/types/components/data-display/DataViews/data-views-display-tabs.d.ts.map +1 -1
  62. package/dist/types/components/data-display/DataViews/data-views-export.d.ts.map +1 -1
  63. package/dist/types/components/data-display/DataViews/data-views-filter-panel.d.ts.map +1 -1
  64. package/dist/types/components/data-display/DataViews/data-views-grid-helpers.d.ts.map +1 -1
  65. package/dist/types/components/data-display/DataViews/data-views-more-fields.d.ts.map +1 -1
  66. package/dist/types/components/data-display/DataViews/data-views-more-filters.d.ts.map +1 -1
  67. package/dist/types/components/data-display/DataViews/data-views-range-pill.d.ts.map +1 -1
  68. package/dist/types/components/data-display/DataViews/data-views-range-presets.d.ts +3 -2
  69. package/dist/types/components/data-display/DataViews/data-views-range-presets.d.ts.map +1 -1
  70. package/dist/types/components/data-display/DataViews/data-views-scopes.d.ts.map +1 -1
  71. package/dist/types/components/data-display/DataViews/data-views-search.d.ts.map +1 -1
  72. package/dist/types/components/data-display/DataViews/data-views-select-all-strip.d.ts.map +1 -1
  73. package/dist/types/components/data-display/DataViews/data-views-table-parts.d.ts.map +1 -1
  74. package/dist/types/components/data-display/DataViews/data-views-view-nav.d.ts +1 -1
  75. package/dist/types/components/data-display/DataViews/data-views-view-nav.d.ts.map +1 -1
  76. package/dist/types/components/data-display/DataViews/index.d.ts +2 -0
  77. package/dist/types/components/data-display/DataViews/index.d.ts.map +1 -1
  78. package/dist/types/components/data-display/DataViews/save-view-preview.d.ts +10 -0
  79. package/dist/types/components/data-display/DataViews/save-view-preview.d.ts.map +1 -0
  80. package/dist/types/components/data-display/DataViews/use-saved-views-controller.d.ts.map +1 -1
  81. package/dist/types/components/data-display/SectionOnboarding/SectionOnboarding.types.d.ts +6 -5
  82. package/dist/types/components/data-display/SectionOnboarding/SectionOnboarding.types.d.ts.map +1 -1
  83. package/dist/types/components/feedback/ConfirmAction/ConfirmAction.hooks.d.ts +1 -3
  84. package/dist/types/components/feedback/ConfirmAction/ConfirmAction.hooks.d.ts.map +1 -1
  85. package/dist/types/components/feedback/ConfirmAction/ConfirmAction.parts.d.ts +4 -1
  86. package/dist/types/components/feedback/ConfirmAction/ConfirmAction.parts.d.ts.map +1 -1
  87. package/dist/types/components/feedback/ConfirmAction/ConfirmAction.types.d.ts +9 -1
  88. package/dist/types/components/feedback/ConfirmAction/ConfirmAction.types.d.ts.map +1 -1
  89. package/dist/types/components/feedback/ConfirmAction/ConfirmButton.d.ts +2 -1
  90. package/dist/types/components/feedback/ConfirmAction/ConfirmButton.d.ts.map +1 -1
  91. package/dist/types/components/feedback/ConfirmAction/index.d.ts +2 -2
  92. package/dist/types/components/feedback/ConfirmAction/index.d.ts.map +1 -1
  93. package/dist/types/components/feedback/ConfirmAction/with-confirmation.d.ts +5 -1
  94. package/dist/types/components/feedback/ConfirmAction/with-confirmation.d.ts.map +1 -1
  95. package/dist/types/components/form/CategorySelect/CategoryPanel.d.ts +4 -1
  96. package/dist/types/components/form/CategorySelect/CategoryPanel.d.ts.map +1 -1
  97. package/dist/types/components/form/CategorySelect/CategoryPanelFoot.d.ts +5 -2
  98. package/dist/types/components/form/CategorySelect/CategoryPanelFoot.d.ts.map +1 -1
  99. package/dist/types/components/form/CategorySelect/CategoryPanelHead.d.ts +5 -2
  100. package/dist/types/components/form/CategorySelect/CategoryPanelHead.d.ts.map +1 -1
  101. package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts +3 -0
  102. package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts.map +1 -1
  103. package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts +5 -0
  104. package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts.map +1 -1
  105. package/dist/types/components/form/CategorySelect/CategoryTrigger.d.ts +4 -1
  106. package/dist/types/components/form/CategorySelect/CategoryTrigger.d.ts.map +1 -1
  107. package/dist/types/components/form/CepField/CepField.d.ts +1 -1
  108. package/dist/types/components/form/CepField/CepField.d.ts.map +1 -1
  109. package/dist/types/components/form/CepField/CepField.types.d.ts +3 -0
  110. package/dist/types/components/form/CepField/CepField.types.d.ts.map +1 -1
  111. package/dist/types/components/form/total-form/Fields/CategoryField.d.ts +4 -1
  112. package/dist/types/components/form/total-form/Fields/CategoryField.d.ts.map +1 -1
  113. package/dist/types/components/form/total-form/Fields/CepField.d.ts +4 -1
  114. package/dist/types/components/form/total-form/Fields/CepField.d.ts.map +1 -1
  115. package/dist/types/components/layout/ContentToolbar/ContentToolbar.types.d.ts +6 -1
  116. package/dist/types/components/layout/ContentToolbar/ContentToolbar.types.d.ts.map +1 -1
  117. package/dist/types/components/layout/ContentToolbar/SavedViewsMenu.d.ts.map +1 -1
  118. package/dist/types/copy.d.ts +99 -0
  119. package/dist/types/copy.d.ts.map +1 -0
  120. package/dist/types/pt-BR.d.ts +28 -0
  121. package/dist/types/pt-BR.d.ts.map +1 -0
  122. package/dist/types/utils/export-rows.d.ts +31 -0
  123. package/dist/types/utils/export-rows.d.ts.map +1 -0
  124. package/dist/types/utils/index.d.ts +1 -0
  125. package/dist/types/utils/index.d.ts.map +1 -1
  126. package/dist/utils.js +50 -0
  127. package/dist/utils.js.map +1 -1
  128. package/package.json +14 -2
  129. package/src/components/data-display/CardKit/CardKebab.tsx +43 -0
  130. package/src/components/data-display/CardKit/CardKit.md +87 -0
  131. package/src/components/data-display/CardKit/CardKit.types.ts +60 -0
  132. package/src/components/data-display/CardKit/card-actions-context.tsx +98 -0
  133. package/src/components/data-display/CardKit/index.ts +19 -0
  134. package/src/components/data-display/CardKit/list-card-parts.tsx +163 -0
  135. package/src/components/data-display/CardKit/row-actions-to-menu.ts +29 -0
  136. package/src/components/data-display/CardKit/use-remove-confirm.tsx +111 -0
  137. package/src/components/data-display/CardKit/use-row-confirm.tsx +96 -0
  138. package/src/components/data-display/DataViews/DataViewsBoard.tsx +7 -4
  139. package/src/components/data-display/DataViews/DeleteViewDialog.tsx +6 -4
  140. package/src/components/data-display/DataViews/ManageViewsDialog.tsx +26 -10
  141. package/src/components/data-display/DataViews/SaveViewModal.tsx +56 -90
  142. package/src/components/data-display/DataViews/base-card.tsx +3 -1
  143. package/src/components/data-display/DataViews/base-list-card-gutters.tsx +3 -1
  144. package/src/components/data-display/DataViews/data-views-bar-controls.tsx +8 -5
  145. package/src/components/data-display/DataViews/data-views-category-pill.tsx +6 -3
  146. package/src/components/data-display/DataViews/data-views-columns-tab.tsx +5 -3
  147. package/src/components/data-display/DataViews/data-views-copy-context.tsx +43 -0
  148. package/src/components/data-display/DataViews/data-views-copy.ts +214 -0
  149. package/src/components/data-display/DataViews/data-views-display-panel.tsx +15 -9
  150. package/src/components/data-display/DataViews/data-views-display-tabs.tsx +19 -29
  151. package/src/components/data-display/DataViews/data-views-export.tsx +9 -10
  152. package/src/components/data-display/DataViews/data-views-filter-panel.tsx +9 -5
  153. package/src/components/data-display/DataViews/data-views-grid-helpers.tsx +5 -2
  154. package/src/components/data-display/DataViews/data-views-more-fields.tsx +5 -3
  155. package/src/components/data-display/DataViews/data-views-more-filters.tsx +6 -3
  156. package/src/components/data-display/DataViews/data-views-range-pill.tsx +9 -5
  157. package/src/components/data-display/DataViews/data-views-range-presets.ts +7 -8
  158. package/src/components/data-display/DataViews/data-views-scopes.tsx +3 -1
  159. package/src/components/data-display/DataViews/data-views-search.tsx +7 -4
  160. package/src/components/data-display/DataViews/data-views-select-all-strip.tsx +5 -3
  161. package/src/components/data-display/DataViews/data-views-table-parts.tsx +7 -2
  162. package/src/components/data-display/DataViews/data-views-view-nav.tsx +22 -16
  163. package/src/components/data-display/DataViews/index.ts +20 -0
  164. package/src/components/data-display/DataViews/save-view-preview.tsx +78 -0
  165. package/src/components/data-display/DataViews/use-saved-views-controller.ts +3 -1
  166. package/src/components/data-display/SectionOnboarding/SectionOnboarding.tsx +3 -3
  167. package/src/components/data-display/SectionOnboarding/SectionOnboarding.types.ts +8 -7
  168. package/src/components/feedback/ConfirmAction/ConfirmAction.hooks.ts +2 -2
  169. package/src/components/feedback/ConfirmAction/ConfirmAction.parts.tsx +12 -5
  170. package/src/components/feedback/ConfirmAction/ConfirmAction.types.ts +9 -1
  171. package/src/components/feedback/ConfirmAction/ConfirmButton.tsx +12 -4
  172. package/src/components/feedback/ConfirmAction/index.ts +2 -2
  173. package/src/components/feedback/ConfirmAction/with-confirmation.tsx +4 -3
  174. package/src/components/form/CategorySelect/CategoryPanel.tsx +18 -6
  175. package/src/components/form/CategorySelect/CategoryPanelFoot.tsx +10 -5
  176. package/src/components/form/CategorySelect/CategoryPanelHead.tsx +8 -3
  177. package/src/components/form/CategorySelect/CategoryPanelList.tsx +17 -10
  178. package/src/components/form/CategorySelect/CategorySelect.tsx +3 -3
  179. package/src/components/form/CategorySelect/CategorySelect.types.ts +5 -0
  180. package/src/components/form/CategorySelect/CategoryTrigger.tsx +5 -1
  181. package/src/components/form/CepField/CepField.tsx +6 -9
  182. package/src/components/form/CepField/CepField.types.ts +3 -0
  183. package/src/components/form/total-form/Fields/CategoryField.tsx +5 -0
  184. package/src/components/form/total-form/Fields/CepField.tsx +5 -0
  185. package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +6 -1
  186. package/src/components/layout/ContentToolbar/SavedViewsMenu.tsx +1 -21
  187. package/src/components/layout/TableFilter/TableFilter.tsx +1 -1
  188. package/src/copy.ts +103 -0
  189. package/src/pt-BR.ts +247 -0
  190. package/src/utils/export-rows.ts +95 -0
  191. package/src/utils/index.ts +1 -0
  192. package/dist/chunk-62XQWGXU.js.map +0 -1
  193. package/dist/chunk-7XLEWBKL.js.map +0 -1
  194. package/dist/chunk-R4TYXKDD.js.map +0 -1
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@12-apps/ui",
3
- "version": "6.1.0",
3
+ "version": "6.3.0",
4
4
  "license": "MIT",
5
- "packageManager": "pnpm@9.0.0",
5
+ "packageManager": "pnpm@10.34.5",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=18.0.0",
@@ -532,6 +532,18 @@
532
532
  "./utility/WorkflowStep": {
533
533
  "types": "./dist/types/components/utility/WorkflowStep/index.d.ts",
534
534
  "default": "./dist/utility/WorkflowStep.js"
535
+ },
536
+ "./data-display/CardKit": {
537
+ "types": "./dist/types/components/data-display/CardKit/index.d.ts",
538
+ "default": "./dist/data-display/CardKit.js"
539
+ },
540
+ "./copy": {
541
+ "types": "./dist/types/copy.d.ts",
542
+ "default": "./dist/copy.js"
543
+ },
544
+ "./pt-BR": {
545
+ "types": "./dist/types/pt-BR.d.ts",
546
+ "default": "./dist/pt-BR.js"
535
547
  }
536
548
  },
537
549
  "typesVersions": {
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import type { JSX } from 'react';
4
+
5
+ import { Button } from '../../form/Button';
6
+ import { DropdownMenu, type DropdownMenuItem } from '../../navigation/DropdownMenu';
7
+
8
+ /**
9
+ * The "⋮" trigger every row and card menu opens from.
10
+ *
11
+ * One component rather than a `DropdownMenu` per entity, so the kebab looks and
12
+ * behaves identically in the card, in the list row and in the table row. The
13
+ * entity's menu owns its own popups; this is the trigger and the item list.
14
+ */
15
+ export function CardKebab({
16
+ items,
17
+ menuLabel,
18
+ dataTestId,
19
+ }: {
20
+ items: DropdownMenuItem[];
21
+ /**
22
+ * The trigger's accessible name — required, and with no default.
23
+ *
24
+ * It is the only text this component renders, and a screen reader announces
25
+ * it verbatim. A default here would be a word in the origin's language that
26
+ * reads as finished to the next adopter right up until somebody using a
27
+ * screen reader meets it.
28
+ */
29
+ menuLabel: string;
30
+ dataTestId?: string;
31
+ }): JSX.Element {
32
+ return (
33
+ <DropdownMenu
34
+ size="sm"
35
+ items={items}
36
+ trigger={
37
+ <Button variant="text" size="sm" aria-label={menuLabel} dataTestId={dataTestId}>
38
+ ⋮
39
+ </Button>
40
+ }
41
+ />
42
+ );
43
+ }
@@ -0,0 +1,87 @@
1
+ # CardKit
2
+
3
+ The furniture an admin list's cards and row menus sit in.
4
+
5
+ `BaseCard` and `BaseListCard` deliberately say nothing about what goes inside
6
+ them: the envelope is the library's and the design is the consumer's. That is
7
+ right for a card component and exactly the wrong place for every entity in an
8
+ admin app to invent its own kebab, its own two-column body, its own label/value
9
+ pair and its own confirm-before-deleting. Those are not per-entity decisions —
10
+ they are the same shape repeated — so they live here once.
11
+
12
+ **The line this kit does not cross** is the entity card itself. A "product card"
13
+ or a "discount card" knows a domain, and a domain is the consumer's.
14
+
15
+ ## What is in it
16
+
17
+ | Export | What it is |
18
+ |---|---|
19
+ | `CardKebab` | The `⋮` trigger every row and card menu opens from |
20
+ | `rowActionsToMenuItems` | One `RowAction[]` → the kebab's items, so a grid and a card cannot drift |
21
+ | `CardActionsProvider` / `useCardActions` | The tenant, the refresh and one shared error snackbar |
22
+ | `DetailColumns` `Fact` `Ledger` `BodyHeading` `TagList` | The expanded list-card body's repeated shapes |
23
+ | `useRowConfirm` | Confirm a grid SELECTION before a destructive write |
24
+ | `useRemoveConfirm` | Confirm a single row from a self-contained menu |
25
+ | `KindCardProps` / `KindListCardProps` | The `renderCard` / `renderListRow` contracts, named |
26
+
27
+ ## Every sentence is yours
28
+
29
+ Three props exist only because a default would have been someone else's
30
+ language shipped as a finished-looking silence:
31
+
32
+ - `CardKebab`'s **`menuLabel`** — the trigger's accessible name, announced
33
+ verbatim by a screen reader.
34
+ - `CardActionsProvider`'s **`errorTitle`** — the one sentence the provider
35
+ renders on its own. The message beside it comes from whatever failed.
36
+ - Both hooks' **`confirmText`** — the verb. `ConfirmOptions` already refuses to
37
+ default one, for the same reason: a confirm button reading "OK" tells the
38
+ operator nothing about what is about to happen.
39
+
40
+ ## Which confirm hook
41
+
42
+ They differ by the surface they serve, not by what they do.
43
+
44
+ **`useRowConfirm`** is for a grid whose actions are declared as `RowAction[]`
45
+ and act on a **selection**. The popup is described from that selection, because
46
+ the per-row kebab entry and the multi-select menu run the same handler —
47
+ "Delete the category Drinks?" and "Delete 4 categories?" are one question asked
48
+ of a different-sized selection, and an operator who multi-selected by mistake is
49
+ exactly who the popup is for. The selection is held while the popup is open, so
50
+ the write sees the rows the operator was looking at when they were asked; the
51
+ list can refresh underneath an open dialog, and re-reading the live selection at
52
+ confirm time is the bug this shape avoids. An empty selection opens nothing.
53
+
54
+ **`useRemoveConfirm`** is for a self-contained menu already inside a
55
+ `CardActionsProvider`. It reads the refresh and the error channel from context
56
+ and answers the `{ ok, error }` envelope those actions use.
57
+
58
+ Both close the three gaps a hand-rolled version keeps re-opening:
59
+
60
+ 1. the write leaves **only** on confirm;
61
+ 2. a refusal is surfaced **twice** — inside the popup, where the operator is
62
+ still deciding, and in the shared snackbar behind it, which is what remains
63
+ once they dismiss it;
64
+ 3. a failed attempt leaves the popup **open** to be retried, instead of
65
+ vanishing over a delete that never happened.
66
+
67
+ ## `useCardActions` throws outside its provider
68
+
69
+ Deliberately, rather than answering a null object. A menu whose `onRefresh`
70
+ silently did nothing would leave the operator looking at a stale row after a
71
+ delete that worked — which reads as the delete having failed. Failing at mount
72
+ says where the provider is missing.
73
+
74
+ ## The context is small on purpose
75
+
76
+ It is **not** an edit-dialog host. A menu that read its dialogs from here would
77
+ stop being portable, because the set of dialogs is per-entity. What it may read
78
+ is three facts: the tenant it acts inside, what to do after a write lands, and
79
+ where to put a failure.
80
+
81
+ ## See also
82
+
83
+ - `exportRows` (`@12-apps/ui/utils`) — the producing half of the grid's Export
84
+ control, for the common case where the host already has the rows.
85
+ - `useServerDataViews` (`@12-apps/app-shell/react`) — the router half of
86
+ `DataViewServer`. It lives there rather than here because it needs a router,
87
+ and a design system should not.
@@ -0,0 +1,60 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import type { CardAspectRatio, DataViewCardSelection } from '../DataViews';
4
+
5
+ /**
6
+ * THE CARD KIT — what an admin list needs above `BaseCard`, and no more.
7
+ *
8
+ * `BaseCard` and `BaseListCard` deliberately say nothing about what goes inside
9
+ * them: the envelope is the library's and the design is the consumer's. That is
10
+ * the right call for a card component and exactly the wrong place for every
11
+ * entity in an admin app to invent its own kebab, its own two-column body, its
12
+ * own label/value pair and its own confirm-before-deleting. Those shapes are
13
+ * not per-entity decisions — they are the same shape repeated — so they live
14
+ * here once.
15
+ *
16
+ * The line this kit does NOT cross is the entity card itself. A "product card"
17
+ * or a "discount card" knows a domain, and a domain is the consumer's. What is
18
+ * here is the furniture such a card sits in.
19
+ */
20
+
21
+ /**
22
+ * Props every KIND CARD receives — the grid's `renderCard` contract, named.
23
+ *
24
+ * `row` is `Record<string, unknown>` rather than a generic on purpose: a grid
25
+ * renders rows of one shape and the renderer is registered per kind, so the
26
+ * cast happens once inside each card rather than threading a type parameter
27
+ * through the grid, the layout and the selection.
28
+ */
29
+ export interface KindCardProps {
30
+ row: Record<string, unknown>;
31
+ selection: DataViewCardSelection;
32
+ /** Force a ratio (a uniform grid); defaults to the card's own. */
33
+ aspectRatio?: CardAspectRatio;
34
+ }
35
+
36
+ /**
37
+ * Props every kind LIST card receives — the "list" layout's counterpart.
38
+ *
39
+ * The same `(row, selection)` pair `renderListRow` hands over, and nothing
40
+ * more. No density: a row inside a list group takes the group's, so a card
41
+ * accepting one could only contradict its list. No `aspectRatio`: a full-width
42
+ * row has no ratio to force.
43
+ */
44
+ export interface KindListCardProps {
45
+ row: Record<string, unknown>;
46
+ selection: DataViewCardSelection;
47
+ }
48
+
49
+ /** One labelled fact inside an expanded body. */
50
+ export interface CardFactProps {
51
+ label: string;
52
+ value: ReactNode;
53
+ }
54
+
55
+ /** One line of a money ledger. `info` tints a line that is not a plain cost. */
56
+ export interface CardLedgerLine {
57
+ label: string;
58
+ value: ReactNode;
59
+ tone?: 'default' | 'info';
60
+ }
@@ -0,0 +1,98 @@
1
+ 'use client';
2
+
3
+ import { createContext, useCallback, useContext, useState, type JSX, type ReactNode } from 'react';
4
+
5
+ import { Snackbar } from '@mui/material';
6
+
7
+ import { Alert } from '../Alert';
8
+
9
+ /**
10
+ * The ambient wiring a self-contained row/card menu needs, and nothing else.
11
+ *
12
+ * A menu that owns its own popups still needs three things it cannot know: the
13
+ * tenant it is acting inside, what to do after a write lands, and somewhere to
14
+ * put a failure. Passing those down through the grid, the layout and the card
15
+ * is four props of drilling per entity; holding them in context is one.
16
+ *
17
+ * Kept deliberately SMALL — it is not an edit-dialog host. A menu that read its
18
+ * dialogs from here would stop being portable, because the set of dialogs is
19
+ * per-entity. What it may read is the three facts below.
20
+ *
21
+ * The provider renders ONE shared error snackbar, which is the whole reason the
22
+ * error channel is here rather than per-menu: before it, every page carried its
23
+ * own error alert and every menu had to find its page's.
24
+ */
25
+ interface CardActionsValue {
26
+ /** The tenant every action in this subtree acts inside. */
27
+ tenantSlug: string;
28
+ /** Re-read the list after a write. */
29
+ onRefresh: () => void;
30
+ /** Surface a failed action's message (the shared snackbar). */
31
+ notifyError: (message: string) => void;
32
+ }
33
+
34
+ const CardActionsContext = createContext<CardActionsValue | null>(null);
35
+
36
+ export function CardActionsProvider({
37
+ tenantSlug,
38
+ onRefresh,
39
+ errorTitle,
40
+ autoHideMs = 6_000,
41
+ children,
42
+ }: {
43
+ tenantSlug: string;
44
+ onRefresh: () => void;
45
+ /**
46
+ * The snackbar's heading — required, no default.
47
+ *
48
+ * It is the one sentence this provider renders on its own, and a default
49
+ * would be the origin's language shipped as a finished-looking silence. The
50
+ * MESSAGE beside it comes from whatever failed, which is already the host's.
51
+ */
52
+ errorTitle: string;
53
+ /** How long the snackbar stays up. */
54
+ autoHideMs?: number;
55
+ children: ReactNode;
56
+ }): JSX.Element {
57
+ const [error, setError] = useState<string | null>(null);
58
+ const notifyError = useCallback((message: string) => setError(message), []);
59
+
60
+ return (
61
+ <CardActionsContext.Provider value={{ tenantSlug, onRefresh, notifyError }}>
62
+ {children}
63
+ <Snackbar
64
+ open={error !== null}
65
+ autoHideDuration={autoHideMs}
66
+ onClose={() => setError(null)}
67
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
68
+ >
69
+ <span>
70
+ <Alert
71
+ variant="danger"
72
+ title={errorTitle}
73
+ description={error ?? ''}
74
+ closable
75
+ onClose={() => setError(null)}
76
+ data-testid="card-action-error"
77
+ />
78
+ </span>
79
+ </Snackbar>
80
+ </CardActionsContext.Provider>
81
+ );
82
+ }
83
+
84
+ /**
85
+ * Read the tenant + refresh + error wiring.
86
+ *
87
+ * THROWS outside a provider rather than answering a null object, deliberately:
88
+ * a menu whose `onRefresh` silently did nothing would leave the operator
89
+ * looking at a stale row after a delete that worked, which reads as the delete
90
+ * having failed. Failing at mount says where the provider is missing.
91
+ */
92
+ export function useCardActions(): CardActionsValue {
93
+ const value = useContext(CardActionsContext);
94
+ if (!value) {
95
+ throw new Error('useCardActions must be used within a CardActionsProvider');
96
+ }
97
+ return value;
98
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * `@12-apps/ui/data-display/CardKit` — the furniture an admin list's cards and
3
+ * row menus sit in.
4
+ *
5
+ * See `./CardKit.types` for where the line falls: the envelope and the repeated
6
+ * shapes are here; the entity card that knows a domain is the consumer's.
7
+ */
8
+ export { CardKebab } from './CardKebab';
9
+ export { CardActionsProvider, useCardActions } from './card-actions-context';
10
+ export { rowActionsToMenuItems } from './row-actions-to-menu';
11
+ export { BodyHeading, DetailColumns, Fact, Ledger, TagList } from './list-card-parts';
12
+ export { useRowConfirm, type RowConfirm, type RowConfirmCopy } from './use-row-confirm';
13
+ export { useRemoveConfirm, type CardWriteResult } from './use-remove-confirm';
14
+ export type {
15
+ CardFactProps,
16
+ CardLedgerLine,
17
+ KindCardProps,
18
+ KindListCardProps,
19
+ } from './CardKit.types';
@@ -0,0 +1,163 @@
1
+ 'use client';
2
+
3
+ import type { JSX, ReactNode } from 'react';
4
+
5
+ import { Box } from '../../../mui/Box';
6
+
7
+ import type { CardFactProps, CardLedgerLine } from './CardKit.types';
8
+
9
+ /**
10
+ * THE SHARED FURNITURE OF AN EXPANDED LIST-CARD BODY.
11
+ *
12
+ * `BaseListCard` says nothing about what goes inside a row's expanded area — it
13
+ * spans the rails and stops, because the design belongs to the consumer. Right
14
+ * for the card; wrong as the place where every entity in an admin app invents
15
+ * its own two-column layout, its own label/value pair and its own ledger.
16
+ *
17
+ * So the four shapes that actually repeat live here, once. Nothing here knows a
18
+ * domain: they take `ReactNode` and lay it out.
19
+ */
20
+
21
+ /** The expanded body's two-column split: detail on the left, a ledger right. */
22
+ export function DetailColumns({ left, right }: { left: ReactNode; right: ReactNode }): JSX.Element {
23
+ return (
24
+ <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
25
+ <Box sx={{ minWidth: 0 }}>{left}</Box>
26
+ <Box sx={{ minWidth: 0 }}>{right}</Box>
27
+ </Box>
28
+ );
29
+ }
30
+
31
+ /**
32
+ * One labelled fact inside an expanded body.
33
+ *
34
+ * Horizontal, not stacked: the body is already a wide area with room for a
35
+ * label beside its value, and stacking them there would ape the summary rails
36
+ * above without their alignment.
37
+ */
38
+ export function Fact({ label, value }: CardFactProps): JSX.Element {
39
+ return (
40
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 2, fontSize: 13, py: 0.25 }}>
41
+ <Box component="span" sx={{ color: 'text.secondary', flexShrink: 0 }}>
42
+ {label}
43
+ </Box>
44
+ <Box component="span" sx={{ minWidth: 0, textAlign: 'right', overflowWrap: 'anywhere' }}>
45
+ {value}
46
+ </Box>
47
+ </Box>
48
+ );
49
+ }
50
+
51
+ /**
52
+ * A money ledger: rows of label/amount, then a ruled total.
53
+ *
54
+ * The total is separated by a RULE rather than by weight alone, because a
55
+ * subtotal and a total in one column with only bold between them is the classic
56
+ * misread — the eye lands on whichever number is larger.
57
+ *
58
+ * The figures arrive pre-formatted. This is a layout, and a component that
59
+ * formatted money would be choosing a locale on the consumer's behalf.
60
+ */
61
+ export function Ledger({
62
+ lines,
63
+ total,
64
+ }: {
65
+ lines: CardLedgerLine[];
66
+ total: { label: string; value: ReactNode };
67
+ }): JSX.Element {
68
+ return (
69
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
70
+ {lines.map((line) => (
71
+ <Box
72
+ key={line.label}
73
+ sx={{
74
+ display: 'flex',
75
+ justifyContent: 'space-between',
76
+ fontSize: 13,
77
+ color: line.tone === 'info' ? 'info.main' : 'text.secondary',
78
+ }}
79
+ >
80
+ <Box component="span">{line.label}</Box>
81
+ <Box component="span" sx={{ fontVariantNumeric: 'tabular-nums' }}>
82
+ {line.value}
83
+ </Box>
84
+ </Box>
85
+ ))}
86
+ <Box
87
+ sx={{
88
+ display: 'flex',
89
+ justifyContent: 'space-between',
90
+ fontWeight: 700,
91
+ borderTop: 1,
92
+ borderColor: 'divider',
93
+ pt: 0.5,
94
+ mt: 0.5,
95
+ }}
96
+ >
97
+ <Box component="span">{total.label}</Box>
98
+ <Box component="span" sx={{ fontVariantNumeric: 'tabular-nums' }}>
99
+ {total.value}
100
+ </Box>
101
+ </Box>
102
+ </Box>
103
+ );
104
+ }
105
+
106
+ /** A heading inside the expanded body, for a section that needs naming. */
107
+ export function BodyHeading({ children }: { children: ReactNode }): JSX.Element {
108
+ return (
109
+ <Box
110
+ sx={{
111
+ fontSize: 11,
112
+ fontWeight: 700,
113
+ letterSpacing: 0.4,
114
+ textTransform: 'uppercase',
115
+ color: 'text.disabled',
116
+ mb: 0.5,
117
+ }}
118
+ >
119
+ {children}
120
+ </Box>
121
+ );
122
+ }
123
+
124
+ /**
125
+ * A wrapping run of small items — permission names, role names, target ids.
126
+ *
127
+ * The whole reason several entities earn a list card at all: a `string[]` has no
128
+ * honest table cell. Truncated to "3 items" it says nothing; printed in full it
129
+ * destroys the column. Here it simply wraps.
130
+ */
131
+ export function TagList({
132
+ items,
133
+ empty,
134
+ }: {
135
+ items: readonly string[];
136
+ /** What to show for an empty run — the consumer's sentence, no default. */
137
+ empty: string;
138
+ }): JSX.Element {
139
+ if (items.length === 0) {
140
+ return <Box sx={{ fontSize: 13, color: 'text.disabled' }}>{empty}</Box>;
141
+ }
142
+ return (
143
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
144
+ {items.map((item) => (
145
+ <Box
146
+ key={item}
147
+ component="span"
148
+ sx={{
149
+ fontSize: 12,
150
+ px: 0.75,
151
+ py: 0.25,
152
+ borderRadius: 1,
153
+ bgcolor: 'action.hover',
154
+ color: 'text.secondary',
155
+ fontFamily: 'monospace',
156
+ }}
157
+ >
158
+ {item}
159
+ </Box>
160
+ ))}
161
+ </Box>
162
+ );
163
+ }
@@ -0,0 +1,29 @@
1
+ import type { DropdownMenuItem } from '../../navigation/DropdownMenu';
2
+ import type { RowAction } from '../DataViews';
3
+
4
+ /**
5
+ * Adapt a list of {@link RowAction}s into single-row kebab items.
6
+ *
7
+ * Applies each action's `isVisible` guard, resolves its per-row `rowLabel`
8
+ * override, carries its colour, and fires `onSelect([row])` on click.
9
+ *
10
+ * The point is that ONE definition drives both surfaces. An entity that already
11
+ * declares its per-row actions as `RowAction[]` — because the table's multi-
12
+ * select menu needs them — gets the card's kebab from the same array through
13
+ * `renderRowMenu` / `renderCard`, with no second list to keep in step. Two
14
+ * lists is how a "Duplicar" that appears in the table and not on the card
15
+ * happens, and nothing reports it.
16
+ */
17
+ export function rowActionsToMenuItems<T extends Record<string, unknown>>(
18
+ actions: RowAction<T>[],
19
+ row: T,
20
+ ): DropdownMenuItem[] {
21
+ return actions
22
+ .filter((action) => action.isVisible?.(row) ?? true)
23
+ .map((action) => ({
24
+ id: action.id,
25
+ label: action.rowLabel?.(row) ?? action.label,
26
+ color: action.color,
27
+ onClick: () => void action.onSelect([row]),
28
+ }));
29
+ }
@@ -0,0 +1,111 @@
1
+ 'use client';
2
+
3
+ import type { ConfirmActionCopy } from "../../../copy";
4
+ import type { JSX } from 'react';
5
+
6
+ import { ConfirmActionDialog, useConfirmAction } from '../../feedback/ConfirmAction';
7
+
8
+ import { useCardActions } from './card-actions-context';
9
+
10
+ /** The `{ ok, error }` envelope a write action answers with. */
11
+ export interface CardWriteResult {
12
+ ok: boolean;
13
+ error?: string;
14
+ }
15
+
16
+ interface RemoveConfirmOptions {
17
+ /** The destructive write. Called ONLY after the operator confirms. */
18
+ write: () => Promise<CardWriteResult>;
19
+ /** The popup's question. */
20
+ title: string;
21
+ /** The row being removed, echoed into the popup so it names a thing. */
22
+ entityName: string;
23
+ /**
24
+ * What happens, and whether it can be walked back. Say which — the difference
25
+ * between a soft delete a recycle bin recovers and one nothing recovers is
26
+ * the whole content of the decision being asked for.
27
+ */
28
+ description: string;
29
+ /** The confirm button's VERB. Required, never defaulted. */
30
+ confirmText: string;
31
+ /** Used when the write fails without a message of its own. */
32
+ fallbackError: string;
33
+ /** The dialog's own words. REQUIRED — this package ships no default copy. */
34
+ copy: ConfirmActionCopy;
35
+ dataTestId: string;
36
+ }
37
+
38
+ interface RemoveConfirm {
39
+ /** Hand this to the menu item's `onClick`: it opens the popup, nothing more. */
40
+ request: () => void;
41
+ /** Render alongside the trigger. */
42
+ dialog: JSX.Element;
43
+ }
44
+
45
+ /**
46
+ * The confirm-before-removing wiring every card/row menu repeats, in one place.
47
+ *
48
+ * It closes the three gaps a hand-rolled version keeps re-opening: the write
49
+ * leaves ONLY on confirm; a refusal is surfaced twice — inside the popup, where
50
+ * the operator is still deciding, and in the shared snackbar behind it — and a
51
+ * failed attempt leaves the popup OPEN to be retried instead of vanishing over
52
+ * a delete that never happened.
53
+ *
54
+ * The difference from {@link useRowConfirm} is which surface it serves: this one
55
+ * is for a SELF-CONTAINED menu that already sits inside a
56
+ * `CardActionsProvider`, so it reads the refresh and the error channel from
57
+ * context and answers the `{ ok, error }` envelope those actions use.
58
+ * `useRowConfirm` is for a grid whose actions are declared as `RowAction[]` and
59
+ * act on a selection.
60
+ *
61
+ * ```tsx
62
+ * const remove = useRemoveConfirm({
63
+ * write: () => deleteCategory({ tenantSlug, id: row.id }),
64
+ * title: copy.deleteTitle,
65
+ * entityName: row.name,
66
+ * description: copy.deleteDescription,
67
+ * confirmText: copy.delete,
68
+ * fallbackError: copy.deleteFailed,
69
+ * dataTestId: 'category-delete-confirm',
70
+ * });
71
+ * ```
72
+ */
73
+ export function useRemoveConfirm({
74
+ write,
75
+ title,
76
+ entityName,
77
+ description,
78
+ confirmText,
79
+ fallbackError,
80
+ dataTestId,
81
+ copy,
82
+ }: RemoveConfirmOptions): RemoveConfirm {
83
+ const { onRefresh, notifyError } = useCardActions();
84
+
85
+ const state = useConfirmAction(async () => {
86
+ const result = await write();
87
+ if (!result.ok) {
88
+ const message = result.error ?? fallbackError;
89
+ notifyError(message);
90
+ // Rejecting is what keeps the popup open carrying `message`; the snackbar
91
+ // above is what remains once the operator dismisses it.
92
+ throw new Error(message);
93
+ }
94
+ onRefresh();
95
+ }, fallbackError);
96
+
97
+ return {
98
+ request: state.request,
99
+ dialog: (
100
+ <ConfirmActionDialog
101
+ state={state}
102
+ copy={copy}
103
+ title={title}
104
+ entityName={entityName}
105
+ description={description}
106
+ confirmText={confirmText}
107
+ dataTestId={dataTestId}
108
+ />
109
+ ),
110
+ };
111
+ }