@lotics/ui 7.18.0 → 7.19.1

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
@@ -684,7 +684,8 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
684
684
  header; the rows below it Divider-separated; a sortable column shows its `SortHeader` sort glyph
685
685
  ALWAYS so it reads as sortable). A `TableRow` with no `onPress` renders a STATIC read-only row (no
686
686
  hover wash / pointer cursor) so `Table` also serves read-only tabular data (a fee breakdown, a spec
687
- sheet), not just interactive registers. Make it SELECTABLE with the `Table` `leading` gutter + `selectAll`
687
+ sheet), not just interactive registers. A pressable `TableRow` REQUIRES `accessibilityLabel`
688
+ ("Open …") — its keyboard door is an empty overlay with no content to derive a name from. Make it SELECTABLE with the `Table` `leading` gutter + `selectAll`
688
689
  slot — a `CheckboxInput` per `TableRow` (`leading`) + a select-all in the header band, the ticked
689
690
  rows `marked`, paired with a `FloatingActionBar`; the selection state is the **`useSelection()`**
690
691
  hook (`selected`/`count`/`toggle`/`setAll`/`allSelected`/`indeterminate`/`clear` — gating which
@@ -698,9 +699,13 @@ was only ever about the review surfaces' sparkles/severity glyphs, not functiona
698
699
  a fixed `width`, so amount/status columns don't jitter).
699
700
  - **Row actions are ALWAYS-VISIBLE siblings, never hover-revealed, never nested.** A pressable row =
700
701
  a role-less surface (the hover/press wash, via DOM `mouseenter` so it covers the whole row) + an
701
- accessible "Open …" door (the body, a button) + trailing actions (⋯, remove ✕, edit) as SIBLINGS of
702
+ accessible "Open …" door (a button) + trailing actions (⋯, remove ✕, edit) as SIBLINGS of
702
703
  the door — a button must not contain another button, so the action is independently pressable and
703
- the ✕ isn't swallowed by the row press (`FileRow` and `PressableRow` are the references). Actions
704
+ the ✕ isn't swallowed by the row press (`FileRow` and `PressableRow` are the references). The door
705
+ wraps the row body ONLY when that body is non-interactive by construction (`FileRow`'s name/meta);
706
+ where the body is arbitrary app content — `TableRow`'s cells, which legitimately carry a `Link` or
707
+ a popover trigger — the door is an EMPTY absolutely-positioned sibling under the cells (tab stop +
708
+ name + focus ring; mouse rides the surface), so a control in a cell never nests inside it. Actions
704
709
  stay **visible** — a hover-only action is invisible to keyboard and touch users. The ONLY exception
705
710
  is a DENSE tree/register where a persistent per-row action would clutter (e.g. the DB sidebar):
706
711
  reveal on hover **OR focus-within**, keeping the action in the DOM + tab order and gating only its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "7.18.0",
3
+ "version": "7.19.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
package/src/agent_run.tsx CHANGED
@@ -379,18 +379,20 @@ function ToolGroup(props: {
379
379
  }
380
380
 
381
381
  const styles = StyleSheet.create({
382
- narration: { paddingHorizontal: 8 },
382
+ // No horizontal self-inset: the run sits flush so the CONSUMER'S gutter governs
383
+ // alignment (an 8px inset made its text misalign with dialog titles / sibling
384
+ // lines in every padded container).
385
+ narration: {},
383
386
  group: { gap: 2 },
384
387
  row: {
385
388
  flexDirection: "row",
386
389
  alignItems: "center",
387
390
  gap: 10,
388
391
  minHeight: CONTROL_HEIGHT,
389
- paddingHorizontal: 8,
390
392
  borderRadius: CONTROL_RADIUS,
391
393
  },
392
394
  dotCol: { width: 18, alignItems: "center" },
393
395
  rowBody: { flex: 1 },
394
396
  // Reasoning body aligns under the "Thinking" label (past the dot column + gap).
395
- reasoning: { paddingLeft: 36, paddingRight: 8, paddingBottom: 4 },
397
+ reasoning: { paddingLeft: 28, paddingBottom: 4 },
396
398
  });
@@ -26,7 +26,10 @@ export interface PressableRowProps {
26
26
  variant?: "register" | "bleed" | "inset";
27
27
  /** Cells + trailing controls. Nested pressables (CTAs, checkboxes, ⋯)
28
28
  * claim their own presses; put the accessible door inside — a plain
29
- * `Pressable` with role="button" + "Open …" label around the row body. */
29
+ * `Pressable` with role="button" + "Open …" label. Wrap the row body with it
30
+ * only when the body is non-interactive by construction; a body that can
31
+ * carry its own controls gets an EMPTY absolutely-positioned door as their
32
+ * sibling instead (a button must not contain a button — see `TableRow`). */
30
33
  children: ReactNode;
31
34
  /** Layout only (gap, minHeight overrides). The surface owns its press
32
35
  * states — never pass backgroundColor for hover/selected. */
package/src/table.tsx CHANGED
@@ -114,14 +114,10 @@ export function Table(props: TableProps) {
114
114
  );
115
115
  }
116
116
 
117
- export interface TableRowProps {
118
- /** Opens the record (the drawer). The door + the full-row surface both call it. */
119
- onPress?: () => void;
117
+ export type TableRowProps = {
120
118
  selected?: boolean;
121
119
  /** Part of a multi-select set — a resting blue tint (the open/hover wash overrides it). */
122
120
  marked?: boolean;
123
- /** Accessible name for the door (the keyboard-focusable open target). */
124
- accessibilityLabel?: string;
125
121
  /** Outside the door, BEFORE the cells (a selection checkbox) — width = Table `leading`. */
126
122
  leading?: ReactNode;
127
123
  /** Outside the door, AFTER the cells (a ⋯ menu / a button) — width = Table `trailing`. */
@@ -130,14 +126,31 @@ export interface TableRowProps {
130
126
  minHeight?: number;
131
127
  /** The `TableCell`s, one per column, in column order. */
132
128
  children: ReactNode;
133
- }
129
+ } & (
130
+ | {
131
+ /** Opens the record (the drawer). The door + the full-row surface both call it. */
132
+ onPress: () => void;
133
+ /** Accessible name for the door (the keyboard-focusable open target). REQUIRED
134
+ * on a pressable row: the door is an empty overlay (cells are its siblings, so
135
+ * a cell control never nests in a button), which means it has no text content
136
+ * to derive a name from — without a label it's a nameless button. */
137
+ accessibilityLabel: string;
138
+ }
139
+ | { onPress?: undefined; accessibilityLabel?: undefined }
140
+ );
134
141
 
135
142
  /**
136
143
  * A register row: a full-bleed `PressableRow` (its hover wash spans the whole row,
137
- * incl. the leading/trailing controls) wrapping the door — an inner
138
- * `role="button"` `Pressable` (the keyboard-accessible open) around the cells —
139
- * with optional `leading`/`trailing` slots OUTSIDE the door so their own presses
140
- * win. Cells map to `Table`'s columns by position.
144
+ * incl. the leading/trailing controls) holding the cells, with the door — the
145
+ * keyboard-accessible `role="button"` open target as an EMPTY absolutely-
146
+ * positioned SIBLING beneath them, never their ancestor. A button must not
147
+ * contain interactive descendants, and cells legitimately carry their own
148
+ * controls (a popover trigger, a `Link`); wrapping the cells in the door nests
149
+ * `<button>` in `<button>` (invalid HTML — React hydration warning). The door
150
+ * carries the tab stop, the accessible name, and the focus ring (spanning the
151
+ * row, rounded to the register wash); mouse presses ride the `PressableRow`
152
+ * surface, and nested controls claim their own. `leading`/`trailing` slots stay
153
+ * independently pressable. Cells map to `Table`'s columns by position.
141
154
  */
142
155
  export function TableRow(props: TableRowProps) {
143
156
  const { onPress, selected, marked, accessibilityLabel, leading, trailing, minHeight = 52, children } = props;
@@ -157,7 +170,7 @@ export function TableRow(props: TableRowProps) {
157
170
  return (
158
171
  <View style={styles.staticRow}>
159
172
  {ctx.leading > 0 ? <View style={{ width: ctx.leading }}>{leading}</View> : null}
160
- <View style={[styles.door, { minHeight }]}>{cells}</View>
173
+ <View style={[styles.cells, { minHeight }]}>{cells}</View>
161
174
  {ctx.trailing > 0 ? <View style={{ width: ctx.trailing, alignItems: "flex-end" }}>{trailing}</View> : null}
162
175
  </View>
163
176
  );
@@ -165,17 +178,21 @@ export function TableRow(props: TableRowProps) {
165
178
 
166
179
  return (
167
180
  <PressableRow onPress={onPress} selected={selected} marked={marked} style={styles.row}>
168
- {ctx.leading > 0 ? <View style={{ width: ctx.leading }}>{leading}</View> : null}
181
+ {ctx.leading > 0 ? <View style={[styles.slot, { width: ctx.leading }]}>{leading}</View> : null}
182
+ {/* An absolutely-positioned sibling paints — and hit-tests — above in-flow
183
+ content, which would swallow every cell control's press; the slots lift
184
+ above it via zIndex 1 (z-index applies to flex items), so the door gets
185
+ only the keyboard. Negative z on the door instead would escape the row
186
+ (no stacking context) and bury the focus ring under the Card background. */}
169
187
  <Pressable
170
188
  accessibilityRole="button"
171
189
  accessibilityLabel={accessibilityLabel}
172
190
  onPress={onPress}
173
191
  {...focusProps}
174
- style={[styles.door, { minHeight }, focusVisible && { boxShadow: FOCUS_RING }]}
175
- >
176
- {cells}
177
- </Pressable>
178
- {ctx.trailing > 0 ? <View style={{ width: ctx.trailing, alignItems: "flex-end" }}>{trailing}</View> : null}
192
+ style={[styles.door, focusVisible && { boxShadow: FOCUS_RING }]}
193
+ />
194
+ <View style={[styles.cells, { minHeight }]}>{cells}</View>
195
+ {ctx.trailing > 0 ? <View style={[styles.slot, { width: ctx.trailing, alignItems: "flex-end" }]}>{trailing}</View> : null}
179
196
  </PressableRow>
180
197
  );
181
198
  }
@@ -219,10 +236,27 @@ const styles = StyleSheet.create({
219
236
  gap: 14,
220
237
  paddingHorizontal: 20,
221
238
  },
239
+ // The keyboard door: an empty overlay spanning the row — the tab stop + focus
240
+ // ring live here, the cells are its SIBLINGS (see TableRow's doc). Radius
241
+ // matches the register wash.
222
242
  door: {
243
+ position: "absolute",
244
+ top: 0,
245
+ right: 0,
246
+ bottom: 0,
247
+ left: 0,
248
+ borderRadius: 10,
249
+ },
250
+ cells: {
223
251
  flex: 1,
224
252
  flexDirection: "row",
225
253
  alignItems: "center",
226
254
  gap: 14,
255
+ zIndex: 1,
256
+ },
257
+ // Leading/trailing gutters carry their own controls — lifted above the door
258
+ // like the cells.
259
+ slot: {
260
+ zIndex: 1,
227
261
  },
228
262
  });