@lotics/cli 0.197.0 → 0.204.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/README.md CHANGED
@@ -92,6 +92,12 @@ It creates no apps of its own: build one in the workspace afterwards
92
92
  copied in — bound onto the tables the model just made — as part of the same run.
93
93
  `setup` refuses a table name your workspace already has; `lotics scaffold apply model.json` is the
94
94
  additive verb — it adopts that table, adds what the model declares beyond it, and deletes nothing.
95
+ Because it never renames, the file and the workspace drift: `lotics scaffold diff model.json`
96
+ prints exactly where and exits non-zero on any difference, and
97
+ `lotics field rename <table> <field> "<new label>"` moves a label on the platform, in the file
98
+ (`--model`) and in every app bound to it (`--apps`) at once. `lotics scaffold apply model.json
99
+ --documents` writes only the file cells of the model's rows, onto the records the first run made —
100
+ rows land only into empty tables, so it is the one way back for attachments that were missed.
95
101
 
96
102
  The other direction, once a workspace works: `lotics scaffold export > model.json` prints its
97
103
  tables (or only `--tables tbl_a,tbl_b`) as that same file, findings on stderr. It is a starting
@@ -163,7 +169,7 @@ lotics org use acme # switch active org by name (or org id)
163
169
  lotics org use "Acme Corp" # names are case-insensitive
164
170
  ```
165
171
 
166
- The org **id** is the identity; the name is a label cached when the key was saved. Rename an org in the app and the cached label goes stale — the old name still works, the new one matches nothing — until `lotics auth whoami` refreshes it. A name matching two saved orgs is refused rather than guessed; use the id.
172
+ The org **id** is the identity; the name is the label the key was saved under, and it is yours — renaming the org in the app never moves it, so the scripts scoping by it keep running. `lotics auth whoami` learns the new name and records it beside the label; both resolve, and `lotics org` prints `label (server: name)` while they differ. A name matching two saved orgs is refused rather than guessed; use the id.
167
173
 
168
174
  ### Working in parallel (worktrees)
169
175
 
@@ -177,7 +183,7 @@ lotics workspace select wks_... # records the workspace in the local pin
177
183
 
178
184
  Each worktree resolves independently; switching the global default in another shell leaves pinned worktrees untouched. `.lotics/` should be gitignored.
179
185
 
180
- Every command that resolves a workspace names its target before it acts — `lotics → <org> / <workspace>` on **stderr**, so stdout stays clean for piping. When nothing in the directory or environment chose the org and it came from the machine-wide default, the line says so and prints the `--local` command to pin — that default is the one another shell can move between two of your commands. `lotics auth whoami` reports the same resolution on demand, including which source won.
186
+ Every command that resolves a workspace names its target before it acts — `lotics → Starters / Equipment & Projects` on **stderr**, so stdout stays clean for piping. Both halves are NAMES, since the line exists to be read before a destructive write; the workspace's name is cached beside its id the first time any command lists it, and until then the line prints the id and says the name is not known yet. When nothing in the directory or environment chose the org and it came from the machine-wide default, the line says so and prints the `--local` command to pin — that default is the one another shell can move between two of your commands. `lotics auth whoami` reports the same resolution on demand, including which source won.
181
187
 
182
188
  ### Resolution precedence
183
189
 
@@ -189,6 +195,8 @@ Every command that resolves a workspace names its target before it acts — `lot
189
195
 
190
196
  `LOTICS_WORKSPACE` (or `--workspace <id>` / `-w`) overrides the workspace at any level. For ephemeral or CI use, set `LOTICS_API_KEY` instead of saving anything.
191
197
 
198
+ **`LOTICS_ORG` is resolved once, before any command runs.** A value matching no saved credential refuses every verb with one sentence — a read, a write, and a check that needs no credential alike — and the refusal lists the orgs this machine does hold, so it is answerable without another command. It refuses even when a key arrives another way: a variable that scopes the command must not go unread while `LOTICS_API_KEY` sends the write somewhere else. When the variable resolves AND a key is supplied, the key decides the org and the command says so. An org keeps the name it was saved under: a server-side rename never moves it, the new name resolves as well, and `lotics org` prints both when they differ.
199
+
192
200
  ### Diagnostics
