@12-apps/ui 6.24.8 → 6.26.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 (151) hide show
  1. package/dist/_internal/components/data-display/Alert/Alert.helpers.js +5 -1
  2. package/dist/_internal/components/data-display/Alert/Alert.js +3 -3
  3. package/dist/_internal/components/data-display/Alert/Alert.styles.js +2 -2
  4. package/dist/_internal/components/data-display/CardKit/card-actions-context.js +4 -4
  5. package/dist/_internal/components/data-display/CardKit/use-remove-confirm.js +5 -5
  6. package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +19 -19
  7. package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +24 -24
  8. package/dist/_internal/components/data-display/DataViews/data-views-category-pill.js +15 -15
  9. package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +16 -16
  10. package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +18 -18
  11. package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +16 -16
  12. package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +4 -4
  13. package/dist/_internal/components/data-display/index.js +3 -3
  14. package/dist/_internal/components/form/CategorySelect/CategoryPanel.js +8 -8
  15. package/dist/_internal/components/form/CategorySelect/CategoryPanelFoot.js +2 -2
  16. package/dist/_internal/components/form/CategorySelect/CategoryPanelHead.js +2 -2
  17. package/dist/_internal/components/form/CategorySelect/CategoryPanelList.js +4 -4
  18. package/dist/_internal/components/form/CategorySelect/CategoryQuickActions.js +2 -2
  19. package/dist/_internal/components/form/CategorySelect/CategoryRows.js +7 -9
  20. package/dist/_internal/components/form/CategorySelect/CategorySelect.js +14 -14
  21. package/dist/_internal/components/form/CategorySelect/CategorySelect.styles.js +3 -1
  22. package/dist/_internal/components/form/CategorySelect/CategoryTrigger.js +2 -2
  23. package/dist/_internal/components/form/CategorySelect/category-keyboard.js +2 -2
  24. package/dist/_internal/components/form/CategorySelect/category-tree.js +11 -3
  25. package/dist/_internal/components/form/CategorySelect/useCategoryCommit.js +2 -2
  26. package/dist/_internal/components/form/CategorySelect/useCategoryKeyHandler.js +3 -3
  27. package/dist/_internal/components/form/CategorySelect/useCategorySelect.js +2 -2
  28. package/dist/_internal/components/form/total-form/Fields/CategoryField.js +15 -15
  29. package/dist/_internal/components/form/total-form/Fields/index.js +16 -16
  30. package/dist/_internal/components/index.js +3 -3
  31. package/dist/{chunk-WNMYWQ7D.js → chunk-4Y6SPFKW.js} +10 -8
  32. package/dist/chunk-4Y6SPFKW.js.map +1 -0
  33. package/dist/{chunk-KM5O4ICK.js → chunk-7LCXGJN5.js} +2 -2
  34. package/dist/{chunk-NLJV4CPF.js → chunk-7MN6I5NK.js} +2 -2
  35. package/dist/{chunk-PN5CQKA2.js → chunk-AXTLFK4T.js} +2 -2
  36. package/dist/{chunk-4TXIZ5WI.js → chunk-CJUOBTLL.js} +2 -2
  37. package/dist/{chunk-57Z2ND3N.js → chunk-CXLAJZSE.js} +2 -2
  38. package/dist/{chunk-E3PUH5IF.js → chunk-EQ7PWGBN.js} +2 -2
  39. package/dist/{chunk-EJQW2IYW.js → chunk-FPFUXRD2.js} +2 -2
  40. package/dist/{chunk-KIHQE5YA.js → chunk-GVDJQMFI.js} +2 -2
  41. package/dist/{chunk-N3QUEBNS.js → chunk-L7VH6IMR.js} +14 -1
  42. package/dist/chunk-L7VH6IMR.js.map +1 -0
  43. package/dist/{chunk-OFOYMZRY.js → chunk-NPN7JTRM.js} +22 -47
  44. package/dist/chunk-NPN7JTRM.js.map +1 -0
  45. package/dist/{chunk-OVLNF4AZ.js → chunk-P7IVAMAU.js} +24 -35
  46. package/dist/chunk-P7IVAMAU.js.map +1 -0
  47. package/dist/chunk-PN4HVM43.js +160 -0
  48. package/dist/chunk-PN4HVM43.js.map +1 -0
  49. package/dist/{chunk-WWM2YTW4.js → chunk-R2AZOWEZ.js} +21 -27
  50. package/dist/chunk-R2AZOWEZ.js.map +1 -0
  51. package/dist/{chunk-2GB5CIN6.js → chunk-RZVUY2MO.js} +8 -4
  52. package/dist/chunk-RZVUY2MO.js.map +1 -0
  53. package/dist/{chunk-FBI6QB3O.js → chunk-TVPPA4RD.js} +3 -3
  54. package/dist/{chunk-KFWIOFMS.js → chunk-TWY5FOBY.js} +3 -3
  55. package/dist/{chunk-6ZY5XU4J.js → chunk-UIZH4DAE.js} +2 -2
  56. package/dist/{chunk-ISH42UC7.js → chunk-USJPZ5Q7.js} +2 -2
  57. package/dist/{chunk-ZUIJ3N7N.js → chunk-V6Z4XS3I.js} +5 -3
  58. package/dist/chunk-V6Z4XS3I.js.map +1 -0
  59. package/dist/{chunk-SYJMI2NP.js → chunk-VNTOBLIB.js} +2 -2
  60. package/dist/{chunk-JM64OEU6.js → chunk-WAPJ2DL3.js} +10 -13
  61. package/dist/chunk-WAPJ2DL3.js.map +1 -0
  62. package/dist/{chunk-ZAB65STX.js → chunk-XHHR6G35.js} +2 -2
  63. package/dist/{chunk-WIT3Y7JT.js → chunk-Y2B3I3IH.js} +2 -2
  64. package/dist/{chunk-R6SNYC3D.js → chunk-Y3KRUT7O.js} +2 -2
  65. package/dist/{chunk-TPLKJHNN.js → chunk-ZC7RQDZ5.js} +8 -7
  66. package/dist/chunk-ZC7RQDZ5.js.map +1 -0
  67. package/dist/{chunk-TFO6GBW7.js → chunk-ZS44PY7T.js} +5 -1
  68. package/dist/chunk-ZS44PY7T.js.map +1 -0
  69. package/dist/{chunk-AJ6XEO4E.js → chunk-ZUY65MFH.js} +2 -2
  70. package/dist/data-display/Alert.js +3 -3
  71. package/dist/data-display/CardKit.js +5 -5
  72. package/dist/data-display/DataViews.js +24 -24
  73. package/dist/form/CategorySelect.js +24 -16
  74. package/dist/form/total-form.js +16 -16
  75. package/dist/native/data-display/Alert.js +18 -3
  76. package/dist/native/data-display/Alert.js.map +1 -1
  77. package/dist/types/components/data-display/Alert/Alert.base.d.ts +28 -2
  78. package/dist/types/components/data-display/Alert/Alert.base.d.ts.map +1 -1
  79. package/dist/types/components/data-display/Alert/Alert.helpers.d.ts +39 -0
  80. package/dist/types/components/data-display/Alert/Alert.helpers.d.ts.map +1 -1
  81. package/dist/types/components/form/CategorySelect/CategoryPanel.d.ts.map +1 -1
  82. package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts +6 -0
  83. package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts.map +1 -1
  84. package/dist/types/components/form/CategorySelect/CategoryQuickActions.d.ts.map +1 -1
  85. package/dist/types/components/form/CategorySelect/CategoryRows.d.ts +12 -24
  86. package/dist/types/components/form/CategorySelect/CategoryRows.d.ts.map +1 -1
  87. package/dist/types/components/form/CategorySelect/CategorySelect.d.ts +10 -7
  88. package/dist/types/components/form/CategorySelect/CategorySelect.d.ts.map +1 -1
  89. package/dist/types/components/form/CategorySelect/CategorySelect.styles.d.ts +11 -0
  90. package/dist/types/components/form/CategorySelect/CategorySelect.styles.d.ts.map +1 -1
  91. package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts +10 -2
  92. package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts.map +1 -1
  93. package/dist/types/components/form/CategorySelect/category-keyboard.d.ts.map +1 -1
  94. package/dist/types/components/form/CategorySelect/category-tree.d.ts +52 -20
  95. package/dist/types/components/form/CategorySelect/category-tree.d.ts.map +1 -1
  96. package/dist/types/components/form/CategorySelect/index.d.ts +1 -1
  97. package/dist/types/components/form/CategorySelect/index.d.ts.map +1 -1
  98. package/dist/types/components/form/CategorySelect/useCategorySelect.d.ts +2 -0
  99. package/dist/types/components/form/CategorySelect/useCategorySelect.d.ts.map +1 -1
  100. package/dist/types-native/components/data-display/Alert/Alert.base.d.ts +28 -2
  101. package/dist/types-native/components/data-display/Alert/Alert.base.d.ts.map +1 -1
  102. package/dist/types-native/components/data-display/Alert/Alert.helpers.d.ts +39 -0
  103. package/dist/types-native/components/data-display/Alert/Alert.helpers.d.ts.map +1 -1
  104. package/dist/types-native/components/data-display/Alert/Alert.native.d.ts.map +1 -1
  105. package/package.json +1 -1
  106. package/src/components/data-display/Alert/Alert.base.ts +29 -2
  107. package/src/components/data-display/Alert/Alert.helpers.ts +50 -0
  108. package/src/components/data-display/Alert/Alert.md +29 -1
  109. package/src/components/data-display/Alert/Alert.native.tsx +8 -4
  110. package/src/components/data-display/Alert/Alert.tsx +5 -5
  111. package/src/components/form/CategorySelect/CategoryPanel.tsx +1 -0
  112. package/src/components/form/CategorySelect/CategoryPanelList.tsx +42 -34
  113. package/src/components/form/CategorySelect/CategoryQuickActions.tsx +6 -4
  114. package/src/components/form/CategorySelect/CategoryRows.tsx +45 -81
  115. package/src/components/form/CategorySelect/CategorySelect.md +47 -13
  116. package/src/components/form/CategorySelect/CategorySelect.styles.ts +13 -0
  117. package/src/components/form/CategorySelect/CategorySelect.tsx +16 -14
  118. package/src/components/form/CategorySelect/CategorySelect.types.ts +10 -2
  119. package/src/components/form/CategorySelect/category-keyboard.ts +27 -31
  120. package/src/components/form/CategorySelect/category-tree.ts +143 -63
  121. package/src/components/form/CategorySelect/index.ts +4 -0
  122. package/src/components/form/CategorySelect/useCategorySelect.ts +15 -1
  123. package/dist/chunk-2GB5CIN6.js.map +0 -1
  124. package/dist/chunk-JM64OEU6.js.map +0 -1
  125. package/dist/chunk-LR6MSKTT.js +0 -124
  126. package/dist/chunk-LR6MSKTT.js.map +0 -1
  127. package/dist/chunk-N3QUEBNS.js.map +0 -1
  128. package/dist/chunk-OFOYMZRY.js.map +0 -1
  129. package/dist/chunk-OVLNF4AZ.js.map +0 -1
  130. package/dist/chunk-TFO6GBW7.js.map +0 -1
  131. package/dist/chunk-TPLKJHNN.js.map +0 -1
  132. package/dist/chunk-WNMYWQ7D.js.map +0 -1
  133. package/dist/chunk-WWM2YTW4.js.map +0 -1
  134. package/dist/chunk-ZUIJ3N7N.js.map +0 -1
  135. /package/dist/{chunk-KM5O4ICK.js.map → chunk-7LCXGJN5.js.map} +0 -0
  136. /package/dist/{chunk-NLJV4CPF.js.map → chunk-7MN6I5NK.js.map} +0 -0
  137. /package/dist/{chunk-PN5CQKA2.js.map → chunk-AXTLFK4T.js.map} +0 -0
  138. /package/dist/{chunk-4TXIZ5WI.js.map → chunk-CJUOBTLL.js.map} +0 -0
  139. /package/dist/{chunk-57Z2ND3N.js.map → chunk-CXLAJZSE.js.map} +0 -0
  140. /package/dist/{chunk-E3PUH5IF.js.map → chunk-EQ7PWGBN.js.map} +0 -0
  141. /package/dist/{chunk-EJQW2IYW.js.map → chunk-FPFUXRD2.js.map} +0 -0
  142. /package/dist/{chunk-KIHQE5YA.js.map → chunk-GVDJQMFI.js.map} +0 -0
  143. /package/dist/{chunk-FBI6QB3O.js.map → chunk-TVPPA4RD.js.map} +0 -0
  144. /package/dist/{chunk-KFWIOFMS.js.map → chunk-TWY5FOBY.js.map} +0 -0
  145. /package/dist/{chunk-6ZY5XU4J.js.map → chunk-UIZH4DAE.js.map} +0 -0
  146. /package/dist/{chunk-ISH42UC7.js.map → chunk-USJPZ5Q7.js.map} +0 -0
  147. /package/dist/{chunk-SYJMI2NP.js.map → chunk-VNTOBLIB.js.map} +0 -0
  148. /package/dist/{chunk-ZAB65STX.js.map → chunk-XHHR6G35.js.map} +0 -0
  149. /package/dist/{chunk-WIT3Y7JT.js.map → chunk-Y2B3I3IH.js.map} +0 -0
  150. /package/dist/{chunk-R6SNYC3D.js.map → chunk-Y3KRUT7O.js.map} +0 -0
  151. /package/dist/{chunk-AJ6XEO4E.js.map → chunk-ZUY65MFH.js.map} +0 -0
