@12-apps/ui 6.12.1 → 6.13.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 (130) hide show
  1. package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +25 -25
  2. package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +27 -27
  3. package/dist/_internal/components/data-display/DataViews/base-card.js +2 -2
  4. package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +5 -5
  5. package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +4 -4
  6. package/dist/_internal/components/data-display/DataViews/base-list-card.js +6 -6
  7. package/dist/_internal/components/data-display/DataViews/data-views-category-pill.js +11 -11
  8. package/dist/_internal/components/data-display/DataViews/data-views-display-panel.js +4 -4
  9. package/dist/_internal/components/data-display/DataViews/data-views-display-tabs.js +3 -3
  10. package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +12 -12
  11. package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +4 -4
  12. package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +23 -23
  13. package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +12 -12
  14. package/dist/_internal/components/data-display/DataViews/data-views-layout-context.js +2 -2
  15. package/dist/_internal/components/data-display/DataViews/data-views-scope-tabs.js +3 -3
  16. package/dist/_internal/components/data-display/DataViews/data-views-shell-toolbar.js +6 -6
  17. package/dist/_internal/components/data-display/DataViews/data-views-toolbar.js +5 -5
  18. package/dist/_internal/components/data-display/DataViews/data-views-types.js +1 -1
  19. package/dist/_internal/components/data-display/DataViews/list-card-rails.js +3 -3
  20. package/dist/_internal/components/data-display/DataViews/use-data-views-state.js +2 -2
  21. package/dist/_internal/components/data-display/DataViews/use-saved-views-controller.js +2 -2
  22. package/dist/_internal/components/form/CategorySelect/CategoryPanel.js +5 -5
  23. package/dist/_internal/components/form/CategorySelect/CategoryPanelList.js +3 -3
  24. package/dist/_internal/components/form/CategorySelect/CategoryQuickActions.js +2 -2
  25. package/dist/_internal/components/form/CategorySelect/CategoryRows.js +2 -2
  26. package/dist/_internal/components/form/CategorySelect/CategorySelect.js +10 -10
  27. package/dist/_internal/components/form/CategorySelect/category-keyboard.js +2 -1
  28. package/dist/_internal/components/form/CategorySelect/category-tree.js +3 -1
  29. package/dist/_internal/components/form/CategorySelect/useCategoryCommit.js +2 -1
  30. package/dist/_internal/components/form/CategorySelect/useCategoryKeyHandler.js +3 -2
  31. package/dist/_internal/components/form/CategorySelect/useCategorySelect.js +2 -2
  32. package/dist/_internal/components/form/total-form/Fields/CategoryField.js +11 -11
  33. package/dist/_internal/components/form/total-form/Fields/index.js +12 -12
  34. package/dist/{chunk-HQBXB5QB.js → chunk-2GB5CIN6.js} +2 -2
  35. package/dist/{chunk-IKBQ7CS2.js → chunk-56SMIHWE.js} +2 -2
  36. package/dist/{chunk-LWYXMCX6.js → chunk-5ZZPQ3MT.js} +4 -4
  37. package/dist/{chunk-JIEEUHNS.js → chunk-AZSFIQIA.js} +8 -8
  38. package/dist/{chunk-S6T722WH.js → chunk-BMAOQDVY.js} +3 -3
  39. package/dist/{chunk-5F4NIRVF.js → chunk-CKZFYGBY.js} +10 -6
  40. package/dist/chunk-CKZFYGBY.js.map +1 -0
  41. package/dist/{chunk-CY5X3HNQ.js → chunk-E2H7MBPM.js} +2 -2
  42. package/dist/{chunk-KWUQZDPL.js → chunk-ECIGNYSI.js} +3 -2
  43. package/dist/chunk-ECIGNYSI.js.map +1 -0
  44. package/dist/{chunk-XQZOMXP3.js → chunk-FZHKKLOO.js} +2 -2
  45. package/dist/{chunk-NXTKZSMT.js → chunk-IN642YHK.js} +2 -2
  46. package/dist/{chunk-QBJVRPTF.js → chunk-ISH42UC7.js} +2 -2
  47. package/dist/{chunk-AGKQBS4C.js → chunk-JHBQZZHT.js} +3 -3
  48. package/dist/{chunk-SVXSBLVD.js → chunk-KEDI6IRA.js} +2 -2
  49. package/dist/{chunk-Z5E2PXGN.js → chunk-LPYB4Y7C.js} +49 -16
  50. package/dist/chunk-LPYB4Y7C.js.map +1 -0
  51. package/dist/{chunk-DVTCZR3K.js → chunk-LR6MSKTT.js} +7 -5
  52. package/dist/chunk-LR6MSKTT.js.map +1 -0
  53. package/dist/{chunk-USBTXEBG.js → chunk-P55UFXYW.js} +2 -2
  54. package/dist/{chunk-C6ATAHBX.js → chunk-QEPGWTJT.js} +1 -1
  55. package/dist/chunk-QEPGWTJT.js.map +1 -0
  56. package/dist/{chunk-6UGRH32J.js → chunk-QRKBBOSY.js} +4 -4
  57. package/dist/{chunk-TY6CTYCF.js → chunk-TOUSE63W.js} +2 -2
  58. package/dist/{chunk-TDSRBCBG.js → chunk-W3UM2KEW.js} +7 -7
  59. package/dist/{chunk-6IAQGSXD.js → chunk-WIEXYVMU.js} +2 -2
  60. package/dist/{chunk-GQTBWJYK.js → chunk-WKRSVGT6.js} +2 -2
  61. package/dist/{chunk-T6V3DKJ5.js → chunk-WLCVOQCJ.js} +2 -2
  62. package/dist/{chunk-GCC66ZSD.js → chunk-WWM2YTW4.js} +29 -13
  63. package/dist/chunk-WWM2YTW4.js.map +1 -0
  64. package/dist/{chunk-ZBYWGL5G.js → chunk-WXHJG2UU.js} +2 -2
  65. package/dist/{chunk-QFHUWD3C.js → chunk-XIYPVNEB.js} +3 -3
  66. package/dist/{chunk-N7SBWD2V.js → chunk-XYRS4JOF.js} +4 -4
  67. package/dist/{chunk-T4JGNZAR.js → chunk-YQJMVUBG.js} +2 -2
  68. package/dist/{chunk-F6FT2LIR.js → chunk-YRFKPRV2.js} +2 -2
  69. package/dist/{chunk-VN6Z6F7R.js → chunk-ZAB65STX.js} +11 -6
  70. package/dist/chunk-ZAB65STX.js.map +1 -0
  71. package/dist/{chunk-HVIB6WMD.js → chunk-ZIPCGZA3.js} +2 -2
  72. package/dist/{chunk-Q5GXVDXL.js → chunk-ZQPG57GA.js} +2 -2
  73. package/dist/{chunk-E3IPN3KN.js → chunk-ZUIJ3N7N.js} +2 -2
  74. package/dist/data-display/DataViews.js +31 -31
  75. package/dist/form/CategorySelect.js +10 -10
  76. package/dist/form/total-form.js +12 -12
  77. package/dist/types/components/data-display/DataViews/data-views-category-pill.d.ts +6 -0
  78. package/dist/types/components/data-display/DataViews/data-views-category-pill.d.ts.map +1 -1
  79. package/dist/types/components/data-display/DataViews/data-views-types.d.ts +13 -0
  80. package/dist/types/components/data-display/DataViews/data-views-types.d.ts.map +1 -1
  81. package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts.map +1 -1
  82. package/dist/types/components/form/CategorySelect/CategoryRows.d.ts +8 -2
  83. package/dist/types/components/form/CategorySelect/CategoryRows.d.ts.map +1 -1
  84. package/dist/types/components/form/CategorySelect/category-keyboard.d.ts +1 -1
  85. package/dist/types/components/form/CategorySelect/category-keyboard.d.ts.map +1 -1
  86. package/dist/types/components/form/CategorySelect/category-tree.d.ts +12 -0
  87. package/dist/types/components/form/CategorySelect/category-tree.d.ts.map +1 -1
  88. package/dist/types/components/form/CategorySelect/useCategoryCommit.d.ts.map +1 -1
  89. package/package.json +1 -1
  90. package/src/components/data-display/DataViews/data-views-category-pill.tsx +7 -0
  91. package/src/components/data-display/DataViews/data-views-types.ts +13 -0
  92. package/src/components/form/CategorySelect/CategoryPanelList.tsx +9 -5
  93. package/src/components/form/CategorySelect/CategoryRows.tsx +64 -23
  94. package/src/components/form/CategorySelect/CategorySelect.md +33 -6
  95. package/src/components/form/CategorySelect/category-keyboard.ts +34 -15
  96. package/src/components/form/CategorySelect/category-tree.ts +17 -8
  97. package/src/components/form/CategorySelect/useCategoryCommit.ts +10 -6
  98. package/dist/chunk-5F4NIRVF.js.map +0 -1
  99. package/dist/chunk-C6ATAHBX.js.map +0 -1
  100. package/dist/chunk-DVTCZR3K.js.map +0 -1
  101. package/dist/chunk-GCC66ZSD.js.map +0 -1
  102. package/dist/chunk-KWUQZDPL.js.map +0 -1
  103. package/dist/chunk-VN6Z6F7R.js.map +0 -1
  104. package/dist/chunk-Z5E2PXGN.js.map +0 -1
  105. /package/dist/{chunk-HQBXB5QB.js.map → chunk-2GB5CIN6.js.map} +0 -0
  106. /package/dist/{chunk-IKBQ7CS2.js.map → chunk-56SMIHWE.js.map} +0 -0
  107. /package/dist/{chunk-LWYXMCX6.js.map → chunk-5ZZPQ3MT.js.map} +0 -0
  108. /package/dist/{chunk-JIEEUHNS.js.map → chunk-AZSFIQIA.js.map} +0 -0
  109. /package/dist/{chunk-S6T722WH.js.map → chunk-BMAOQDVY.js.map} +0 -0
  110. /package/dist/{chunk-CY5X3HNQ.js.map → chunk-E2H7MBPM.js.map} +0 -0
  111. /package/dist/{chunk-XQZOMXP3.js.map → chunk-FZHKKLOO.js.map} +0 -0
  112. /package/dist/{chunk-NXTKZSMT.js.map → chunk-IN642YHK.js.map} +0 -0
  113. /package/dist/{chunk-QBJVRPTF.js.map → chunk-ISH42UC7.js.map} +0 -0
  114. /package/dist/{chunk-AGKQBS4C.js.map → chunk-JHBQZZHT.js.map} +0 -0
  115. /package/dist/{chunk-SVXSBLVD.js.map → chunk-KEDI6IRA.js.map} +0 -0
  116. /package/dist/{chunk-USBTXEBG.js.map → chunk-P55UFXYW.js.map} +0 -0
  117. /package/dist/{chunk-6UGRH32J.js.map → chunk-QRKBBOSY.js.map} +0 -0
  118. /package/dist/{chunk-TY6CTYCF.js.map → chunk-TOUSE63W.js.map} +0 -0
  119. /package/dist/{chunk-TDSRBCBG.js.map → chunk-W3UM2KEW.js.map} +0 -0
  120. /package/dist/{chunk-6IAQGSXD.js.map → chunk-WIEXYVMU.js.map} +0 -0
  121. /package/dist/{chunk-GQTBWJYK.js.map → chunk-WKRSVGT6.js.map} +0 -0
  122. /package/dist/{chunk-T6V3DKJ5.js.map → chunk-WLCVOQCJ.js.map} +0 -0
  123. /package/dist/{chunk-ZBYWGL5G.js.map → chunk-WXHJG2UU.js.map} +0 -0
  124. /package/dist/{chunk-QFHUWD3C.js.map → chunk-XIYPVNEB.js.map} +0 -0
  125. /package/dist/{chunk-N7SBWD2V.js.map → chunk-XYRS4JOF.js.map} +0 -0
  126. /package/dist/{chunk-T4JGNZAR.js.map → chunk-YQJMVUBG.js.map} +0 -0
  127. /package/dist/{chunk-F6FT2LIR.js.map → chunk-YRFKPRV2.js.map} +0 -0
  128. /package/dist/{chunk-HVIB6WMD.js.map → chunk-ZIPCGZA3.js.map} +0 -0
  129. /package/dist/{chunk-Q5GXVDXL.js.map → chunk-ZQPG57GA.js.map} +0 -0
  130. /package/dist/{chunk-E3IPN3KN.js.map → chunk-ZUIJ3N7N.js.map} +0 -0
