@lotics/ui 47.3.0 → 47.4.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
@@ -17,7 +17,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
17
17
  | [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` + `loticsResolve()` — the pre-bundle list and the whole `resolve` block a custom-code app's `vite.config.ts` imports rather than hand-carries, dev-link included). Read before building any screen; reuse first. |
18
18
  | [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, choosing a CHOICE control by option count, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (`InlineFiles` for a record ROW, the `FilesEditor` COMPOUND for a whole section, plus the three-way file INTAKE), stage gates, and the commit-on-blur vs action-press ordering law. |
19
19
  | [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values; findings, provenance, confidence; **after the run** — a stored record that fills up from several writers; the whole run in a dialog, **stopping**; **review surfaces compose from atoms** — `DiffValue`, `DiffMark`, `useChangeSet`. |
20
- | [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS before reaching for parts), canvas + content column, heading altitude, register vs inset rows, the button ladder, master-detail `Drawer`, the register's rhythm, craft and TRIAGE bands, where the accent goes, theming as an app-level ESCAPE HATCH, color discipline, typography, **one fact one surface** (a chooser over the thing it chooses is a SELECTOR, not a second card grid; a control that only swaps one number for another is replaced by showing both), **a width floor answers can this be READ and never can this be operated** (the operating question passes a row whose every string is already cut), **a status column earns its slot by carrying a REASON rather than a state** (the floor asks for the fact to be visible, not for a badge), and where a commit sits. |
20
+ | [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS before reaching for parts), canvas + content column, heading altitude, register vs inset rows, the button ladder, master-detail `Drawer`, the register's rhythm, craft and TRIAGE bands, where the accent goes, theming as an app-level ESCAPE HATCH, color discipline, typography, **one fact one surface** (a chooser over the thing it chooses is a SELECTOR, not a second card grid; a control that only swaps one number for another is replaced by showing both), **a width floor answers can this be READ and never can this be operated** (the operating question passes a row whose every string is already cut), **a status column earns its slot by carrying a REASON rather than a state** (the floor asks for the fact to be visible, not for a badge), **a row subject's supporting line takes a REFERENCE and never a sentence** (an explanation truncates to the clause the reader already knew), and where a commit sits. |
21
21
  | [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built — and the answer when one "looks bad"** — the other docs say what good looks like; this one says how to find out whether you achieved it. Run it BEFORE the deploy: a typecheck, a lint, a test and `app check` are all blind to how a screen reads. GATES that outrank every treatment question, then the probes, each naming what to COLLECT and the SIGNATURE in the numbers — including the truncation sweep (a row whose halves both measure fine and whose text is cut) the summary-vs-facet check (a strip that keeps describing the unfiltered set) and the derivable-column test (a status badge its neighbour already answers). Plus the extraction snippets, what measurement cannot see, and how to act on a finding. |
22
22
  | [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
23
23
  | [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
package/docs/catalog.md CHANGED
@@ -1125,8 +1125,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1125
1125
  choice. → `data_entry.md` §"Choosing a CHOICE control" — and check the states really ARE
1126
1126
  exclusive: independent capabilities that happen to be exclusive today bake today's
1127
1127
  combinations into the type.
1128
- - **`chip_group`** — `ChipGroup`: pill single-select over a small visible set. `data_entry.md`
1129
- §"Choosing a CHOICE control" for which control the count and the job call for.
1128
+ - **`chip_group`** — `ChipGroup`: pill single-select over a small visible set. The row wraps by
1129
+ default; `overflow="scroll"` keeps it to ONE row for a register FILTER, where six options
1130
+ carrying counts wrap to three rows — 136px on a phone, more than the search band and the view
1131
+ switcher combined. Deliberately NOT a screen-width branch: width is not the question, the job
1132
+ is, and both jobs occur at every width. Never `scroll` on a form field or a view control —
1133
+ chips are worth more than a `Picker` there only because every option is visible, and a swipe
1134
+ is exactly what takes that away. → `data_entry.md` §"Choosing a CHOICE control" for which
1135
+ control the count and the job call for.
1130
1136
  - **`checkbox`** — `Checkbox`: the bare square check control. Bare means UNLABELLED — reach for
1131
1137
  `CheckboxInput` unless something else already names it (a table's select-all, a row's
1132
1138
  leading slot). A square means CHOSEN; a ring means DONE, and spending the wrong one teaches a
@@ -1015,7 +1015,9 @@ by what KIND of thing they are.
1015
1015
  stays one consistent band. Never a bare `TextInputField` + search icon.
1016
1016
  - **`ChipGroup`** is THE one-of-N lens (≤ ~10 options the user flips between; include "All";
1017
1017
  counts in the label). Filters ONE list to a SUBSET — including by process stage / lifecycle
1018
- state.
1018
+ state. As a FILTER above a register it takes `overflow="scroll"` and costs one row on a phone
1019
+ instead of three; a VIEW CONTROL ("how are they arranged") and a form field keep the default
1020
+ wrap. The line is the toolbar's own — which rows vs how they are arranged — not a width.
1019
1021
  - **`FilterChip`** is THE secondary-dimension filter — a compact chip that opens a composed
1020
1022
  popover editor (`OptionList` multi / `Slider range` / `Counter` / date range). Single-select
1021
1023
  closes on pick via the `{({ close }) => …}` render prop.
@@ -1621,6 +1623,15 @@ Deciding from the CONTENT instead — prose gets `sm`, reference data gets `xs`
1621
1623
  that holds both. An account name beside a contract number is one line, and it takes ONE rung: the
1622
1624
  pair's, not each half's.
1623
1625
 
1626
+ **That line takes a REFERENCE, never a sentence.** It is what tells two rows with the same subject
1627
+ apart — a code, a counterparty, a date — so it is short by nature and survives every width. A
1628
+ sentence put there truncates to its opening clause in any column narrow enough to be a column, and
1629
+ the opening clause of an explanation is the half the reader already knew: *"Reordered after the
1630
+ downtown branch's…"*. It measures as an ordinary truncation and reads as a row that says nothing.
1631
+ The sentence is not homeless — a reason, a note, a hand-typed instruction belongs in what the row
1632
+ OPENS, beside the values it explains, where a full line is available. Where a row has no detail to
1633
+ open, the sentence is telling you the register wanted one.
1634
+
1624
1635
  **A STACKED PAIR takes `leading="tight"` — it is not prose.** The ramp's body rungs carry prose
1625
1636
  leading (14/24, ratio 1.71), which is right for a paragraph and wrong for two lines that are one
1626
1637
  object: it leaves ~10px of empty box between them, so the pair reads as two separate lines. Beside
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "47.3.0",
3
+ "version": "47.4.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./accordion": "./src/accordion.tsx",
@@ -1,4 +1,4 @@
1
- import { StyleSheet, View, type ViewStyle } from "react-native";
1
+ import { ScrollView, StyleSheet, View, type ViewStyle } from "react-native";
2
2
  import { colors } from "./colors";
3
3
  import { Icon, type IconName } from "./icon";
4
4
  import { solid, type ColorName } from "./colors";
@@ -6,8 +6,9 @@ import { Text } from "./text";
6
6
  import { PressableHighlight } from "./pressable_highlight";
7
7
  import { chipSurfaceStyle } from "./control_surface";
8
8
 
9
- // One-of-N chips: every option visible, one tap to switch, the row wraps on
10
- // narrow widths. Quiet zinc styling bordered white at rest, and the active one
9
+ // One-of-N chips: every option visible, one tap to switch. The row WRAPS by
10
+ // default; a hot filter with many options passes `overflow="scroll"` to keep
11
+ // itself to one row. Quiet zinc styling — bordered white at rest, and the active one
11
12
  // keeps that same ground and takes a doubled zinc-900 EDGE instead, so a
12
13
  // selection never spends the brand and never speaks the row's language (the
13
14
  // reasoning is on `chipSurfaceStyle`). Colour stays reserved for status +
@@ -76,12 +77,34 @@ export interface ChipGroupProps<T extends string = string> {
76
77
  * yet) is valid. */
77
78
  value: T;
78
79
  onValueChange: (value: T) => void;
80
+ /**
81
+ * What the row does when it runs out of width.
82
+ *
83
+ * `"wrap"` (the default) keeps EVERY option on screen, which is the whole
84
+ * reason a form field or a composer reaches for chips instead of a `Picker` —
85
+ * a required single-select that hides half its values behind a swipe is a
86
+ * worse `Picker`, not a better one. Those sets are 2-3 short options and fit
87
+ * a phone anyway.
88
+ *
89
+ * `"scroll"` is for the other job: a HOT FILTER above a register, where the
90
+ * set is long enough and the labels carry counts. Six status options wrap to
91
+ * THREE ROWS at 390px — 136px, more than the search band and the view
92
+ * switcher combined, and the largest single block above the first record.
93
+ * One row costs 40px; the leading options stay a tap away and the tail is a
94
+ * swipe.
95
+ *
96
+ * Not derived from screen width, because width is not the question — the JOB
97
+ * is, and both jobs occur at every width. A `ScrollView` whose content fits
98
+ * does not scroll, so this is a no-op on a desktop either way.
99
+ */
100
+ overflow?: "wrap" | "scroll";
79
101
  }
80
102
 
81
103
  export function ChipGroup<T extends string = string>(props: ChipGroupProps<T>) {
82
- const { accessibilityLabel, options, value, onValueChange } = props;
83
- return (
84
- <View style={{ flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 8 }}>
104
+ const { accessibilityLabel, options, value, onValueChange, overflow = "wrap" } = props;
105
+
106
+ const inner = (
107
+ <>
85
108
  {options.map((option) => {
86
109
  const active = option.value === value;
87
110
  return (
@@ -140,11 +163,22 @@ export function ChipGroup<T extends string = string>(props: ChipGroupProps<T>) {
140
163
  </PressableHighlight>
141
164
  );
142
165
  })}
143
- </View>
166
+ </>
167
+ );
168
+
169
+ return overflow === "scroll" ? (
170
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollRow}>
171
+ {inner}
172
+ </ScrollView>
173
+ ) : (
174
+ <View style={styles.wrapRow}>{inner}</View>
144
175
  );
145
176
  }
146
177
 
147
178
  const styles = StyleSheet.create({
179
+ wrapRow: { flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 8 },
180
+ // paddingRight so the last chip does not sit flush against the edge mid-scroll
181
+ scrollRow: { flexDirection: "row", alignItems: "center", gap: 8, paddingRight: 8 },
148
182
  dot: { width: 6, height: 6, borderRadius: 3 },
149
183
  chip: {
150
184
  flexDirection: "row",