@@ -9,14 +9,15 @@ import {
9
9
  chevronButtonSx,
10
10
  markSx,
11
11
  radioSx,
12
+ rowIndent,
12
13
  rowMetaSx,
13
14
  rowNameSx,
14
15
  rowSx,
15
16
  sectionHeadSx
16
- } from "./chunk-TFO6GBW7.js";
17
+ } from "./chunk-ZS44PY7T.js";
17
18
  import {
18
19
  highlightSegments
19
- } from "./chunk-LR6MSKTT.js";
20
+ } from "./chunk-PN4HVM43.js";
20
21
 
21
22
  // src/components/form/CategorySelect/CategoryRows.tsx
22
23
  import Box from "@mui/material/Box/index.js";
@@ -44,15 +45,15 @@ function rowMeta(selectedCount, showCounts, count) {
44
45
  if (showCounts && count !== void 0) return { text: String(count), selected: false };
45
46
  return null;
46
47
  }
47
- function HeadRowDisclosure({
48
+ function RowDisclosure({
48
49
  option,
49
50
  expanded,
50
- expandable,
51
+ branch,
51
52
  onToggleExpanded,
52
53
  dataTestId,
53
54
  copy
54
55
  }) {
55
- if (!expandable) {
56
+ if (!branch) {
56
57
  return /* @__PURE__ */ jsx(Box, { sx: { width: METRICS.chevronButton, flex: "0 0 auto" } });
57
58
  }
58
59
  return /* @__PURE__ */ jsx(
@@ -72,7 +73,7 @@ function HeadRowDisclosure({
72
73
  }
73
74
  );
74
75
  }
75
- function HeadRowControl({
76
+ function RowControl({
76
77
  checkState,
77
78
  single
78
79
  }) {
@@ -80,15 +81,19 @@ function HeadRowControl({
80
81
  if (single) return /* @__PURE__ */ jsx(CategoryRadio, { on: checkState === "on" });
81
82
  return /* @__PURE__ */ jsx(CategoryCheckbox, { state: checkState });
82
83
  }
83
- function CategoryHeadRow({
84
+ function rowTestId(dataTestId, depth, id) {
85
+ return depth === 0 ? `${dataTestId}-category-${id}` : `${dataTestId}-option-${id}`;
86
+ }
87
+ function CategoryTreeRow({
84
88
  option,
85
89
  query,
90
+ depth,
91
+ branch,
86
92
  expanded,
87
93
  active,
88
94
  sheet,
89
95
  checkState,
90
96
  single,
91
- expandable,
92
97
  selectedCount,
93
98
  showCounts,
94
99
  onToggleExpanded,
@@ -103,58 +108,29 @@ function CategoryHeadRow({
103
108
  component: "div",
104
109
  role: checkState ? "option" : "button",
105
110
  "aria-selected": checkState ? checkState === "on" : void 0,
106
- "aria-expanded": expandable ? expanded : void 0,
107
- "data-testid": `${dataTestId}-category-${option.id}`,
108
- sx: (theme) => rowSx(theme, active, sheet),
111
+ "aria-expanded": branch ? expanded : void 0,
112
+ "data-testid": rowTestId(dataTestId, depth, option.id),
113
+ sx: (theme) => ({ ...rowSx(theme, active, sheet), ...rowIndent(depth) }),
109
114
  onClick: onActivate,
110
115
  children: [
111
116
  /* @__PURE__ */ jsx(
112
- HeadRowDisclosure,
117
+ RowDisclosure,
113
118
  {
114
119
  option,
115
120
  expanded,
116
- expandable,
121
+ branch,
117
122
  onToggleExpanded,
118
123
  dataTestId,
119
124
  copy
120
125
  }
121
126
  ),
122
- /* @__PURE__ */ jsx(HeadRowControl, { checkState, single }),
123
- /* @__PURE__ */ jsx(Box, { component: "span", sx: (theme) => rowNameSx(theme, true), children: /* @__PURE__ */ jsx(HighlightedName, { text: option.name, query }) }),
127
+ /* @__PURE__ */ jsx(RowControl, { checkState, single }),
128
+ /* @__PURE__ */ jsx(Box, { component: "span", sx: (theme) => rowNameSx(theme, depth === 0), children: /* @__PURE__ */ jsx(HighlightedName, { text: option.name, query }) }),
124
129
  meta && /* @__PURE__ */ jsx(Box, { component: "span", sx: (theme) => rowMetaSx(theme, meta.selected), children: meta.text })
125
130
  ]
126
131
  }
127
132
  );
128
133
  }
129
- function SubcategoryRow({
130
- option,
131
- query,
132
- selected,
133
- active,
134
- sheet,
135
- single,
136
- showCounts,
137
- onActivate,
138
- dataTestId
139
- }) {
140
- return /* @__PURE__ */ jsxs(
141
- Box,
142
- {
143
- component: "div",
144
- role: "option",
145
- "aria-selected": selected,
146
- "data-testid": `${dataTestId}-option-${option.id}`,
147
- sx: (theme) => ({ ...rowSx(theme, active, sheet), paddingLeft: "14px" }),
148
- onClick: onActivate,
149
- children: [
150
- /* @__PURE__ */ jsx(Box, { sx: { width: METRICS.chevronButton, flex: "0 0 auto" } }),
151
- single ? /* @__PURE__ */ jsx(CategoryRadio, { on: selected }) : /* @__PURE__ */ jsx(CategoryCheckbox, { state: selected ? "on" : "off" }),
152
- /* @__PURE__ */ jsx(Box, { component: "span", sx: (theme) => rowNameSx(theme, false), children: /* @__PURE__ */ jsx(HighlightedName, { text: option.name, query }) }),
153
- showCounts && option.count !== void 0 && /* @__PURE__ */ jsx(Box, { component: "span", sx: (theme) => rowMetaSx(theme, false), children: option.count })
154
- ]
155
- }
156
- );
157
- }
158
134
  function CategorySectionHeading({
159
135
  option,
160
136
  query
@@ -166,8 +142,7 @@ export {
166
142
  HighlightedName,
167
143
  CategoryCheckbox,
168
144
  CategoryRadio,
169
- CategoryHeadRow,
170
- SubcategoryRow,
145
+ CategoryTreeRow,
171
146
  CategorySectionHeading
172
147
  };
173
- //# sourceMappingURL=chunk-OFOYMZRY.js.map
148
+ //# sourceMappingURL=chunk-NPN7JTRM.js.map
@@ -0,0 +1 @@
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 rowIndent,\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 CategoryTreeRowProps {\n option: CategorySelectOption;\n query: string;\n /** 0 for a top-level category; each level below indents one more step. */\n depth: number;\n /** True when there is something under this row to fold. */\n branch: boolean;\n expanded: boolean;\n active: boolean;\n sheet: boolean;\n /** Absent where the row cannot be picked — a category acting as a frame. */\n checkState?: CategoryCheckState;\n /** Single-select draws a radio for the control: choosing a row is exclusive. */\n single: boolean;\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 leaves are picked below, 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/** The disclosure chevron, or the space it would have taken on a leaf row. */\nfunction RowDisclosure({\n option,\n expanded,\n branch,\n onToggleExpanded,\n dataTestId,\n copy,\n}: Pick<\n CategoryTreeRowProps,\n 'option' | 'expanded' | 'branch' | 'onToggleExpanded' | 'dataTestId' | 'copy'\n>): React.JSX.Element {\n if (!branch) {\n return <Box sx={{ width: METRICS.chevronButton, flex: '0 0 auto' }} />;\n }\n return (\n <Box\n component=\"button\"\n type=\"button\"\n tabIndex={-1}\n aria-label={expanded ? copy.collapseCategory(option.name) : copy.expandCategory(option.name)}\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 );\n}\n\n/** The row's selection control, or the space it would have taken on a frame. */\nfunction RowControl({\n checkState,\n single,\n}: Pick<CategoryTreeRowProps, 'checkState' | 'single'>): React.JSX.Element {\n if (!checkState) return <Box sx={{ width: METRICS.boxSize, flex: '0 0 auto' }} />;\n // Single-select commits the moment a row is chosen, so the control is a radio\n // there: a checkbox would promise the accumulation this mode does not do.\n if (single) return <CategoryRadio on={checkState === 'on'} />;\n return <CategoryCheckbox state={checkState} />;\n}\n\n/**\n * The test id a row answers to.\n *\n * Keyed on DEPTH rather than on whether the row has children, because that is\n * what the ids meant when the tree was two levels and specs were written against\n * them: `-category-<id>` is a top-level row — childless or not — and `-option-`\n * is anything filed under one.\n */\nfunction rowTestId(dataTestId: string, depth: number, id: string): string {\n return depth === 0 ? `${dataTestId}-category-${id}` : `${dataTestId}-option-${id}`;\n}\n\n/**\n * One row of the tree: disclosure chevron, optional control, name, meta.\n *\n * The chevron is its OWN button inside the row — clicking it only folds, while\n * clicking the row does the row's job (expand as a frame, or mark when the row\n * is selectable). A LEAF has neither a fold nor a frame to be: it draws no\n * chevron, and it always carries the control, because it is what you pick.\n */\nexport function CategoryTreeRow({\n option,\n query,\n depth,\n branch,\n expanded,\n active,\n sheet,\n checkState,\n single,\n selectedCount,\n showCounts,\n onToggleExpanded,\n onActivate,\n dataTestId,\n copy,\n}: CategoryTreeRowProps): 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={branch ? expanded : undefined}\n data-testid={rowTestId(dataTestId, depth, option.id)}\n sx={(theme) => ({ ...rowSx(theme, active, sheet), ...rowIndent(depth) })}\n onClick={onActivate}\n >\n <RowDisclosure\n option={option}\n expanded={expanded}\n branch={branch}\n onToggleExpanded={onToggleExpanded}\n dataTestId={dataTestId}\n copy={copy}\n />\n <RowControl checkState={checkState} single={single} />\n <Box component=\"span\" sx={(theme) => rowNameSx(theme, depth === 0)}>\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\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":"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","names":[]}
@@ -1,17 +1,17 @@
1
1
  import {
2
- CategoryHeadRow,
3
2
  CategorySectionHeading,
4
- SubcategoryRow
5
- } from "./chunk-OFOYMZRY.js";
3
+ CategoryTreeRow
4
+ } from "./chunk-NPN7JTRM.js";
6
5
  import {
7
6
  emptySx,
8
7
  listSx,
9
8
  skeletonSx
10
- } from "./chunk-TFO6GBW7.js";
9
+ } from "./chunk-ZS44PY7T.js";
11
10
  import {
12
11
  categoryCheckState,
13
- isLeafCategory
14
- } from "./chunk-LR6MSKTT.js";
12
+ isLeafCategory,
13
+ leavesOf
14
+ } from "./chunk-PN4HVM43.js";
15
15
 
16
16
  // src/components/form/CategorySelect/CategoryPanelList.tsx
17
17
  import Box from "@mui/material/Box/index.js";
@@ -60,51 +60,40 @@ function NoResults({
60
60
  ] })
61
61
  ] });
62
62
  }
63
- function CategoryGroupRows({
64
- group,
63
+ function CategoryNodeRows({
64
+ node,
65
+ depth,
65
66
  props
66
67
  }) {
67
68
  const { draft, query, single, allowParentSelection, rowIds, activeIndex, sheet } = props;
68
- const categoryId = group.category.id;
69
+ const categoryId = node.category.id;
70
+ const leaf = isLeafCategory(node);
69
71
  const expanded = props.isExpanded(categoryId);
70
- const selectable = allowParentSelection || isLeafCategory(group);
71
- const asHeading = single && !selectable;
72
- const selectedCount = group.subcategories.filter((sub) => draft.has(sub.id)).length;
72
+ const selectable = allowParentSelection || leaf;
73
+ const asHeading = single && !selectable && !props.deepTree;
74
+ const selectedCount = leaf ? 0 : leavesOf(node).filter((id) => draft.has(id)).length;
73
75
  return /* @__PURE__ */ jsxs(Fragment, { children: [
74
- asHeading ? /* @__PURE__ */ jsx(CategorySectionHeading, { option: group.category, query }) : /* @__PURE__ */ jsx(
75
- CategoryHeadRow,
76
+ asHeading ? /* @__PURE__ */ jsx(CategorySectionHeading, { option: node.category, query }) : /* @__PURE__ */ jsx(
77
+ CategoryTreeRow,
76
78
  {
77
- option: group.category,
79
+ option: node.category,
78
80
  query,
81
+ depth,
82
+ branch: !leaf,
79
83
  expanded,
80
84
  active: rowIds[activeIndex] === categoryId,
81
85
  sheet,
82
- checkState: selectable ? categoryCheckState(group, draft) : void 0,
86
+ ...selectable ? { checkState: categoryCheckState(node, draft) } : {},
83
87
  single,
84
- expandable: !isLeafCategory(group),
85
88
  selectedCount,
86
89
  showCounts: props.showCounts,
87
90
  onToggleExpanded: () => props.onToggleExpanded(categoryId),
88
- onActivate: () => props.onActivateCategory(group),
91
+ onActivate: () => leaf ? props.onActivateSubcategory(categoryId) : props.onActivateCategory(node),
89
92
  dataTestId: props.dataTestId,
90
93
  copy: props.copy
91
94
  }
92
95
  ),
93
- (expanded || asHeading) && group.subcategories.map((sub) => /* @__PURE__ */ jsx(
94
- SubcategoryRow,
95
- {
96
- option: sub,
97
- query,
98
- selected: draft.has(sub.id),
99
- active: rowIds[activeIndex] === sub.id,
100
- sheet,
101
- single,
102
- showCounts: props.showCounts,
103
- onActivate: () => props.onActivateSubcategory(sub.id),
104
- dataTestId: props.dataTestId
105
- },
106
- sub.id
107
- ))
96
+ (expanded || asHeading) && node.subcategories.map((sub) => /* @__PURE__ */ jsx(CategoryNodeRows, { node: sub, depth: depth + 1, props }, sub.category.id))
108
97
  ] });
109
98
  }
110
99
  function CategoryPanelList(props) {
@@ -133,7 +122,7 @@ function CategoryPanelList(props) {
133
122
  }
134
123
  );
135
124
  }
136
- return /* @__PURE__ */ jsx(Fragment, { children: groups.map((group) => /* @__PURE__ */ jsx(CategoryGroupRows, { group, props }, group.category.id)) });
125
+ return /* @__PURE__ */ jsx(Fragment, { children: groups.map((group) => /* @__PURE__ */ jsx(CategoryNodeRows, { node: group, depth: 0, props }, group.category.id)) });
137
126
  };
138
127
  return /* @__PURE__ */ jsx(
139
128
  Box,
@@ -152,4 +141,4 @@ function CategoryPanelList(props) {
152
141
  export {
153
142
  CategoryPanelList
154
143
  };
155
- //# sourceMappingURL=chunk-OVLNF4AZ.js.map
144
+ //# sourceMappingURL=chunk-P7IVAMAU.js.map
@@ -0,0 +1 @@
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, isLeafCategory, leavesOf } from './category-tree';\nimport { CategorySectionHeading, CategoryTreeRow } 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 /**\n * True when the tree has a level BELOW the subcategories. It turns the\n * section-heading treatment off — see {@link CategoryNodeRows}. Derived from\n * the options the panel state already holds, never a caller's decision.\n */\n deepTree: 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/**\n * One node: its heading or its row, then whichever children are visible.\n *\n * The SECTION-HEADING treatment — an inert uppercase label with its children\n * always on show — belongs to a two-level tree and only there. It is what makes\n * the \"mover para…\" picker read as \"category, then the subcategory you pick\",\n * and it works because a two-level category has nothing to fold TO: everything\n * under it is already the thing you came for. A third level breaks both halves\n * at once — the heading has no chevron, so the panel opens onto every item in\n * the catalogue, which is the flat list this component exists to replace. So a\n * deep tree drops the treatment and every frame becomes a foldable row instead.\n */\nfunction CategoryNodeRows({\n node,\n depth,\n props,\n}: {\n node: CategoryGroup;\n depth: number;\n props: PanelListProps;\n}): React.JSX.Element {\n const { draft, query, single, allowParentSelection, rowIds, activeIndex, sheet } = props;\n const categoryId = node.category.id;\n const leaf = isLeafCategory(node);\n const expanded = props.isExpanded(categoryId);\n // A childless category is the leaf, so it is selectable whatever the mode says\n // — the heading reading has no subcategory to point at, and would leave the\n // row inert.\n const selectable = allowParentSelection || leaf;\n const asHeading = single && !selectable && !props.deepTree;\n const selectedCount = leaf ? 0 : leavesOf(node).filter((id) => draft.has(id)).length;\n\n return (\n <>\n {asHeading ? (\n <CategorySectionHeading option={node.category} query={query} />\n ) : (\n <CategoryTreeRow\n option={node.category}\n query={query}\n depth={depth}\n branch={!leaf}\n expanded={expanded}\n active={rowIds[activeIndex] === categoryId}\n sheet={sheet}\n {...(selectable ? { checkState: categoryCheckState(node, draft) } : {})}\n single={single}\n selectedCount={selectedCount}\n showCounts={props.showCounts}\n onToggleExpanded={() => props.onToggleExpanded(categoryId)}\n onActivate={() =>\n leaf ? props.onActivateSubcategory(categoryId) : props.onActivateCategory(node)\n }\n dataTestId={props.dataTestId}\n copy={props.copy}\n />\n )}\n {(expanded || asHeading) &&\n node.subcategories.map((sub) => (\n <CategoryNodeRows key={sub.category.id} node={sub} depth={depth + 1} props={props} />\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 <CategoryNodeRows key={group.category.id} node={group} depth={0} 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":[]}
@@ -0,0 +1,160 @@
1
+ // src/components/form/CategorySelect/category-tree.ts
2
+ function foldText(text) {
3
+ return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
4
+ }
5
+ function onParentCycle(startId, parents) {
6
+ const seen = /* @__PURE__ */ new Set([startId]);
7
+ let current = parents.get(startId);
8
+ while (current !== void 0) {
9
+ if (seen.has(current)) return true;
10
+ seen.add(current);
11
+ current = parents.get(current);
12
+ }
13
+ return false;
14
+ }
15
+ function buildCategoryGroups(options) {
16
+ const nodes = /* @__PURE__ */ new Map();
17
+ const parents = /* @__PURE__ */ new Map();
18
+ options.forEach((option) => {
19
+ if (nodes.has(option.id)) return;
20
+ nodes.set(option.id, { category: option, subcategories: [] });
21
+ if (option.parentId && option.parentId !== option.id) parents.set(option.id, option.parentId);
22
+ });
23
+ const roots = [];
24
+ nodes.forEach((node, id) => {
25
+ const parentId = parents.get(id);
26
+ const parent = parentId === void 0 ? void 0 : nodes.get(parentId);
27
+ if (parent && !onParentCycle(id, parents)) parent.subcategories.push(node);
28
+ else roots.push(node);
29
+ });
30
+ return roots;
31
+ }
32
+ function isLeafCategory(group) {
33
+ return group.subcategories.length === 0;
34
+ }
35
+ function collectLeafIds(groups) {
36
+ return groups.flatMap((group) => leavesOf(group));
37
+ }
38
+ function leavesOf(group) {
39
+ if (isLeafCategory(group)) return [group.category.id];
40
+ return group.subcategories.flatMap((sub) => leavesOf(sub));
41
+ }
42
+ function collectBranchIds(groups) {
43
+ return groups.flatMap(
44
+ (group) => isLeafCategory(group) ? [] : [group.category.id, ...collectBranchIds(group.subcategories)]
45
+ );
46
+ }
47
+ function findGroup(groups, id) {
48
+ for (const group of groups) {
49
+ if (group.category.id === id) return group;
50
+ const found = findGroup(group.subcategories, id);
51
+ if (found) return found;
52
+ }
53
+ return void 0;
54
+ }
55
+ function categoryPath(groups, id) {
56
+ for (const group of groups) {
57
+ if (group.category.id === id) return [group.category.name];
58
+ const below = categoryPath(group.subcategories, id);
59
+ if (below) return [group.category.name, ...below];
60
+ }
61
+ return null;
62
+ }
63
+ function treeDepth(groups) {
64
+ return groups.reduce(
65
+ (deepest, group) => Math.max(deepest, 1 + treeDepth(group.subcategories)),
66
+ 0
67
+ );
68
+ }
69
+ function categoryCheckState(group, selected) {
70
+ const leaves = leavesOf(group);
71
+ const hits = leaves.filter((id) => selected.has(id)).length;
72
+ if (hits === 0) return "off";
73
+ return hits === leaves.length ? "on" : "partial";
74
+ }
75
+ function toggleCategoryLeaves(group, selected) {
76
+ const next = new Set(selected);
77
+ const leaves = leavesOf(group);
78
+ const turningOff = categoryCheckState(group, selected) === "on";
79
+ leaves.forEach((id) => turningOff ? next.delete(id) : next.add(id));
80
+ return next;
81
+ }
82
+ function toggleLeaf(id, selected) {
83
+ const next = new Set(selected);
84
+ if (!next.delete(id)) next.add(id);
85
+ return next;
86
+ }
87
+ function summarizeSelection(groups, selected) {
88
+ return groups.flatMap((group) => {
89
+ const state = categoryCheckState(group, selected);
90
+ if (state === "off") return [];
91
+ if (state === "on") {
92
+ return [
93
+ { id: group.category.id, label: group.category.name, whole: !isLeafCategory(group) }
94
+ ];
95
+ }
96
+ return summarizeSelection(group.subcategories, selected);
97
+ });
98
+ }
99
+ function removeChip(groups, chipId, selected) {
100
+ const next = new Set(selected);
101
+ const group = findGroup(groups, chipId);
102
+ if (group) {
103
+ leavesOf(group).forEach((id) => next.delete(id));
104
+ return next;
105
+ }
106
+ next.delete(chipId);
107
+ return next;
108
+ }
109
+ function filterCategoryGroups(groups, query) {
110
+ const needle = foldText(query.trim());
111
+ if (!needle) return groups;
112
+ const walk = (nodes) => nodes.flatMap((node) => {
113
+ if (foldText(node.category.name).includes(needle)) return [node];
114
+ const subcategories = walk(node.subcategories);
115
+ return subcategories.length > 0 ? [{ category: node.category, subcategories }] : [];
116
+ });
117
+ return walk(groups);
118
+ }
119
+ function highlightSegments(text, query) {
120
+ const needle = foldText(query.trim());
121
+ if (!needle) return [{ text, match: false }];
122
+ const at = foldText(text).indexOf(needle);
123
+ if (at < 0) return [{ text, match: false }];
124
+ return [
125
+ { text: text.slice(0, at), match: false },
126
+ { text: text.slice(at, at + needle.length), match: true },
127
+ { text: text.slice(at + needle.length), match: false }
128
+ ].filter((segment) => segment.text.length > 0);
129
+ }
130
+ function flattenRows(groups, isExpanded) {
131
+ const walk = (nodes, depth, parentId) => nodes.flatMap((node) => {
132
+ const id = node.category.id;
133
+ const branch = !isLeafCategory(node);
134
+ const row = { id, depth, branch, ...parentId ? { parentId } : {} };
135
+ if (!branch || !isExpanded(id)) return [row];
136
+ return [row, ...walk(node.subcategories, depth + 1, id)];
137
+ });
138
+ return walk(groups, 0);
139
+ }
140
+
141
+ export {
142
+ foldText,
143
+ buildCategoryGroups,
144
+ isLeafCategory,
145
+ collectLeafIds,
146
+ leavesOf,
147
+ collectBranchIds,
148
+ findGroup,
149
+ categoryPath,
150
+ treeDepth,
151
+ categoryCheckState,
152
+ toggleCategoryLeaves,
153
+ toggleLeaf,
154
+ summarizeSelection,
155
+ removeChip,
156
+ filterCategoryGroups,
157
+ highlightSegments,
158
+ flattenRows
159
+ };
160
+ //# sourceMappingURL=chunk-PN4HVM43.js.map
@@ -0,0 +1 @@
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(/[̀-ͯ]/g, '')\n .toLowerCase();\n}\n\n/**\n * Whether following `parentId` up from `startId` ever comes back to a node it\n * already passed.\n *\n * Two levels made a cycle impossible by construction: a child was attached to a\n * ROOT or promoted, so nothing could ever point back down. An arbitrary depth\n * attaches every node to whichever node its `parentId` names, and a payload\n * where two rows name each other would build a ring that every walk below —\n * flattening, filtering, counting leaves — follows until the stack gives out. So\n * the chain is checked rather than trusted, and a node on a ring is promoted to\n * top level, which is what {@link buildCategoryGroups} already does with a\n * parent it cannot resolve.\n */\nfunction onParentCycle(startId: string, parents: ReadonlyMap<string, string>): boolean {\n const seen = new Set<string>([startId]);\n let current = parents.get(startId);\n while (current !== undefined) {\n if (seen.has(current)) return true;\n seen.add(current);\n current = parents.get(current);\n }\n return false;\n}\n\n/**\n * Nest a flat `parentId` list into a tree of ANY depth, preserving input order\n * at every level.\n *\n * An option whose `parentId` names nothing in the payload is promoted to top\n * level rather than dropped — a category is never invisible just because its\n * parent was filtered out of the response. A duplicate id keeps its first row,\n * so a repeated payload cannot place the same node twice.\n */\nexport function buildCategoryGroups(options: CategorySelectOption[]): CategoryGroup[] {\n const nodes = new Map<string, CategoryGroup>();\n const parents = new Map<string, string>();\n options.forEach((option) => {\n if (nodes.has(option.id)) return;\n nodes.set(option.id, { category: option, subcategories: [] });\n if (option.parentId && option.parentId !== option.id) parents.set(option.id, option.parentId);\n });\n\n const roots: CategoryGroup[] = [];\n nodes.forEach((node, id) => {\n const parentId = parents.get(id);\n const parent = parentId === undefined ? undefined : nodes.get(parentId);\n if (parent && !onParentCycle(id, parents)) parent.subcategories.push(node);\n else roots.push(node);\n });\n return roots;\n}\n\n/**\n * Whether this node IS the leaf, because nothing sits under it.\n *\n * The leaf-only default makes a category a heading and the subcategory the thing\n * you pick — but a childless category has no subcategory to offer instead, so\n * that reading leaves it unpickable and the row does nothing at all. It is the\n * leaf, so it is selectable in its own right, whatever `allowParentSelection`\n * says. Everything below already agreed; only the rows and the activation did\n * not, which is what made a childless category unselectable on the estoque\n * filter while `Marcar tudo` could still select it.\n */\nexport function isLeafCategory(group: CategoryGroup): boolean {\n return group.subcategories.length === 0;\n}\n\n/** Every selectable leaf id across the whole tree, in display order. */\nexport function collectLeafIds(groups: CategoryGroup[]): string[] {\n return groups.flatMap((group) => leavesOf(group));\n}\n\n/**\n * The leaves a node stands for — every leaf BELOW it, or itself when childless.\n *\n * Recursive rather than one level down, so a category three levels deep still\n * stands for the items at the bottom of it and not for the subcategories in\n * between: the value a caller receives stays uniformly leaves, which is the\n * property that lets a chip, a tri-state checkbox and `Marcar tudo` agree.\n */\nexport function leavesOf(group: CategoryGroup): string[] {\n if (isLeafCategory(group)) return [group.category.id];\n return group.subcategories.flatMap((sub) => leavesOf(sub));\n}\n\n/** Every node that can be unfolded, at any depth — what \"Expandir tudo\" opens. */\nexport function collectBranchIds(groups: CategoryGroup[]): string[] {\n return groups.flatMap((group) =>\n isLeafCategory(group) ? [] : [group.category.id, ...collectBranchIds(group.subcategories)],\n );\n}\n\n/** The node carrying `id`, searched at every depth. */\nexport function findGroup(groups: CategoryGroup[], id: string): CategoryGroup | undefined {\n for (const group of groups) {\n if (group.category.id === id) return group;\n const found = findGroup(group.subcategories, id);\n if (found) return found;\n }\n return undefined;\n}\n\n/**\n * The names from the top-level category down to `id`, or `null` when absent.\n *\n * The single-select trigger reads this back as `Pai › Filha › Item`, so a chosen\n * row keeps the context that tells `Massas` (mercearia) from `Massas` (pratos\n * principais) once the panel has closed over it.\n */\nexport function categoryPath(groups: CategoryGroup[], id: string): string[] | null {\n for (const group of groups) {\n if (group.category.id === id) return [group.category.name];\n const below = categoryPath(group.subcategories, id);\n if (below) return [group.category.name, ...below];\n }\n return null;\n}\n\n/**\n * How many levels the deepest branch has. A flat list is 1, the classic\n * category/subcategory tree is 2, anything more is a tree with items in it.\n */\nexport function treeDepth(groups: CategoryGroup[]): number {\n return groups.reduce(\n (deepest, group) => Math.max(deepest, 1 + treeDepth(group.subcategories)),\n 0,\n );\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 node that is fully\n * selected becomes ONE chip bearing its name, rather than one chip per leaf —\n * \"Bebidas\" instead of six drinks.\n *\n * A partly-selected node hands the question down to its own children, so the\n * chip that appears is always the DEEPEST node that is completely selected.\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 // `whole` says the chip stands for MORE than itself, so it is the node\n // having children that decides it — not the node being fully selected,\n // which a leaf always is the moment it is picked at all.\n return [\n { id: group.category.id, label: group.category.name, whole: !isLeafCategory(group) },\n ];\n }\n return summarizeSelection(group.subcategories, selected);\n });\n}\n\n/**\n * Drop the leaves a chip stands for. A chip naming a node removes everything\n * under it; a chip naming nothing in the tree removes just that id.\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 = findGroup(groups, 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 every level, keeping each hit under its parents.\n *\n * A node that matches keeps its whole subtree, so \"Bebidas\" shows the entire\n * group. A node that does not match keeps only the descendants that do — the hit\n * still arrives with its parents visible above it, which is the context that\n * 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 const walk = (nodes: CategoryGroup[]): CategoryGroup[] =>\n nodes.flatMap((node) => {\n if (foldText(node.category.name).includes(needle)) return [node];\n const subcategories = walk(node.subcategories);\n return subcategories.length > 0 ? [{ category: node.category, subcategories }] : [];\n });\n return walk(groups);\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 id: string;\n /** 0 for a top-level category, one more for each level below it. */\n depth: number;\n /** True when the row has children to unfold — what → and ← act on. */\n branch: boolean;\n /** Owning node id — lets ← jump from a child back to its parent. */\n parentId?: string;\n}\n\n/** The visible rows, in order, honouring which nodes are expanded. */\nexport function flattenRows(\n groups: CategoryGroup[],\n isExpanded: (categoryId: string) => boolean,\n): CategoryRowRef[] {\n const walk = (nodes: CategoryGroup[], depth: number, parentId?: string): CategoryRowRef[] =>\n nodes.flatMap((node) => {\n const id = node.category.id;\n const branch = !isLeafCategory(node);\n const row: CategoryRowRef = { id, depth, branch, ...(parentId ? { parentId } : {}) };\n if (!branch || !isExpanded(id)) return [row];\n return [row, ...walk(node.subcategories, depth + 1, id)];\n });\n return walk(groups, 0);\n}\n"],"mappings":"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","names":[]}
@@ -1,13 +1,13 @@
1
1
  import {
2
- isLeafCategory
3
- } from "./chunk-LR6MSKTT.js";
2
+ findGroup
3
+ } from "./chunk-PN4HVM43.js";
4
4
 
5
5
  // src/components/form/CategorySelect/category-keyboard.ts
6
6
  function activeRow(context) {
7
7
  return context.activeIndex >= 0 ? context.rows[context.activeIndex] : void 0;
8
8
  }
9
9
  function groupOf(context, id) {
10
- return context.groups.find((group) => group.category.id === id);
10
+ return findGroup(context.groups, id);
11
11
  }
12
12
  function moveCursor(context, delta) {
13
13
  const { rows, activeIndex } = context;
@@ -17,26 +17,22 @@ function moveCursor(context, delta) {
17
17
  }
18
18
  function handleRight(context) {
19
19
  const row = activeRow(context);
20
- if (row?.kind === "category") context.expand(row.id);
20
+ if (row?.branch) context.expand(row.id);
21
21
  }
22
22
  function handleLeft(context) {
23
23
  const row = activeRow(context);
24
24
  if (!row) return;
25
- if (row.kind === "category") {
25
+ if (row.branch) {
26
26
  context.collapse(row.id);
27
27
  return;
28
28
  }
29
29
  if (!row.parentId) return;
30
30
  context.collapse(row.parentId);
31
- const parentIndex = context.rows.findIndex(
32
- (candidate) => candidate.kind === "category" && candidate.id === row.parentId
33
- );
31
+ const parentIndex = context.rows.findIndex((candidate) => candidate.id === row.parentId);
34
32
  if (parentIndex >= 0) context.setActiveIndex(parentIndex);
35
33
  }
36
- function activateCategoryRow(context, categoryId) {
37
- const group = groupOf(context, categoryId);
38
- if (!group) return;
39
- if (!context.allowParentSelection && !isLeafCategory(group)) {
34
+ function activateBranchRow(context, categoryId) {
35
+ if (!context.allowParentSelection) {
40
36
  context.toggleExpanded(categoryId);
41
37
  return;
42
38
  }
@@ -44,17 +40,20 @@ function activateCategoryRow(context, categoryId) {
44
40
  context.pick(categoryId);
45
41
  return;
46
42
  }
47
- context.toggleCategory(group);
43
+ const group = groupOf(context, categoryId);
44
+ if (group) context.toggleCategory(group);
48
45
  }
49
- function handleSpace(context) {
50
- const row = activeRow(context);
51
- if (!row) return;
52
- if (row.kind === "subcategory") {
53
- if (context.single) context.pick(row.id);
54
- else context.toggleSubcategory(row.id);
46
+ function activateRow(context, row) {
47
+ if (row.branch) {
48
+ activateBranchRow(context, row.id);
55
49
  return;
56
50
  }
57
- activateCategoryRow(context, row.id);
51
+ if (context.single) context.pick(row.id);
52
+ else context.toggleSubcategory(row.id);
53
+ }
54
+ function handleSpace(context) {
55
+ const row = activeRow(context);
56
+ if (row) activateRow(context, row);
58
57
  }
59
58
  function handleEnter(context) {
60
59
  const row = activeRow(context);
@@ -62,12 +61,7 @@ function handleEnter(context) {
62
61
  context.commit();
63
62
  return;
64
63
  }
65
- if (!row) return;
66
- if (row.kind === "subcategory") {
67
- context.pick(row.id);
68
- return;
69
- }
70
- activateCategoryRow(context, row.id);
64
+ if (row) activateRow(context, row);
71
65
  }
72
66
  var HANDLERS = {
73
67
  ArrowDown: (context) => moveCursor(context, 1),
@@ -90,4 +84,4 @@ function handleCategoryKeyDown(key, editingSearchText, context) {
90
84
  export {
91
85
  handleCategoryKeyDown
92
86
  };
93
- //# sourceMappingURL=chunk-WWM2YTW4.js.map
87
+ //# sourceMappingURL=chunk-R2AZOWEZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/form/CategorySelect/category-keyboard.ts"],"sourcesContent":["import { findGroup, 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 findGroup(context.groups, 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 row that folds. On a leaf it is a no-op: there is nothing deeper. */\nfunction handleRight(context: CategoryKeyContext): void {\n const row = activeRow(context);\n if (row?.branch) context.expand(row.id);\n}\n\n/** ← collapses a foldable row, or walks a leaf back up to its parent. */\nfunction handleLeft(context: CategoryKeyContext): void {\n const row = activeRow(context);\n if (!row) return;\n if (row.branch) {\n context.collapse(row.id);\n return;\n }\n if (!row.parentId) return;\n context.collapse(row.parentId);\n const parentIndex = context.rows.findIndex((candidate) => candidate.id === row.parentId);\n if (parentIndex >= 0) context.setActiveIndex(parentIndex);\n}\n\n/**\n * What a FOLDABLE row does when it is activated — the one answer the click, the\n * Space key and (in single-select) Enter all take, so the three cannot disagree\n * about a row the way they did over a childless category.\n *\n * A row that is only a frame expands instead of marking: it has no control, so\n * \"mark\" has nothing to mean there. A childless category is not a frame — it is\n * the leaf — so it never reaches here.\n */\nfunction activateBranchRow(context: CategoryKeyContext, categoryId: string): void {\n if (!context.allowParentSelection) {\n context.toggleExpanded(categoryId);\n return;\n }\n if (context.single) {\n context.pick(categoryId);\n return;\n }\n const group = groupOf(context, categoryId);\n if (group) context.toggleCategory(group);\n}\n\n/** Mark the row under the cursor — or fold it, when it is only a frame. */\nfunction activateRow(context: CategoryKeyContext, row: CategoryRowRef): void {\n if (row.branch) {\n activateBranchRow(context, row.id);\n return;\n }\n if (context.single) context.pick(row.id);\n else context.toggleSubcategory(row.id);\n}\n\n/** Space marks the row under the cursor. */\nfunction handleSpace(context: CategoryKeyContext): void {\n const row = activeRow(context);\n if (row) activateRow(context, row);\n}\n\nfunction handleEnter(context: CategoryKeyContext): void {\n const row = activeRow(context);\n if (!context.single) {\n context.commit();\n return;\n }\n if (row) activateRow(context, row);\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,UAAU,QAAQ,QAAQ,EAAE;AACrC;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,OAAQ,SAAQ,OAAO,IAAI,EAAE;AACxC;AAGA,SAAS,WAAW,SAAmC;AACrD,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,CAAC,IAAK;AACV,MAAI,IAAI,QAAQ;AACd,YAAQ,SAAS,IAAI,EAAE;AACvB;AAAA,EACF;AACA,MAAI,CAAC,IAAI,SAAU;AACnB,UAAQ,SAAS,IAAI,QAAQ;AAC7B,QAAM,cAAc,QAAQ,KAAK,UAAU,CAAC,cAAc,UAAU,OAAO,IAAI,QAAQ;AACvF,MAAI,eAAe,EAAG,SAAQ,eAAe,WAAW;AAC1D;AAWA,SAAS,kBAAkB,SAA6B,YAA0B;AAChF,MAAI,CAAC,QAAQ,sBAAsB;AACjC,YAAQ,eAAe,UAAU;AACjC;AAAA,EACF;AACA,MAAI,QAAQ,QAAQ;AAClB,YAAQ,KAAK,UAAU;AACvB;AAAA,EACF;AACA,QAAM,QAAQ,QAAQ,SAAS,UAAU;AACzC,MAAI,MAAO,SAAQ,eAAe,KAAK;AACzC;AAGA,SAAS,YAAY,SAA6B,KAA2B;AAC3E,MAAI,IAAI,QAAQ;AACd,sBAAkB,SAAS,IAAI,EAAE;AACjC;AAAA,EACF;AACA,MAAI,QAAQ,OAAQ,SAAQ,KAAK,IAAI,EAAE;AAAA,MAClC,SAAQ,kBAAkB,IAAI,EAAE;AACvC;AAGA,SAAS,YAAY,SAAmC;AACtD,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,IAAK,aAAY,SAAS,GAAG;AACnC;AAEA,SAAS,YAAY,SAAmC;AACtD,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,CAAC,QAAQ,QAAQ;AACnB,YAAQ,OAAO;AACf;AAAA,EACF;AACA,MAAI,IAAK,aAAY,SAAS,GAAG;AACnC;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,10 +1,12 @@
1
1
  import {
2
2
  buildCategoryGroups,
3
+ collectBranchIds,
3
4
  filterCategoryGroups,
4
5
  flattenRows,
5
6
  toggleCategoryLeaves,
6
- toggleLeaf
7
- } from "./chunk-LR6MSKTT.js";
7
+ toggleLeaf,
8
+ treeDepth
9
+ } from "./chunk-PN4HVM43.js";
8
10
 
9
11
  // src/components/form/CategorySelect/useCategorySelect.ts
10
12
  import { useCallback, useMemo, useRef, useState } from "react";
@@ -23,7 +25,7 @@ function useExpandedCategories(allGroups, searching) {
23
25
  }, []);
24
26
  const setAllExpanded = useCallback(
25
27
  (shouldExpand) => {
26
- setExpanded(shouldExpand ? new Set(allGroups.map((g) => g.category.id)) : /* @__PURE__ */ new Set());
28
+ setExpanded(shouldExpand ? new Set(collectBranchIds(allGroups)) : /* @__PURE__ */ new Set());
27
29
  },
28
30
  [allGroups]
29
31
  );
@@ -56,6 +58,7 @@ function useCategorySelect({
56
58
  () => filterCategoryGroups(allGroups, query),
57
59
  [allGroups, query]
58
60
  );
61
+ const deepTree = useMemo(() => treeDepth(allGroups) > 2, [allGroups]);
59
62
  const expansion = useExpandedCategories(allGroups, query.trim().length > 0);
60
63
  const selection = useDraftSelection(selected);
61
64
  const rows = useMemo(
@@ -85,6 +88,7 @@ function useCategorySelect({
85
88
  query,
86
89
  visibleGroups,
87
90
  allGroups,
91
+ deepTree,
88
92
  rows,
89
93
  activeIndex,
90
94
  setQuery,
@@ -98,4 +102,4 @@ function useCategorySelect({
98
102
  export {
99
103
  useCategorySelect
100
104
  };
101
- //# sourceMappingURL=chunk-2GB5CIN6.js.map
105
+ //# sourceMappingURL=chunk-RZVUY2MO.js.map