@lotics/ui 17.0.0 → 17.0.2

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.
@@ -275,10 +275,26 @@ aligns with nothing.
275
275
  date, a menu and children of its own. There is no separate subtask shape to outgrow, and no
276
276
  built-in chevron: collapsing is the app's call (hold a boolean, render the nested list or don't).
277
277
 
278
- **State flows from the LEAVES.** Where a parent's position is computable from its children, DERIVE
279
- it and render the parent's status read-only an editable copy lets a row disagree with its own
280
- children, and makes the operator enter one fact twice. Give the parent's ring the bulk gesture
281
- instead (tick it, every child ticks). A parent with nothing to derive from is set by hand.
278
+ **State flows from the LEAVES.** Where a row's position is computable from what it owns, DERIVE
279
+ it a stored status is a second copy of what the fields already say, and two copies drift. Give
280
+ the parent's ring the bulk gesture where its children ARE the fact (tick it, every child ticks);
281
+ where the ring answers a DIFFERENT field the children do not determine a date, say — coupling
282
+ them asserts something the operator never said.
283
+
284
+ **`TaskFields` holds CONTROLS.** Where exactly one field determines a row's status, that cell IS
285
+ the field's editor: a read-only copy above an editable control puts one value on the row twice,
286
+ under two labels. Everything else derived is prose in `TaskCaption`. A read-only pill parked in
287
+ the column sits `TASK_TEXT_INSET` left of its neighbours (a `variant="cell"` control insets its
288
+ text by that much; a bare `Badge` by nothing) and reads as pressable when it is not. A `Badge`
289
+ INSIDE a cell control is fine — it inherits that cell's inset, and the 12px its label sits in by
290
+ is the dot and its gap, not padding.
291
+
292
+ **A status reports what is OUTSTANDING, never what is settled.** A finished row already says so
293
+ twice — a full ring and its date — so naming the settled condition is a third copy of one fact.
294
+ The empty end is worse than redundant: "every document is in" and "nobody has looked yet" are the
295
+ same empty list, so a badge asserting the first states something no one established. Stay silent
296
+ at both ends, and separate "is it under way" (which drives the ring) from "has it anything to
297
+ say" (which drives the caption) — fusing them forces every pending state to invent a label.
282
298
 
283
299
  `CheckCircle` has THREE positions (`state="none" | "partial" | "done"`), and `partial` is not
284
300
  optional dressing: a step whose vocabulary reads "No portal account → Account created → Filed
package/docs/templates.md CHANGED
@@ -355,9 +355,11 @@ billing, and quick-capture templates. Top → bottom:
355
355
  HOME stays their DATA section (the colocation law) with descriptions naming their
356
356
  consumers ("printed on the delivery note"). The picker rows ride `TaskList`/`TaskItem`
357
357
  (`controlWidth={24}` for the `CheckboxInput` in `TaskStatus`) — the compound owns the row
358
- geometry and the indent; hand-rolling the anatomy is how alignment drifts. Warning and fix are CO-LOCATED on the row. The mark NAMES what's missing (the
359
- `TaskFields` line: 13px `circle-alert` + "Needs: …" the actual field labels — muted at rest so
360
- gaps show WITHOUT checking, warning once checked because it now blocks). The FIX sits
358
+ geometry and the indent; hand-rolling the anatomy is how alignment drifts. Warning and fix are CO-LOCATED on the row. The mark NAMES what's missing — a
359
+ **`TaskCaption`**, not a field cell: 13px `circle-alert` + "Needs: …" the actual field labels,
360
+ muted at rest so gaps show WITHOUT checking, warning once checked because it now blocks. It is
361
+ a SENTENCE about the row, and `TaskFields` is the column the reader edits and scans down, so
362
+ prose put there lines up with nothing and reads as a control it is not. The FIX sits
361
363
  right under it in the row's `TaskDetail` — an `Inset` (a FORM surface, NEVER a `Callout`:
362
364
  a warning Callout is an ARIA `alert`, wrong around a form) with a `FormTextInput` per gap
363
365
  (label + the consumer hint as `description`) and a "Save fields" (secondary — the section
@@ -32,7 +32,7 @@ import { CheckCircle } from "@lotics/ui/check_circle";
32
32
  import { ProgressBar } from "@lotics/ui/progress_bar";
33
33
  import { FilterChip } from "@lotics/ui/filter_chip";
34
34
  import { CaptureRow } from "@lotics/ui/capture_row";
35
- import { TaskActions, TaskDetail, TaskFields, TaskItem, TaskList, TaskStatus, TaskTitle } from "@lotics/ui/task";
35
+ import { TaskActions, TaskCaption, TaskDetail, TaskFields, TaskItem, TaskList, TaskStatus, TaskTitle, TASK_TEXT_INSET } from "@lotics/ui/task";
36
36
  import { ActionMenu, type ActionMenuItem } from "@lotics/ui/action_menu";
37
37
  import { SuggestionChip } from "@lotics/ui/suggestion_chip";
38
38
  import { OptionList } from "@lotics/ui/option_list";
@@ -1900,7 +1900,10 @@ export function TplRecord() {
1900
1900
  <CheckCircle state={sub.done ? "done" : "none"} onChange={(on) => toggleSubtask(t.id, sub.key, on)} accessibilityLabel={sub.label} />
1901
1901
  </TaskStatus>
1902
1902
  <TaskTitle>
1903
- <Text size="sm" color={sub.done ? "muted" : "default"} decoration={sub.done ? "lineThrough" : undefined}>{sub.label}</Text>
1903
+ {/* The parent's title is an inline editor, which insets its
1904
+ own text; a plain `Text` child must add the same or the
1905
+ two columns of titles do not line up. */}
1906
+ <Text size="sm" color={sub.done ? "muted" : "default"} decoration={sub.done ? "lineThrough" : undefined} style={{ paddingHorizontal: TASK_TEXT_INSET }}>{sub.label}</Text>
1904
1907
  </TaskTitle>
1905
1908
  </TaskItem>
1906
1909
  ))}
