@antscorp/ui 0.1.2 → 0.1.4

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 (99) hide show
  1. package/README.md +26 -2
  2. package/dist/atoms/AddButton.d.ts +3 -6
  3. package/dist/atoms/Chip.d.ts +2 -5
  4. package/dist/atoms/Chip.js +1 -1
  5. package/dist/atoms/InfoTip.js +2 -0
  6. package/dist/atoms/LabeledDivider.d.ts +1 -1
  7. package/dist/atoms/PanelSearchInput.d.ts +1 -1
  8. package/dist/atoms/RemoveButton.d.ts +2 -5
  9. package/dist/atoms/TextAction.d.ts +5 -6
  10. package/dist/atoms/TruncatedOption.d.ts +1 -1
  11. package/dist/cli.js +248 -0
  12. package/dist/doc/schema.d.ts +34 -0
  13. package/dist/doc/schema.js +33 -0
  14. package/dist/docs.json +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +53 -51
  17. package/dist/lib/iso.d.ts +2 -2
  18. package/dist/lib/iso.js +16 -8
  19. package/dist/lib/tokenEditorExtensions.js +1 -1
  20. package/dist/lib/useLoadingShown.d.ts +1 -1
  21. package/dist/molecules/AccentCard.d.ts +2 -3
  22. package/dist/molecules/AccentCard.js +2 -2
  23. package/dist/molecules/CheckRow.d.ts +3 -8
  24. package/dist/molecules/ChoiceTiles.d.ts +1 -1
  25. package/dist/molecules/ChoiceTiles.js +1 -1
  26. package/dist/molecules/CommitTextField.d.ts +1 -19
  27. package/dist/molecules/CommitTextField.js +21 -18
  28. package/dist/molecules/ConfirmDialog.d.ts +2 -7
  29. package/dist/molecules/DateTimeInput.d.ts +6 -3
  30. package/dist/molecules/DateTimeInput.js +82 -73
  31. package/dist/molecules/FieldGroup.d.ts +1 -1
  32. package/dist/molecules/FieldRow.d.ts +1 -1
  33. package/dist/molecules/MetaTable.d.ts +1 -1
  34. package/dist/molecules/PhoneFrame.d.ts +1 -1
  35. package/dist/molecules/RemovableRow.d.ts +1 -1
  36. package/dist/molecules/RemovableRow.js +32 -32
  37. package/dist/molecules/RevealRadioGroup.d.ts +2 -9
  38. package/dist/molecules/RevealSwitch.d.ts +2 -7
  39. package/dist/molecules/Select.d.ts +8 -24
  40. package/dist/molecules/Select.js +50 -44
  41. package/dist/molecules/SideLabelRow.d.ts +1 -1
  42. package/dist/molecules/SplitButton.d.ts +1 -1
  43. package/dist/molecules/StatusButton.d.ts +2 -7
  44. package/dist/molecules/TagSelect.d.ts +2 -2
  45. package/dist/molecules/TransferLayout.d.ts +1 -1
  46. package/dist/molecules/TreeOptions.d.ts +4 -3
  47. package/dist/molecules/TreeOptions.js +67 -44
  48. package/dist/organisms/HourOfDayRow.js +2 -2
  49. package/dist/organisms/HourOfWeekGrid.js +2 -2
  50. package/dist/organisms/RuleGroups.d.ts +3 -3
  51. package/dist/organisms/SearchableGroupedList.d.ts +1 -1
  52. package/dist/organisms/TransferPicker.d.ts +1 -1
  53. package/dist/organisms/TreeSearchPanel.js +50 -47
  54. package/dist/organisms/TreeSelect.d.ts +3 -3
  55. package/dist/ui.css +1 -1
  56. package/package.json +36 -19
  57. package/src/atoms/AddButton.tsx +3 -7
  58. package/src/atoms/Chip.tsx +7 -9
  59. package/src/atoms/InfoTip.tsx +2 -2
  60. package/src/atoms/LabeledDivider.tsx +1 -1
  61. package/src/atoms/PanelSearchInput.tsx +1 -1
  62. package/src/atoms/RemoveButton.tsx +2 -6
  63. package/src/atoms/TextAction.tsx +6 -8
  64. package/src/atoms/TruncatedOption.tsx +1 -1
  65. package/src/doc/schema.ts +67 -0
  66. package/src/index.ts +2 -0
  67. package/src/lib/iso.ts +15 -3
  68. package/src/lib/tokenEditorExtensions.ts +1 -1
  69. package/src/lib/useLoadMore.ts +1 -2
  70. package/src/lib/useLoadingShown.ts +1 -1
  71. package/src/molecules/AccentCard.tsx +11 -10
  72. package/src/molecules/CheckRow.tsx +3 -9
  73. package/src/molecules/ChoiceTiles.tsx +6 -4
  74. package/src/molecules/CommitTextField.tsx +15 -20
  75. package/src/molecules/ConfirmDialog.tsx +2 -8
  76. package/src/molecules/DateTimeInput.tsx +86 -68
  77. package/src/molecules/DurationInput.tsx +2 -2
  78. package/src/molecules/FieldGroup.tsx +1 -1
  79. package/src/molecules/FieldRow.tsx +1 -1
  80. package/src/molecules/FrequencyLimitRow.tsx +1 -1
  81. package/src/molecules/MetaTable.tsx +1 -1
  82. package/src/molecules/PhoneFrame.tsx +2 -2
  83. package/src/molecules/RemovableRow.tsx +3 -3
  84. package/src/molecules/RevealRadioGroup.tsx +5 -10
  85. package/src/molecules/RevealSwitch.tsx +2 -8
  86. package/src/molecules/Select.tsx +23 -23
  87. package/src/molecules/SideLabelRow.tsx +2 -2
  88. package/src/molecules/SplitButton.tsx +1 -1
  89. package/src/molecules/StatusButton.tsx +2 -8
  90. package/src/molecules/TagSelect.tsx +5 -5
  91. package/src/molecules/TransferLayout.tsx +2 -3
  92. package/src/molecules/TreeOptions.tsx +89 -65
  93. package/src/organisms/HourOfDayRow.tsx +5 -3
  94. package/src/organisms/HourOfWeekGrid.tsx +5 -3
  95. package/src/organisms/RuleGroups.tsx +3 -3
  96. package/src/organisms/SearchableGroupedList.tsx +2 -2
  97. package/src/organisms/TransferPicker.tsx +3 -3
  98. package/src/organisms/TreeSearchPanel.tsx +2 -2
  99. package/src/organisms/TreeSelect.tsx +4 -4
