@lotics/ui 45.5.1 → 45.6.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/AGENTS.md +9 -5
- package/docs/catalog.md +14 -2
- package/docs/composition.md +27 -0
- package/package.json +1 -1
- package/src/empty_state.tsx +65 -6
- package/src/list_item.tsx +27 -4
package/AGENTS.md
CHANGED
|
@@ -68,11 +68,15 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
68
68
|
`HeadingAltitudeContext`. Do not look for a per-surface heading variant and do not hand-pick a
|
|
69
69
|
size: a `##` inside a drawer is 24px over the drawer's own 20px title.
|
|
70
70
|
→ [composition.md §The heading ramp](./docs/composition.md).
|
|
71
|
-
- **`EmptyState` carries NO verb,
|
|
72
|
-
picked by what the region can ASSERT: `Skeleton`/`Loading` in flight →
|
|
73
|
-
(`message`/`detail`/`onRetry`) on failure → `EmptyState` (succeeded, found
|
|
74
|
-
`CompletionState` on done. An alert glyph inside an empty state claims the read
|
|
75
|
-
nothing is known.
|
|
71
|
+
- **`EmptyState` carries NO verb, a FAILED read is not an empty one, and a BAND is not a region.**
|
|
72
|
+
Four region states, picked by what the region can ASSERT: `Skeleton`/`Loading` in flight →
|
|
73
|
+
**`ErrorState`** (`message`/`detail`/`onRetry`) on failure → `EmptyState` (succeeded, found
|
|
74
|
+
nothing) → `CompletionState` on done. An alert glyph inside an empty state claims the read
|
|
75
|
+
succeeded when nothing is known. How much quiet it reserves is a density axis — `compact` for
|
|
76
|
+
one band of a record (left-aligned, and glyph-less because an inline mark beside `sm` text
|
|
77
|
+
cannot be both legible and cap-aligned in this icon set), the default for a whole region.
|
|
78
|
+
→ [catalog.md §Status / feedback](./docs/catalog.md),
|
|
79
|
+
[composition.md §Empty states](./docs/composition.md).
|
|
76
80
|
- **Progress: ONE compound, TWO axes — and both are about meaning.** A run of markable rows is
|
|
77
81
|
`checklist` (`Checklist` + `ChecklistGroup` + `ChecklistItem`, over `stepper`). **`connected`**
|
|
78
82
|
asks *is this a sequence* — on for one record walking positions, off for N items handled in any
|
package/docs/catalog.md
CHANGED
|
@@ -927,11 +927,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
927
927
|
- **`callout`** — `Callout`, `CalloutTitle`, `CalloutText`, `CalloutActions` (`tone`
|
|
928
928
|
info|success|warning|error|neutral): inline status band — a MESSAGE (`warning`/`error`
|
|
929
929
|
are ARIA `alert`s). For a form / nested content surface use **`Inset`**, never a Callout.
|
|
930
|
-
- **`empty_state`** — `EmptyState`:
|
|
930
|
+
- **`empty_state`** — `EmptyState`: placeholder for a read that SUCCEEDED and found
|
|
931
931
|
nothing — `message` + `hint` + an optional `icon` anchor, and **no verb at all**. It cannot hold
|
|
932
932
|
the section's add (that lives on the heading row, where it does not move — § The add-placement
|
|
933
933
|
law in composition.md), a no-results empty is HINT-only since the filters that emptied it carry
|
|
934
|
-
their own clear, and a FAILED read is `ErrorState`, not this.
|
|
934
|
+
their own clear, and a FAILED read is `ErrorState`, not this. **`compact`** for one BAND of a
|
|
935
|
+
record — left-aligned, glyph-less, 56px against the default's 178 — because the
|
|
936
|
+
heading above it has already proved the surface rendered; the centred default speaks for a
|
|
937
|
+
whole region, where nothing else is in view to say so. → composition.md § Empty states.
|
|
935
938
|
- **`error_state`** — `ErrorState`: the region-scale FAILED read — `message` + optional `detail`
|
|
936
939
|
+ `onRetry` (the kit renders the button and words it from the locale pack, so "try again" reads
|
|
937
940
|
the same everywhere). The fourth of the region states: `Skeleton`/`Loading` in flight → this on
|
|
@@ -1474,6 +1477,15 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1474
1477
|
when a slot is passed: `right` is routinely conditional, and an anatomy that flipped on that
|
|
1475
1478
|
would make two rows in one list press differently. Nothing to opt into — a decorative
|
|
1476
1479
|
`right` simply stops being part of the press target.
|
|
1480
|
+
**The row OWNS its `title`/`description` pair, so do not hand-roll one beside it.** Both
|
|
1481
|
+
lines are `sm`, `leading="tight"`, no gap between them — the stacked-pair law in
|
|
1482
|
+
[composition.md](./composition.md), applied by the component rather than restated at each
|
|
1483
|
+
call site. It shipped rendering that pair as PROSE (two 24px boxes plus a 2px gap = 12px
|
|
1484
|
+
between the lines, against the 6px the rule prescribes), and the height was sized around
|
|
1485
|
+
that mistake at 64, which left a correctly-sized `md` mark adrift in it; the row is 56 now.
|
|
1486
|
+
A `description` passed as a NODE keeps its own leading — the component only sets it on the
|
|
1487
|
+
string branch, because forcing a line box on somebody else's subtree re-spaces content that
|
|
1488
|
+
may not be a line of text at all.
|
|
1477
1489
|
- **`table`** — `Table` + `TableRow` + `TableCell`: the paginated high-volume register
|
|
1478
1490
|
(columns defined once, `sortLabels` localizable; rows are `PressableRow`-based).
|
|
1479
1491
|
CONTAINER-RESPONSIVE with no prop (measures itself, like `Breakdown`/`DetailTable`): when
|
package/docs/composition.md
CHANGED
|
@@ -2011,6 +2011,33 @@ wearing an alert glyph. The three empties get DIFFERENT copy:
|
|
|
2011
2011
|
- **No search/filter results** → help recover: restate the scope ("Không có lô nào khớp
|
|
2012
2012
|
'ABC'"), hint the fix ("Thử từ khóa khác / xóa bộ lọc"). Never a blank panel or bare "No data".
|
|
2013
2013
|
|
|
2014
|
+
**How much QUIET it reserves depends on what it speaks for — `compact` inside a record.** The
|
|
2015
|
+
default holds a screenful because nothing else is in view: collapsing to two grey lines would
|
|
2016
|
+
read as a rendering bug rather than a state, which is the first of the three jobs. Inside a
|
|
2017
|
+
record that premise is false. The band's heading above it, the sections below it and the add
|
|
2018
|
+
verb beside it have already proved the surface rendered; the only open question is what this one
|
|
2019
|
+
band holds, and a screenful is a long way to say "nothing yet". Measured on a four-section
|
|
2020
|
+
record where three bands can be empty at once, the default spent **178px per band** and pushed
|
|
2021
|
+
the section the reader opened the record FOR under the fold on a 768px screen — every probe
|
|
2022
|
+
passes on that page, because nothing is wrong with any element on it.
|
|
2023
|
+
|
|
2024
|
+
So: `compact` on a band, the default on a region. It is a DENSITY axis, not a second component —
|
|
2025
|
+
same three jobs, same two strings, same no-verb rule. Compact also sets its text to the LEFT,
|
|
2026
|
+
because the heading directly above it establishes that edge and a centred block beneath a
|
|
2027
|
+
left-aligned heading reads as a fragment of a different page (§ Canvas & content column).
|
|
2028
|
+
|
|
2029
|
+
**An inline mark beside `sm` text cannot be both legible and aligned, so compact takes no glyph.**
|
|
2030
|
+
Worth knowing as a general fact about this icon set, not just about this component: it is lucide
|
|
2031
|
+
at a fixed stroke on a 24 viewBox, so the artwork fills 20/24 of whatever box you give it. Beside
|
|
2032
|
+
`sm` text the CAP HEIGHT is 11px, so a mark has to be ~13px to sit ON the line instead of
|
|
2033
|
+
straddling it — and at 13–16px a glyph with interior detail (`package`, `receipt`, `file-text`)
|
|
2034
|
+
closes up into a smudge. At 20px it reads, and its 16.7px of artwork then stands half again as
|
|
2035
|
+
tall as the capitals beside it, rising above them and dropping past the baseline. Three sizes,
|
|
2036
|
+
three different complaints, no size that answers both. Reach for a mark beside text only where
|
|
2037
|
+
the glyph is SIMPLE (`check`, a chevron, a dot) or the text is `md` or larger; a detailed glyph
|
|
2038
|
+
wants the block form, where it gets 28px and a line of its own. Here the anchoring is already
|
|
2039
|
+
done by the band's heading, which is the premise the whole variant rests on.
|
|
2040
|
+
|
|
2014
2041
|
### Buttons & action labels — the outcome, verb-first
|
|
2015
2042
|
|
|
2016
2043
|
A label names the OUTCOME of pressing — verb + object, 2–4 words (GOV.UK: "Save and continue",
|
package/package.json
CHANGED
package/src/empty_state.tsx
CHANGED
|
@@ -3,20 +3,64 @@ import { Text } from "./text";
|
|
|
3
3
|
import { Icon, type IconName } from "./icon";
|
|
4
4
|
import { colors } from "./colors";
|
|
5
5
|
|
|
6
|
-
interface
|
|
6
|
+
interface EmptyStateBase {
|
|
7
7
|
/** What's empty, in plain language ("Không có nhân viên nào khớp"). */
|
|
8
8
|
message: string;
|
|
9
9
|
/** What the user can do about it ("Thử từ khóa khác"). */
|
|
10
10
|
hint?: string;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The whole region: centred, generous, with an optional glyph anchoring it. */
|
|
14
|
+
interface EmptyRegionProps extends EmptyStateBase {
|
|
15
|
+
compact?: false;
|
|
16
|
+
/** Lucide glyph centred above the message — a visual anchor so the empty
|
|
17
|
+
* region reads as a deliberate state, not two lone lines of muted text. */
|
|
13
18
|
icon?: IconName;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
22
|
+
* ONE BAND of a record, rather than the whole region.
|
|
23
|
+
*
|
|
24
|
+
* The default reserves a screenful of quiet because it speaks for everything in
|
|
25
|
+
* view: nothing else is on the surface, so collapsing to two grey lines would
|
|
26
|
+
* read as a rendering bug. Inside a record that premise is false — the heading
|
|
27
|
+
* above it, the sections below it and the add verb beside it all say the surface
|
|
28
|
+
* rendered fine, and the only open question is what this one band holds.
|
|
29
|
+
*
|
|
30
|
+
* Measured on a four-section record where three bands can be empty at once, the
|
|
31
|
+
* default cost 178px per band and pushed the section a reader had opened the
|
|
32
|
+
* record FOR below the fold on a 768px screen. Compact says the same thing in
|
|
33
|
+
* 56, and sets to the LEFT — a band's heading already establishes that edge, and
|
|
34
|
+
* a centred block under a left-aligned heading reads as a fragment of another
|
|
35
|
+
* page.
|
|
36
|
+
*
|
|
37
|
+
* IT TAKES NO GLYPH, and that is a property of this icon set rather than a
|
|
38
|
+
* preference. The set is lucide at a fixed stroke on a 24 viewBox, so its
|
|
39
|
+
* artwork fills 20/24 of whatever box it is given:
|
|
40
|
+
*
|
|
41
|
+
* - beside `sm` text the CAP HEIGHT is 11px, so an inline mark has to be
|
|
42
|
+
* ~13px to sit ON the line rather than straddle it;
|
|
43
|
+
* - at 13-16px a glyph with interior detail (`package`, `receipt`,
|
|
44
|
+
* `file-text`) closes up and reads as a smudge;
|
|
45
|
+
* - at 20px it reads, but its artwork is 16.7px against that 11px cap — half
|
|
46
|
+
* again as tall as the capitals, rising above them and dropping past the
|
|
47
|
+
* baseline.
|
|
48
|
+
*
|
|
49
|
+
* There is no size that is both legible and aligned, which is why tuning it
|
|
50
|
+
* three times produced three different complaints. The anchor's job — "this
|
|
51
|
+
* region is deliberate, not stray text" — is already done here by the heading
|
|
52
|
+
* and its add button, which is the very premise this variant rests on. So the
|
|
53
|
+
* type refuses the prop rather than the component silently dropping it.
|
|
54
|
+
*/
|
|
55
|
+
interface EmptyBandProps extends EmptyStateBase {
|
|
56
|
+
compact: true;
|
|
57
|
+
icon?: never;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type EmptyStateProps = EmptyRegionProps | EmptyBandProps;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Placeholder for an empty list, filter result, or record band.
|
|
20
64
|
*
|
|
21
65
|
* IT CARRIES NO VERB — the prop is gone, not narrowed. This took an
|
|
22
66
|
* `action?: ReactNode` and two different things went into it, both wrong here:
|
|
@@ -35,6 +79,17 @@ interface EmptyStateProps {
|
|
|
35
79
|
* filters that emptied it carry their own clear.
|
|
36
80
|
*/
|
|
37
81
|
export function EmptyState(props: EmptyStateProps) {
|
|
82
|
+
if (props.compact) {
|
|
83
|
+
return (
|
|
84
|
+
// `tight` on BOTH lines — the two are one object, and prose leading puts
|
|
85
|
+
// enough empty box between them to read as two (§ Typography). The pair
|
|
86
|
+
// measured 44px for 26px of type before this.
|
|
87
|
+
<View style={styles.compactContainer}>
|
|
88
|
+
<Text size="sm" color="muted" leading="tight">{props.message}</Text>
|
|
89
|
+
{props.hint ? <Text size="xs" color="muted" leading="tight">{props.hint}</Text> : null}
|
|
90
|
+
</View>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
38
93
|
return (
|
|
39
94
|
<View style={styles.container}>
|
|
40
95
|
{props.icon ? (
|
|
@@ -70,4 +125,8 @@ const styles = StyleSheet.create({
|
|
|
70
125
|
line: {
|
|
71
126
|
textAlign: "center",
|
|
72
127
|
},
|
|
128
|
+
compactContainer: {
|
|
129
|
+
paddingVertical: 8,
|
|
130
|
+
gap: 2,
|
|
131
|
+
},
|
|
73
132
|
});
|
package/src/list_item.tsx
CHANGED
|
@@ -57,15 +57,29 @@ export function ListItem(props: ListItemProps) {
|
|
|
57
57
|
const body = (
|
|
58
58
|
<>
|
|
59
59
|
{left}
|
|
60
|
+
{/* A STACKED PAIR, and it was rendered as prose. Both lines are `sm`, so
|
|
61
|
+
the rungs were never the problem — the LEADING was: two 24px prose line
|
|
62
|
+
boxes around 14px glyphs leave 5px of empty box above and below each,
|
|
63
|
+
which is 10px between them, and a `gap: 2` on the container made 12. The
|
|
64
|
+
composition rule this surface is supposed to follow puts a pair at 6:
|
|
65
|
+
`leading="tight"` on both body-rung lines and NO gap, because each tight
|
|
66
|
+
box already contributes 3px a side. Twice the intended distance is what
|
|
67
|
+
makes a name and its role read as two separate lines rather than one
|
|
68
|
+
object, which is how the defect gets reported — as "too far apart",
|
|
69
|
+
never as its cause.
|
|
70
|
+
|
|
71
|
+
`tight` goes on the string branch only. A caller passing a NODE owns its
|
|
72
|
+
own leading, and forcing one on somebody else's subtree would silently
|
|
73
|
+
re-space a description that is not a line of text at all. */}
|
|
60
74
|
<View style={styles.textContainer}>
|
|
61
75
|
{!!title && (
|
|
62
|
-
<Text weight="medium" numberOfLines={1} userSelect="none">
|
|
76
|
+
<Text weight="medium" leading="tight" numberOfLines={1} userSelect="none">
|
|
63
77
|
{title}
|
|
64
78
|
</Text>
|
|
65
79
|
)}
|
|
66
80
|
{!!description &&
|
|
67
81
|
(typeof description === "string" ? (
|
|
68
|
-
<Text size="sm" color="zinc-500" numberOfLines={1} userSelect="none">
|
|
82
|
+
<Text size="sm" color="zinc-500" leading="tight" numberOfLines={1} userSelect="none">
|
|
69
83
|
{description}
|
|
70
84
|
</Text>
|
|
71
85
|
) : (
|
|
@@ -157,18 +171,27 @@ function PressRow(props: {
|
|
|
157
171
|
}
|
|
158
172
|
|
|
159
173
|
const styles = StyleSheet.create({
|
|
174
|
+
// 56, not 64. The height was sized around a pair rendered as prose — two 24px
|
|
175
|
+
// boxes plus a 2px gap is 50, and 64 left 14px of slack the row did not ask
|
|
176
|
+
// for, which is what made a correctly-sized `md` mark (28px, the documented
|
|
177
|
+
// rung for a person in a list) look adrift in it. A tight pair is 40, so 56
|
|
178
|
+
// gives it the same 8-a-side the rest of the kit's rows carry. A title-only
|
|
179
|
+
// row keeps one 20px line centred in the same box, so the two shapes still
|
|
180
|
+
// share a beat.
|
|
160
181
|
container: {
|
|
161
182
|
flexDirection: "row",
|
|
162
183
|
alignItems: "center",
|
|
163
184
|
gap: 8,
|
|
164
|
-
height:
|
|
185
|
+
height: 56,
|
|
165
186
|
paddingHorizontal: 8,
|
|
166
187
|
marginHorizontal: -8,
|
|
167
188
|
borderRadius: 12,
|
|
168
189
|
},
|
|
190
|
+
// NO GAP. Each tight line box already contributes 3px above and below its
|
|
191
|
+
// glyphs, so the pair reads at 6px — a gap here is a third source of space
|
|
192
|
+
// between two lines meant to read as one object.
|
|
169
193
|
textContainer: {
|
|
170
194
|
flex: 1,
|
|
171
|
-
gap: 2,
|
|
172
195
|
alignItems: "flex-start",
|
|
173
196
|
},
|
|
174
197
|
selected: {
|