@@ -2261,15 +2264,23 @@ export function TplRecord() {
2261
2264
  <CheckboxInput accessibilityLabel={f.label} checked={checked} onChange={(on) => toggleForm(f.id, on)} />
2262
2265
  </TaskStatus>
2263
2266
  <TaskTitle>
2264
- <Text size="sm" style={{ flexShrink: 1 }}>{f.label}</Text>
2267
+ {/* A plain-`Text` title carries the cell inset itself, or it sits
2268
+ out of line with the editable titles elsewhere on the page and
2269
+ with its own caption below. */}
2270
+ <Text size="sm" style={{ flexShrink: 1, paddingHorizontal: TASK_TEXT_INSET }}>{f.label}</Text>
2265
2271
  </TaskTitle>
2272
+ {/* "Needs: Tax ID, Delivery address" is a SENTENCE about this row, so it
2273
+ is a `TaskCaption` — its own line under the title. It was a
2274
+ `TaskFields` cell, which is the column the reader EDITS and scans
2275
+ down: prose there lines up with nothing, squeezes the cells beside
2276
+ it, and reads as a control it is not. */}
2266
2277
  {missing.length > 0 ? (
2267
- <TaskFields>
2268
- <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 5 }}>
2269
- <Icon name="circle-alert" size={13} color={checked ? colors.amber[500] : colors.zinc[400]} />
2270
- <Text size="xs" color={checked ? "warning" : "muted"} style={{ flexShrink: 1 }}>{`Needs: ${missing.map((k) => NEEDS[k].label).join(", ")}`}</Text>
2271
- </View>
2272
- </TaskFields>
2278
+ <TaskCaption>
2279
+ <View style={{ flexDirection: "row", alignItems: "flex-start", gap: 5 }}>
2280
+ <Icon name="circle-alert" size={13} color={checked ? colors.amber[500] : colors.zinc[400]} />
2281
+ <Text size="xs" color={checked ? "warning" : "muted"} style={{ flexShrink: 1 }}>{`Needs: ${missing.map((k) => NEEDS[k].label).join(", ")}`}</Text>
2282
+ </View>
2283
+ </TaskCaption>
2273
2284
  ) : null}
2274
2285
  {checked && missing.length > 0 ? (
2275
2286
  <TaskDetail>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "17.0.0",
3
+ "version": "17.0.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
package/src/checkbox.tsx CHANGED
@@ -9,6 +9,12 @@ interface CheckboxProps {
9
9
 
10
10
  export function Checkbox(props: CheckboxProps) {
11
11
  const { checked, indeterminate } = props;
12
+ // `indeterminate` FILLS the box, exactly as `checked` does — the mixed state is a painted
13
+ // box with a dash on every platform that has one, and the dash is drawn in `white`. Keying
14
+ // the fill on `checked` alone meant a mixed-but-unchecked control drew a white dash on the
15
+ // white background: invisible. Every `selectAll` therefore reported "nothing selected" while
16
+ // rows were selected — under-reporting, which is the worse direction to be wrong in.
17
+ const filled = checked || indeterminate === true;
12
18
 
13
19
  return (
14
20
  <View
@@ -19,7 +25,7 @@ export function Checkbox(props: CheckboxProps) {
19
25
  alignItems: "center",
20
26
  borderWidth: 1,
21
27
  borderColor: colors.border,
22
- backgroundColor: checked ? colors.zinc["800"] : colors.background,
28
+ backgroundColor: filled ? colors.zinc["800"] : colors.background,
23
29
  borderRadius: 6,
24
30
  }}
25
31
  >
package/src/task.tsx CHANGED
@@ -71,13 +71,15 @@ const DENSE_ROW = 32;
71
71
  /** Horizontal rhythm between the row's slots. */
72
72
  const ROW_GAP = 12;
73
73
  /**
74
- * The inline controls rest inside 8px of horizontal padding, so a `variant="cell"` title's
75
- * TEXT starts 8px inside its box. Everything that hangs beneath a row — the caption, the
76
- * detail, a nested list — adds the same inset, so it lines up with the words rather than with
77
- * the editor's invisible box. A plain-`Text` title should carry it too (`TASK_TEXT_INSET`),
78
- * or its caption will sit 8px to its right.
74
+ * How far a `variant="cell"` control insets its own text: 1px of transparent border plus 8px
75
+ * of padding. Everything that hangs beneath a row — the caption, the detail, a nested list —
76
+ * adds the same, so it lines up with the WORDS rather than with the editor's invisible box. A
77
+ * plain-`Text` title carries it too, or it sits out of line with the editable titles around it.
78
+ *
79
+ * It was 8 — the padding alone, forgetting the border — which put every caption, detail block
80
+ * and nested list 1px left of the title it belonged to.
79
81
  */
80
- export const TASK_TEXT_INSET = 8;
82
+ export const TASK_TEXT_INSET = 9;
81
83
 
82
84
  export interface TaskListProps {
83
85
  children: ReactNode;