@lotics/cli 0.245.0 → 0.247.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/AGENTS.md +2 -3
- package/README.md +12 -13
- package/dist/probe_page.js +104 -219
- package/dist/src/cli.js +26776 -38022
- package/dist/src/client.d.ts +9 -63
- package/dist/src/client.js +2 -4
- package/docs/building_an_app.md +55 -57
- package/docs/cli_reference.md +13 -14
- package/docs/migration.md +49 -58
- package/package.json +1 -1
- package/docs/clauses.md +0 -135
package/AGENTS.md
CHANGED
|
@@ -8,14 +8,13 @@ conventions are, and where the traps are.
|
|
|
8
8
|
| `lotics --help` | The verb inventory (§ COMMANDS) and global flags. The verb LIST is generated and never stale; the prose beside each verb is hand-written, so where it disagrees with `docs/cli_reference.md`, the reference wins. |
|
|
9
9
|
| `lotics tools` · `lotics tools <name>` | The agent tool registry and one tool's full JSON Schema. |
|
|
10
10
|
| `lotics docs` · `lotics docs <area>[/<section>]` | Every reference the packages installed beside the project actually ship — `@lotics/app-runtime`, `@lotics/ui` and the document engines each carry their own, and this index only covers THIS package. Discovered by looking, not by a list, so it reports the installed VERSION of each: a doc always describes the code that is really there. Capped at a page: a doc that does not fit hands back its opening and the addresses into it, so the next call is smaller than the last. |
|
|
11
|
-
| `lotics docs model` · `lotics docs model/<section>` | How to write a `model.json` — the file a workspace is built from, and the apps
|
|
11
|
+
| `lotics docs model` · `lotics docs model/<section>` | How to write a `model.json` — the file a workspace is built from: its tables, how a row of each is recognised (`records`), and the apps stated over them. Carried inside this CLI at its own version, since it describes this CLI's model checker, and paged like every doc: the first page is the working order and the section addresses. Two forms: `{"from": "<preset-slug>", "variants", "rename", "entities", "rows", "apply"}`, which names a preset by slug and carries only what this business differs by, and the full one, spelled out, for when no preset is the trade: every top-level key, every field type with the config it needs, the row format, the rules, and a worked example. Offline. It also covers the `apply` list — published packages copied in after the model's own tables, each with an optional `bind` onto them — and the `preset` block a published model carries. `lotics scaffold check` then proves the file — offline, except for the one read a `from` file's preset needs — including every branch of a preset merged onto its base; `lotics setup` applies it and refuses a table name the workspace already has, `lotics scaffold apply` adopts that table and adds what is missing, and the WORKSPACE remembers what each alias became, so every later run binds by id and a relabel on either side is a rename it reports rather than a second table it adds. `lotics scaffold export` goes the other way — a workspace printed as one of these files, to edit into another business's model; a starting point, never a source of truth. |
|
|
12
12
|
| [docs/building_an_app.md](./docs/building_an_app.md) | The SEQUENCE — scaffold, model, types, queries, workflows, screens, ship — and the deploy-free inner loop. The other references describe contracts; this one is the order they go in and why. Read it once before starting an app. |
|
|
13
|
-
| [docs/clauses.md](./docs/clauses.md) | Every clause a plan's app or screen can state, what it draws, and a plan fragment stating it — generated from the schema, so it lists no clause the schema lacks and misses none it has. |
|
|
14
13
|
| [docs/cli_reference.md](./docs/cli_reference.md) | Per-command contracts, flags, exit codes, and gotchas — the detail `--help` compresses. Read it before hand-building a `set_app_*` payload: several tools REPLACE rather than patch, and a CLI verb already owns the safe assembly. |
|
|
15
14
|
| [docs/data_model.md](./docs/data_model.md) | How tables RELATE — one entity per table and the NAME-OVERLAP probe that says when a split has broken, one vocabulary wherever values are copied between tables, a copy boundary that accounts for every source field, provenance as a link rather than a flag, a declared natural key so find-or-create never compares rendered text, and why derived DEPTH costs more than row count. Separate from building_an_app because every workspace starts with tables and many never get an app. The within-table half (one fact, one column) is stated at `create_table` / `update_table`, where you meet it while deciding. |
|
|
16
15
|
| [docs/document_templates.md](./docs/document_templates.md) | Generating PDF/Excel/Word/email from reusable templates. |
|
|
17
16
|
| [docs/knowledge_docs.md](./docs/knowledge_docs.md) | Authoring the workspace facts an agent can't guess; who can read a doc; catalog-then-stage retrieval. |
|
|
18
|
-
| [docs/migration.md](./docs/migration.md) | What to DO when a release changes the shape of a project this CLI owns. Read once, when something already on disk no longer matches what the CLI writes — for one, an app built from a
|
|
17
|
+
| [docs/migration.md](./docs/migration.md) | What to DO when a release changes the shape of a project this CLI owns. Read once, when something already on disk no longer matches what the CLI writes — for one, an app built from a model is `app.json` version 2, rendered by `@lotics/app-runtime`. |
|
|
19
18
|
| [README.md](./README.md) | Install, auth, and worked examples. |
|
|
20
19
|
|
|
21
20
|
## Two surfaces, and the trap between them
|
package/README.md
CHANGED
|
@@ -31,8 +31,6 @@ package (reachable at `node_modules/@lotics/cli/docs/*.md` once installed):
|
|
|
31
31
|
- [`docs/building_an_app.md`](docs/building_an_app.md) — building a custom-code app end to end:
|
|
32
32
|
the sequence the steps go in (clarify → model → types → queries → workflows → screens → ship)
|
|
33
33
|
and the deploy-free inner loop. Read it once before starting an app.
|
|
34
|
-
- [`docs/clauses.md`](docs/clauses.md) — every clause a plan's app or screen can state, what it
|
|
35
|
-
draws, and a plan fragment stating it, generated from the schema.
|
|
36
34
|
- [`docs/document_templates.md`](docs/document_templates.md) — generate finished documents
|
|
37
35
|
(PDF, Excel, Word, email) by filling reusable templates: the five template types, the
|
|
38
36
|
create → generate → chain lifecycle, and the marker capabilities.
|
|
@@ -70,8 +68,8 @@ selects each.
|
|
|
70
68
|
|
|
71
69
|
## Or describe your own workspace
|
|
72
70
|
|
|
73
|
-
A `model.json` names the tables, fields, options, links, views, roles and first rows,
|
|
74
|
-
`lotics setup` builds
|
|
71
|
+
A `model.json` names the tables, fields, options, links, views, roles and first rows, how a row
|
|
72
|
+
of each table is recognised, and the apps over them; `lotics setup` builds the tables. **Start from a preset when one is your trade** — name it instead of
|
|
75
73
|
restating it:
|
|
76
74
|
|
|
77
75
|
```json
|
|
@@ -347,20 +345,21 @@ lotics knowledge rm kdc_... # archive
|
|
|
347
345
|
## Custom-code apps
|
|
348
346
|
|
|
349
347
|
```bash
|
|
350
|
-
# SEE THE APP BEFORE IT EXISTS. The
|
|
351
|
-
# WOULD become and every read is answered from the model's own `rows
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
355
|
-
#
|
|
356
|
-
# of its dependency set (`~/.lotics/render/`, shared
|
|
357
|
-
# installed once from the registry — the kit and the
|
|
358
|
-
# — and rewritten from the
|
|
348
|
+
# SEE THE APP BEFORE IT EXISTS. The model's app is compiled against the workspace the
|
|
349
|
+
# model WOULD become and every read is answered from the model's own `rows` (a few
|
|
350
|
+
# synthesized for an entity that states none), so there is no app row, no table and
|
|
351
|
+
# no credential in the run — then the same headless walk `app check --screens` takes,
|
|
352
|
+
# at the same two widths, measured by the same probes: the register, the record in
|
|
353
|
+
# its door, the add. Exits 1 on any finding and prints what each phase cost. It
|
|
354
|
+
# renders in the cached project of its dependency set (`~/.lotics/render/`, shared
|
|
355
|
+
# with `app check --screens`), installed once from the registry — the kit and the
|
|
356
|
+
# runtime are published packages — and rewritten from the model on every run.
|
|
359
357
|
lotics app preview model.json#sales_desk
|
|
360
358
|
lotics app preview model.json#sales_desk --shots ./shots --width 375
|
|
361
359
|
|
|
362
360
|
# Scaffold / pull / deploy a Vite+React+TS app project
|
|
363
361
|
lotics app create "Sales Desk" # scaffold + deploy v1
|
|
362
|
+
lotics app create "Sales Desk" --from model.json#sales_desk # the model's app, as app.json
|
|
364
363
|
lotics app create "Orders API" --api # no screens: its declarations are the whole surface,
|
|
365
364
|
# so nothing is built and nothing is deployed
|
|
366
365
|
lotics app pull app_... # bootstrap an existing app locally (incl. .lotics/*)
|
package/dist/probe_page.js
CHANGED
|
@@ -421,6 +421,15 @@ var __loticsProbe = (() => {
|
|
|
421
421
|
};
|
|
422
422
|
}
|
|
423
423
|
var SINGLE_LINE_FIELDS = /* @__PURE__ */ new Set(["text", "search", "tel", "url", "email", "number"]);
|
|
424
|
+
function clippingAncestor(el, stop) {
|
|
425
|
+
for (let at = el.parentElement; at !== null; at = at.parentElement) {
|
|
426
|
+
const style = getComputedStyle(at);
|
|
427
|
+
const x = style.overflowX || style.overflow;
|
|
428
|
+
if (x === "hidden" || x === "clip") return at;
|
|
429
|
+
if (at === stop) return null;
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
424
433
|
function truncatedLeaves(root = document.body) {
|
|
425
434
|
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
426
435
|
const clippedAway2 = /^inset\(\s*50%/;
|
|
@@ -438,7 +447,16 @@ var __loticsProbe = (() => {
|
|
|
438
447
|
if (box.width <= 1 && box.height <= 1) continue;
|
|
439
448
|
if (clippedAway2.test(style.getPropertyValue("clip-path"))) continue;
|
|
440
449
|
total += 1;
|
|
441
|
-
if (style.overflow === "visible")
|
|
450
|
+
if (style.overflow === "visible") {
|
|
451
|
+
const within2 = el.closest('.lotics-fact-groups, [role="listbox"]');
|
|
452
|
+
const clip = within2 === null ? null : clippingAncestor(el, within2);
|
|
453
|
+
if (clip !== null) {
|
|
454
|
+
const edge = clip.getBoundingClientRect();
|
|
455
|
+
const shown = Math.min(box.right, edge.right) - Math.max(box.left, edge.left);
|
|
456
|
+
if (box.right > edge.right + 1 || box.left < edge.left - 1) cut.push({ text: text.slice(0, 40), axis: "width", has: Math.max(0, Math.round(shown)), needs: Math.round(box.width) });
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
442
460
|
const states = (property) => {
|
|
443
461
|
const value = style.getPropertyValue(property);
|
|
444
462
|
return value !== "" && value !== "none";
|
|
@@ -459,7 +477,7 @@ var __loticsProbe = (() => {
|
|
|
459
477
|
const walk = (node) => {
|
|
460
478
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
461
479
|
const parent = node.parentElement;
|
|
462
|
-
if (!parent || skipTag.test(parent.tagName) || parent.closest(
|
|
480
|
+
if (!parent || skipTag.test(parent.tagName) || parent.tagName === "TEXTAREA" || parent.closest('[data-content], [contenteditable="true"]') !== null) return;
|
|
463
481
|
const text = node.textContent || "";
|
|
464
482
|
if (!text.trim()) return;
|
|
465
483
|
total += 1;
|
|
@@ -694,8 +712,9 @@ var __loticsProbe = (() => {
|
|
|
694
712
|
}
|
|
695
713
|
|
|
696
714
|
// src/probe_collectors/anatomy.ts
|
|
697
|
-
var
|
|
698
|
-
var
|
|
715
|
+
var FRAME = ".lotics-record-frame";
|
|
716
|
+
var HEADER = ".lotics-record-header";
|
|
717
|
+
var BLOCK = ".lotics-record-frame__block";
|
|
699
718
|
var PRIMARY = '.lotics-button[data-variant="primary"]';
|
|
700
719
|
var FIRST_VIEWPORTS = [
|
|
701
720
|
{ width: 1280, height: 800 },
|
|
@@ -705,11 +724,10 @@ var __loticsProbe = (() => {
|
|
|
705
724
|
return FIRST_VIEWPORTS.reduce((near, one) => Math.abs(one.width - width) < Math.abs(near.width - width) ? one : near);
|
|
706
725
|
}
|
|
707
726
|
function surfaces(root) {
|
|
708
|
-
return within(root,
|
|
709
|
-
const head =
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
return [{ body, root: head, name: words2(title) || nameOf(head) || "(unnamed record)" }];
|
|
727
|
+
return within(root, FRAME).filter(onScreen).map((frame) => {
|
|
728
|
+
const head = frame.closest(".lotics-drawer__popup") ?? frame;
|
|
729
|
+
const title = within(head, `${HEADER}__title`).find(onScreen);
|
|
730
|
+
return { frame, root: head, name: words2(title) || nameOf(head) || "(unnamed record)" };
|
|
713
731
|
});
|
|
714
732
|
}
|
|
715
733
|
function words2(el) {
|
|
@@ -720,44 +738,51 @@ var __loticsProbe = (() => {
|
|
|
720
738
|
function headingPrimaries(root = document.body) {
|
|
721
739
|
const drawn = surfaces(root);
|
|
722
740
|
const loud = drawn.flatMap(
|
|
723
|
-
(surface) => within(surface.
|
|
741
|
+
(surface) => within(surface.frame, PRIMARY).filter(
|
|
742
|
+
(act) => onScreen(act) && act.closest(BLOCK) !== null && (act.closest(`${FRAME}__action`) !== null || act.closest(".lotics-section-heading") !== null)
|
|
743
|
+
).map((act) => ({ record: surface.name, act: nameOf(act) || words2(act) }))
|
|
724
744
|
);
|
|
725
745
|
return { total: drawn.length, loud };
|
|
726
746
|
}
|
|
727
|
-
function
|
|
747
|
+
function actsBelowFold(root = document.body, width = window.innerWidth) {
|
|
728
748
|
const fold = firstViewport(width).height;
|
|
729
|
-
const
|
|
730
|
-
const
|
|
731
|
-
|
|
749
|
+
const acts = surfaces(root).flatMap((surface) => {
|
|
750
|
+
const seat = within(surface.frame, `${FRAME}__act`).find(onScreen);
|
|
751
|
+
const button = seat === void 0 ? void 0 : within(seat, ".lotics-button").find(onScreen);
|
|
752
|
+
return button === void 0 ? [] : [{ record: surface.name, act: nameOf(button) || words2(button), bottom: Math.round(button.getBoundingClientRect().bottom) }];
|
|
732
753
|
});
|
|
733
|
-
return { total:
|
|
754
|
+
return { total: acts.length, fold, below: acts.filter((one) => one.bottom > fold) };
|
|
734
755
|
}
|
|
735
|
-
function
|
|
756
|
+
function factRepeats(root = document.body) {
|
|
736
757
|
let total = 0;
|
|
737
758
|
const repeated = surfaces(root).flatMap((surface) => {
|
|
738
|
-
const
|
|
739
|
-
if (
|
|
759
|
+
const header = within(surface.root, HEADER).find(onScreen);
|
|
760
|
+
if (header === void 0) return [];
|
|
740
761
|
const head = new Set(
|
|
741
|
-
within(
|
|
762
|
+
within(header, "*").filter((el) => el.childElementCount === 0 && el.closest('button, [role="button"], [role="menu"]') === null && onScreen(el)).map((el) => said(words2(el))).filter((text) => text.length > 1)
|
|
742
763
|
);
|
|
743
|
-
const
|
|
744
|
-
total +=
|
|
745
|
-
return
|
|
746
|
-
const
|
|
747
|
-
|
|
764
|
+
const fields = within(surface.frame, ".lotics-fact-groups__field").filter(onScreen);
|
|
765
|
+
total += fields.length;
|
|
766
|
+
return fields.flatMap((field) => {
|
|
767
|
+
const label = within(field, ".lotics-form-field__label")[0];
|
|
768
|
+
const value = within(field, "*").filter((el) => el.childElementCount === 0 && (label === void 0 || !label.contains(el)) && onScreen(el)).map((el) => words2(el)).join(" ").trim();
|
|
769
|
+
return value !== "" && head.has(said(value)) ? [{ record: surface.name, field: words2(label), value }] : [];
|
|
748
770
|
});
|
|
749
771
|
});
|
|
750
772
|
return { total, repeated };
|
|
751
773
|
}
|
|
752
|
-
function
|
|
774
|
+
function emptyBlocks(root = document.body) {
|
|
753
775
|
let total = 0;
|
|
754
776
|
const empty = surfaces(root).flatMap((surface) => {
|
|
755
|
-
const
|
|
756
|
-
total +=
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
777
|
+
const blocks = within(surface.frame, BLOCK).filter(onScreen);
|
|
778
|
+
total += blocks.length;
|
|
779
|
+
return blocks.flatMap((block) => {
|
|
780
|
+
const body = Array.from(block.children).filter(
|
|
781
|
+
(child) => !child.matches(`.lotics-section-heading, ${FRAME}__strip`) && onScreen(child)
|
|
782
|
+
);
|
|
783
|
+
const drawn = body.some((child) => words2(child) !== "" || child.querySelector("img, svg, input, textarea, button") !== null);
|
|
784
|
+
return drawn ? [] : [{ record: surface.name, block: block.getAttribute("aria-label") ?? "" }];
|
|
785
|
+
});
|
|
761
786
|
});
|
|
762
787
|
return { total, empty };
|
|
763
788
|
}
|
|
@@ -768,7 +793,7 @@ var __loticsProbe = (() => {
|
|
|
768
793
|
function clutterCensus(root = document.body) {
|
|
769
794
|
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
770
795
|
const chrome = 'nav, [role="banner"]';
|
|
771
|
-
const recordSurface = '.lotics-record-
|
|
796
|
+
const recordSurface = '.lotics-record-frame, [role="dialog"]:not([data-side])';
|
|
772
797
|
const headings = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
|
|
773
798
|
const noteFloor = 3;
|
|
774
799
|
const formFloor = 3;
|
|
@@ -778,7 +803,7 @@ var __loticsProbe = (() => {
|
|
|
778
803
|
const sameEdge = 8;
|
|
779
804
|
const words5 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
|
|
780
805
|
const surfaces2 = within(root, recordSurface).filter(onScreen);
|
|
781
|
-
const called2 = (el) => el.matches(headings) ? words5(el) || "the section" : el.matches(recordSurface) ? words5(el.querySelector(".lotics-record-
|
|
806
|
+
const called2 = (el) => el.matches(headings) ? words5(el) || "the section" : el.matches(recordSurface) ? words5(el.querySelector(".lotics-record-header__title, h1, h2")) || "the record" : "the screen";
|
|
782
807
|
const rules = [];
|
|
783
808
|
const primary = '[data-variant="primary"]';
|
|
784
809
|
const acts = within(root, primary).filter(
|
|
@@ -884,34 +909,6 @@ var __loticsProbe = (() => {
|
|
|
884
909
|
count: twice,
|
|
885
910
|
offenders: repeated
|
|
886
911
|
});
|
|
887
|
-
const absent = /^[—–-]$/;
|
|
888
|
-
let facts = 0;
|
|
889
|
-
let standing = 0;
|
|
890
|
-
const open = [];
|
|
891
|
-
for (const surface of surfaces2) {
|
|
892
|
-
for (const block of within(surface, ".lotics-record-facts")) {
|
|
893
|
-
const fold = block.querySelector(".lotics-record-facts__more");
|
|
894
|
-
if (fold === null) continue;
|
|
895
|
-
for (const band of Array.from(block.querySelectorAll(".lotics-record-facts__group"))) {
|
|
896
|
-
if ((fold.compareDocumentPosition(band) & Node.DOCUMENT_POSITION_FOLLOWING) === 0) continue;
|
|
897
|
-
for (const cell of Array.from(band.querySelectorAll(".lotics-record-facts__fact"))) {
|
|
898
|
-
const value = cell.querySelector(".lotics-record-facts__value");
|
|
899
|
-
if (value === null || !onScreen(cell)) continue;
|
|
900
|
-
facts += 1;
|
|
901
|
-
if (!absent.test((value.textContent || "").trim())) continue;
|
|
902
|
-
standing += 1;
|
|
903
|
-
if (open.length < 3) open.push(`"${words5(cell.querySelector(".lotics-record-facts__label")) || words5(cell)}" is unstated and drawn`);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
rules.push({
|
|
909
|
-
law: "hierarchy.md",
|
|
910
|
-
rule: "How a record derives them",
|
|
911
|
-
total: facts,
|
|
912
|
-
count: standing,
|
|
913
|
-
offenders: open
|
|
914
|
-
});
|
|
915
912
|
let boxes = 0;
|
|
916
913
|
let overReserved = 0;
|
|
917
914
|
const reserved = [];
|
|
@@ -1497,9 +1494,15 @@ var __loticsProbe = (() => {
|
|
|
1497
1494
|
);
|
|
1498
1495
|
return Math.max(0, width - drawn);
|
|
1499
1496
|
}
|
|
1500
|
-
function
|
|
1501
|
-
const
|
|
1502
|
-
|
|
1497
|
+
function slotGaps(register) {
|
|
1498
|
+
const first = register.slots.map((slot) => slot.cells[0]).filter((cell) => cell !== void 0);
|
|
1499
|
+
const boxes = first.map((cell) => cell.getBoundingClientRect()).sort((a, b) => a.left - b.left);
|
|
1500
|
+
if (boxes.length < 2) return { gap: 0, beyond: 0 };
|
|
1501
|
+
const drawn = boxes.slice(1).map((box, index) => Math.max(0, box.left - boxes[index].right));
|
|
1502
|
+
const parent = first[0].parentElement;
|
|
1503
|
+
const declared = parent === null ? Number.NaN : Number.parseFloat(getComputedStyle(parent).columnGap);
|
|
1504
|
+
const gap = Number.isFinite(declared) ? declared : Math.min(...drawn);
|
|
1505
|
+
return { gap: Math.round(gap), beyond: drawn.reduce((sum, one) => sum + Math.max(0, one - gap), 0) };
|
|
1503
1506
|
}
|
|
1504
1507
|
function shedWithRoom(root = document.body) {
|
|
1505
1508
|
let total = 0;
|
|
@@ -1508,8 +1511,8 @@ var __loticsProbe = (() => {
|
|
|
1508
1511
|
const shed = shedOf(register.root);
|
|
1509
1512
|
if (shed.length === 0 || register.rows.length === 0) continue;
|
|
1510
1513
|
total += shed.length;
|
|
1511
|
-
const
|
|
1512
|
-
const
|
|
1514
|
+
const { gap, beyond } = slotGaps(register);
|
|
1515
|
+
const unused = Math.round(register.slots.reduce((sum, slot) => sum + spare(slot), 0) + beyond);
|
|
1513
1516
|
for (const column of shed) if (column.width + gap <= unused) found.push({ ...column, gap, unused });
|
|
1514
1517
|
}
|
|
1515
1518
|
return { total, shed: found };
|
|
@@ -1573,39 +1576,10 @@ var __loticsProbe = (() => {
|
|
|
1573
1576
|
|
|
1574
1577
|
// src/probe_collectors/right_form.ts
|
|
1575
1578
|
var DAY_RUN_PART = ".lotics-timeline__dayhead";
|
|
1576
|
-
var SIDE = 'aside, [role="complementary"], nav, [role="navigation"]';
|
|
1577
|
-
var IN_PAGE = 'button, summary, [role="button"], [role="link"], [role="tab"], [role="menuitem"], a[href^="#"]';
|
|
1578
|
-
var HEADING3 = 'h1, h2, h3, h4, h5, h6, [role="heading"]';
|
|
1579
|
-
var RECORD_ROWS = 'table, [role="table"], [role="grid"], [role="treegrid"], [role="row"], [role="list"], ul, ol';
|
|
1580
|
-
var PARKED = [
|
|
1581
|
-
{
|
|
1582
|
-
is: 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [role="textbox"], [role="searchbox"], [role="combobox"], [role="spinbutton"], [role="slider"], [role="checkbox"], [role="radio"], [role="switch"], form, [role="form"]',
|
|
1583
|
-
called: "a field"
|
|
1584
|
-
},
|
|
1585
|
-
{ is: 'table, [role="table"], [role="grid"], [role="treegrid"], [role="row"], [role="listbox"]', called: "a list of rows" },
|
|
1586
|
-
{ is: 'article, [role="article"], [role="feed"], [role="log"]', called: "a conversation" },
|
|
1587
|
-
{ is: 'img, svg, picture, canvas, [role="img"], [role="figure"]', called: "a picture" },
|
|
1588
|
-
{ is: "a[href]", called: "a link out of the page" }
|
|
1589
|
-
];
|
|
1590
1579
|
function rightFormCensus(root = document.body) {
|
|
1591
1580
|
const skipTag = /^(SCRIPT|STYLE|TITLE|META|LINK|NOSCRIPT|TEMPLATE)$/;
|
|
1592
|
-
const recordSurface = '.lotics-record-page, [role="dialog"]';
|
|
1593
1581
|
const columnFloor = 3;
|
|
1594
|
-
const twoTrackWidth = 1016;
|
|
1595
|
-
const trackHeight = 240;
|
|
1596
1582
|
const words5 = (el) => el === null ? "" : (el.textContent || "").trim().slice(0, 40);
|
|
1597
|
-
const surfaces2 = within(root, recordSurface).filter(onScreen);
|
|
1598
|
-
const called2 = (el) => words5(el.querySelector(".lotics-record-page__heading, h1, h2")) || "the record";
|
|
1599
|
-
const readingInside = (side) => {
|
|
1600
|
-
for (const kind of PARKED) {
|
|
1601
|
-
for (const el of Array.from(side.querySelectorAll(kind.is))) {
|
|
1602
|
-
if (!onScreen(el) || el.closest(IN_PAGE) !== null) continue;
|
|
1603
|
-
const named = nameOf(el).slice(0, 40) || words5(el);
|
|
1604
|
-
return named === "" ? kind.called : `${kind.called} ("${named}")`;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
return null;
|
|
1608
|
-
};
|
|
1609
1583
|
const rules = [];
|
|
1610
1584
|
const choosers = within(root, 'select, [role="listbox"]').filter(onScreen);
|
|
1611
1585
|
let twoOption = 0;
|
|
@@ -1624,7 +1598,7 @@ var __loticsProbe = (() => {
|
|
|
1624
1598
|
}
|
|
1625
1599
|
rules.push({
|
|
1626
1600
|
law: "data_entry.md",
|
|
1627
|
-
rule: "Choosing a CHOICE control \u2014
|
|
1601
|
+
rule: "Choosing a CHOICE control \u2014 by the set, never taste",
|
|
1628
1602
|
total: choosers.length,
|
|
1629
1603
|
count: twoOption,
|
|
1630
1604
|
offenders: pairs
|
|
@@ -1701,105 +1675,12 @@ var __loticsProbe = (() => {
|
|
|
1701
1675
|
}
|
|
1702
1676
|
}
|
|
1703
1677
|
rules.push({
|
|
1704
|
-
law: "
|
|
1678
|
+
law: "screen.md",
|
|
1705
1679
|
rule: "Which device answers the question \u2192 Read what happened, or what is planned, in time",
|
|
1706
1680
|
total: columns.length,
|
|
1707
1681
|
count: grouped,
|
|
1708
1682
|
offenders: runs
|
|
1709
1683
|
});
|
|
1710
|
-
const namesItself = (region) => {
|
|
1711
|
-
const heading = Array.from(region.querySelectorAll(HEADING3)).find((el) => el.closest("summary") === null);
|
|
1712
|
-
return heading !== void 0 || region.querySelector(RECORD_ROWS) !== null;
|
|
1713
|
-
};
|
|
1714
|
-
let folds = 0;
|
|
1715
|
-
let sections = 0;
|
|
1716
|
-
const hidden = [];
|
|
1717
|
-
for (const surface of surfaces2) {
|
|
1718
|
-
for (const fold of Array.from(surface.querySelectorAll(".lotics-accordion__trigger, details, [aria-expanded]"))) {
|
|
1719
|
-
if (!onScreen(fold) || fold.closest('[role="complementary"]') !== null) continue;
|
|
1720
|
-
if (fold.hasAttribute("aria-haspopup") || fold.matches(HOLDS_A_VALUE)) continue;
|
|
1721
|
-
folds += 1;
|
|
1722
|
-
const opened = fold.tagName === "DETAILS" ? [fold] : pointedAt(fold, "aria-controls");
|
|
1723
|
-
if (!fold.matches(".lotics-accordion__trigger") && !opened.some(namesItself)) continue;
|
|
1724
|
-
sections += 1;
|
|
1725
|
-
if (hidden.length < 3) {
|
|
1726
|
-
const named = nameOf(fold).slice(0, 40);
|
|
1727
|
-
hidden.push(`${called2(surface)}: "${named === "" ? "(unnamed)" : named}" is a section behind a fold`);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
rules.push({
|
|
1732
|
-
law: "templates.md",
|
|
1733
|
-
rule: "Which device answers the question \u2192 See what NAMES this record",
|
|
1734
|
-
total: folds,
|
|
1735
|
-
count: sections,
|
|
1736
|
-
offenders: hidden
|
|
1737
|
-
});
|
|
1738
|
-
let read = 0;
|
|
1739
|
-
let split = 0;
|
|
1740
|
-
const tracks = [];
|
|
1741
|
-
if (window.innerWidth >= twoTrackWidth) {
|
|
1742
|
-
for (const surface of surfaces2) {
|
|
1743
|
-
read += 1;
|
|
1744
|
-
const box = surface.getBoundingClientRect();
|
|
1745
|
-
const owners = [surface, ...Array.from(surface.querySelectorAll("*"))];
|
|
1746
|
-
let found = null;
|
|
1747
|
-
for (const owner of owners) {
|
|
1748
|
-
const columnsHere = Array.from(owner.children).filter((child) => {
|
|
1749
|
-
if (!onScreen(child)) return false;
|
|
1750
|
-
if (child.matches(SIDE) || child.querySelector(SIDE) !== null) return false;
|
|
1751
|
-
const rect = child.getBoundingClientRect();
|
|
1752
|
-
return rect.height >= trackHeight && rect.height >= box.height * 0.5 && rect.width >= box.width * 0.2;
|
|
1753
|
-
});
|
|
1754
|
-
for (let a = 0; a < columnsHere.length && found === null; a++) {
|
|
1755
|
-
for (let b = a + 1; b < columnsHere.length && found === null; b++) {
|
|
1756
|
-
const left = columnsHere[a].getBoundingClientRect();
|
|
1757
|
-
const right = columnsHere[b].getBoundingClientRect();
|
|
1758
|
-
const apart = left.right <= right.left + 8 || right.right <= left.left + 8;
|
|
1759
|
-
const overlap = Math.min(left.bottom, right.bottom) - Math.max(left.top, right.top);
|
|
1760
|
-
const together = Math.max(left.right, right.right) - Math.min(left.left, right.left);
|
|
1761
|
-
if (apart && overlap >= Math.min(left.height, right.height) * 0.5 && together >= box.width * 0.7) {
|
|
1762
|
-
found = [columnsHere[a], columnsHere[b]];
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
if (found !== null) break;
|
|
1767
|
-
}
|
|
1768
|
-
if (found !== null) {
|
|
1769
|
-
split += 1;
|
|
1770
|
-
if (tracks.length < 3) {
|
|
1771
|
-
const left = Math.round(found[0].getBoundingClientRect().width);
|
|
1772
|
-
const right = Math.round(found[1].getBoundingClientRect().width);
|
|
1773
|
-
tracks.push(`${called2(surface)}: two columns side by side, ${left}px and ${right}px`);
|
|
1774
|
-
}
|
|
1775
|
-
continue;
|
|
1776
|
-
}
|
|
1777
|
-
for (const side of Array.from(surface.querySelectorAll(SIDE))) {
|
|
1778
|
-
if (!onScreen(side)) continue;
|
|
1779
|
-
const rect = side.getBoundingClientRect();
|
|
1780
|
-
if (rect.width > box.width * 0.6) continue;
|
|
1781
|
-
const column = Array.from(surface.querySelectorAll("*")).some((other) => {
|
|
1782
|
-
if (other === side || side.contains(other) || other.contains(side) || !onScreen(other)) return false;
|
|
1783
|
-
const beside = other.getBoundingClientRect();
|
|
1784
|
-
const apart = beside.left >= rect.right - 8 || beside.right <= rect.left + 8;
|
|
1785
|
-
const overlap = Math.min(beside.bottom, rect.bottom) - Math.max(beside.top, rect.top);
|
|
1786
|
-
return apart && beside.width >= box.width * 0.4 && overlap >= rect.height * 0.5;
|
|
1787
|
-
});
|
|
1788
|
-
if (!column) continue;
|
|
1789
|
-
const parked = readingInside(side);
|
|
1790
|
-
if (parked === null) continue;
|
|
1791
|
-
split += 1;
|
|
1792
|
-
if (tracks.length < 3) tracks.push(`${called2(surface)}: ${parked} beside the reading column`);
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
rules.push({
|
|
1797
|
-
law: "hierarchy.md",
|
|
1798
|
-
rule: "Depth \u2014 four surfaces, four jobs",
|
|
1799
|
-
total: read,
|
|
1800
|
-
count: split,
|
|
1801
|
-
offenders: tracks
|
|
1802
|
-
});
|
|
1803
1684
|
return rules;
|
|
1804
1685
|
}
|
|
1805
1686
|
|
|
@@ -1977,22 +1858,22 @@ var __loticsProbe = (() => {
|
|
|
1977
1858
|
record_primary: {
|
|
1978
1859
|
law: "hierarchy.md",
|
|
1979
1860
|
section: "One primary act per surface",
|
|
1980
|
-
fix: "Draw a
|
|
1861
|
+
fix: "Draw a block's add secondary: the record's one primary act is the act open now, in its `ActPanel`."
|
|
1981
1862
|
},
|
|
1982
|
-
|
|
1863
|
+
act_fold: {
|
|
1983
1864
|
law: "hierarchy.md",
|
|
1984
|
-
section: "
|
|
1985
|
-
fix: "
|
|
1865
|
+
section: "Budgets a reviewer can count",
|
|
1866
|
+
fix: "Seat the open act where the frame pins it \u2014 `RecordFrame`'s `act`, never a button of the page's own \u2014 so it stays in view while the facts run on."
|
|
1986
1867
|
},
|
|
1987
|
-
|
|
1868
|
+
fact_repeats: {
|
|
1988
1869
|
law: "hierarchy.md",
|
|
1989
1870
|
section: "One fact, one place",
|
|
1990
|
-
fix: "Take the
|
|
1871
|
+
fix: "Take the field out of the facts: the header already states it \u2014 its title, its status, the line under it or its figure."
|
|
1991
1872
|
},
|
|
1992
|
-
|
|
1873
|
+
empty_block: {
|
|
1993
1874
|
law: "hierarchy.md",
|
|
1994
|
-
section: "
|
|
1995
|
-
fix: "Draw
|
|
1875
|
+
section: "Whitespace is structure",
|
|
1876
|
+
fix: "Draw the block's body, or its empty line: a block under a heading that draws nothing is a press that leads nowhere."
|
|
1996
1877
|
},
|
|
1997
1878
|
render: {
|
|
1998
1879
|
law: "reviewing.md",
|
|
@@ -2018,7 +1899,7 @@ var __loticsProbe = (() => {
|
|
|
2018
1899
|
fix: "Move a fact down a tier, fold it, or take it off the screen \u2014 a budget failing is a tiering question, never a permission to shrink something."
|
|
2019
1900
|
},
|
|
2020
1901
|
right_form: {
|
|
2021
|
-
law: "
|
|
1902
|
+
law: "screen.md",
|
|
2022
1903
|
section: "Which device answers the question",
|
|
2023
1904
|
fix: "Draw what the JOB names: the device index gives the entry and the wrong device beside it, and a shape is picked by what the reader must do rather than by what the data is."
|
|
2024
1905
|
},
|
|
@@ -2224,37 +2105,37 @@ var __loticsProbe = (() => {
|
|
|
2224
2105
|
...signature("record_primary"),
|
|
2225
2106
|
total: primaries.total,
|
|
2226
2107
|
count: primaries.loud.length,
|
|
2227
|
-
detail: primaries.loud.slice(0, 5).map((one) => `"${one.record}": "${one.act}" is primary in a
|
|
2108
|
+
detail: primaries.loud.slice(0, 5).map((one) => `"${one.record}": "${one.act}" is primary in a block's heading row, beside the record's open act`)
|
|
2228
2109
|
});
|
|
2229
2110
|
}
|
|
2230
|
-
const folded =
|
|
2111
|
+
const folded = actsBelowFold(surface);
|
|
2231
2112
|
if (folded.below.length > 0) {
|
|
2232
2113
|
findings.push({
|
|
2233
|
-
probe: "
|
|
2234
|
-
...signature("
|
|
2114
|
+
probe: "act_fold",
|
|
2115
|
+
...signature("act_fold"),
|
|
2235
2116
|
total: folded.total,
|
|
2236
2117
|
count: folded.below.length,
|
|
2237
|
-
detail: folded.below.slice(0, 5).map((one) => `"${one.record}":
|
|
2118
|
+
detail: folded.below.slice(0, 5).map((one) => `"${one.record}": "${one.act}" ends at ${one.bottom}px, past the ${folded.fold}px first viewport`)
|
|
2238
2119
|
});
|
|
2239
2120
|
}
|
|
2240
|
-
const repeats =
|
|
2121
|
+
const repeats = factRepeats(surface);
|
|
2241
2122
|
if (repeats.repeated.length > 0) {
|
|
2242
2123
|
findings.push({
|
|
2243
|
-
probe: "
|
|
2244
|
-
...signature("
|
|
2124
|
+
probe: "fact_repeats",
|
|
2125
|
+
...signature("fact_repeats"),
|
|
2245
2126
|
total: repeats.total,
|
|
2246
2127
|
count: repeats.repeated.length,
|
|
2247
|
-
detail: repeats.repeated.slice(0, 5).map((one) => `"${one.record}": the
|
|
2128
|
+
detail: repeats.repeated.slice(0, 5).map((one) => `"${one.record}": the fact "${one.field}" reads "${one.value}", which the header already says`)
|
|
2248
2129
|
});
|
|
2249
2130
|
}
|
|
2250
|
-
const
|
|
2251
|
-
if (
|
|
2131
|
+
const blocks = emptyBlocks(surface);
|
|
2132
|
+
if (blocks.empty.length > 0) {
|
|
2252
2133
|
findings.push({
|
|
2253
|
-
probe: "
|
|
2254
|
-
...signature("
|
|
2255
|
-
total:
|
|
2256
|
-
count:
|
|
2257
|
-
detail:
|
|
2134
|
+
probe: "empty_block",
|
|
2135
|
+
...signature("empty_block"),
|
|
2136
|
+
total: blocks.total,
|
|
2137
|
+
count: blocks.empty.length,
|
|
2138
|
+
detail: blocks.empty.slice(0, 5).map((one) => `"${one.record}": the block "${one.block}" draws nothing`)
|
|
2258
2139
|
});
|
|
2259
2140
|
}
|
|
2260
2141
|
findings.push(...censusFindings("clutter", clutterCensus(surface)));
|
|
@@ -2339,8 +2220,8 @@ var __loticsProbe = (() => {
|
|
|
2339
2220
|
function recordLinks() {
|
|
2340
2221
|
const here = location.pathname + location.search;
|
|
2341
2222
|
const doors2 = Array.from(
|
|
2342
|
-
document.querySelectorAll('a[href], [data-opens="page"], button, [role="button"], [role="link"]')
|
|
2343
|
-
).filter((door) => door.matches('a[href], [data-opens="page"]') || isEmptyDoor(door));
|
|
2223
|
+
document.querySelectorAll('a[href], [data-opens="page"], [data-opens="event"], button, [role="button"], [role="link"]')
|
|
2224
|
+
).filter((door) => door.matches('a[href], [data-opens="page"], [data-opens="event"]') || isEmptyDoor(door));
|
|
2344
2225
|
return doors2.filter((door) => {
|
|
2345
2226
|
if (door.closest('nav, [role="banner"], [role="tablist"]') !== null) return false;
|
|
2346
2227
|
if (doors2.some((other) => other !== door && other.contains(door))) return false;
|
|
@@ -2440,6 +2321,9 @@ var __loticsProbe = (() => {
|
|
|
2440
2321
|
const folds = Array.from(document.querySelectorAll('[aria-expanded="false"]')).filter(
|
|
2441
2322
|
(el) => reachable(el) && el.getAttribute("aria-haspopup") === null
|
|
2442
2323
|
);
|
|
2324
|
+
const tabs = Array.from(document.querySelectorAll('[role="tab"][aria-selected="false"]')).filter(
|
|
2325
|
+
(el) => onScreen(el) && el.closest('nav, [role="banner"]') === null && !open.some((overlay) => overlay.contains(el))
|
|
2326
|
+
);
|
|
2443
2327
|
const acts = Array.from(
|
|
2444
2328
|
document.querySelectorAll('[data-variant="primary"], [data-variant="secondary"]')
|
|
2445
2329
|
).filter((el) => {
|
|
@@ -2462,7 +2346,8 @@ var __loticsProbe = (() => {
|
|
|
2462
2346
|
...menus.slice(0, 1).map((el) => ({ kind: "menu", el })),
|
|
2463
2347
|
...folds.slice(0, 1).map((el) => ({ kind: "fold", el })),
|
|
2464
2348
|
...acts.map((el) => ({ kind: "dialog", el })),
|
|
2465
|
-
...parties.map((el) => ({ kind: "party", el }))
|
|
2349
|
+
...parties.map((el) => ({ kind: "party", el })),
|
|
2350
|
+
...tabs.map((el) => ({ kind: "tab", el }))
|
|
2466
2351
|
];
|
|
2467
2352
|
}
|
|
2468
2353
|
function doors() {
|