@@ -65,6 +65,10 @@ interface CategoryHeadRowProps {
65
65
  sheet: boolean;
66
66
  /** Absent in leaf-only mode, where the category is a heading with no checkbox. */
67
67
  checkState?: CategoryCheckState;
68
+ /** Single-select draws a radio for the control: choosing a row is exclusive. */
69
+ single: boolean;
70
+ /** False for a childless category — there is nothing under it to disclose. */
71
+ expandable: boolean;
68
72
  selectedCount: number;
69
73
  showCounts: boolean;
70
74
  onToggleExpanded: () => void;
@@ -84,12 +88,59 @@ function rowMeta(
84
88
  return null;
85
89
  }
86
90
 
91
+ /** The disclosure chevron, or the space it would have taken on a childless row. */
92
+ function HeadRowDisclosure({
93
+ option,
94
+ expanded,
95
+ expandable,
96
+ onToggleExpanded,
97
+ dataTestId,
98
+ copy,
99
+ }: Pick<
100
+ CategoryHeadRowProps,
101
+ 'option' | 'expanded' | 'expandable' | 'onToggleExpanded' | 'dataTestId' | 'copy'
102
+ >): React.JSX.Element {
103
+ if (!expandable) {
104
+ return <Box sx={{ width: METRICS.chevronButton, flex: '0 0 auto' }} />;
105
+ }
106
+ return (
107
+ <Box
108
+ component="button"
109
+ type="button"
110
+ tabIndex={-1}
111
+ aria-label={expanded ? copy.collapseCategory(option.name) : copy.expandCategory(option.name)}
112
+ data-testid={`${dataTestId}-expand-${option.id}`}
113
+ sx={(theme) => chevronButtonSx(theme, expanded)}
114
+ onClick={(event: React.MouseEvent) => {
115
+ event.stopPropagation();
116
+ onToggleExpanded();
117
+ }}
118
+ >
119
+ <DisclosureGlyph />
120
+ </Box>
121
+ );
122
+ }
123
+
124
+ /** The row's selection control, or the space it would have taken on a heading. */
125
+ function HeadRowControl({
126
+ checkState,
127
+ single,
128
+ }: Pick<CategoryHeadRowProps, 'checkState' | 'single'>): React.JSX.Element {
129
+ if (!checkState) return <Box sx={{ width: METRICS.boxSize, flex: '0 0 auto' }} />;
130
+ // Single-select commits the moment a row is chosen, so the control is a radio
131
+ // there: a checkbox would promise the accumulation this mode does not do.
132
+ if (single) return <CategoryRadio on={checkState === 'on'} />;
133
+ return <CategoryCheckbox state={checkState} />;
134
+ }
135
+
87
136
  /**
88
137
  * A top-level category row: disclosure chevron, optional checkbox, name, meta.
89
138
  *
90
139
  * The chevron is its OWN button inside the row button — clicking it only folds,
91
140
  * while clicking the row does the row's job (expand as a heading, or tick when
92
- * parents are selectable).
141
+ * the category is selectable). A CHILDLESS category has neither a fold nor a
142
+ * heading to be: it draws no chevron, and it carries the control, because it is
143
+ * itself the leaf.
93
144
  */
94
145
  export function CategoryHeadRow({
95
146
  option,
@@ -98,6 +149,8 @@ export function CategoryHeadRow({
98
149
  active,
99
150
  sheet,
100
151
  checkState,
152
+ single,
153
+ expandable,
101
154
  selectedCount,
102
155
  showCounts,
103
156
  onToggleExpanded,
@@ -111,32 +164,20 @@ export function CategoryHeadRow({
111
164
  component="div"
112
165
  role={checkState ? 'option' : 'button'}
113
166
  aria-selected={checkState ? checkState === 'on' : undefined}
114
- aria-expanded={expanded}
167
+ aria-expanded={expandable ? expanded : undefined}
115
168
  data-testid={`${dataTestId}-category-${option.id}`}
116
169
  sx={(theme) => rowSx(theme, active, sheet)}
117
170
  onClick={onActivate}
118
171
  >
119
- <Box
120
- component="button"
121
- type="button"
122
- tabIndex={-1}
123
- aria-label={
124
- expanded ? copy.collapseCategory(option.name) : copy.expandCategory(option.name)
125
- }
126
- data-testid={`${dataTestId}-expand-${option.id}`}
127
- sx={(theme) => chevronButtonSx(theme, expanded)}
128
- onClick={(event: React.MouseEvent) => {
129
- event.stopPropagation();
130
- onToggleExpanded();
131
- }}
132
- >
133
- <DisclosureGlyph />
134
- </Box>
135
- {checkState ? (
136
- <CategoryCheckbox state={checkState} />
137
- ) : (
138
- <Box sx={{ width: METRICS.boxSize, flex: '0 0 auto' }} />
139
- )}
172
+ <HeadRowDisclosure
173
+ option={option}
174
+ expanded={expanded}
175
+ expandable={expandable}
176
+ onToggleExpanded={onToggleExpanded}
177
+ dataTestId={dataTestId}
178
+ copy={copy}
179
+ />
180
+ <HeadRowControl checkState={checkState} single={single} />
140
181
  <Box component="span" sx={(theme) => rowNameSx(theme, true)}>
141
182
  <HighlightedName text={option.name} query={query} />
142
183
  </Box>
@@ -32,9 +32,36 @@ const options = [
32
32
  Never pre-indent `name` (`"— Águas"`). The row draws the nesting; a baked-in dash
33
33
  travels everywhere the label goes, including the closed trigger.
34
34
 
35
- A category with no children is selectable in its own right. An option whose
36
- `parentId` matches nothing is promoted to top level rather than dropped, so a
37
- partial payload never makes a category invisible.
35
+ An option whose `parentId` matches nothing is promoted to top level rather than
36
+ dropped, so a partial payload never makes a category invisible.
37
+
38
+ ## Which rows are selectable
39
+
40
+ `allowParentSelection` is the switch, and it gates exactly one thing: whether a
41
+ parent that HAS children can be ticked in its own right.
42
+
43
+ | `allowParentSelection` | has children | the row |
44
+ | --- | --- | --- |
45
+ | `false` (default) | yes | a **heading** — no control; its subcategories are what you tick |
46
+ | `false` | no | **selectable** — it is itself the leaf |
47
+ | `true` | yes | **selectable** — ticking it selects **every subcategory under it** |
48
+ | `true` | no | **selectable** |
49
+
50
+ Two of those deserve their reasons written down.
51
+
52
+ **A parent with children, flag on, selects its CHILDREN** — not an id of its own.
53
+ That is what makes the value a caller receives uniform: always leaves, never a
54
+ mix of leaves and groups that every consumer would have to re-expand. The chip
55
+ tray collapses a fully-selected category back to one chip bearing the category's
56
+ name, so the user still sees "Bebidas" rather than six drinks.
57
+
58
+ **A childless category is selectable whatever the flag says.** The leaf-only
59
+ reading makes the category a heading and the subcategory the thing you pick — but
60
+ a childless category has no subcategory to offer instead, so that reading leaves
61
+ the row inert: no control, and a disclosure that opens onto nothing. It IS the
62
+ leaf, so it carries the control and draws no chevron. The rest of the component
63
+ always agreed: `leavesOf` returns its own id, `Marcar tudo` ticks it, and a chip
64
+ appears for it. The flag has nothing to gate there.
38
65
 
39
66
  ## The two modes
40
67
 
@@ -87,8 +114,8 @@ reads `Pai › Filha`, so the chosen leaf keeps its context.
87
114
  | `↑` `↓` | Move the cursor |
88
115
  | `→` | Expand the category |
89
116
  | `←` | Collapse it, or jump from a subcategory to its parent |
90
- | `Space` | Mark the row (single-select: pick it) |
91
- | `Enter` | Apply (single-select: pick) |
117
+ | `Space` | Mark the row (single-select: pick it) — a heading category expands |
118
+ | `Enter` | Apply (single-select: pick, and expand on a heading, as `Space` does) |
92
119
  | `Esc` | Cancel, returning focus to the trigger |
93
120
 
94
121
  `Space`, `←` and `→` belong to the search field **only while it holds text**. The
@@ -122,7 +149,7 @@ design's own values.
122
149
  | `loading` | `boolean` | `false` | Skeleton rows |
123
150
  | `fullWidth` | `boolean` | `false` | |
124
151
  | `showCounts` | `boolean` | `false` | Trailing `count` per row |
125
- | `allowParentSelection` | `boolean` | `false` | Makes categories selectable |
152
+ | `allowParentSelection` | `boolean` | `false` | Lets a parent **with children** be ticked (selecting all of them); see the table above |
126
153
  | `onCreateCategory` | `() => void` | — | CTA on the empty catalogue |
127
154
  | `dataTestId` | `string` | `'category-select'` | |
128
155
 
@@ -1,4 +1,4 @@
1
- import type { CategoryRowRef } from './category-tree';
1
+ import { isLeafCategory, type CategoryRowRef } from './category-tree';
2
2
  import type { CategoryGroup } from './CategorySelect.types';
3
3
 
4
4
  /** What a key handler is allowed to do to the panel. */
@@ -63,33 +63,52 @@ function handleLeft(context: CategoryKeyContext): void {
63
63
  }
64
64
 
65
65
  /**
66
- * Space marks. On a category that is only a HEADING it expands instead — the
67
- * row has no checkbox, so "mark" has nothing to mean there.
66
+ * What a category row does when it is activated — the one answer the click, the
67
+ * Space key and (in single-select) Enter all take, so the three cannot disagree
68
+ * about a row the way they did over a childless category.
69
+ *
70
+ * A category that is only a HEADING expands instead of marking: the row has no
71
+ * control, so "mark" has nothing to mean there. A childless category is not a
72
+ * heading — it is the leaf — so it marks.
68
73
  */
69
- function handleSpace(context: CategoryKeyContext): void {
70
- const row = activeRow(context);
71
- if (!row) return;
74
+ function activateCategoryRow(context: CategoryKeyContext, categoryId: string): void {
75
+ const group = groupOf(context, categoryId);
76
+ if (!group) return;
77
+ if (!context.allowParentSelection && !isLeafCategory(group)) {
78
+ context.toggleExpanded(categoryId);
79
+ return;
80
+ }
72
81
  if (context.single) {
73
- context.pick(row.id);
82
+ context.pick(categoryId);
74
83
  return;
75
84
  }
85
+ context.toggleCategory(group);
86
+ }
87
+
88
+ /** Space marks the row under the cursor. */
89
+ function handleSpace(context: CategoryKeyContext): void {
90
+ const row = activeRow(context);
91
+ if (!row) return;
76
92
  if (row.kind === 'subcategory') {
77
- context.toggleSubcategory(row.id);
93
+ if (context.single) context.pick(row.id);
94
+ else context.toggleSubcategory(row.id);
78
95
  return;
79
96
  }
80
- const group = groupOf(context, row.id);
81
- if (!group) return;
82
- if (context.allowParentSelection) context.toggleCategory(group);
83
- else context.toggleExpanded(row.id);
97
+ activateCategoryRow(context, row.id);
84
98
  }
85
99
 
86
100
  function handleEnter(context: CategoryKeyContext): void {
87
101
  const row = activeRow(context);
88
- if (context.single) {
89
- if (row) context.pick(row.id);
102
+ if (!context.single) {
103
+ context.commit();
104
+ return;
105
+ }
106
+ if (!row) return;
107
+ if (row.kind === 'subcategory') {
108
+ context.pick(row.id);
90
109
  return;
91
110
  }
92
- context.commit();
111
+ activateCategoryRow(context, row.id);
93
112
  }
94
113
 
95
114
  /** Key → behaviour. A map keeps this dispatch flat instead of a long if-chain. */
@@ -47,20 +47,29 @@ export function buildCategoryGroups(options: CategorySelectOption[]): CategoryGr
47
47
  return [...groups.values(), ...orphans];
48
48
  }
49
49
 
50
+ /**
51
+ * Whether this category IS the leaf, because nothing sits under it.
52
+ *
53
+ * The leaf-only default makes a category a heading and the subcategory the thing
54
+ * you pick — but a childless category has no subcategory to offer instead, so
55
+ * that reading leaves it unpickable and the row does nothing at all. It is the
56
+ * leaf, so it is selectable in its own right, whatever `allowParentSelection`
57
+ * says. Everything below already agreed; only the rows and the activation did
58
+ * not, which is what made a childless category unselectable on the estoque
59
+ * filter while `Marcar tudo` could still select it.
60
+ */
61
+ export function isLeafCategory(group: CategoryGroup): boolean {
62
+ return group.subcategories.length === 0;
63
+ }
64
+
50
65
  /** Every selectable leaf id across all groups, in display order. */
51
66
  export function collectLeafIds(groups: CategoryGroup[]): string[] {
52
- return groups.flatMap((group) =>
53
- group.subcategories.length > 0
54
- ? group.subcategories.map((sub) => sub.id)
55
- : [group.category.id],
56
- );
67
+ return groups.flatMap((group) => leavesOf(group));
57
68
  }
58
69
 
59
70
  /** The leaves a category stands for — its subcategories, or itself when childless. */
60
71
  export function leavesOf(group: CategoryGroup): string[] {
61
- return group.subcategories.length > 0
62
- ? group.subcategories.map((sub) => sub.id)
63
- : [group.category.id];
72
+ return isLeafCategory(group) ? [group.category.id] : group.subcategories.map((sub) => sub.id);
64
73
  }
65
74
 
66
75
  export type CategoryCheckState = 'off' | 'partial' | 'on';
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from 'react';
2
2
 
3
+ import { isLeafCategory } from './category-tree';
3
4
  import type { CategoryGroup, CategorySelectProps } from './CategorySelect.types';
4
5
  import type { CategorySelectState } from './useCategorySelect';
5
6
 
@@ -63,18 +64,21 @@ export function useCategoryCommit({
63
64
  }, [props]);
64
65
 
65
66
  // A category row means different things per mode: pick the whole category,
66
- // tick all its leaves, or — when it is only a heading — just fold it.
67
+ // tick all its leaves, or — when it is only a heading — just fold it. A
68
+ // CHILDLESS category is never the last of those: it is the leaf, so it acts
69
+ // like one even in the leaf-only default.
67
70
  const activateCategory = useCallback(
68
71
  (group: CategoryGroup) => {
69
- if (allowParentSelection && single) {
70
- pick(group.category.id);
72
+ const selectable = allowParentSelection || isLeafCategory(group);
73
+ if (!selectable) {
74
+ toggleExpanded(group.category.id);
71
75
  return;
72
76
  }
73
- if (allowParentSelection) {
74
- toggleCategory(group);
77
+ if (single) {
78
+ pick(group.category.id);
75
79
  return;
76
80
  }
77
- toggleExpanded(group.category.id);
81
+ toggleCategory(group);
78
82
  },
79
83
  [allowParentSelection, single, pick, toggleCategory, toggleExpanded],
80
84
  );
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/form/CategorySelect/CategoryPanelList.tsx"],"sourcesContent":["'use client';\n\nimport type { CategorySelectCopy } from \"../../../copy\";\nimport Box from '@mui/material/Box/index.js';\nimport Button from '@mui/material/Button/index.js';\nimport Skeleton from '@mui/material/Skeleton/index.js';\n\nimport { categoryCheckState } from './category-tree';\nimport {\n CategoryHeadRow,\n CategorySectionHeading,\n SubcategoryRow,\n} from './CategoryRows';\nimport { emptySx, listSx, skeletonSx } from './CategorySelect.styles';\nimport type { CategoryGroup } from './CategorySelect.types';\n\n/** The design sets these labels in sentence case, not MUI's default caps. */\nconst SENTENCE_CASE = { textTransform: 'none' } as const;\n\n/** How many skeleton rows stand in for the catalogue while it loads. */\nconst SKELETON_ROWS = 6;\n\ninterface PanelListProps {\n /** The words this panel renders. REQUIRED — no default copy. */\n copy: CategorySelectCopy;\n groups: CategoryGroup[];\n query: string;\n draft: ReadonlySet<string>;\n rowIds: string[];\n activeIndex: number;\n sheet: boolean;\n single: boolean;\n loading: boolean;\n showCounts: boolean;\n allowParentSelection: boolean;\n isExpanded: (categoryId: string) => boolean;\n onToggleExpanded: (categoryId: string) => void;\n onActivateCategory: (group: CategoryGroup) => void;\n onActivateSubcategory: (id: string) => void;\n onClearQuery: () => void;\n onCreateCategory?: (name?: string) => void;\n listRef: React.RefObject<HTMLDivElement | null>;\n dataTestId: string;\n}\n\nfunction LoadingRows(): React.JSX.Element {\n return (\n <>\n {Array.from({ length: SKELETON_ROWS }, (_unused, index) => (\n <Skeleton key={index} variant=\"rounded\" sx={skeletonSx} />\n ))}\n </>\n );\n}\n\n/** No categories exist at all — an onboarding moment, not an error. */\nfunction EmptyCatalogue({\n onCreateCategory,\n dataTestId,\n copy,\n}: {\n onCreateCategory?: (name?: string) => void;\n dataTestId: string;\n copy: CategorySelectCopy;\n}): React.JSX.Element {\n return (\n <Box sx={emptySx} data-testid={`${dataTestId}-empty-catalogue`}>\n <strong>{copy.emptyTitle}</strong>\n <p>{copy.purpose}</p>\n {onCreateCategory && (\n <Button variant=\"contained\" size=\"small\" sx={SENTENCE_CASE} onClick={() => onCreateCategory()}>\n {copy.createCategory}\n </Button>\n )}\n </Box>\n );\n}\n\n/** The search found nothing — offer creation, or the way back. */\nfunction NoResults({\n query,\n onClearQuery,\n onCreateCategory,\n dataTestId,\n copy,\n}: {\n query: string;\n onClearQuery: () => void;\n onCreateCategory?: (name?: string) => void;\n dataTestId: string;\n copy: CategorySelectCopy;\n}): React.JSX.Element {\n return (\n <Box sx={emptySx} data-testid={`${dataTestId}-no-results`}>\n <strong>{copy.noResults.title(query)}</strong>\n <p>{copy.noResults.hint}</p>\n <Box sx={{ display: 'flex', gap: '6px', justifyContent: 'center' }}>\n <Button variant=\"outlined\" size=\"small\" sx={SENTENCE_CASE} onClick={onClearQuery}>\n {copy.noResults.clearSearch}\n </Button>\n {onCreateCategory && (\n <Button\n variant=\"contained\"\n size=\"small\"\n sx={SENTENCE_CASE}\n data-testid={`${dataTestId}-create`}\n onClick={() => onCreateCategory(query.trim())}\n >\n {copy.noResults.create(query.trim())}\n </Button>\n )}\n </Box>\n </Box>\n );\n}\n\n/** One group: its heading or head row, then whichever children are visible. */\nfunction CategoryGroupRows({\n group,\n props,\n}: {\n group: CategoryGroup;\n props: PanelListProps;\n}): React.JSX.Element {\n const { draft, query, single, allowParentSelection, rowIds, activeIndex, sheet } = props;\n const categoryId = group.category.id;\n const expanded = props.isExpanded(categoryId);\n const asHeading = single && !allowParentSelection;\n const selectedCount = group.subcategories.filter((sub) => draft.has(sub.id)).length;\n\n return (\n <>\n {asHeading ? (\n <CategorySectionHeading option={group.category} query={query} />\n ) : (\n <CategoryHeadRow\n option={group.category}\n query={query}\n expanded={expanded}\n active={rowIds[activeIndex] === categoryId}\n sheet={sheet}\n checkState={\n allowParentSelection ? categoryCheckState(group, draft) : undefined\n }\n selectedCount={selectedCount}\n showCounts={props.showCounts}\n onToggleExpanded={() => props.onToggleExpanded(categoryId)}\n onActivate={() => props.onActivateCategory(group)}\n dataTestId={props.dataTestId}\n copy={props.copy}\n />\n )}\n {(expanded || asHeading) &&\n group.subcategories.map((sub) => (\n <SubcategoryRow\n key={sub.id}\n option={sub}\n query={query}\n selected={draft.has(sub.id)}\n active={rowIds[activeIndex] === sub.id}\n sheet={sheet}\n single={single}\n showCounts={props.showCounts}\n onActivate={() => props.onActivateSubcategory(sub.id)}\n dataTestId={props.dataTestId}\n />\n ))}\n </>\n );\n}\n\n/** The scrolling body of the panel, including its three non-list states. */\nexport function CategoryPanelList(props: PanelListProps): React.JSX.Element {\n const { groups, loading, query, listRef, sheet, dataTestId, copy } = props;\n\n const body = (): React.JSX.Element => {\n if (loading) return <LoadingRows />;\n if (groups.length === 0 && query.trim().length > 0) {\n return (\n <NoResults copy={copy}\n query={query}\n onClearQuery={props.onClearQuery}\n onCreateCategory={props.onCreateCategory}\n dataTestId={dataTestId}\n />\n );\n }\n if (groups.length === 0) {\n return (\n <EmptyCatalogue copy={copy}\n onCreateCategory={props.onCreateCategory}\n dataTestId={dataTestId}\n />\n );\n }\n return (\n <>\n {groups.map((group) => (\n <CategoryGroupRows key={group.category.id} group={group} props={props} />\n ))}\n </>\n );\n };\n\n return (\n <Box\n ref={listRef}\n role=\"listbox\"\n aria-multiselectable={props.single ? undefined : true}\n tabIndex={-1}\n data-testid={`${dataTestId}-list`}\n sx={listSx(sheet)}\n >\n {body()}\n </Box>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/data-display/DataViews/data-views-types.ts"],"sourcesContent":["import type { GridColumn, GridSort } from \"../DataGrid\";\nimport type { DropdownMenuItem } from \"../../navigation/DropdownMenu\";\n// A re-export does not bring the name into local scope, and the state/query\n// types below are written in terms of it.\nimport type { RangeValue } from \"./data-views-range-types\";\n\n/**\n * Shared types for the reusable admin \"DataViews\" table (FUT-88): a filter bar\n * (search-across-all + typed multi-select pills + column visibility) whose full\n * state — filters + sort + visible columns — a saved view (FUT-89) can capture\n * and restore. Each table declares its own columns + filter fields.\n */\n\n/**\n * A single, reusable row action. ONE definition drives BOTH the per-row \"⋮\"\n * kebab menu and the bulk-actions menu (tabwoah model): `onSelect` receives the\n * affected rows — `[row]` from a kebab, the whole selection from the bulk menu.\n * Single-item-only actions (e.g. \"Editar\") set `bulk: false` so they never\n * appear in the bulk menu.\n */\nexport interface RowAction<T extends Record<string, unknown>> {\n /** Stable id (also the `data-testid` suffix). */\n id: string;\n /** Menu label. In the kebab, `rowLabel` overrides it per row when provided. */\n label: string;\n /** Menu-item colour (e.g. `\"error\"` for destructive). */\n color?: DropdownMenuItem[\"color\"];\n /**\n * Runs on the affected rows: `[row]` from the kebab, all selected from bulk.\n * May be async — the bulk menu awaits it and only clears the selection once it\n * settles (keeping the selection on failure so the user can retry).\n */\n onSelect: (rows: T[]) => void | Promise<void>;\n /** Include in the bulk menu (2+ selected). Defaults to `true`. */\n bulk?: boolean;\n /** Include in the per-row kebab. Defaults to `true`. */\n row?: boolean;\n /** Per-row kebab label (e.g. `row.active ? \"Inativar\" : \"Ativar\"`). */\n rowLabel?: (row: T) => string;\n /**\n * Per-row applicability (e.g. state-machine guards). `false` hides the\n * kebab item for that row; in bulk the action runs only on the passing\n * rows and is dropped when none pass. Defaults to applicable.\n */\n isVisible?: (row: T) => boolean;\n}\n\n/** Selection state handed to a card renderer so it can drive its own checkbox. */\nexport interface DataViewCardSelection {\n selected: boolean;\n onToggleSelect: () => void;\n /**\n * Card SIZE multiplier from the toolbar zoom slider. The grid sizes the card's\n * width to `base × scale`; a card passes this to {@link BaseCard} `scale` so its\n * padding + typography grow by the same factor — the whole card scales together\n * (scale 2 → ~2× size). The proportion (aspect ratio) stays constant.\n */\n scale: number;\n}\n\n/**\n * The fixed proportions a {@link BaseCard} tile can take. The tile keeps this\n * ratio and grows in BOTH width and height as the zoom slider widens its grid\n * track, so a grid of cards stays visually uniform.\n */\nexport type CardAspectRatio = \"1:1\" | \"4:3\" | \"16:9\" | \"9:16\" | \"3:4\" | \"3:1\";\n\n/** Numeric width/height factor for each {@link CardAspectRatio} (CSS `aspect-ratio`). */\nexport const CARD_ASPECT_RATIOS: Record<CardAspectRatio, number> = {\n \"1:1\": 1,\n \"4:3\": 4 / 3,\n \"16:9\": 16 / 9,\n \"9:16\": 9 / 16,\n \"3:4\": 3 / 4,\n /**\n * The BANNER tile: too short for media, sized for two lines of text.\n *\n * Every other ratio here is picture-shaped, because they were added for cards\n * built around a thumbnail. A card whose subject is a NAME and a NUMBER — a\n * cart, an order, anything triaged rather than browsed — spends most of a 4:3\n * tile on an icon that repeats down the whole grid and says nothing.\n */\n \"3:1\": 3,\n};\n\n/** One selectable value inside a filter pill. */\nexport interface FilterOption {\n value: string;\n label: string;\n /**\n * Parent option's `value`, for a hierarchical field (`control: \"category\"`).\n * Ignored by the flat controls, so one option list serves both.\n */\n parentId?: string | null;\n}\n\n/** A typed, multi-select filter pill's configuration for a table. */\nexport interface FilterFieldConfig<T extends Record<string, unknown>> {\n /** Stable id; also the default row key when no `accessor` is given. */\n id: string;\n label: string;\n options: FilterOption[];\n /**\n * How the field renders in the filter panel. `checkboxes` (default) is best for\n * a few fixed options; `multiselect` is a compact dropdown for long option\n * lists; `category` is the hierarchical picker, for an option set that nests\n * via {@link FilterOption.parentId} — it renders parents as headings and\n * their children as the selectable rows, rather than flattening the tree into\n * one indistinguishable list.\n */\n control?: \"checkboxes\" | \"multiselect\" | \"category\";\n /**\n * Forces this filter to render as a searchable multi-select dropdown with the\n * search box always shown, regardless of how many options are currently\n * loaded. Use it when the option set can grow unbounded (e.g. a filter backed\n * by a relation such as Roles or Categories). Omit it for a closed value set\n * (an enum), which keeps the plain checkbox / \"search only when long\"\n * behavior.\n */\n searchEnabled?: boolean;\n /**\n * Reads the row's value(s) for this field. A row matches the pill when any of\n * its values is selected. Defaults to `String(row[id])`.\n */\n accessor?: (row: T) => string | string[];\n}\n\n/**\n * The range types live in `data-views-range-types` (this file outgrew the\n * size gate); they are re-exported here so `data-views-types` stays the one\n * name every consumer imports the model from.\n */\nexport type {\n DayRangeFieldConfig,\n NumberRangeFieldConfig,\n RangeFieldConfig,\n RangeFieldKind,\n RangePreset,\n RangeValue,\n} from \"./data-views-range-types\";\n\n/** A grid column that can join the search scan and the column-visibility menu. */\nexport interface DataViewColumn<T extends Record<string, unknown>> extends GridColumn<T> {\n /** Included in \"search across all columns\" (needs a resolvable value). */\n searchable?: boolean;\n /** Toggleable in the column-visibility menu. Defaults to true; set false for\n * structural columns (image thumbnail, actions kebab) that must always show. */\n hideable?: boolean;\n}\n\n/**\n * The DataViews layout (view type): the dense \"Tabela\" grid, the \"Grade\" of\n * cards, the \"Lista\" of full-width rows, or the \"Quadro\" board of state columns.\n * Lives with the state types rather than with the toggle because a saved view\n * CAPTURES it — see {@link DataViewState.layout}.\n */\nexport type DataViewsLayout = \"cards\" | \"list\" | \"board\" | \"table\";\n\n/** The full, serializable state a view captures. */\nexport interface DataViewState {\n /** Free text matched across every `searchable` column. */\n search: string;\n /** fieldId → selected option values (empty/absent = no constraint). */\n pills: Record<string, string[]>;\n /** fieldId → selected numeric range (min/max). Absent/empty = no constraint. */\n ranges?: Record<string, RangeValue>;\n sortBy: GridSort[];\n /** Column ids kept visible; a hideable column not listed here is hidden. */\n visibleColumns: string[];\n /**\n * Column ids in READING ORDER. Absent ⇒ the order the table declared them in.\n *\n * Order is view state for the same reason visibility is: reordering columns is\n * an operator deciding what to read FIRST, and a saved view that restored the\n * columns but not their order would restore half a decision. Ids the table no\n * longer declares are ignored at render, and ids missing from a stale list\n * fall in after it — so a column added since the view was saved appears rather\n * than disappearing.\n */\n order?: string[];\n /**\n * The active SCOPE id — the page-level partition the scope tabs select (see\n * `ScopeConfig`). Optional, so a table that declares no scopes is untouched\n * and no view persisted before scopes existed breaks.\n *\n * Stored raw and RESOLVED at read time: the declared scopes change underneath\n * a saved view, and a view naming a scope that has since been removed must\n * fall back at render rather than break.\n */\n scope?: string;\n /**\n * The layout the view was saved in. Optional — absent means \"whatever the user\n * is currently using\", which is the pre-existing behaviour.\n *\n * A LAYOUT FALLBACK IS NOT A LAYOUT CHANGE: a table rendered as stacked rows\n * on a phone still has `\"table\"` stored here, so the view round-trips the\n * stored layout regardless of the device that opened it.\n */\n layout?: DataViewsLayout;\n}\n\n/** A pristine view state showing every given column and no filters/sort. */\nexport function emptyViewState(visibleColumns: string[]): DataViewState {\n return { search: \"\", pills: {}, ranges: {}, sortBy: [], visibleColumns };\n}\n\n/**\n * The URL-driven slice of {@link DataViewState} — the controls a server-mode host\n * mirrors from the address bar (search / pills / ranges / sort), WITHOUT\n * `visibleColumns`. Supplying it re-applies those controls when its reference\n * changes (e.g. browser back/forward or a deep-link on the same route), merging\n * over the current state so a user's show/hide column choices are preserved —\n * unlike `appliedState`, which fully replaces the state to apply a saved view.\n */\nexport type DataViewSyncState = Pick<\n DataViewState,\n \"search\" | \"pills\" | \"ranges\" | \"sortBy\" | \"scope\"\n>;\n\n/**\n * The query a SERVER-mode table emits whenever its filter/sort/page changes\n * (FUT-180). The host translates this into a backend request (see\n * `@12-apps/shared-helpers/search`) — in server mode the grid never filters,\n * sorts, or paginates in the browser; it renders exactly the page the server\n * returned. `page` is 1-based to match the backend contract.\n */\nexport interface DataViewQuery {\n search: string;\n pills: Record<string, string[]>;\n ranges: Record<string, RangeValue>;\n sortBy: GridSort[];\n /**\n * The RESOLVED active scope (see `resolveScope`), when the table declares\n * scopes. The KEY IS ABSENT for a table that declares none — not present-and-\n * undefined — so a host that never opted in sees a byte-identical query.\n *\n * It carries the resolved id, never the stored one: a deep link or a saved\n * view naming a scope the table no longer declares must not put that id on the\n * wire, where the backend would reject it and replace the page with an error.\n */\n scope?: string;\n page: number;\n pageSize: number;\n}\n\n/**\n * Server-mode wiring for a DataViews table. Its PRESENCE flips the table from\n * in-browser filtering/sorting/pagination to backend-driven: the rows handed in\n * are already the current page, `totalCount` is the unpaginated total, and\n * `onQueryChange` fires (search debounced by the host) whenever the user changes\n * a filter, the sort, or the page. This is the ONLY way an admin list should run\n * (see the app-side `DataViewsTable` wrapper, which requires it).\n */\nexport interface DataViewServer {\n /** Total matched rows across all pages (drives the counter + page count). */\n totalCount: number;\n /** Current 1-based page. */\n page: number;\n /** Rows per page. */\n pageSize: number;\n /** Fired when the effective query changes; the host re-fetches the page. */\n onQueryChange: (query: DataViewQuery) => void;\n /**\n * Per-scope totals for the scope tabs: `scopeId → count`. THE ONLY PIECE OF\n * THIS CONTRACT THAT CANNOT BE DONE CLIENT-SIDE — a count computed from the\n * loaded page is wrong the moment there is a second page, which is the\n * concrete bug scopes exist to fix.\n *\n * Compute it over search + pills + ranges while IGNORING the active scope.\n * Computed WITH the scope applied, every inactive tab reads zero, which is\n * worse than no counts at all.\n *\n * Omit it and the tabs render with no numbers. Nothing is ever invented from\n * the loaded rows to fill the gap.\n */\n scopeCounts?: Record<string, number>;\n}\n\n/**\n * Coerce an opaque persisted state (a saved view's JSON) into a valid\n * DataViewState, filling any missing part with a sane default. Guards against\n * legacy/older shapes so applying a saved view never renders an empty grid.\n */\nexport function coerceViewState(raw: unknown, allColumnIds: string[]): DataViewState {\n const value = (raw ?? {}) as Partial<DataViewState>;\n return {\n search: typeof value.search === \"string\" ? value.search : \"\",\n pills: asRecord<string[]>(value.pills),\n ranges: asRecord<RangeValue>(value.ranges),\n sortBy: Array.isArray(value.sortBy) ? value.sortBy : [],\n visibleColumns: Array.isArray(value.visibleColumns) ? value.visibleColumns : allColumnIds,\n order: Array.isArray(value.order) ? value.order : undefined,\n // Carried through UNVALIDATED against the declared scopes on purpose: a view\n // written before scopes existed has none (⇒ undefined ⇒ the first declared\n // scope at render), and one naming a since-removed scope keeps its stored id\n // here and falls back at READ time. Resolving here would rewrite the view.\n scope: typeof value.scope === \"string\" ? value.scope : undefined,\n layout: isLayout(value.layout) ? value.layout : undefined,\n };\n}\n\n/** Every layout this build knows how to render — the guard's single source. */\nexport const DATA_VIEWS_LAYOUTS: readonly DataViewsLayout[] = [\"cards\", \"list\", \"board\", \"table\"];\n\n/** Is this a layout this build knows how to render? Guards persisted JSON. */\nfunction isLayout(value: unknown): value is DataViewsLayout {\n return DATA_VIEWS_LAYOUTS.includes(value as DataViewsLayout);\n}\n\n/** A persisted map, or an empty one — persisted JSON can hold anything. */\nfunction asRecord<V>(value: unknown): Record<string, V> {\n return value !== null && typeof value === \"object\" ? (value as Record<string, V>) : {};\n}\n\n/** A saved view as consumed by the DataViews UI (FUT-89), with ownership flag. */\nexport interface SavedViewSummary {\n id: string;\n name: string;\n description: string | null;\n state: DataViewState;\n shared: boolean;\n pinned: boolean;\n isDefault: boolean;\n /** True when the current user owns the view (may edit/delete/pin/share it). */\n isOwner: boolean;\n}\n\n/** A saved-view repository record (opaque `state`), as returned to server pages. */\ninterface SavedViewRecordLike {\n id: string;\n name: string;\n description: string | null;\n state: unknown;\n shared: boolean;\n pinned: boolean;\n isDefault: boolean;\n isOwner: boolean;\n}\n\n/** Map a repository record to the client summary (opaque state → DataViewState). */\nexport function toSavedViewSummary(record: SavedViewRecordLike): SavedViewSummary {\n return { ...record, state: (record.state ?? {}) as DataViewState };\n}\n\n/**\n * Dependency-injection contracts for the framework-agnostic DataViewsTableBase:\n * the host app supplies saved-view persistence + router side-effects so the ui\n * component imports neither a backend (server actions) nor a framework router.\n */\n\n/** A saved view's editable fields, as the host persists them. */\nexport interface DataViewSaveInput {\n name: string;\n description: string;\n state: DataViewState;\n shared: boolean;\n pinned: boolean;\n isDefault: boolean;\n}\n\n/** Result of a persistence call — mirrors the app's `Result` without importing it. */\nexport type DataViewMutationResult = { ok: true } | { ok: false; error: string };\n\n/** Injected saved-view persistence (create/update/delete), wired by the host. */\nexport interface DataViewPersistence {\n create: (input: DataViewSaveInput) => Promise<DataViewMutationResult>;\n update: (id: string, input: DataViewSaveInput) => Promise<DataViewMutationResult>;\n remove: (id: string) => Promise<DataViewMutationResult>;\n}\n\n/** Injected router side-effects: sync the `?view=` param and re-fetch on change. */\nexport interface DataViewRouter {\n syncViewParam: (viewId: string | null) => void;\n refresh: () => void;\n}\n"],"mappings":";AAoEO,IAAM,qBAAsD;AAAA,EACjE,OAAO;AAAA,EACP,OAAO,IAAI;AAAA,EACX,QAAQ,KAAK;AAAA,EACb,QAAQ,IAAI;AAAA,EACZ,OAAO,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASX,OAAO;AACT;AAuHO,SAAS,eAAe,gBAAyC;AACtE,SAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,GAAG,QAAQ,CAAC,GAAG,QAAQ,CAAC,GAAG,eAAe;AACzE;AA+EO,SAAS,gBAAgB,KAAc,cAAuC;AACnF,QAAM,QAAS,OAAO,CAAC;AACvB,SAAO;AAAA,IACL,QAAQ,OAAO,MAAM,WAAW,WAAW,MAAM,SAAS;AAAA,IAC1D,OAAO,SAAmB,MAAM,KAAK;AAAA,IACrC,QAAQ,SAAqB,MAAM,MAAM;AAAA,IACzC,QAAQ,MAAM,QAAQ,MAAM,MAAM,IAAI,MAAM,SAAS,CAAC;AAAA,IACtD,gBAAgB,MAAM,QAAQ,MAAM,cAAc,IAAI,MAAM,iBAAiB;AAAA,IAC7E,OAAO,MAAM,QAAQ,MAAM,KAAK,IAAI,MAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA,IAKlD,OAAO,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;AAAA,IACvD,QAAQ,SAAS,MAAM,MAAM,IAAI,MAAM,SAAS;AAAA,EAClD;AACF;AAGO,IAAM,qBAAiD,CAAC,SAAS,QAAQ,SAAS,OAAO;AAGhG,SAAS,SAAS,OAA0C;AAC1D,SAAO,mBAAmB,SAAS,KAAwB;AAC7D;AAGA,SAAS,SAAY,OAAmC;AACtD,SAAO,UAAU,QAAQ,OAAO,UAAU,WAAY,QAA8B,CAAC;AACvF;AA4BO,SAAS,mBAAmB,QAA+C;AAChF,SAAO,EAAE,GAAG,QAAQ,OAAQ,OAAO,SAAS,CAAC,EAAoB;AACnE;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/form/CategorySelect/category-tree.ts"],"sourcesContent":["import type {\n CategoryGroup,\n CategorySelectionChip,\n CategorySelectOption,\n} from './CategorySelect.types';\n\n/**\n * Fold a string to its comparison form: no diacritics, lower case.\n *\n * Portuguese category names are full of them (\"Águas\", \"Cápsulas\", \"Grãos\"), and\n * an admin typing \"agua\" or \"graos\" on a hurried phone keyboard means the same\n * thing. Comparing folded forms on both sides makes the search accent-blind in\n * both directions.\n */\nexport function foldText(text: string): string {\n return text\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .toLowerCase();\n}\n\n/**\n * Group a flat `parentId` list into top level + children, preserving input order.\n *\n * An option whose `parentId` names no top-level row is promoted to top level\n * rather than dropped — a category is never invisible just because its parent\n * was filtered out of the payload, or because the tree is deeper than two levels.\n */\nexport function buildCategoryGroups(options: CategorySelectOption[]): CategoryGroup[] {\n const roots = options.filter((option) => !option.parentId);\n const rootIds = new Set(roots.map((root) => root.id));\n const groups = new Map<string, CategoryGroup>(\n roots.map((root) => [root.id, { category: root, subcategories: [] }]),\n );\n const orphans: CategoryGroup[] = [];\n\n options.forEach((option) => {\n if (!option.parentId) return;\n const parent = groups.get(option.parentId);\n if (parent) {\n parent.subcategories.push(option);\n return;\n }\n if (!rootIds.has(option.id)) orphans.push({ category: option, subcategories: [] });\n });\n\n return [...groups.values(), ...orphans];\n}\n\n/** Every selectable leaf id across all groups, in display order. */\nexport function collectLeafIds(groups: CategoryGroup[]): string[] {\n return groups.flatMap((group) =>\n group.subcategories.length > 0\n ? group.subcategories.map((sub) => sub.id)\n : [group.category.id],\n );\n}\n\n/** The leaves a category stands for — its subcategories, or itself when childless. */\nexport function leavesOf(group: CategoryGroup): string[] {\n return group.subcategories.length > 0\n ? group.subcategories.map((sub) => sub.id)\n : [group.category.id];\n}\n\nexport type CategoryCheckState = 'off' | 'partial' | 'on';\n\n/** Whether none, some, or all of a category's leaves are selected. */\nexport function categoryCheckState(\n group: CategoryGroup,\n selected: ReadonlySet<string>,\n): CategoryCheckState {\n const leaves = leavesOf(group);\n const hits = leaves.filter((id) => selected.has(id)).length;\n if (hits === 0) return 'off';\n return hits === leaves.length ? 'on' : 'partial';\n}\n\n/** Select every leaf of a category, or clear them all when already complete. */\nexport function toggleCategoryLeaves(\n group: CategoryGroup,\n selected: ReadonlySet<string>,\n): Set<string> {\n const next = new Set(selected);\n const leaves = leavesOf(group);\n const turningOff = categoryCheckState(group, selected) === 'on';\n leaves.forEach((id) => (turningOff ? next.delete(id) : next.add(id)));\n return next;\n}\n\n/** Add an id when absent, remove it when present. */\nexport function toggleLeaf(id: string, selected: ReadonlySet<string>): Set<string> {\n const next = new Set(selected);\n if (!next.delete(id)) next.add(id);\n return next;\n}\n\n/**\n * Collapse a raw id set into the chips a human reads: a category that is fully\n * selected becomes ONE chip bearing the category's name, rather than one chip\n * per subcategory — \"Bebidas\" instead of six drinks.\n */\nexport function summarizeSelection(\n groups: CategoryGroup[],\n selected: ReadonlySet<string>,\n): CategorySelectionChip[] {\n return groups.flatMap((group): CategorySelectionChip[] => {\n const state = categoryCheckState(group, selected);\n if (state === 'off') return [];\n if (state === 'on') {\n return [{ id: group.category.id, label: group.category.name, whole: true }];\n }\n return group.subcategories\n .filter((sub) => selected.has(sub.id))\n .map((sub) => ({ id: sub.id, label: sub.name, whole: false }));\n });\n}\n\n/**\n * Drop the leaves a chip stands for. A `whole` chip removes the whole category;\n * any other chip removes just that leaf.\n */\nexport function removeChip(\n groups: CategoryGroup[],\n chipId: string,\n selected: ReadonlySet<string>,\n): Set<string> {\n const next = new Set(selected);\n const group = groups.find((candidate) => candidate.category.id === chipId);\n if (group) {\n leavesOf(group).forEach((id) => next.delete(id));\n return next;\n }\n next.delete(chipId);\n return next;\n}\n\n/**\n * Search across BOTH levels, keeping every hit under its parent.\n *\n * A category that matches keeps all of its subcategories, so \"Bebidas\" shows the\n * whole group. A category that does not match keeps only the subcategories that\n * do — the hit still arrives with its parent visible above it, which is the\n * context that tells \"Massas\" (mercearia) from \"Massas\" (pratos principais).\n */\nexport function filterCategoryGroups(groups: CategoryGroup[], query: string): CategoryGroup[] {\n const needle = foldText(query.trim());\n if (!needle) return groups;\n return groups.flatMap((group) => {\n if (foldText(group.category.name).includes(needle)) return [group];\n const subcategories = group.subcategories.filter((sub) =>\n foldText(sub.name).includes(needle),\n );\n return subcategories.length > 0 ? [{ category: group.category, subcategories }] : [];\n });\n}\n\n/** A run of text, flagged when it is the part that matched the query. */\ninterface HighlightSegment {\n text: string;\n match: boolean;\n}\n\n/**\n * Split a label around the query hit so the view can mark it.\n *\n * Slicing uses indices from the FOLDED text but cuts the ORIGINAL, which is safe\n * because folding is per-character here: NFD-stripping combining marks and\n * lower-casing both preserve offsets for the scripts these names use.\n */\nexport function highlightSegments(text: string, query: string): HighlightSegment[] {\n const needle = foldText(query.trim());\n if (!needle) return [{ text, match: false }];\n const at = foldText(text).indexOf(needle);\n if (at < 0) return [{ text, match: false }];\n return [\n { text: text.slice(0, at), match: false },\n { text: text.slice(at, at + needle.length), match: true },\n { text: text.slice(at + needle.length), match: false },\n ].filter((segment) => segment.text.length > 0);\n}\n\n/** A flattened row, so keyboard navigation can walk what is actually on screen. */\nexport interface CategoryRowRef {\n kind: 'category' | 'subcategory';\n id: string;\n /** Owning category id — lets ← jump from a subcategory back to its parent. */\n parentId?: string;\n}\n\n/** The visible rows, in order, honouring which categories are expanded. */\nexport function flattenRows(\n groups: CategoryGroup[],\n isExpanded: (categoryId: string) => boolean,\n): CategoryRowRef[] {\n return groups.flatMap((group) => {\n const head: CategoryRowRef = { kind: 'category', id: group.category.id };\n if (!isExpanded(group.category.id)) return [head];\n return [\n head,\n ...group.subcategories.map(\n (sub): CategoryRowRef => ({\n kind: 'subcategory',\n id: sub.id,\n parentId: group.category.id,\n }),\n ),\n ];\n });\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/form/CategorySelect/category-keyboard.ts"],"sourcesContent":["import type { CategoryRowRef } from './category-tree';\nimport type { CategoryGroup } from './CategorySelect.types';\n\n/** What a key handler is allowed to do to the panel. */\nexport interface CategoryKeyContext {\n rows: CategoryRowRef[];\n activeIndex: number;\n groups: CategoryGroup[];\n /** Single-select mode picks on Enter/Space instead of toggling a draft. */\n single: boolean;\n /** Categories are headings, not checkboxes, unless parents are selectable. */\n allowParentSelection: boolean;\n setActiveIndex: (next: number) => void;\n toggleExpanded: (categoryId: string) => void;\n expand: (categoryId: string) => void;\n collapse: (categoryId: string) => void;\n toggleCategory: (group: CategoryGroup) => void;\n toggleSubcategory: (id: string) => void;\n commit: () => void;\n cancel: () => void;\n pick: (id: string) => void;\n}\n\n/** The row under the keyboard cursor, if the cursor is on one. */\nfunction activeRow(context: CategoryKeyContext): CategoryRowRef | undefined {\n return context.activeIndex >= 0 ? context.rows[context.activeIndex] : undefined;\n}\n\nfunction groupOf(context: CategoryKeyContext, id: string): CategoryGroup | undefined {\n return context.groups.find((group) => group.category.id === id);\n}\n\nfunction moveCursor(context: CategoryKeyContext, delta: number): void {\n const { rows, activeIndex } = context;\n if (rows.length === 0) return;\n const next =\n delta > 0\n ? Math.min(rows.length - 1, activeIndex + 1)\n : Math.max(0, activeIndex - 1);\n context.setActiveIndex(next);\n}\n\n/** → opens a category. On a subcategory it is a no-op: there is nothing deeper. */\nfunction handleRight(context: CategoryKeyContext): void {\n const row = activeRow(context);\n if (row?.kind === 'category') context.expand(row.id);\n}\n\n/** ← collapses a category, or walks a subcategory back up to its parent. */\nfunction handleLeft(context: CategoryKeyContext): void {\n const row = activeRow(context);\n if (!row) return;\n if (row.kind === 'category') {\n context.collapse(row.id);\n return;\n }\n if (!row.parentId) return;\n context.collapse(row.parentId);\n const parentIndex = context.rows.findIndex(\n (candidate) => candidate.kind === 'category' && candidate.id === row.parentId,\n );\n if (parentIndex >= 0) context.setActiveIndex(parentIndex);\n}\n\n/**\n * Space marks. On a category that is only a HEADING it expands instead — the\n * row has no checkbox, so \"mark\" has nothing to mean there.\n */\nfunction handleSpace(context: CategoryKeyContext): void {\n const row = activeRow(context);\n if (!row) return;\n if (context.single) {\n context.pick(row.id);\n return;\n }\n if (row.kind === 'subcategory') {\n context.toggleSubcategory(row.id);\n return;\n }\n const group = groupOf(context, row.id);\n if (!group) return;\n if (context.allowParentSelection) context.toggleCategory(group);\n else context.toggleExpanded(row.id);\n}\n\nfunction handleEnter(context: CategoryKeyContext): void {\n const row = activeRow(context);\n if (context.single) {\n if (row) context.pick(row.id);\n return;\n }\n context.commit();\n}\n\n/** Key → behaviour. A map keeps this dispatch flat instead of a long if-chain. */\nconst HANDLERS: Record<string, (context: CategoryKeyContext) => void> = {\n ArrowDown: (context) => moveCursor(context, 1),\n ArrowUp: (context) => moveCursor(context, -1),\n ArrowRight: handleRight,\n ArrowLeft: handleLeft,\n ' ': handleSpace,\n Enter: handleEnter,\n Escape: (context) => context.cancel(),\n};\n\n/**\n * Keys the SEARCH BOX has a claim on: Space types a space, and the horizontal\n * arrows move the caret.\n *\n * They are only yielded while the field actually holds text. The panel opens\n * with focus in an EMPTY search box, so yielding unconditionally — as the\n * prototype does — would make Space and ←/→ dead on arrival for anyone driving\n * the tree from the keyboard, which is most of the point of having it. With no\n * text there is no caret to move and no word to space, so the tree takes them;\n * type one character and they revert to the field.\n */\nconst TEXT_ENTRY_KEYS = new Set([' ', 'ArrowLeft', 'ArrowRight']);\n\n/**\n * Drive the tree from the keyboard.\n *\n * Returns `true` when the key was consumed, so the caller can `preventDefault`\n * only for keys this actually handled — an unhandled key must still reach the\n * search input untouched.\n */\nexport function handleCategoryKeyDown(\n key: string,\n editingSearchText: boolean,\n context: CategoryKeyContext,\n): boolean {\n if (editingSearchText && TEXT_ENTRY_KEYS.has(key)) return false;\n const handler = HANDLERS[key];\n if (!handler) return false;\n handler(context);\n return true;\n}\n"],"mappings":";AAwBA,SAAS,UAAU,SAAyD;AAC1E,SAAO,QAAQ,eAAe,IAAI,QAAQ,KAAK,QAAQ,WAAW,IAAI;AACxE;AAEA,SAAS,QAAQ,SAA6B,IAAuC;AACnF,SAAO,QAAQ,OAAO,KAAK,CAAC,UAAU,MAAM,SAAS,OAAO,EAAE;AAChE;AAEA,SAAS,WAAW,SAA6B,OAAqB;AACpE,QAAM,EAAE,MAAM,YAAY,IAAI;AAC9B,MAAI,KAAK,WAAW,EAAG;AACvB,QAAM,OACJ,QAAQ,IACJ,KAAK,IAAI,KAAK,SAAS,GAAG,cAAc,CAAC,IACzC,KAAK,IAAI,GAAG,cAAc,CAAC;AACjC,UAAQ,eAAe,IAAI;AAC7B;AAGA,SAAS,YAAY,SAAmC;AACtD,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,KAAK,SAAS,WAAY,SAAQ,OAAO,IAAI,EAAE;AACrD;AAGA,SAAS,WAAW,SAAmC;AACrD,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,CAAC,IAAK;AACV,MAAI,IAAI,SAAS,YAAY;AAC3B,YAAQ,SAAS,IAAI,EAAE;AACvB;AAAA,EACF;AACA,MAAI,CAAC,IAAI,SAAU;AACnB,UAAQ,SAAS,IAAI,QAAQ;AAC7B,QAAM,cAAc,QAAQ,KAAK;AAAA,IAC/B,CAAC,cAAc,UAAU,SAAS,cAAc,UAAU,OAAO,IAAI;AAAA,EACvE;AACA,MAAI,eAAe,EAAG,SAAQ,eAAe,WAAW;AAC1D;AAMA,SAAS,YAAY,SAAmC;AACtD,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,CAAC,IAAK;AACV,MAAI,QAAQ,QAAQ;AAClB,YAAQ,KAAK,IAAI,EAAE;AACnB;AAAA,EACF;AACA,MAAI,IAAI,SAAS,eAAe;AAC9B,YAAQ,kBAAkB,IAAI,EAAE;AAChC;AAAA,EACF;AACA,QAAM,QAAQ,QAAQ,SAAS,IAAI,EAAE;AACrC,MAAI,CAAC,MAAO;AACZ,MAAI,QAAQ,qBAAsB,SAAQ,eAAe,KAAK;AAAA,MACzD,SAAQ,eAAe,IAAI,EAAE;AACpC;AAEA,SAAS,YAAY,SAAmC;AACtD,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,QAAQ,QAAQ;AAClB,QAAI,IAAK,SAAQ,KAAK,IAAI,EAAE;AAC5B;AAAA,EACF;AACA,UAAQ,OAAO;AACjB;AAGA,IAAM,WAAkE;AAAA,EACtE,WAAW,CAAC,YAAY,WAAW,SAAS,CAAC;AAAA,EAC7C,SAAS,CAAC,YAAY,WAAW,SAAS,EAAE;AAAA,EAC5C,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ,CAAC,YAAY,QAAQ,OAAO;AACtC;AAaA,IAAM,kBAAkB,oBAAI,IAAI,CAAC,KAAK,aAAa,YAAY,CAAC;AASzD,SAAS,sBACd,KACA,mBACA,SACS;AACT,MAAI,qBAAqB,gBAAgB,IAAI,GAAG,EAAG,QAAO;AAC1D,QAAM,UAAU,SAAS,GAAG;AAC5B,MAAI,CAAC,QAAS,QAAO;AACrB,UAAQ,OAAO;AACf,SAAO;AACT;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/data-display/DataViews/data-views-category-pill.tsx"],"sourcesContent":["\"use client\";\n\nimport { useDataViewsCopy } from \"./data-views-copy-context\";\nimport { CategorySelect } from \"../../form/CategorySelect\";\nimport { MultiSelectDropdown } from \"../../layout/ContentToolbar\";\n\nimport type { FilterFieldConfig, FilterOption } from \"./data-views-types\";\n\n/**\n * Diff a whole-array selection back into per-value toggles.\n *\n * The filter state's only channel is `onTogglePill(field, value, checked)`, but\n * a tree hands back the entire selection at once. Diffing here keeps that one\n * channel the single way a pill ever changes, rather than giving the\n * hierarchical control a private path into the state.\n */\nexport function emitPillDiff(\n fieldId: string,\n before: readonly string[],\n after: readonly string[],\n onTogglePill: (fieldId: string, value: string, checked: boolean) => void,\n): void {\n const had = new Set(before);\n const wants = new Set(after);\n after.forEach((value) => {\n if (!had.has(value)) onTogglePill(fieldId, value, true);\n });\n before.forEach((value) => {\n if (!wants.has(value)) onTogglePill(fieldId, value, false);\n });\n}\n\n/** A filter option as the category tree wants it. */\nconst toCategoryOption = (option: FilterOption) => ({\n id: option.value,\n name: option.label,\n parentId: option.parentId ?? null,\n});\n\ninterface PillControlProps<T extends Record<string, unknown>> {\n fieldId: string;\n pill: FilterFieldConfig<T>;\n selected: string[];\n onTogglePill: (fieldId: string, value: string, checked: boolean) => void;\n onClearField: (fieldId: string) => void;\n onOpenChange?: (open: boolean) => void;\n testIdPrefix: string;\n /** `stacked` in the filter panel, `pill` on the inline bar. */\n layout?: \"stacked\" | \"pill\";\n}\n\n/**\n * One filter pill: the hierarchical category tree, or the flat multi-select.\n *\n * A category set nests, and flattening it into one list is what made \"Massas\"\n * (mercearia) indistinguishable from \"Massas\" (pratos principais). `control:\n * \"category\"` opts a field into the tree, which shows each hit under its parent.\n */\nexport function PillControl<T extends Record<string, unknown>>({\n fieldId,\n pill,\n selected,\n onTogglePill,\n onClearField,\n onOpenChange,\n testIdPrefix,\n layout = \"pill\",\n}: PillControlProps<T>): React.JSX.Element {\n const copy = useDataViewsCopy();\n const testId = `${testIdPrefix}-filter-${fieldId}`;\n if (pill.control === \"category\") {\n return (\n <CategorySelect\n copy={copy.categorySelect}\n placeholder={pill.label}\n label={layout === \"stacked\" ? pill.label : undefined}\n fullWidth={layout === \"stacked\"}\n options={pill.options.map(toCategoryOption)}\n value={selected}\n onChange={(next) => emitPillDiff(fieldId, selected, next, onTogglePill)}\n dataTestId={testId}\n />\n );\n }\n return (\n <MultiSelectDropdown\n extraOptionsHeading={copy.filters.optionsHeading}\n clearText={copy.filters.clear}\n clearLabel={copy.filters.clearRange(pill.label)}\n label={pill.label}\n options={pill.options}\n selected={new Set(selected)}\n onToggle={(value, checked) => onTogglePill(fieldId, value, checked)}\n onClear={() => onClearField(fieldId)}\n allLabel={copy.filters.allOption}\n searchable={pill.searchEnabled ? true : undefined}\n searchPlaceholder={copy.filters.optionSearchPlaceholder}\n noResultsLabel={copy.filters.optionsEmpty}\n layout={layout}\n onOpenChange={onOpenChange}\n data-testid={testId}\n />\n );\n}\n"],"mappings":";;;;;;;;;;;AAwEM;AAxDC,SAAS,aACd,SACA,QACA,OACA,cACM;AACN,QAAM,MAAM,IAAI,IAAI,MAAM;AAC1B,QAAM,QAAQ,IAAI,IAAI,KAAK;AAC3B,QAAM,QAAQ,CAAC,UAAU;AACvB,QAAI,CAAC,IAAI,IAAI,KAAK,EAAG,cAAa,SAAS,OAAO,IAAI;AAAA,EACxD,CAAC;AACD,SAAO,QAAQ,CAAC,UAAU;AACxB,QAAI,CAAC,MAAM,IAAI,KAAK,EAAG,cAAa,SAAS,OAAO,KAAK;AAAA,EAC3D,CAAC;AACH;AAGA,IAAM,mBAAmB,CAAC,YAA0B;AAAA,EAClD,IAAI,OAAO;AAAA,EACX,MAAM,OAAO;AAAA,EACb,UAAU,OAAO,YAAY;AAC/B;AAqBO,SAAS,YAA+C;AAAA,EAC7D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AACX,GAA2C;AACzC,QAAM,OAAO,iBAAiB;AAC9B,QAAM,SAAS,GAAG,YAAY,WAAW,OAAO;AAChD,MAAI,KAAK,YAAY,YAAY;AAC/B,WACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,KAAK;AAAA,QACX,aAAa,KAAK;AAAA,QAClB,OAAO,WAAW,YAAY,KAAK,QAAQ;AAAA,QAC3C,WAAW,WAAW;AAAA,QACtB,SAAS,KAAK,QAAQ,IAAI,gBAAgB;AAAA,QAC1C,OAAO;AAAA,QACP,UAAU,CAAC,SAAS,aAAa,SAAS,UAAU,MAAM,YAAY;AAAA,QACtE,YAAY;AAAA;AAAA,IACd;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,qBAAqB,KAAK,QAAQ;AAAA,MAClC,WAAW,KAAK,QAAQ;AAAA,MACxB,YAAY,KAAK,QAAQ,WAAW,KAAK,KAAK;AAAA,MAC9C,OAAO,KAAK;AAAA,MACZ,SAAS,KAAK;AAAA,MACd,UAAU,IAAI,IAAI,QAAQ;AAAA,MAC1B,UAAU,CAAC,OAAO,YAAY,aAAa,SAAS,OAAO,OAAO;AAAA,MAClE,SAAS,MAAM,aAAa,OAAO;AAAA,MACnC,UAAU,KAAK,QAAQ;AAAA,MACvB,YAAY,KAAK,gBAAgB,OAAO;AAAA,MACxC,mBAAmB,KAAK,QAAQ;AAAA,MAChC,gBAAgB,KAAK,QAAQ;AAAA,MAC7B;AAAA,MACA;AAAA,MACA,eAAa;AAAA;AAAA,EACf;AAEJ;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/form/CategorySelect/useCategoryCommit.ts"],"sourcesContent":["import { useCallback, useMemo } from 'react';\n\nimport type { CategoryGroup, CategorySelectProps } from './CategorySelect.types';\nimport type { CategorySelectState } from './useCategorySelect';\n\n/** The verbs a row, a footer button or a key press can invoke. */\nexport interface CategoryActions {\n /** Publish the draft to the caller and close (multi-select). */\n commit: () => void;\n /** Choose one id and close (single-select). */\n pick: (id: string) => void;\n /** Close without publishing, returning focus to the trigger. */\n cancel: () => void;\n /** Empty the APPLIED selection straight from the closed trigger. */\n clearSelection: () => void;\n activateCategory: (group: CategoryGroup) => void;\n activateSubcategory: (id: string) => void;\n}\n\ninterface UseCategoryCommitArgs {\n props: CategorySelectProps;\n state: CategorySelectState;\n single: boolean;\n allowParentSelection: boolean;\n}\n\n/**\n * The commit semantics, which are the one thing the two modes genuinely differ\n * on: multi-select batches into a draft and publishes on Apply, single-select\n * publishes the moment a row is chosen.\n */\nexport function useCategoryCommit({\n props,\n state,\n single,\n allowParentSelection,\n}: UseCategoryCommitArgs): CategoryActions {\n const { closePanel, triggerRef, toggleCategory, toggleSubcategory, toggleExpanded } = state;\n\n const closeAndRefocus = useCallback(() => {\n closePanel();\n triggerRef.current?.focus();\n }, [closePanel, triggerRef]);\n\n const commit = useCallback(() => {\n if (props.mode === 'single') return;\n props.onChange([...state.draft]);\n closeAndRefocus();\n }, [props, state.draft, closeAndRefocus]);\n\n const pick = useCallback(\n (id: string) => {\n if (props.mode !== 'single') return;\n props.onChange(id);\n closeAndRefocus();\n },\n [props, closeAndRefocus],\n );\n\n const clearSelection = useCallback(() => {\n if (props.mode === 'single') props.onChange(null);\n else props.onChange([]);\n }, [props]);\n\n // A category row means different things per mode: pick the whole category,\n // tick all its leaves, or — when it is only a heading — just fold it.\n const activateCategory = useCallback(\n (group: CategoryGroup) => {\n if (allowParentSelection && single) {\n pick(group.category.id);\n return;\n }\n if (allowParentSelection) {\n toggleCategory(group);\n return;\n }\n toggleExpanded(group.category.id);\n },\n [allowParentSelection, single, pick, toggleCategory, toggleExpanded],\n );\n\n const activateSubcategory = useCallback(\n (id: string) => {\n if (single) pick(id);\n else toggleSubcategory(id);\n },\n [single, pick, toggleSubcategory],\n );\n\n return useMemo(\n () => ({\n commit,\n pick,\n cancel: closeAndRefocus,\n clearSelection,\n activateCategory,\n activateSubcategory,\n }),\n [commit, pick, closeAndRefocus, clearSelection, activateCategory, activateSubcategory],\n );\n}\n"],"mappings":";AAAA,SAAS,aAAa,eAAe;AA+B9B,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2C;AACzC,QAAM,EAAE,YAAY,YAAY,gBAAgB,mBAAmB,eAAe,IAAI;AAEtF,QAAM,kBAAkB,YAAY,MAAM;AACxC,eAAW;AACX,eAAW,SAAS,MAAM;AAAA,EAC5B,GAAG,CAAC,YAAY,UAAU,CAAC;AAE3B,QAAM,SAAS,YAAY,MAAM;AAC/B,QAAI,MAAM,SAAS,SAAU;AAC7B,UAAM,SAAS,CAAC,GAAG,MAAM,KAAK,CAAC;AAC/B,oBAAgB;AAAA,EAClB,GAAG,CAAC,OAAO,MAAM,OAAO,eAAe,CAAC;AAExC,QAAM,OAAO;AAAA,IACX,CAAC,OAAe;AACd,UAAI,MAAM,SAAS,SAAU;AAC7B,YAAM,SAAS,EAAE;AACjB,sBAAgB;AAAA,IAClB;AAAA,IACA,CAAC,OAAO,eAAe;AAAA,EACzB;AAEA,QAAM,iBAAiB,YAAY,MAAM;AACvC,QAAI,MAAM,SAAS,SAAU,OAAM,SAAS,IAAI;AAAA,QAC3C,OAAM,SAAS,CAAC,CAAC;AAAA,EACxB,GAAG,CAAC,KAAK,CAAC;AAIV,QAAM,mBAAmB;AAAA,IACvB,CAAC,UAAyB;AACxB,UAAI,wBAAwB,QAAQ;AAClC,aAAK,MAAM,SAAS,EAAE;AACtB;AAAA,MACF;AACA,UAAI,sBAAsB;AACxB,uBAAe,KAAK;AACpB;AAAA,MACF;AACA,qBAAe,MAAM,SAAS,EAAE;AAAA,IAClC;AAAA,IACA,CAAC,sBAAsB,QAAQ,MAAM,gBAAgB,cAAc;AAAA,EACrE;AAEA,QAAM,sBAAsB;AAAA,IAC1B,CAAC,OAAe;AACd,UAAI,OAAQ,MAAK,EAAE;AAAA,UACd,mBAAkB,EAAE;AAAA,IAC3B;AAAA,IACA,CAAC,QAAQ,MAAM,iBAAiB;AAAA,EAClC;AAEA,SAAO;AAAA,IACL,OAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,QAAQ,MAAM,iBAAiB,gBAAgB,kBAAkB,mBAAmB;AAAA,EACvF;AACF;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/form/CategorySelect/CategoryRows.tsx"],"sourcesContent":["'use client';\n\nimport Box from '@mui/material/Box/index.js';\n\nimport type { CategorySelectCopy } from '../../../copy';\nimport { CheckGlyph, DisclosureGlyph } from './CategoryIcons';\nimport { highlightSegments, type CategoryCheckState } from './category-tree';\nimport {\n checkboxBarSx,\n checkboxSx,\n chevronButtonSx,\n markSx,\n METRICS,\n radioSx,\n rowMetaSx,\n rowNameSx,\n rowSx,\n sectionHeadSx,\n} from './CategorySelect.styles';\nimport type { CategorySelectOption } from './CategorySelect.types';\n\n/** A label with the search hit marked. */\nexport function HighlightedName({\n text,\n query,\n}: {\n text: string;\n query: string;\n}): React.JSX.Element {\n const segments = highlightSegments(text, query);\n return (\n <>\n {segments.map((segment, index) =>\n segment.match ? (\n <Box component=\"mark\" key={`${segment.text}-${index}`} sx={markSx}>\n {segment.text}\n </Box>\n ) : (\n <span key={`${segment.text}-${index}`}>{segment.text}</span>\n ),\n )}\n </>\n );\n}\n\n/** Tri-state checkbox. `partial` draws the bar the prototype uses for \"some\". */\nexport function CategoryCheckbox({ state }: { state: CategoryCheckState }): React.JSX.Element {\n return (\n <Box sx={(theme) => checkboxSx(theme, state)} aria-hidden=\"true\">\n {state === 'on' && <CheckGlyph />}\n {state === 'partial' && <Box sx={checkboxBarSx} />}\n </Box>\n );\n}\n\nexport function CategoryRadio({ on }: { on: boolean }): React.JSX.Element {\n return <Box sx={(theme) => radioSx(theme, on)} aria-hidden=\"true\" />;\n}\n\ninterface CategoryHeadRowProps {\n option: CategorySelectOption;\n query: string;\n expanded: boolean;\n active: boolean;\n sheet: boolean;\n /** Absent in leaf-only mode, where the category is a heading with no checkbox. */\n checkState?: CategoryCheckState;\n selectedCount: number;\n showCounts: boolean;\n onToggleExpanded: () => void;\n onActivate: () => void;\n dataTestId: string;\n copy: CategorySelectCopy;\n}\n\n/** The trailing number: how many children are picked, else the item count. */\nfunction rowMeta(\n selectedCount: number,\n showCounts: boolean,\n count: number | undefined,\n): { text: string; selected: boolean } | null {\n if (selectedCount > 0) return { text: String(selectedCount), selected: true };\n if (showCounts && count !== undefined) return { text: String(count), selected: false };\n return null;\n}\n\n/**\n * A top-level category row: disclosure chevron, optional checkbox, name, meta.\n *\n * The chevron is its OWN button inside the row button — clicking it only folds,\n * while clicking the row does the row's job (expand as a heading, or tick when\n * parents are selectable).\n */\nexport function CategoryHeadRow({\n option,\n query,\n expanded,\n active,\n sheet,\n checkState,\n selectedCount,\n showCounts,\n onToggleExpanded,\n onActivate,\n dataTestId,\n copy,\n}: CategoryHeadRowProps): React.JSX.Element {\n const meta = rowMeta(selectedCount, showCounts, option.count);\n return (\n <Box\n component=\"div\"\n role={checkState ? 'option' : 'button'}\n aria-selected={checkState ? checkState === 'on' : undefined}\n aria-expanded={expanded}\n data-testid={`${dataTestId}-category-${option.id}`}\n sx={(theme) => rowSx(theme, active, sheet)}\n onClick={onActivate}\n >\n <Box\n component=\"button\"\n type=\"button\"\n tabIndex={-1}\n aria-label={\n expanded ? copy.collapseCategory(option.name) : copy.expandCategory(option.name)\n }\n data-testid={`${dataTestId}-expand-${option.id}`}\n sx={(theme) => chevronButtonSx(theme, expanded)}\n onClick={(event: React.MouseEvent) => {\n event.stopPropagation();\n onToggleExpanded();\n }}\n >\n <DisclosureGlyph />\n </Box>\n {checkState ? (\n <CategoryCheckbox state={checkState} />\n ) : (\n <Box sx={{ width: METRICS.boxSize, flex: '0 0 auto' }} />\n )}\n <Box component=\"span\" sx={(theme) => rowNameSx(theme, true)}>\n <HighlightedName text={option.name} query={query} />\n </Box>\n {meta && (\n <Box component=\"span\" sx={(theme) => rowMetaSx(theme, meta.selected)}>\n {meta.text}\n </Box>\n )}\n </Box>\n );\n}\n\ninterface SubcategoryRowProps {\n option: CategorySelectOption;\n query: string;\n selected: boolean;\n active: boolean;\n sheet: boolean;\n /** Single-select mode draws a radio; multi-select draws a checkbox. */\n single: boolean;\n showCounts: boolean;\n onActivate: () => void;\n dataTestId: string;\n}\n\nexport function SubcategoryRow({\n option,\n query,\n selected,\n active,\n sheet,\n single,\n showCounts,\n onActivate,\n dataTestId,\n}: SubcategoryRowProps): React.JSX.Element {\n return (\n <Box\n component=\"div\"\n role=\"option\"\n aria-selected={selected}\n data-testid={`${dataTestId}-option-${option.id}`}\n sx={(theme) => ({ ...rowSx(theme, active, sheet), paddingLeft: '14px' })}\n onClick={onActivate}\n >\n <Box sx={{ width: METRICS.chevronButton, flex: '0 0 auto' }} />\n {single ? (\n <CategoryRadio on={selected} />\n ) : (\n <CategoryCheckbox state={selected ? 'on' : 'off'} />\n )}\n <Box component=\"span\" sx={(theme) => rowNameSx(theme, false)}>\n <HighlightedName text={option.name} query={query} />\n </Box>\n {showCounts && option.count !== undefined && (\n <Box component=\"span\" sx={(theme) => rowMetaSx(theme, false)}>\n {option.count}\n </Box>\n )}\n </Box>\n );\n}\n\n/** The category as a non-interactive heading (single-select, leaf-only). */\nexport function CategorySectionHeading({\n option,\n query,\n}: {\n option: CategorySelectOption;\n query: string;\n}): React.JSX.Element {\n return (\n <Box sx={sectionHeadSx}>\n <HighlightedName text={option.name} query={query} />\n </Box>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAEA,OAAO,SAAS;AA6BZ,mBAGM,KAcN,YAjBA;AATG,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AACF,GAGsB;AACpB,QAAM,WAAW,kBAAkB,MAAM,KAAK;AAC9C,SACE,gCACG,mBAAS;AAAA,IAAI,CAAC,SAAS,UACtB,QAAQ,QACN,oBAAC,OAAI,WAAU,QAAwC,IAAI,QACxD,kBAAQ,QADgB,GAAG,QAAQ,IAAI,IAAI,KAAK,EAEnD,IAEA,oBAAC,UAAuC,kBAAQ,QAArC,GAAG,QAAQ,IAAI,IAAI,KAAK,EAAkB;AAAA,EAEzD,GACF;AAEJ;AAGO,SAAS,iBAAiB,EAAE,MAAM,GAAqD;AAC5F,SACE,qBAAC,OAAI,IAAI,CAAC,UAAU,WAAW,OAAO,KAAK,GAAG,eAAY,QACvD;AAAA,cAAU,QAAQ,oBAAC,cAAW;AAAA,IAC9B,UAAU,aAAa,oBAAC,OAAI,IAAI,eAAe;AAAA,KAClD;AAEJ;AAEO,SAAS,cAAc,EAAE,GAAG,GAAuC;AACxE,SAAO,oBAAC,OAAI,IAAI,CAAC,UAAU,QAAQ,OAAO,EAAE,GAAG,eAAY,QAAO;AACpE;AAmBA,SAAS,QACP,eACA,YACA,OAC4C;AAC5C,MAAI,gBAAgB,EAAG,QAAO,EAAE,MAAM,OAAO,aAAa,GAAG,UAAU,KAAK;AAC5E,MAAI,cAAc,UAAU,OAAW,QAAO,EAAE,MAAM,OAAO,KAAK,GAAG,UAAU,MAAM;AACrF,SAAO;AACT;AASO,SAAS,gBAAgB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA4C;AAC1C,QAAM,OAAO,QAAQ,eAAe,YAAY,OAAO,KAAK;AAC5D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,MAAM,aAAa,WAAW;AAAA,MAC9B,iBAAe,aAAa,eAAe,OAAO;AAAA,MAClD,iBAAe;AAAA,MACf,eAAa,GAAG,UAAU,aAAa,OAAO,EAAE;AAAA,MAChD,IAAI,CAAC,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,MACzC,SAAS;AAAA,MAET;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,MAAK;AAAA,YACL,UAAU;AAAA,YACV,cACE,WAAW,KAAK,iBAAiB,OAAO,IAAI,IAAI,KAAK,eAAe,OAAO,IAAI;AAAA,YAEjF,eAAa,GAAG,UAAU,WAAW,OAAO,EAAE;AAAA,YAC9C,IAAI,CAAC,UAAU,gBAAgB,OAAO,QAAQ;AAAA,YAC9C,SAAS,CAAC,UAA4B;AACpC,oBAAM,gBAAgB;AACtB,+BAAiB;AAAA,YACnB;AAAA,YAEA,8BAAC,mBAAgB;AAAA;AAAA,QACnB;AAAA,QACC,aACC,oBAAC,oBAAiB,OAAO,YAAY,IAErC,oBAAC,OAAI,IAAI,EAAE,OAAO,QAAQ,SAAS,MAAM,WAAW,GAAG;AAAA,QAEzD,oBAAC,OAAI,WAAU,QAAO,IAAI,CAAC,UAAU,UAAU,OAAO,IAAI,GACxD,8BAAC,mBAAgB,MAAM,OAAO,MAAM,OAAc,GACpD;AAAA,QACC,QACC,oBAAC,OAAI,WAAU,QAAO,IAAI,CAAC,UAAU,UAAU,OAAO,KAAK,QAAQ,GAChE,eAAK,MACR;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAeO,SAAS,eAAe;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA2C;AACzC,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,MAAK;AAAA,MACL,iBAAe;AAAA,MACf,eAAa,GAAG,UAAU,WAAW,OAAO,EAAE;AAAA,MAC9C,IAAI,CAAC,WAAW,EAAE,GAAG,MAAM,OAAO,QAAQ,KAAK,GAAG,aAAa,OAAO;AAAA,MACtE,SAAS;AAAA,MAET;AAAA,4BAAC,OAAI,IAAI,EAAE,OAAO,QAAQ,eAAe,MAAM,WAAW,GAAG;AAAA,QAC5D,SACC,oBAAC,iBAAc,IAAI,UAAU,IAE7B,oBAAC,oBAAiB,OAAO,WAAW,OAAO,OAAO;AAAA,QAEpD,oBAAC,OAAI,WAAU,QAAO,IAAI,CAAC,UAAU,UAAU,OAAO,KAAK,GACzD,8BAAC,mBAAgB,MAAM,OAAO,MAAM,OAAc,GACpD;AAAA,QACC,cAAc,OAAO,UAAU,UAC9B,oBAAC,OAAI,WAAU,QAAO,IAAI,CAAC,UAAU,UAAU,OAAO,KAAK,GACxD,iBAAO,OACV;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAGO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AACF,GAGsB;AACpB,SACE,oBAAC,OAAI,IAAI,eACP,8BAAC,mBAAgB,MAAM,OAAO,MAAM,OAAc,GACpD;AAEJ;","names":[]}