193
201
 
194
202
  Every request identifies the CLI (`user-agent: lotics-cli/<version> node/<v> <platform>`) and names the command that made it (`x-lotics-cli-command: app.workflow.set`), so a failure in the server's logs can be traced to the verb and version that produced it. Neither header carries arguments: the command chain stops before any id, path, `@file`, or JSON payload.
@@ -353,6 +361,13 @@ lotics app codegen # import { F, OPT } from "../.lotics/
353
361
  # the code calls that nothing bound, undeclared capabilities, query drift. Exits 1 on what a deploy refuses, so CI can gate on it.
354
362
  lotics app check
355
363
 
364
+ # --screens adds the rendered surface: each screen the app's navigation reaches,
365
+ # and the first record each one opens onto a PAGE, headless in Chrome at 1280
366
+ # and 375, measured against @lotics/ui docs/reviewing.md. --screen and --width
367
+ # narrow it while you iterate on one screen.
368
+ lotics app check --screens
369
+ lotics app check --screens --screen "Đơn hàng" --width 375
370
+
356
371
  # Running an app's alias is a TOOL call, like every other tool. Exits non-zero
357
372
  # when the RUN failed, not only when the call did.
358
373
  lotics run run_app_workflow '{"app_id":"app_...","alias":"issueInvoice","inputs":{"record_id":"rec_..."}}'
@@ -365,7 +380,8 @@ lotics run run_app_workflow '{...}' --cleanup # also deletes created records (
365
380
  # Edit workflow bodies as files. `app pull` writes src/workflows/<alias>.ts (the
366
381
  # faithful server source, wrapped + referencing its .lotics/workflows/<alias>.globals.d.ts);
367
382
  # edit the body, then push it back through set_app_workflow — the server verifies it
368
- # (deploy still never authors workflows). `app workflow check` runs the server's own
383
+ # (a deploy calls the same verb for every alias the project holds ahead of the app, so
384
+ # this is the one-alias spelling of it). `app workflow check` runs the server's own
369
385
  # JS-subset parser + type-check locally, so a body that passes is one `set` will accept
370
386
  # (`set` still adds name resolution + lint + structural checks, which need the workspace):
371
387
  lotics app workflow pull # rewrite src/workflows/*.ts + globals from the server
@@ -394,7 +410,7 @@ LOTICS_UI_SRC=/abs/monorepo/packages/ui/src lotics app deploy -m "..." # warns
394
410
  lotics app dev # unset ⇒ @lotics/ui resolves from node_modules again
395
411
  ```
396
412
 
397
- `app codegen` reads `package.json#lotics.queries` to decide which tables to put in `app_fields.ts`; widen the set with `package.json#lotics.codegen.tables` (an array of `tbl_…` ids) for tables the app only writes via workflows.
413
+ `app codegen` puts a table in `app_fields.ts` when a declared query reads it or a bound workflow writes it — the queries come from `package.json#lotics.queries`, the written tables from each binding's own `table_ids`, so a table only a workflow body touches is addressable by alias with nothing to declare.
398
414
 
399
415
  ## SDK
400
416
 
