@lotics/ui 44.5.0 → 44.6.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/docs/reviewing.md CHANGED
@@ -236,6 +236,13 @@ A resting register is the cheapest thing to screenshot and the least likely to h
236
236
  (a feed, a thread) above a bounded one buries it, and the burial deepens with use.
237
237
  - **Press every control** and look at the state you land in. Commonest miss: a value rendering as
238
238
  a coloured chip in a register and as bare grey text in its own EDITOR.
239
+ - **TAB through it, then diff what a keyboard reaches against what LOOKS pressable.** A pointer
240
+ finds anything with an `onPress`; a keyboard finds only what carries a role and a tab stop, and
241
+ the two lists come apart silently — the screen looks identical either way, and the author
242
+ testing with a mouse never learns. The reliable producer is styled text: underline, a hover
243
+ wash, a "verb" colour applied to a plain container, which reads as a control to the eye while
244
+ announcing as prose. Anything the eye calls a control and the tab order omits is unreachable,
245
+ not merely awkward. Count both lists; a difference is the finding.
239
246
  - **Diff a promoted field against where it came from.** Promoting means MOVING; a copy left behind
240
247
  gives one field two editors.
241
248
  - **Read every string in its SETTLED state**, not the streaming one.
@@ -473,6 +480,36 @@ receives `mouseenter`, because the pointer is already stationary over it — so
473
480
  dies at the moment of the swap and reads as a flash. Keep one node across the swap rather than
474
481
  restoring the state by hand.
475
482
 
483
+ ### Reachability — what the eye calls a control against what the tab order holds
484
+
485
+ Two lists off one render. The first is what a keyboard can operate; the second is everything that
486
+ LOOKS operable — styled text is the producer, so underline is the cheapest thing to sweep for.
487
+
488
+ ```js
489
+ () => {
490
+ const root = document.querySelector('[role="dialog"]') ?? document.body;
491
+ const operable = [...root.querySelectorAll(
492
+ 'a[href], button, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"]), input, select, textarea',
493
+ )];
494
+ const looksPressable = [...root.querySelectorAll("*")].filter((e) => {
495
+ if (e.children.length || !(e.textContent || "").trim()) return false;
496
+ const cs = getComputedStyle(e);
497
+ return cs.textDecorationLine.includes("underline") || cs.cursor === "pointer";
498
+ });
499
+ const unreachable = looksPressable.filter((e) => !e.closest(
500
+ 'a[href], button, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])',
501
+ ));
502
+ return {
503
+ operable: operable.length,
504
+ unreachable: unreachable.map((e) => (e.textContent || "").trim().slice(0, 40)),
505
+ };
506
+ }
507
+ ```
508
+
509
+ `unreachable` must come back empty. A name in it is a control only a pointer can reach — and the
510
+ count beside it is the other half of the reading: a whole pane offering two or three tab stops is
511
+ usually not a minimal screen, it is a screen whose verbs are all prose.
512
+
476
513
  Driving the kit's own anatomies — a `PressDoor` row, a portalled overlay, a custom pointer drag —
477
514
  has three gotchas of its own: [testing.md](./testing.md).
478
515
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "44.5.0",
3
+ "version": "44.6.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
package/src/locale.tsx CHANGED
@@ -83,6 +83,12 @@ export interface LoticsLocale {
83
83
  * a phase's rows. The kit owns the wording so one app does not say "Show" while
84
84
  * the next says "Expand" for the same gesture. */
85
85
  checklist: { expand: string; collapse: string };
86
+ /** `TextDisclosure` supplies the VERB so one app cannot say "Show" where the
87
+ * next says "View" for the same gesture — the same reason `checklist` above
88
+ * owns its pair. It takes the revealed thing as a noun and builds the phrase,
89
+ * because word order is not shared: English puts the verb first, and a
90
+ * language that does not would otherwise be stuck with a prefix. */
91
+ textDisclosure: { show: (label: string) => string; hide: (label: string) => string };
86
92
  sectionHeading: { info: string };
87
93
  /** `ErrorState`'s retry button. The kit owns the wording so "try again" is
88
94
  * phrased identically everywhere instead of hand-written per app. */
@@ -288,6 +294,7 @@ export const en: LoticsLocale = {
288
294
  ledger: { rowDetails: (label) => `${label} details` },
289
295
  stepper: { complete: "Complete step", progress: "Progress" },
290
296
  checklist: { expand: "Show", collapse: "Hide" },
297
+ textDisclosure: { show: (label) => `Show ${label}`, hide: (label) => `Hide ${label}` },
291
298
  sectionHeading: { info: "About this data" },
292
299
  errorState: { retry: "Try again" },
293
300
  chip: { remove: "Remove" },
@@ -466,6 +473,7 @@ export const vi: LoticsLocale = {
466
473
  ledger: { rowDetails: (label) => `Chi tiết ${label}` },
467
474
  stepper: { complete: "Hoàn thành bước", progress: "Tiến trình" },
468
475
  checklist: { expand: "Mở", collapse: "Thu gọn" },
476
+ textDisclosure: { show: (label) => `Xem ${label}`, hide: (label) => `Ẩn ${label}` },
469
477
  sectionHeading: { info: "Giải thích dữ liệu" },
470
478
  errorState: { retry: "Thử lại" },
471
479
  chip: { remove: "Xóa" },
@@ -1,5 +1,6 @@
1
1
  import { Pressable } from "react-native";
2
2
  import { TextLink } from "./text_link";
3
+ import { useLoticsLocale } from "./locale";
3
4
  import type { TextSize } from "./text";
4
5
 
5
6
  export interface TextDisclosureProps {
@@ -52,6 +53,7 @@ export interface TextDisclosureProps {
52
53
  */
53
54
  export function TextDisclosure(props: TextDisclosureProps) {
54
55
  const { expanded, onToggle, label, size = "sm", accessibilityLabel, testID } = props;
56
+ const locale = useLoticsLocale();
55
57
  return (
56
58
  <Pressable
57
59
  onPress={() => onToggle(!expanded)}
@@ -63,7 +65,7 @@ export function TextDisclosure(props: TextDisclosureProps) {
63
65
  testID={testID}
64
66
  >
65
67
  <TextLink size={size} color="muted">
66
- {expanded ? `Hide ${label}` : `Show ${label}`}
68
+ {expanded ? locale.textDisclosure.hide(label) : locale.textDisclosure.show(label)}
67
69
  </TextLink>
68
70
  </Pressable>
69
71
  );