@@ -40,7 +40,7 @@ export type TransferLayoutProps = {
40
40
  };
41
41
 
42
42
  /**
43
- * Two columns side by side in one frame, as fe-app's audience picker: a header strip over two
43
+ * Two columns side by side in one frame: a header strip over two
44
44
  * equal columns of a fixed height whose lists scroll on their own, 1px lines between the parts
45
45
  * and no gap, and an optional footer under the start column. The headers and the lists' titles
46
46
  * sit outside the scrolling areas. Pure layout: every part is a slot. It holds no state.
@@ -126,8 +126,7 @@ export function TransferLayout({
126
126
 
127
127
  const LINE_COLOR = "var(--ants-mh-gray-200)";
128
128
 
129
- // fe-app's UITargetSelection grid (WrapperHeaderSelect, WrapperSelectItem, WrapperButtonFooter):
130
- // lines of its platinum grey, a 41px header strip, columns of a fixed height.
129
+ // Platinum grey grid lines, a 41px header strip, columns of a fixed height.
131
130
  const styles = stylex.create({
132
131
  frame: {
133
132
  gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)",
@@ -1,3 +1,4 @@
1
+ import { useTreeFocus } from "@astryxdesign/core/hooks";
1
2
  import { Icon } from "@astryxdesign/core/Icon";
2
3
  import { useIndicator } from "@astryxdesign/core/Indicator";
3
4
  import { Text } from "@astryxdesign/core/Text";
@@ -9,7 +10,7 @@ import {
9
10
  } from "@astryxdesign/core/theme/tokens.stylex";
10
11
  import { themeProps } from "@astryxdesign/core/utils";
11
12
  import * as stylex from "@stylexjs/stylex";
12
- import { type KeyboardEvent, type ReactNode, useState } from "react";
13
+ import { Fragment, type ReactNode, useId, useState } from "react";
13
14
 
14
15
  /** One row of a two-level tree to pick from: a leaf, or a group of leaves. */
