@lotics/ui 18.2.0 → 19.0.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/MIGRATION.md +65 -0
- package/docs/catalog.md +78 -26
- package/docs/data_entry.md +55 -21
- package/docs/templates.md +17 -17
- package/examples/tpl_item_list.tsx +26 -26
- package/examples/tpl_record.tsx +38 -38
- package/package.json +1 -1
- package/src/detail_row.tsx +7 -1
- package/src/linked_record_box.tsx +30 -13
- package/src/sources.tsx +11 -1
- package/src/task.tsx +347 -135
- package/src/task_metrics.test.ts +48 -0
- package/src/task_metrics.ts +55 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { TASK_ROW_BAND, TASK_ROW_GAP, TASK_TITLE_LINE, taskGutter, taskTitleSlack } from "./task_metrics";
|
|
3
|
+
|
|
4
|
+
describe("task row geometry", () => {
|
|
5
|
+
it("puts the control's centre and the title's first text line on the SAME y, at every density", () => {
|
|
6
|
+
for (const band of Object.values(TASK_ROW_BAND)) {
|
|
7
|
+
// The control centres in the band; the title's first line centres at its slack + half a
|
|
8
|
+
// line. A density whose band breaks this equality sags the control off its own title.
|
|
9
|
+
expect(taskTitleSlack(band) + TASK_TITLE_LINE / 2).toBe(band / 2);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("pins the first line on any band the family builds, not only the density table's", () => {
|
|
14
|
+
// A sub-field's line is the TALLER of the row band and the control it holds (a 40px inline
|
|
15
|
+
// editor does not fit a `dense` 32 row), so the law has to hold on bands the density table
|
|
16
|
+
// never names.
|
|
17
|
+
for (let band = TASK_TITLE_LINE; band <= 64; band += 2) {
|
|
18
|
+
expect(taskTitleSlack(band) + TASK_TITLE_LINE / 2).toBe(band / 2);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("lands every density on whole pixels", () => {
|
|
23
|
+
for (const band of Object.values(TASK_ROW_BAND)) {
|
|
24
|
+
expect(Number.isInteger(taskTitleSlack(band))).toBe(true);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("never pulls a title above its row when the band is shorter than a line of text", () => {
|
|
29
|
+
expect(taskTitleSlack(TASK_TITLE_LINE - 8)).toBe(0);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("task row gutters", () => {
|
|
34
|
+
it("gives a pinned control its own width plus the gap that separates it from the content", () => {
|
|
35
|
+
// ONE rule for BOTH edges: the leading ring's column and the trailing ⋯'s column are the same
|
|
36
|
+
// arithmetic, which is what lets a row reserve them as plain padding and leave a content box
|
|
37
|
+
// every line — title, caption, sub-row, detail, nested list — can span exactly.
|
|
38
|
+
for (const width of [20, 24, 28, 40]) {
|
|
39
|
+
expect(taskGutter(width)).toBe(width + TASK_ROW_GAP);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("charges NOTHING for a gutter a list declines", () => {
|
|
44
|
+
// A list whose rows carry no ⋯ passes `actionWidth={0}` and its content must run to the
|
|
45
|
+
// container's edge. Leaving the gap behind would bill it 12px for a column it never uses.
|
|
46
|
+
expect(taskGutter(0)).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The task row's geometry — the two laws a style object cannot state on its own.
|
|
3
|
+
*
|
|
4
|
+
* VERTICALLY, a row's FIRST LINE is a BAND: the leading control centres in that band, and the
|
|
5
|
+
* title's first text line centres in it too. Both centres are `band / 2`, which is what keeps a
|
|
6
|
+
* column of controls straight however tall the rows grow underneath them.
|
|
7
|
+
*
|
|
8
|
+
* HORIZONTALLY, a row is a GUTTER, one content box, and a GUTTER: each pinned control's column
|
|
9
|
+
* is its own width plus the row gap, and the same `taskGutter` sizes both edges — so the ⋯ on
|
|
10
|
+
* the right is the mirror of the ring on the left, not a second rule that happens to look alike.
|
|
11
|
+
*
|
|
12
|
+
* RN-free on purpose (Vitest cannot parse `react-native`): `task.tsx` renders these numbers,
|
|
13
|
+
* `task_metrics.test.ts` asserts the laws they exist to hold.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The band per density — `comfortable` is a finger's tap target, `dense` trades it for rows on
|
|
18
|
+
* screen. It is also the row's MINIMUM height: with nothing hanging beneath it, the first line
|
|
19
|
+
* IS the row.
|
|
20
|
+
*/
|
|
21
|
+
export const TASK_ROW_BAND = { comfortable: 44, dense: 32 } as const;
|
|
22
|
+
|
|
23
|
+
/** The kit's `Text size="sm"` line box — one line of a task title, wrapped or not. */
|
|
24
|
+
export const TASK_TITLE_LINE = 20;
|
|
25
|
+
|
|
26
|
+
/** The horizontal rhythm between a row's slots — and the air between a pinned control and the
|
|
27
|
+
* content beside it, which is what makes it half of a gutter. */
|
|
28
|
+
export const TASK_ROW_GAP = 12;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ONE gutter — the column a PINNED control owns on either edge of a row: its own width plus the
|
|
32
|
+
* gap that separates it from the content. The item reserves it as padding, the control sits in
|
|
33
|
+
* it, and what is left between the two gutters is the row's content box: every line the item
|
|
34
|
+
* renders (title, caption, sub-row, detail, a nested list) spans exactly that box, so the right
|
|
35
|
+
* edge is straight even down a list where only SOME rows carry a ⋯.
|
|
36
|
+
*
|
|
37
|
+
* Zero width is zero gutter, gap included — a list whose rows carry no menu pays nothing for a
|
|
38
|
+
* column it never uses, and a reserved-but-empty 12px is still paying.
|
|
39
|
+
*/
|
|
40
|
+
export function taskGutter(controlWidth: number): number {
|
|
41
|
+
return controlWidth === 0 ? 0 : controlWidth + TASK_ROW_GAP;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The slack a PLAIN-TEXT title carries above AND below its text so its FIRST line centres on
|
|
46
|
+
* the band. Symmetric padding, not `minHeight` + centring, is what holds that first line in
|
|
47
|
+
* place when the title wraps: centring a BLOCK of two lines drops the first one half a line
|
|
48
|
+
* above the control, which is the misalignment this geometry exists to prevent.
|
|
49
|
+
*
|
|
50
|
+
* Clamped at 0 — a band shorter than a line of text starts the text at the row's top rather
|
|
51
|
+
* than pulling it above the row.
|
|
52
|
+
*/
|
|
53
|
+
export function taskTitleSlack(band: number): number {
|
|
54
|
+
return Math.max(0, (band - TASK_TITLE_LINE) / 2);
|
|
55
|
+
}
|