@@ -21,9 +21,12 @@ var __loticsProbe = (() => {
21
21
  // src/probe_page.ts
22
22
  var probe_page_exports = {};
23
23
  __export(probe_page_exports, {
24
- pressTab: () => pressTab,
24
+ PROBES: () => PROBES,
25
+ openRecord: () => openRecord,
26
+ openScreen: () => openScreen,
27
+ recordRows: () => recordRows,
25
28
  run: () => run,
26
- tabs: () => tabs,
29
+ screens: () => screens,
27
30
  textLeafCount: () => textLeafCount
28
31
  });
29
32
 
@@ -95,38 +98,107 @@ var __loticsProbe = (() => {
95
98
  }
96
99
  function reportedValues() {
97
100
  const numeric = /^[-+−]?\d[\d.,]*\s?(?:%|[A-Za-z₫đ$€]{1,4})?$/;
101
+ const counted = /(?:%|[A-Za-z₫đ$€]{1,4})$/;
102
+ const delta = /^[↑↓→▲▼]\s*[-+−]?\d[\d.,]*\s?%$/;
98
103
  const dateLike = /\d[/:-]\d/;
99
104
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
100
105
  const notAValue = 'a, button, [role="tab"], [role="button"], input, th, [role="columnheader"], nav';
101
106
  const pictureMin = 32;
102
- const samples = [];
103
- let bare = 0;
107
+ const nameReach = 360;
108
+ const values = [];
109
+ const labels = [];
110
+ let deltas = 0;
104
111
  const walk = (node) => {
105
112
  if (node.nodeType === Node.TEXT_NODE) {
106
113
  const text = (node.textContent || "").trim();
107
114
  const parent = node.parentElement;
108
115
  if (!text || !parent || skipTag.test(parent.tagName)) return;
109
116
  if (typeof parent.checkVisibility === "function" && !parent.checkVisibility()) return;
110
- if (!numeric.test(text) || dateLike.test(text)) return;
117
+ const rect = parent.getBoundingClientRect();
118
+ if (rect.width === 0 && rect.height === 0) return;
119
+ if (delta.test(text)) {
120
+ deltas += 1;
121
+ return;
122
+ }
123
+ const leaf = { text, left: Math.round(rect.left), right: Math.round(rect.right), top: Math.round(rect.top) };
124
+ if (!numeric.test(text) || dateLike.test(text)) {
125
+ labels.push(leaf);
126
+ return;
127
+ }
111
128
  if (parent.closest(notAValue)) return;
112
- bare += 1;
113
- if (samples.length < 6) samples.push(text);
129
+ values.push(leaf);
114
130
  return;
115
131
  }
116
132
  node.childNodes.forEach(walk);
117
133
  };
118
134
  walk(document.body);
135
+ const clusters = [];
136
+ for (const leaf of [...values].sort((a, b) => a.left - b.left || a.top - b.top)) {
137
+ const cluster = clusters.find((c) => leaf.left < c.right && leaf.right > c.left);
138
+ if (cluster !== void 0) {
139
+ cluster.items.push(leaf);
140
+ cluster.left = Math.min(cluster.left, leaf.left);
141
+ cluster.right = Math.max(cluster.right, leaf.right);
142
+ } else {
143
+ clusters.push({ left: leaf.left, right: leaf.right, items: [leaf] });
144
+ }
145
+ }
146
+ const columns = [];
147
+ let bare = 0;
148
+ let quantities = 0;
149
+ for (const cluster of clusters) {
150
+ const items = [...cluster.items].sort((a, b) => a.top - b.top);
151
+ const ordinal = items.length >= 3 && items.every((item, index) => item.text === String(index + 1));
152
+ if (ordinal) continue;
153
+ const top = items[0];
154
+ const above = labels.filter((label) => label.top < top.top && top.top - label.top <= nameReach && label.left < cluster.right && label.right > cluster.left).sort((a, b) => b.top - a.top)[0];
155
+ const beside = labels.filter((label) => Math.abs(label.top - top.top) <= 8 && label.right <= top.left).sort((a, b) => b.right - a.right)[0];
156
+ const scaled = items.filter((item) => counted.test(item.text)).length;
157
+ bare += items.length;
158
+ quantities += scaled;
159
+ columns.push({
160
+ slot: (above ?? beside)?.text.slice(0, 40) ?? "",
161
+ size: items.length,
162
+ quantities: scaled,
163
+ samples: items.slice(0, 4).map((item) => item.text)
164
+ });
165
+ }
166
+ columns.sort((a, b) => b.size - a.size);
119
167
  const drawn = Array.from(document.querySelectorAll('[role="meter"], [role="progressbar"], canvas')).filter((el) => {
120
168
  const r = el.getBoundingClientRect();
121
169
  return r.width > 0 && r.height > 0;
122
170
  });
171
+ const reads = (el) => {
172
+ const role = el.getAttribute("role");
173
+ if (role !== "meter" && role !== "progressbar") return true;
174
+ const stated = (name) => {
175
+ const raw = el.getAttribute(name);
176
+ return raw === null || raw.trim() === "" ? Number.NaN : Number(raw);
177
+ };
178
+ const bound = (name, fallback) => {
179
+ const value = stated(name);
180
+ return Number.isFinite(value) ? value : fallback;
181
+ };
182
+ const now = stated("aria-valuenow");
183
+ if (!Number.isFinite(now)) return false;
184
+ const floor = bound("aria-valuemin", 0);
185
+ const top = bound("aria-valuemax", 100);
186
+ return top > floor && now < top;
187
+ };
123
188
  const pictures = Array.from(document.querySelectorAll('[role="img"], svg, img')).filter((el) => {
124
189
  if (el.getAttribute("aria-hidden") === "true") return false;
125
190
  const r = el.getBoundingClientRect();
126
191
  return r.width >= pictureMin && r.height >= pictureMin;
127
192
  });
128
- const encoded = drawn.length + pictures.length;
129
- return { total: bare + encoded, bare, encoded, samples };
193
+ const encoded = drawn.filter(reads).length + pictures.length + deltas;
194
+ return {
195
+ total: bare + encoded,
196
+ bare,
197
+ quantities,
198
+ encoded,
199
+ devices: drawn.length + pictures.length + deltas,
200
+ columns
201
+ };
130
202
  }
131
203
  function internalIds() {
132
204
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
@@ -149,14 +221,21 @@ var __loticsProbe = (() => {
149
221
  return { total, ids: [...ids] };
150
222
  }
151
223
  function tabStrips() {
152
- const strips = Array.from(document.querySelectorAll('[role="tablist"]')).filter((el) => {
224
+ const all = Array.from(document.querySelectorAll('[role="tablist"]')).filter((el) => {
153
225
  const r = el.getBoundingClientRect();
154
226
  return r.width > 0 && r.height > 0;
155
- }).map((el) => Array.from(el.querySelectorAll('[role="tab"]')).map((tab) => (tab.textContent || "").trim()));
156
- return { total: strips.length, strips };
227
+ });
228
+ const counted = all.filter((el) => el.getAttribute("data-order") !== "sequence");
229
+ return {
230
+ total: all.length,
231
+ counted: counted.length,
232
+ strips: counted.map((el) => Array.from(el.querySelectorAll('[role="tab"]')).map((tab) => (tab.textContent || "").trim()))
233
+ };
157
234
  }
158
235
  function truncatedLeaves() {
159
236
  const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
237
+ const clippedAway = /^inset\(\s*50%/;
238
+ const breakable = /[\t\n\v\f\r \u200b\u3000]/;
160
239
  const cut = [];
161
240
  let total = 0;
162
241
  for (const el of Array.from(document.querySelectorAll("*"))) {
@@ -164,8 +243,17 @@ var __loticsProbe = (() => {
164
243
  const text = (el.textContent || "").trim();
165
244
  if (!text) continue;
166
245
  if (typeof el.checkVisibility === "function" && !el.checkVisibility()) continue;
246
+ const style = getComputedStyle(el);
247
+ const box = el.getBoundingClientRect();
248
+ if (box.width <= 1 && box.height <= 1) continue;
249
+ if (clippedAway.test(style.getPropertyValue("clip-path"))) continue;
167
250
  total += 1;
168
- if (getComputedStyle(el).overflow === "visible") continue;
251
+ if (style.overflow === "visible") continue;
252
+ const states = (property) => {
253
+ const value = style.getPropertyValue(property);
254
+ return value !== "" && value !== "none";
255
+ };
256
+ if ((states("-webkit-line-clamp") || states("line-clamp")) && breakable.test(text)) continue;
169
257
  if (el.scrollWidth > el.clientWidth + 1) {
170
258
  cut.push({ text: text.slice(0, 40), axis: "width", has: el.clientWidth, needs: el.scrollWidth });
171
259
  } else if (el.scrollHeight > el.clientHeight + 1) {
@@ -224,6 +312,7 @@ var __loticsProbe = (() => {
224
312
  if (depth === 0 || el.children.length === 0) return null;
225
313
  return leafOf(el.children[0], depth - 1);
226
314
  };
315
+ const sameColumn = 8;
227
316
  let total = 0;
228
317
  const overDemoted = [];
229
318
  for (const el of Array.from(document.querySelectorAll("div"))) {
@@ -237,6 +326,7 @@ var __loticsProbe = (() => {
237
326
  const a = first.getBoundingClientRect();
238
327
  const b = second.getBoundingClientRect();
239
328
  if (b.top < a.bottom - 2) continue;
329
+ if (Math.abs(b.left - a.left) > sameColumn) continue;
240
330
  if (a.height === 0 || b.height === 0) continue;
241
331
  total += 1;
242
332
  if (px(supporting) < px(subject) && weight(subject) > weight(supporting)) {
@@ -301,9 +391,59 @@ var __loticsProbe = (() => {
301
391
  }
302
392
 
303
393
  // src/probe_page.ts
394
+ var PROBES = {
395
+ money_edges: {
396
+ section: "6",
397
+ fix: "One right edge per money column: give every figure in it the same alignment and the same unit."
398
+ },
399
+ reported_values: {
400
+ section: "9",
401
+ fix: "Encode the column the screen is ABOUT \u2014 a mark, a meter, a chart or a picture \u2014 and leave the rest as text; a meter whose maximum is the bound it alarms at reads the same on every row, so it encodes nothing."
402
+ },
403
+ internal_ids: {
404
+ section: "8",
405
+ fix: "A key is not a name: render the record's own label, and keep the id in the link."
406
+ },
407
+ tab_strips: {
408
+ section: "8a",
409
+ fix: "One navigation strip per screen. A second is a filter wearing navigation's clothes \u2014 draw it as one."
410
+ },
411
+ truncation: {
412
+ section: "8g",
413
+ fix: "Shed a trailing column, or widen this one until the value fits \u2014 a date, a code or a figure cut inside itself reads as a different one."
414
+ },
415
+ banned_glyph: {
416
+ section: "10",
417
+ fix: "No punctuation doing a word's job \u2014 separate the two facts with layout, or write the word."
418
+ },
419
+ compact_money_words: {
420
+ section: "10",
421
+ fix: "Abbreviate in the currency's own words: tr/t\u1EF7 beside \u20AB, K/M/B beside $ and \u20AC."
422
+ },
423
+ stacked_pairs: {
424
+ section: "1",
425
+ fix: "Demote once, not twice: either the size steps down or the subject gets the weight."
426
+ },
427
+ row_marks: {
428
+ section: "9",
429
+ fix: "One rung per row: a mark's size follows what the row is ABOUT, so the supporting mark takes the smaller one."
430
+ },
431
+ render: {
432
+ section: "8",
433
+ fix: "Open the screen and read it: nothing below this was measured, so a clean run over the rest means nothing."
434
+ },
435
+ settle: {
436
+ section: "9c",
437
+ fix: "A screen that never stops changing has a render that re-renders or a request that never lands \u2014 find which."
438
+ }
439
+ };
440
+ function signature(probe) {
441
+ return PROBES[probe];
442
+ }
304
443
  var MONEY_EDGES_ALLOWED = 2;
305
444
  var MONEY_COLUMN_MIN = 3;
306
445
  var BARE_VALUES_FLOOR = 8;
446
+ var QUANTITY_COLUMN_MIN = 2;
307
447
  function run(options) {
308
448
  const findings = [];
309
449
  const money = moneyColumns();
@@ -311,30 +451,37 @@ var __loticsProbe = (() => {
311
451
  if (ragged.length > 0) {
312
452
  findings.push({
313
453
  probe: "money_edges",
454
+ ...signature("money_edges"),
314
455
  total: money.total,
315
456
  count: ragged.length,
316
457
  detail: ragged.slice(0, 5).map((column) => `x=${column.x}: ${column.edges.length} right edges over ${column.size} figures \u2014 ${column.samples.join(" | ")}`)
317
458
  });
318
459
  }
319
460
  const values = reportedValues();
320
- if (values.bare >= BARE_VALUES_FLOOR && values.encoded === 0) {
461
+ const measurable = values.columns.some((column) => column.quantities >= QUANTITY_COLUMN_MIN) || values.devices > 0;
462
+ if (values.bare >= BARE_VALUES_FLOOR && values.encoded === 0 && measurable) {
321
463
  findings.push({
322
464
  probe: "reported_values",
465
+ ...signature("reported_values"),
323
466
  total: values.total,
324
467
  count: values.bare,
325
- detail: [`${values.bare} values as bare text, 0 as a mark, meter, chart or picture \u2014 e.g. ${values.samples.join(", ")}`]
468
+ detail: [
469
+ values.devices === 0 ? `${values.bare} values as bare text, nothing drawn as a mark, meter, chart or picture` : `${values.bare} values as bare text; ${values.devices} drawn, none of them reading a value`,
470
+ ...values.columns.slice(0, 5).map((column) => ` ${column.slot === "" ? "(unnamed)" : column.slot}: ${column.size} \u2014 ${column.samples.join(", ")}`)
471
+ ]
326
472
  });
327
473
  }
328
474
  const ids = internalIds();
329
475
  if (ids.ids.length > 0) {
330
- findings.push({ probe: "internal_ids", total: ids.total, count: ids.ids.length, detail: ids.ids.slice(0, 5) });
476
+ findings.push({ probe: "internal_ids", ...signature("internal_ids"), total: ids.total, count: ids.ids.length, detail: ids.ids.slice(0, 5) });
331
477
  }
332
478
  const strips = tabStrips();
333
- if (strips.total > 1) {
479
+ if (strips.counted > 1) {
334
480
  findings.push({
335
481
  probe: "tab_strips",
482
+ ...signature("tab_strips"),
336
483
  total: strips.total,
337
- count: strips.total,
484
+ count: strips.counted,
338
485
  detail: strips.strips.map((strip) => strip.join(" / "))
339
486
  });
340
487
  }
@@ -343,6 +490,7 @@ var __loticsProbe = (() => {
343
490
  if (cut.cut.length > 0) {
344
491
  findings.push({
345
492
  probe: "truncation",
493
+ ...signature("truncation"),
346
494
  total: cut.total,
347
495
  count: cut.cut.length,
348
496
  detail: cut.cut.slice(0, 5).map((leaf) => `"${leaf.text}" needs ${leaf.needs}px of ${leaf.axis} in ${leaf.has}px`)
@@ -351,12 +499,13 @@ var __loticsProbe = (() => {
351
499
  }
352
500
  const glyphs = bannedGlyphs();
353
501
  if (glyphs.found.length > 0) {
354
- findings.push({ probe: "banned_glyph", total: glyphs.total, count: glyphs.found.length, detail: glyphs.found.slice(0, 5) });
502
+ findings.push({ probe: "banned_glyph", ...signature("banned_glyph"), total: glyphs.total, count: glyphs.found.length, detail: glyphs.found.slice(0, 5) });
355
503
  }
356
504
  const compact = compactMoneyWords();
357
505
  if (compact.foreign.length > 0) {
358
506
  findings.push({
359
507
  probe: "compact_money_words",
508
+ ...signature("compact_money_words"),
360
509
  total: compact.total,
361
510
  count: compact.foreign.length,
362
511
  detail: compact.foreign.slice(0, 5).map((text) => `"${text}" abbreviates in the other currency's words`)
@@ -366,6 +515,7 @@ var __loticsProbe = (() => {
366
515
  if (pairs.overDemoted.length > 0) {
367
516
  findings.push({
368
517
  probe: "stacked_pairs",
518
+ ...signature("stacked_pairs"),
369
519
  total: pairs.total,
370
520
  count: pairs.overDemoted.length,
371
521
  detail: pairs.overDemoted.slice(0, 5).map((pair) => `"${pair.subject}" ${pair.sizes[0]}px/${pair.weights[0]} over "${pair.supporting}" ${pair.sizes[1]}px/${pair.weights[1]}`)
@@ -375,6 +525,7 @@ var __loticsProbe = (() => {
375
525
  if (marks.mismatches.length > 0) {
376
526
  findings.push({
377
527
  probe: "row_marks",
528
+ ...signature("row_marks"),
378
529
  total: marks.total,
379
530
  count: marks.mismatches.length,
380
531
  detail: marks.mismatches.slice(0, 5).map((row) => `y=${row.y}: ${row.marks.join(", ")}`)
@@ -386,8 +537,10 @@ var __loticsProbe = (() => {
386
537
  leaves: textLeafCount(),
387
538
  money: money.total,
388
539
  bare: values.bare,
540
+ quantities: values.quantities,
389
541
  encoded: values.encoded,
390
- strips: strips.total
542
+ devices: values.devices,
543
+ strips: strips.counted
391
544
  }
392
545
  };
393
546
  }
@@ -405,16 +558,53 @@ var __loticsProbe = (() => {
405
558
  walk(document.body);
406
559
  return count;
407
560
  }
408
- function tabs() {
561
+ function navItems() {
562
+ const nav = document.querySelector("nav");
563
+ if (nav === null) return [];
564
+ return Array.from(nav.querySelectorAll('a[href], [role="link"]')).filter(
565
+ (item) => (item.textContent || "").trim() !== ""
566
+ );
567
+ }
568
+ function tabItems() {
409
569
  const strip = document.querySelector('[role="tablist"]');
410
570
  if (strip === null) return [];
411
- return Array.from(strip.querySelectorAll('[role="tab"]')).map((tab) => (tab.textContent || "").trim());
571
+ return Array.from(strip.querySelectorAll('[role="tab"]'));
412
572
  }
413
- function pressTab(index) {
414
- const strip = document.querySelector('[role="tablist"]');
415
- const tab = strip?.querySelectorAll('[role="tab"]')[index];
416
- if (!(tab instanceof HTMLElement)) return false;
417
- tab.click();
573
+ function screens() {
574
+ const nav = navItems();
575
+ if (nav.length > 0) return { source: "nav", labels: nav.map((item) => (item.textContent || "").trim()) };
576
+ const tabs = tabItems();
577
+ if (tabs.length > 0) return { source: "tabs", labels: tabs.map((item) => (item.textContent || "").trim()) };
578
+ return { source: "root", labels: [] };
579
+ }
580
+ function openScreen(source, index) {
581
+ const items = source === "nav" ? navItems() : source === "tabs" ? tabItems() : [];
582
+ const item = items[index];
583
+ if (item === void 0) return false;
584
+ item.click();
585
+ return true;
586
+ }
587
+ function recordLinks() {
588
+ const here = location.pathname + location.search;
589
+ const doors = Array.from(document.querySelectorAll('a[href], [data-opens="page"]'));
590
+ return doors.filter((door) => {
591
+ if (door.closest('nav, [role="banner"], [role="tablist"]') !== null) return false;
592
+ if (doors.some((other) => other !== door && other.contains(door))) return false;
593
+ const rect = door.getBoundingClientRect();
594
+ if (rect.width === 0 || rect.height === 0) return false;
595
+ const href = door.getAttribute("href");
596
+ if (href === null) return true;
597
+ if (href === "" || href.startsWith("http") || href.startsWith("#")) return false;
598
+ return href !== here;
599
+ });
600
+ }
601
+ function recordRows() {
602
+ return recordLinks().map((door) => (door.textContent || "").trim() || door.getAttribute("href") || "");
603
+ }
604
+ function openRecord(index) {
605
+ const door = recordLinks()[index];
606
+ if (door === void 0) return false;
607
+ door.click();
418
608
  return true;
419
609
  }
420
610
  return __toCommonJS(probe_page_exports);