@lotics/ui 6.2.0 → 6.4.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.
package/AGENTS.md CHANGED
@@ -205,6 +205,10 @@ builds the new record and attaches it; existing matches attach directly. The cre
205
205
  BELOW matches by default (the keyboard highlight lands on the first MATCH, so Enter on a partial
206
206
  picks it, never a duplicate); pass `customOptionPlacement="top"` to pin it above. Use
207
207
  `reflectSelection={false}` and render the attached record below as a card with a Change action.
208
+ For input-level status that must stay visible while results scroll (validity feedback on the typed
209
+ value, a result count, a secondary "create" affordance), pass `footer={({ close }) => …}` — a pinned
210
+ row below the listbox, OUTSIDE keyboard option-nav; call `close()` from a footer action that hands
211
+ off elsewhere (so the popover dismisses cleanly instead of floating over what you navigated to).
208
212
 
209
213
  ### Line items — create → preview → edit (a composition, not a primitive)
210
214
  For a list of records you build then revise (invoice rows, repair lines, config entries), each
@@ -757,7 +761,7 @@ list · list_item · menu_button · menu_list_item · detail_row · pressable_ro
757
761
  check_circle (CheckCircle — the completion ring: an empty ring that springs to a filled check when done, distinct from the square checkbox; the task/to-do/checklist toggle. Compose task rows directly, no Task component) · action_menu ·
758
762
  floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
759
763
  columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
760
- sort_header · table · data_grid (DataGrid — the inline-managed grouped table: a grouped, sortable grid of LIVE inline-editor cells (`columns[].cell` → ANY field) + optional per-row `leading` (a CheckCircle) + `renderGroupFooter` (per-group add, align with the exported `gridRowStyle`) + `labels` (localize the sort-header a11y via `SortHeaderLabels`). Owns header/sections/rows; consumer owns data + sort/group/filter/collapse state + toolbar. Renders ALL rows — MODERATE data; 10k+ → the paginated `Table` register. Examples: `tpl_task_board`, `tpl_pipeline`) · pagination · accordion · stepper (Stepper + Step — done/current/upcoming/warning/complete progress on a track (horizontal) or spine (vertical); compound `<Step status>children` OR data `steps[]`+`current`; the guided-run / agent-feed primitive — subsumes the old StepList) ·
764
+ sort_header · table · data_grid (DataGrid — the inline-managed grouped table: a grouped, sortable grid of LIVE inline-editor cells (`columns[].cell` → ANY field) + optional per-row `leading` (a CheckCircle) + `renderGroupFooter` (per-group add, align with the exported `gridRowStyle`) + `labels` (localize the sort-header a11y via `SortHeaderLabels`). Owns header/sections/rows; consumer owns data + sort/group/filter/collapse state + toolbar. Renders ALL rows — MODERATE data; 10k+ → the paginated `Table` register. Examples: `tpl_task_board`, `tpl_pipeline`) · pagination · accordion · stepper (Stepper + Step — done/current/upcoming/warning/complete progress on a track (horizontal) or spine (vertical); compound `<Step status>children` OR data `steps[]`+`current`; **navigable** via `Step.onPress` (both orientations — the whole step is the tap target) + `active` to wash the selected one, so it doubles as a section/phase switcher; the guided-run / agent-feed primitive — subsumes the old StepList) ·
761
765
  step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · popover · tooltip ·
762
766
  alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