15
16
  export type TreeItem = {
@@ -53,17 +54,20 @@ type Row = {
53
54
  };
54
55
 
55
56
  /**
56
- * The rows of a two-level tree to pick from, as fe-app's tree list: a group row shows its chevron
57
+ * The rows of a two-level tree to pick from: a group row shows its chevron
57
58
  * at the end (right when closed, down when open) and only opens and closes; a leaf is picked; a
58
59
  * disabled row does neither. Rows are the Selector's option rows (its styles, its option-row and
59
60
  * check theme targets, its check on the picked leaf at the end, where a group shows its chevron),
60
61
  * so a theme styles both alike: the picked row's look and whether a check shows are the theme's.
61
- * Tab reaches the picked leaf (else the first enabled row), Up and Down move between rows, Enter
62
- * and Space act. Its state is which groups are open.
62
+ * Tab initially reaches the picked leaf (else the first enabled row); arrows, Home and End move
63
+ * through enabled rows, Right opens or enters a group, Left collapses or returns to its parent,
64
+ * and Enter and Space act. Its state is which groups are open and which row has focus.
63
65
  */
64
66
  export function TreeOptions(props: TreeOptionsProps): ReactNode {
65
67
  const { items, value } = props;
66
68
  const size = props.size ?? "md";
69
+ const id = useId();
70
+ const [active, setActive] = useState<string>();
67
71
  const [open, setOpen] = useState<ReadonlySet<string>>(
68
72
  () =>
69
73
  new Set(
@@ -87,7 +91,9 @@ export function TreeOptions(props: TreeOptionsProps): ReactNode {
87
91
  const Check = useIndicator("check");
88
92
  const enabled = rows.filter((row) => row.item.isDisabled !== true);
89
93
  const tabbable =
90
- enabled.find((row) => row.isOpen === undefined && row.item.value === value) ?? enabled[0];
94
+ enabled.find((row) => row.item.value === active) ??
95
+ enabled.find((row) => row.isOpen === undefined && row.item.value === value) ??
96
+ enabled[0];
91
97
  const act = (row: Row): void => {
92
98
  if (row.item.isDisabled === true) return;
93
99
  if (row.isOpen === undefined) {
@@ -98,56 +104,84 @@ export function TreeOptions(props: TreeOptionsProps): ReactNode {
98
104
  if (!next.delete(row.item.value)) next.add(row.item.value);
99
105
  setOpen(next);
100
106
  };
107
+ const activate = (key: string | undefined): void => {
108
+ const row = rows.find((candidate) => candidate.item.value === key);
109
+ if (row !== undefined) act(row);
110
+ };
111
+ const { treeRef, handleKeyDown } = useTreeFocus({
112
+ itemSelector: '[role="treeitem"]:not([aria-disabled="true"])',
113
+ onToggleExpand: activate,
114
+ onActivate: (_item, key) => {
115
+ activate(key);
116
+ return true;
117
+ },
118
+ });
119
+ const groupId = (row: Row): string => `${id}-${encodeURIComponent(row.item.value)}`;
120
+ const renderRow = (row: Row): ReactNode => (
121
+ <div
122
+ key={row.item.value}
123
+ role="treeitem"
124
+ aria-label={row.item.label}
125
+ data-tree-id={row.item.value}
126
+ aria-level={row.level}
127
+ aria-expanded={row.isOpen}
128
+ aria-owns={row.isOpen === true ? groupId(row) : undefined}
129
+ aria-selected={row.isOpen === undefined ? row.item.value === value : undefined}
130
+ aria-disabled={row.item.isDisabled === true || undefined}
131
+ tabIndex={row.item === tabbable?.item ? 0 : -1}
132
+ onFocus={() => {
133
+ setActive(row.item.value);
134
+ }}
135
+ onClick={() => {
136
+ act(row);
137
+ }}
138
+ {...rowProps(row, size, value)}
139
+ >
140
+ {/* The option's content and mark column, as `Selector.tsx:1407-1431`. */}
141
+ {/* One line cut with an ellipsis, with Text's tooltip for the whole label;
142
+ Astryx's SelectorOption would run under the mark. */}
143
+ <span {...stylex.props(styles.content)}>
144
+ <Text type="inherit" color="inherit" maxLines={1}>
145
+ {row.item.label}
146
+ </Text>
147
+ </span>
148
+ {/* A leaf's column sets no display of its own: the theme may hide it with its check. */}
149
+ <span {...stylex.props(styles.mark, row.isOpen !== undefined && styles.markCentred)}>
150
+ {row.isOpen === undefined ? (
151
+ <Check
152
+ state={row.item.value === value ? "checked" : "unchecked"}
153
+ size="sm"
154
+ isDisabled={row.item.isDisabled === true}
155
+ {...themeProps("selector-check")}
156
+ />
157
+ ) : (
158
+ <Icon icon={row.isOpen ? "chevronDown" : "chevronRight"} size="sm" color="secondary" />
159
+ )}
160
+ </span>
161
+ </div>
162
+ );
101
163
  return (
102
- // Raw elements: a flat tree (rows with `aria-level`), which Astryx's TreeList is not: it puts
103
- // a group's chevron first, fe-app at the end.
104
- <div role="tree" aria-label={props.label} onKeyDown={moveFocus}>
105
- {rows.map((row) => (
106
- <div
107
- key={row.item.value}
108
- role="treeitem"
109
- aria-level={row.level}
110
- aria-expanded={row.isOpen}
111
- aria-selected={row.isOpen === undefined ? row.item.value === value : undefined}
112
- aria-disabled={row.item.isDisabled === true || undefined}
113
- tabIndex={row === tabbable ? 0 : -1}
114
- onClick={() => {
115
- act(row);
116
- }}
117
- onKeyDown={(event) => {
118
- if (event.key !== "Enter" && event.key !== " ") return;
119
- event.preventDefault();
120
- act(row);
121
- }}
122
- {...rowProps(row, size, value)}
123
- >
124
- {/* The option's content and mark column, as `Selector.tsx:1407-1431`. */}
125
- {/* One line cut with an ellipsis, the whole label in Text's tooltip when cut, as
126
- fe-app's tree (`Tree/Item.jsx:235`); Astryx's SelectorOption would run under the mark. */}
127
- <span {...stylex.props(styles.content)}>
128
- <Text type="inherit" color="inherit" maxLines={1}>
129
- {row.item.label}
130
- </Text>
131
- </span>
132
- {/* A leaf's column sets no display of its own: the theme may hide it with its check. */}
133
- <span {...stylex.props(styles.mark, row.isOpen !== undefined && styles.markCentred)}>
134
- {row.isOpen === undefined ? (
135
- <Check
136
- state={row.item.value === value ? "checked" : "unchecked"}
137
- size="sm"
138
- isDisabled={row.item.isDisabled === true}
139
- {...themeProps("selector-check")}
140
- />
141
- ) : (
142
- <Icon
143
- icon={row.isOpen ? "chevronDown" : "chevronRight"}
144
- size="sm"
145
- color="secondary"
146
- />
164
+ // Raw tree roles preserve the trailing chevron layout; each parent owns its sibling group.
165
+ <div
166
+ role="tree"
167
+ aria-label={props.label}
168
+ ref={(element) => {
169
+ treeRef.current = element;
170
+ }}
171
+ onKeyDown={handleKeyDown}
172
+ >
173
+ {rows
174
+ .filter((row) => row.level === 1)
175
+ .map((row) => (
176
+ <Fragment key={row.item.value}>
177
+ {renderRow(row)}
178
+ {row.isOpen === true && (
179
+ <div role="group" id={groupId(row)}>
180
+ {row.item.children?.map((child) => renderRow({ item: child, level: 2 }))}
181
+ </div>
147
182
  )}
148
- </span>
149
- </div>
150
- ))}
183
+ </Fragment>
184
+ ))}
151
185
  </div>
152
186
  );
153
187
  }
@@ -178,22 +212,12 @@ function rowProps(
178
212
  };
179
213
  }
180
214
 
181
- // Up and Down move the keyboard to the row above or below.
182
- function moveFocus(event: KeyboardEvent<HTMLDivElement>): void {
183
- const step = event.key === "ArrowDown" ? 1 : event.key === "ArrowUp" ? -1 : 0;
184
- if (step === 0) return;
185
- event.preventDefault();
186
- const rows = [...event.currentTarget.querySelectorAll<HTMLElement>('[role="treeitem"]')];
187
- const at = rows.findIndex((row) => row === event.target);
188
- rows[Math.min(Math.max(at + step, 0), rows.length - 1)]?.focus();
189
- }
190
-
191
215
  // Astryx `Selector/Selector.tsx` styles: `item` (345-366, without its radius), `itemContent`
192
216
  // (367-373), `itemMarkColumn` (379-389), `itemHighlighted` (395-397, here on hover and keyboard
193
217
  // focus, as the rows take focus, never on the picked row), `itemDisabled` (401-404) and
194
218
  // `itemSizeStyles` (452-460). The radius and the picked row's background and weight
195
219
  // (`itemSelected`) are left to the theme's `selector-option-row`. Only `child` is ours: a
196
- // group's leaves one indent step in, as fe-app's.
220
+ // group's leaves one indent step in.
197
221
  const styles = stylex.create({
198
222
  row: {
199
223
  boxSizing: "border-box",
@@ -250,7 +274,7 @@ export function leafOf(items: readonly TreeItem[], value: string): TreeItem | un
250
274
  return undefined;
251
275
  }
252
276
 
253
- // The rows a search leaves, as fe-app's `searchTree`: a case-insensitive substring of the label.
277
+ // The rows a search leaves: a case-insensitive substring of the label.
254
278
  // A leaf stays when it matches; a group when its label or a child's matches, keeping only the
255
279
  // children that match.
256
280
  export function matchTree(items: readonly TreeItem[], search: string): TreeItem[] {
@@ -1,3 +1,4 @@
1
+ import type { AccentRole } from "@antscorp/astryx-theme-antsomi/palette";
1
2
  import { FieldStatus } from "@astryxdesign/core/FieldStatus";
2
3
  import { ScrollableArea } from "@astryxdesign/core/ScrollableArea";
3
4
  import { colorVars } from "@astryxdesign/core/theme/tokens.stylex";
@@ -115,6 +116,7 @@ export function HourOfDayRow(props: HourOfDayRowProps): ReactNode {
115
116
  }
116
117
 
117
118
  // As the week grid's: 24px cells, 11px text.
119
+ const accentHover: AccentRole = "var(--ants-accent-hover)";
118
120
  const styles = stylex.create({
119
121
  grid: { width: "max-content", userSelect: "none", cursor: "pointer" },
120
122
  locked: { cursor: "default" },
@@ -133,18 +135,18 @@ const styles = stylex.create({
133
135
  },
134
136
  on: { backgroundColor: colorVars["--color-accent"] },
135
137
  off: { backgroundColor: colorVars["--color-neutral"] },
136
- // fe-app's cells (SelectHoursOfDay Box) turn its hover blue under the pointer, picked or not,
138
+ // Cells turn hover blue under the pointer, picked or not,
137
139
  // while they can be changed.
138
140
  liveOn: {
139
141
  backgroundColor: {
140
142
  default: colorVars["--color-accent"],
141
- ":hover": { default: null, "@media (hover: hover)": "var(--ants-mh-blue-50)" },
143
+ ":hover": { default: null, "@media (hover: hover)": accentHover },
142
144
  },
143
145
  },
144
146
  liveOff: {
145
147
  backgroundColor: {
146
148
  default: colorVars["--color-neutral"],
147
- ":hover": { default: null, "@media (hover: hover)": "var(--ants-mh-blue-50)" },
149
+ ":hover": { default: null, "@media (hover: hover)": accentHover },
148
150
  },
149
151
  },
150
152
  axis: { color: colorVars["--color-text-secondary"], fontSize: 11, whiteSpace: "nowrap" },
@@ -1,3 +1,4 @@
1
+ import type { AccentRole } from "@antscorp/astryx-theme-antsomi/palette";
1
2
  import { FieldStatus } from "@astryxdesign/core/FieldStatus";
2
3
  import { ScrollableArea } from "@astryxdesign/core/ScrollableArea";
3
4
  import { Text } from "@astryxdesign/core/Text";
@@ -239,6 +240,7 @@ export function HourOfWeekGrid(props: HourOfWeekGridProps): ReactNode {
239
240
  }
240
241
 
241
242
  // Sizes follow the form it came from: 24px cells, a 76px column of day names, 11px text.
243
+ const accentHover: AccentRole = "var(--ants-accent-hover)";
242
244
  const styles = stylex.create({
243
245
  // Every cell picks, as in the form it came from; the cells inherit it.
244
246
  grid: { width: "max-content", userSelect: "none", cursor: "pointer" },
@@ -283,18 +285,18 @@ const styles = stylex.create({
283
285
  },
284
286
  on: { backgroundColor: colorVars["--color-accent"] },
285
287
  off: { backgroundColor: colorVars["--color-neutral"] },
286
- // fe-app's cells (SelectHoursOfDay Box) turn its hover blue under the pointer, picked or not,
288
+ // Cells turn hover blue under the pointer, picked or not,
287
289
  // while they can be changed.
288
290
  liveOn: {
289
291
  backgroundColor: {
290
292
  default: colorVars["--color-accent"],
291
- ":hover": { default: null, "@media (hover: hover)": "var(--ants-mh-blue-50)" },
293
+ ":hover": { default: null, "@media (hover: hover)": accentHover },
292
294
  },
293
295
  },
294
296
  liveOff: {
295
297
  backgroundColor: {
296
298
  default: colorVars["--color-neutral"],
297
- ":hover": { default: null, "@media (hover: hover)": "var(--ants-mh-blue-50)" },
299
+ ":hover": { default: null, "@media (hover: hover)": accentHover },
298
300
  },
299
301
  },
300
302
  axis: {
@@ -55,11 +55,11 @@ export type RuleGroupsProps<R> = {
55
55
  };
56
56
 
57
57
  /**
58
- * Rules in groups, as fe-app's attribute filter: each rule a card with an accent stripe, a title
58
+ * Rules in groups: each rule a card with an accent stripe, a title
59
59
  * and a close button; the word "and" under each card but a group's last, and a pill button adding
60
60
  * a rule at the end of the group; a dashed divider with "or" between groups, and a pill button
61
- * adding a group after the last; each word in capitals, as fe-app's. Unlike a list's add, an add at its limit (`maxRules`,
62
- * `maxGroups`) is gone, as fe-app's. The only rule left has no close button, so the groups are
61
+ * adding a group after the last; each word in capitals. Unlike a list's add, an add at its limit (`maxRules`,
62
+ * `maxGroups`) is hidden. The only rule left has no close button, so the groups are
63
63
  * never emptied from here; with no group at all only the button adding one shows. It holds no
64
64
  * state.
65
65
  */
@@ -66,7 +66,7 @@ export type SearchableGroupedListProps<I> = {
66
66
  /**
67
67
  * Caps the height, the groups scrolling under the search box, in a pop-over say; with no
68
68
  * `maxHeight` it fills its parent's height, in a side panel say. The list then pads its own edges,
69
- * so the search row (its magnifier at the end, as fe-app's) and the rule under it reach the
69
+ * so the search row (its magnifier at the end) and the rule under it reach the
70
70
  * edges of a flush (unpadded) surface, and so does the scrollbar.
71
71
  */
72
72
  readonly scroll?: { readonly maxHeight?: number | undefined; readonly label: string } | undefined;
@@ -271,7 +271,7 @@ const styles = stylex.create({
271
271
  framed: { minHeight: 0 },
272
272
  capped: (maxHeight: number) => ({ maxHeight }),
273
273
  scroll: { minHeight: 0 },
274
- // A folding group's row, as fe-app's picker: 10px sides, 36px high when folded.
274
+ // A folding group's row: 10px sides, 36px high when folded.
275
275
  fold: { paddingInline: 10, paddingBlock: 6 },
276
276
  flush: { marginInline: -10 },
277
277
  title: { fontSize: typeScaleVars["--text-label-size"] },
@@ -142,7 +142,7 @@ export type TransferPickerProps<V extends string> = Pick<
142
142
  const LOADING_ROWS = 6;
143
143
 
144
144
  /**
145
- * Picks options in two columns of one frame, as fe-app's target selection: a header strip with
145
+ * Picks options in two columns of one frame: a header strip with
146
146
  * the search (and the host's controls) over the options to tick, and the host's controls with
147
147
  * "Remove all" over the picked ones in framed rows with a remove button. Searching matches every
148
148
  * word, in any case; it stays cheap for thousands of options, and a new search scrolls the
@@ -177,7 +177,7 @@ export function TransferPicker<V extends string>(props: TransferPickerProps<V>):
177
177
  <HStack gap={2} align="center" justify={props.endHeader === undefined ? "end" : "between"}>
178
178
  {props.endHeader}
179
179
  {removeAll !== undefined && (
180
- // fe-app keeps it pressable while nothing is picked.
180
+ // Keep the clear action pressable while nothing is picked.
181
181
  <TextAction
182
182
  label={removeAll}
183
183
  isDisabled={isDisabled}
@@ -365,7 +365,7 @@ function OptionsHeading<V extends string>(
365
365
  isLabelHidden
366
366
  isDisabled={props.isDisabled ?? false}
367
367
  isReadOnly={props.isReadOnly ?? false}
368
- // fe-app's box: one mark for "some or all picked", and a click clears them.
368
+ // One mark for "some or all picked", and a click clears them.
369
369
  value={props.value.length > 0 ? "indeterminate" : false}
370
370
  onChange={props.onSelectAll}
371
371
  />
@@ -14,7 +14,7 @@ import { LoadMoreRow } from "../molecules/LoadMoreRow";
14
14
  import { matchTree, TreeOptions } from "../molecules/TreeOptions";
15
15
  import type { TreeSelectItem, TreeSelectProps } from "./TreeSelect";
16
16
 
17
- // fe-app's tree list is at most 200px high (UISelectCondition `Tree/index.jsx`).
17
+ // Cap the tree list at 200px so longer results scroll inside the panel.
18
18
  const LIST_HEIGHT = 200;
19
19
 
20
20
  /** What `TreeSearchPanel` takes. */
@@ -64,7 +64,7 @@ export function TreeSearchPanel(props: TreeSearchPanelProps): ReactNode {
64
64
  const { rootRef, sentinelRef } = useLoadMore({
65
65
  ...more,
66
66
  hasLoadMoreError: labels.loadMoreError !== undefined,
67
- itemCount: shown.length,
67
+ itemCount: shown.reduce((count, item) => count + 1 + (item.children?.length ?? 0), 0),
68
68
  });
69
69
  const groups = shown.filter((item) => item.children !== undefined).length;
70
70
  return (
@@ -88,7 +88,7 @@ export type TreeSelectProps = Pick<LoadMoreOptions, "onLoadMore" | "hasMore" | "
88
88
  readonly isLoading?: boolean | undefined;
89
89
  /** The trigger's width, in pixels or any CSS length such as `"100%"`; as wide as its content without it. */
90
90
  readonly width?: number | string | undefined;
91
- /** The trigger's size, `md` by default (30px, as fe-app's controls). */
91
+ /** The trigger's size, `md` by default (the theme's 30px control height). */
92
92
  readonly size?: "sm" | "md" | undefined;
93
93
  /** Dims the trigger and blocks it. */
94
94
  readonly isDisabled?: boolean | undefined;
@@ -105,8 +105,8 @@ export type TreeSelectProps = Pick<LoadMoreOptions, "onLoadMore" | "hasMore" | "
105
105
  };
106
106
 
107
107
  /**
108
- * A Selector-like trigger opening a popover with a search box over a two-level tree, as fe-app's
109
- * `UISelectCondition use="tree"`: a group row (its chevron at the end) only opens and closes, a
108
+ * A Selector-like trigger opening a popover with a search box over a two-level tree:
109
+ * a group row (its chevron at the end) only opens and closes, a
110
110
  * leaf is picked and closes the popover, a disabled row is not picked. The search keeps a leaf
111
111
  * whose label matches and a group whose label or a child's matches, with only its matching
112
112
  * children, every group open; with `onSearchChange` the host searches instead. With
@@ -289,7 +289,7 @@ const styles = stylex.create({
289
289
  cursor: { default: "pointer", ":disabled": "default" },
290
290
  },
291
291
  // With an addon the frame has the input look and lays out the addon, the line and the trigger;
292
- // fe-app's SelectAssociatedTag: 5px before the tag, the line 6px after it and 1px before the
292
+ // The tag's inset: 5px before the tag, the line 6px after it and 1px before the
293
293
  // select (no spacing step is 5px, 6px or 1px).
294
294
  frame: {
295
295
  display: "flex",