763
767
  CalloutText · CalloutActions) · kpi_card · kpi_strip · metric · trend_chip · sparkline ·
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "6.2.0",
3
+ "version": "6.4.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
package/src/combobox.tsx CHANGED
@@ -67,6 +67,13 @@ export interface ComboboxProps<T extends string = string, D = unknown> {
67
67
  placeholder?: string;
68
68
  recentsLabel?: string;
69
69
  emptyText?: string;
70
+ /** A pinned row rendered at the bottom of the popover (below the results, outside
71
+ * the scroll area) whenever the popover is open. For input-level status that must
72
+ * stay visible while results scroll — validity feedback, a result count, a "create"
73
+ * affordance. It sits OUTSIDE the listbox, so its content is auxiliary (not an
74
+ * option) and never enters keyboard option-nav. Receives `close` to dismiss the
75
+ * popover — call it from an action that navigates elsewhere. */
76
+ footer?: (api: { close: () => void }) => ReactNode;
70
77
  accessibilityLabel?: string;
71
78
  disabled?: boolean;
72
79
  /** Show an in-input clear ✕ whenever the input has text (a reflected selection
@@ -111,6 +118,7 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
111
118
  searchDebounceMs = 200,
112
119
  icon,
113
120
  placeholder,
121
+ footer,
114
122
  accessibilityLabel = "Results",
115
123
  disabled = false,
116
124
  clearable = false,
@@ -169,6 +177,14 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
169
177
  [onValueChange, isServer, debouncedSearch, onSearchChange, reflectSelection],
170
178
  );
171
179
 
180
+ // Dismiss the popover from the footer — drop focus too, mirroring the popover's
181
+ // own self-close (so the input's keyboard focus ring doesn't linger after a
182
+ // footer action hands off elsewhere).
183
+ const close = useCallback(() => {
184
+ setOpen(false);
185
+ inputRef.current?.blur();
186
+ }, []);
187
+
172
188
  const list = useOptionList<T, false, D>({
173
189
  options,
174
190
  value: single?.value ?? null,
@@ -347,6 +363,7 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
347
363
  })
348
364
  )}
349
365
  </ScrollView>
366
+ {footer ? <View style={styles.footer}>{footer({ close })}</View> : null}
350
367
  </View>
351
368
  </PopoverContent>
352
369
  </Popover>
@@ -386,4 +403,10 @@ const styles = StyleSheet.create({
386
403
  justifyContent: "center",
387
404
  paddingVertical: 16,
388
405
  },
406
+ footer: {
407
+ borderTopWidth: 1,
408
+ borderTopColor: colors.border,
409
+ paddingHorizontal: 8,
410
+ paddingVertical: 6,
411
+ },
389
412
  });
package/src/stepper.tsx CHANGED
@@ -56,7 +56,8 @@ interface StepPositional {
56
56
  export interface StepProps extends StepPositional {
57
57
  status: StepStatus;
58
58
  children: ReactNode;
59
- /** Make the step navigable; the active one holds a wash so a panel can sit beside it (vertical). */
59
+ /** Make the step navigable (both orientations) the whole step is the tap target.
60
+ * `active` washes the selected one (a panel can also sit beside it in vertical). */
60
61
  onPress?: () => void;
61
62
  active?: boolean;
62
63
  accessibilityLabel?: string;
@@ -116,16 +117,34 @@ export function Step(props: StepProps) {
116
117
  const { orientation, color, live } = useContext(StepperContext);
117
118
 
118
119
  if (orientation === "horizontal") {
119
- return (
120
- <View style={styles.hStep}>
120
+ const inner = (
121
+ <>
121
122
  <View style={styles.hTrackRow}>
122
123
  <View style={[styles.hTrack, { backgroundColor: _leftFilled ? color : colors.zinc[200] }]} />
123
124
  <Marker status={status} color={color} live={live} />
124
125
  <View style={[styles.hTrack, { backgroundColor: _rightFilled ? color : colors.zinc[200] }]} />
125
126
  </View>
126
127
  <View style={styles.hLabel}>{children}</View>
127
- </View>
128
+ </>
128
129
  );
130
+ // Navigable: the whole step (marker + label + its track halves) is the tap
131
+ // target — generous, no dead zones; `active` washes the selected one.
132
+ if (onPress) {
133
+ return (
134
+ <View style={styles.hStepSlot}>
135
+ <PressableHighlight
136
+ focusRing
137
+ onPress={onPress}
138
+ accessibilityRole="button"
139
+ accessibilityLabel={accessibilityLabel}
140
+ style={[styles.hPress, active ? styles.hActive : null]}
141
+ >
142
+ {inner}
143
+ </PressableHighlight>
144
+ </View>
145
+ );
146
+ }
147
+ return <View style={styles.hStep}>{inner}</View>;
129
148
  }
130
149
 
131
150
  const body = <View style={[styles.vRowBox, active ? styles.vActive : null]}>{children}</View>;
@@ -210,6 +229,9 @@ const NODE = 18;
210
229
  const styles = StyleSheet.create({
211
230
  hRow: { flexDirection: "row" },
212
231
  hStep: { flex: 1, alignItems: "center", gap: 8 },
232
+ hStepSlot: { flex: 1 },
233
+ hPress: { width: "100%", alignItems: "center", gap: 8, borderRadius: 8, paddingVertical: 4 },
234
+ hActive: { backgroundColor: colors.zinc[100] },
213
235
  hTrackRow: { flexDirection: "row", alignItems: "center", width: "100%" },
214
236
  hTrack: { flex: 1, height: 2 },
215
237
  hLabel: { alignItems: "center" },