@lotics/ui 46.12.0 → 46.13.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/catalog.md +35 -0
- package/package.json +3 -1
- package/src/avatar_group.tsx +2 -29
- package/src/mark_stack.ts +54 -0
- package/src/thumbnail_stack.tsx +151 -0
package/docs/catalog.md
CHANGED
|
@@ -125,6 +125,28 @@ someone wants `md`. Initials are the first and **last** word of the name — "V
|
|
|
125
125
|
Watson" → MW — because a Vietnamese name's first two words are the family and middle names
|
|
126
126
|
that a whole office shares.
|
|
127
127
|
|
|
128
|
+
### Several THINGS in one cell — `ThumbnailStack`
|
|
129
|
+
|
|
130
|
+
`ThumbnailStack` is `AvatarGroup`'s sibling over a different subject: a capped, overlapping set
|
|
131
|
+
of PICTURES answering "an order of what?", "which parts?", "what arrived?" in one register cell.
|
|
132
|
+
Pass `{ id, label, image }` per thing; `max` (default 3) caps it and the rest become the `+N`
|
|
133
|
+
mark at the end of the stack. Both components read their separating edge from `mark_stack.ts`,
|
|
134
|
+
so a row carrying people in one column and goods in the next reads as one device used twice
|
|
135
|
+
rather than two — but they **tuck by different fractions, and that is deliberate**: a face is
|
|
136
|
+
edge-anchored and survives being covered two thirds of the way, while a product shot centres its
|
|
137
|
+
subject in studio margin, so the facepile's ratio lands the remainder on top of the last
|
|
138
|
+
photograph. Both ratios live in `mark_stack.ts` beside each other, with the reason.
|
|
139
|
+
|
|
140
|
+
What differs from a facepile follows from a thing not being a person: the marks are ROUNDED
|
|
141
|
+
SQUARES (round is the shape the kit spends on a face), and there is **no initials fallback** —
|
|
142
|
+
"CA" is not a stand-in for a bag of coffee, so a thing with no picture renders a neutral
|
|
143
|
+
placeholder and its `label` carries the meaning to a screen reader. Inert, like the facepile:
|
|
144
|
+
its home is a pressable row and it must not hold a second destination.
|
|
145
|
+
|
|
146
|
+
**Reach for it wherever a row states a COUNT of things it could show instead.** A register whose
|
|
147
|
+
rows carry a bare number reads as a ledger of nothing in particular; the pictures are the row's
|
|
148
|
+
own data, and they say what the business is before a word is read.
|
|
149
|
+
|
|
128
150
|
### A select-field value
|
|
129
151
|
|
|
130
152
|
`OptionBadge` (a stored `select` value as its CONFIGURED colored badge) — never hand-map
|
|
@@ -691,6 +713,19 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
691
713
|
- **`group_avatar`** — `GroupAvatar`: a name's initials in a zinc rounded square (`size` — the
|
|
692
714
|
same rung scale AND the same initials rule as `Avatar`, default `md`); the avatar for
|
|
693
715
|
image-less entities — groups, organizations. A person → `Avatar`/`MemberChip`.
|
|
716
|
+
- **`thumbnail_stack`** — `ThumbnailStack`: the same capped stack over THINGS rather than
|
|
717
|
+
parties — a set of product, document or unit pictures answering "an order of what?" in one
|
|
718
|
+
register cell. Takes `{ id, label, image? }`, shows `max` (default 3) and makes the rest the
|
|
719
|
+
`+N` mark at the END of the stack. Rounded squares, not discs, and **no initials fallback**: a
|
|
720
|
+
name is a fair stand-in for a face and a useless one for a bag of coffee, so a picture-less
|
|
721
|
+
thing renders a neutral placeholder while its `label` carries the meaning to a screen reader.
|
|
722
|
+
Marks `announce` (the stack shows no text) and it is INERT, for the same reason the facepile
|
|
723
|
+
is. Reach for it wherever a row states a COUNT of things it could show instead.
|
|
724
|
+
- **`mark_stack`** — the constants a capped stack of marks is built from: `MARK_RING` (the
|
|
725
|
+
translucent separating edge, shared, so the two stacks read as one device) plus a tuck ratio
|
|
726
|
+
EACH — `MARK_OVERLAP_RATIO` for faces and the shallower `THUMBNAIL_OVERLAP_RATIO` for things,
|
|
727
|
+
because a face survives being covered two thirds of the way and a centred product shot does
|
|
728
|
+
not. They sit together so the difference reads as a decision rather than a typo.
|
|
694
729
|
- **`avatar_group`** — `AvatarGroup`: a SET of parties as overlapping marks — "who is on this" in
|
|
695
730
|
one cell. Takes `{ id, name, kind: "person" | "group", image? }`, shows `max` of them (default
|
|
696
731
|
3) and makes the rest a `+N` that is the LAST MARK IN THE STACK, not loose text beside it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "46.
|
|
3
|
+
"version": "46.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./accordion": "./src/accordion.tsx",
|
|
@@ -171,6 +171,7 @@
|
|
|
171
171
|
"./list_item": "./src/list_item.tsx",
|
|
172
172
|
"./loading": "./src/loading.tsx",
|
|
173
173
|
"./locale": "./src/locale.tsx",
|
|
174
|
+
"./mark_stack": "./src/mark_stack.ts",
|
|
174
175
|
"./markdown": {
|
|
175
176
|
"react-native": "./src/markdown.tsx",
|
|
176
177
|
"default": "./src/markdown.web.tsx"
|
|
@@ -279,6 +280,7 @@
|
|
|
279
280
|
"react-native": "./src/theme.ts",
|
|
280
281
|
"default": "./src/theme.web.ts"
|
|
281
282
|
},
|
|
283
|
+
"./thumbnail_stack": "./src/thumbnail_stack.tsx",
|
|
282
284
|
"./time_columns": "./src/time_columns.tsx",
|
|
283
285
|
"./time_options": "./src/time_options.ts",
|
|
284
286
|
"./time_picker": "./src/time_picker.tsx",
|
package/src/avatar_group.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { Text } from "./text";
|
|
|
5
5
|
import { AVATAR_PX, AVATAR_TEXT, type AvatarSize } from "./avatar_size";
|
|
6
6
|
import { colors } from "./colors";
|
|
7
7
|
import { useLoticsLocale } from "./locale";
|
|
8
|
+
import { MARK_OVERLAP_RATIO, MARK_RING } from "./mark_stack";
|
|
8
9
|
|
|
9
10
|
/** One party in the set. A PERSON takes the disc; a group or an organization
|
|
10
11
|
* takes the rounded square, which is `Avatar`'s own shape axis and the one
|
|
@@ -25,34 +26,6 @@ export interface AvatarGroupPrincipal {
|
|
|
25
26
|
* separable at a glance and a number is the more honest summary. */
|
|
26
27
|
const DEFAULT_MAX = 3;
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
* How far each mark tucks under the one before it, as a fraction of the rung.
|
|
30
|
-
*
|
|
31
|
-
* A FRACTION, not a pixel constant, so the stack is the same object at every
|
|
32
|
-
* rung — a fixed inset barely stacks at `lg` and eats the marks at `sm`. Just
|
|
33
|
-
* under a third leaves each mark's whole leading edge, which is as much of an
|
|
34
|
-
* image or a pair of initials as the reader needs to tell the parties apart.
|
|
35
|
-
*/
|
|
36
|
-
const OVERLAP_RATIO = 0.3;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The hairline separating each mark from the one it covers.
|
|
40
|
-
*
|
|
41
|
-
* TRANSLUCENT INK, never the surface colour — and that is what lets this
|
|
42
|
-
* component overlap at all. The conventional facepile rings each mark in the
|
|
43
|
-
* page background so the discs read as separate, which binds the ring to a
|
|
44
|
-
* colour the row does not keep: a register row washes to `zinc[50]` on hover,
|
|
45
|
-
* `zinc[100]` as the open record and `zinc[200]` while pressed, so a white
|
|
46
|
-
* ring is a halo in three of the four states — and those are the states a
|
|
47
|
-
* reader is in when they are actually looking at the row. A translucent dark
|
|
48
|
-
* edge DARKENS whatever is behind it instead of matching it, so it holds on
|
|
49
|
-
* every ground. `FileBadge` separates itself from its own surface the same way.
|
|
50
|
-
*
|
|
51
|
-
* A `boxShadow` rather than a border, because a border grows the box: a stacked
|
|
52
|
-
* mark would render 2px wider than the identical mark in the next column.
|
|
53
|
-
*/
|
|
54
|
-
const MARK_RING = "0 0 0 1px rgba(0,0,0,0.12)";
|
|
55
|
-
|
|
56
29
|
/**
|
|
57
30
|
* A SET of parties as marks — the answer to "who is on this" in one cell.
|
|
58
31
|
*
|
|
@@ -116,7 +89,7 @@ export function AvatarGroup(props: AvatarGroupProps) {
|
|
|
116
89
|
const shown = principals.slice(0, max);
|
|
117
90
|
const hidden = principals.length - shown.length;
|
|
118
91
|
const px = AVATAR_PX[size];
|
|
119
|
-
const overlap = Math.round(px *
|
|
92
|
+
const overlap = Math.round(px * MARK_OVERLAP_RATIO);
|
|
120
93
|
// Descending, so the FIRST mark ends up on top. The remainder is one more
|
|
121
94
|
// thing to lay under, hence its own slot in the depth.
|
|
122
95
|
const depth = shown.length + (hidden > 0 ? 1 : 0);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two constants a capped stack of marks is built from.
|
|
3
|
+
*
|
|
4
|
+
* They live here, side by side, because there are two stacks — a set of PARTIES
|
|
5
|
+
* (`avatar_group`) and a set of THINGS (`thumbnail_stack`) — and the decision
|
|
6
|
+
* each constant encodes is only legible NEXT TO the other one. The edge is
|
|
7
|
+
* shared: a stack that separated itself differently would read as a second
|
|
8
|
+
* device rather than as the same one over a different subject. The tuck is NOT,
|
|
9
|
+
* and the two ratios sitting together is what stops the difference looking like
|
|
10
|
+
* a typo in one of them.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* How far one FACE tucks under the one before it, as a fraction of the rung.
|
|
15
|
+
*
|
|
16
|
+
* A FRACTION, not a pixel constant, so the stack is the same object at every
|
|
17
|
+
* rung — a fixed inset barely stacks at `lg` and eats the marks at `sm`. Just
|
|
18
|
+
* under a third leaves each mark's whole leading edge, which is as much of an
|
|
19
|
+
* image or a pair of initials as the reader needs to tell the marks apart.
|
|
20
|
+
*/
|
|
21
|
+
export const MARK_OVERLAP_RATIO = 0.3;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The same tuck, for a stack of THINGS — and it is deliberately shallower.
|
|
25
|
+
*
|
|
26
|
+
* A face is edge-anchored: hair, a jaw, a colour, all readable from the leading
|
|
27
|
+
* third, which is what lets a facepile overlap so hard. A product shot is the
|
|
28
|
+
* opposite — the subject is CENTRED with studio margin around it, so a third of
|
|
29
|
+
* the tile is exactly the part that carries the object. At the face ratio the
|
|
30
|
+
* remainder lands on top of the last photograph and reads as covering it rather
|
|
31
|
+
* than as following it, which is the one thing the mark must not do: it is a
|
|
32
|
+
* statement ABOUT the set, not a member of it competing for the same glance.
|
|
33
|
+
*
|
|
34
|
+
* Shallow rather than zero, because the marks still have to read as one object.
|
|
35
|
+
*/
|
|
36
|
+
export const THUMBNAIL_OVERLAP_RATIO = 0.15;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The hairline separating each mark from the one it covers.
|
|
40
|
+
*
|
|
41
|
+
* TRANSLUCENT INK, never the surface colour — and that is what lets a stack
|
|
42
|
+
* overlap at all. The conventional facepile rings each mark in the page
|
|
43
|
+
* background so the marks read as separate, which binds the ring to a colour
|
|
44
|
+
* the row does not keep: a register row washes to `zinc[50]` on hover,
|
|
45
|
+
* `zinc[100]` as the open record and `zinc[200]` while pressed, so a white ring
|
|
46
|
+
* is a halo in three of the four states — and those are the states a reader is
|
|
47
|
+
* in when they are actually looking at the row. A translucent dark edge
|
|
48
|
+
* DARKENS whatever is behind it instead of matching it, so it holds on every
|
|
49
|
+
* ground. `FileBadge` separates itself from its own surface the same way.
|
|
50
|
+
*
|
|
51
|
+
* A `boxShadow` rather than a border, because a border grows the box: a stacked
|
|
52
|
+
* mark would render 2px wider than the identical mark in the next column.
|
|
53
|
+
*/
|
|
54
|
+
export const MARK_RING = "0 0 0 1px rgba(0,0,0,0.12)";
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Image, View, type StyleProp, type ViewStyle } from "react-native";
|
|
2
|
+
import { Icon } from "./icon";
|
|
3
|
+
import { Text } from "./text";
|
|
4
|
+
import { AVATAR_PX, AVATAR_TEXT, type AvatarSize } from "./avatar_size";
|
|
5
|
+
import { colors } from "./colors";
|
|
6
|
+
import { useLoticsLocale } from "./locale";
|
|
7
|
+
import { MARK_RING, THUMBNAIL_OVERLAP_RATIO } from "./mark_stack";
|
|
8
|
+
|
|
9
|
+
/** One THING in the set — a product, a document, a unit. Not a party: a thing
|
|
10
|
+
* has no initials worth reading, so it is its picture or it is a placeholder. */
|
|
11
|
+
export interface ThumbnailStackItem {
|
|
12
|
+
/** Stable identity — the React key. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** What the thing is, for the screen reader. The stack shows no text. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** The thing's own picture. Absent renders the neutral placeholder. */
|
|
17
|
+
image?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Three reads as "a few"; past that the pictures stop being separable at a
|
|
21
|
+
* glance and a number is the more honest summary. Same default as a facepile,
|
|
22
|
+
* for the same reason. */
|
|
23
|
+
const DEFAULT_MAX = 3;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A SET of THINGS as pictures — the answer to "an order of what?" in one cell.
|
|
27
|
+
*
|
|
28
|
+
* The sibling of `AvatarGroup`, over a different subject. The two share their
|
|
29
|
+
* separating edge (`mark_stack.ts`) so a row carrying both reads as one device
|
|
30
|
+
* used twice. What differs is everything that follows from a thing not being a
|
|
31
|
+
* person — including how hard they tuck:
|
|
32
|
+
*
|
|
33
|
+
* **A shallower overlap.** A face is edge-anchored and survives being covered
|
|
34
|
+
* two thirds of the way; a product shot centres its subject in studio margin,
|
|
35
|
+
* so the face ratio puts the remainder on top of the last photograph. Both
|
|
36
|
+
* ratios live in `mark_stack.ts` beside each other, with the reason.
|
|
37
|
+
*
|
|
38
|
+
* **Square, not round.** Round is the shape the kit spends on a person's face.
|
|
39
|
+
* A rounded square is what `Avatar` already gives an organization, and a crate
|
|
40
|
+
* of goods belongs on that side of the axis.
|
|
41
|
+
*
|
|
42
|
+
* **No initials.** `Avatar` falls back to a person's initials because a name is
|
|
43
|
+
* a reasonable stand-in for a face. "CA" is not a stand-in for a bag of coffee
|
|
44
|
+
* — it is noise wearing the shape of information — so a thing with no picture
|
|
45
|
+
* renders a neutral placeholder that says only "something is here", and the
|
|
46
|
+
* label carries the meaning to anyone listening.
|
|
47
|
+
*
|
|
48
|
+
* **The remainder is the last mark IN the stack**, not text after it: `+9`
|
|
49
|
+
* trailing the pictures as loose type reads as a separate fact about the row
|
|
50
|
+
* instead of as the rest of this one.
|
|
51
|
+
*
|
|
52
|
+
* **It is INERT.** Its home is a register row, and a row that already presses
|
|
53
|
+
* must not hold a second destination four pixels to the left.
|
|
54
|
+
*
|
|
55
|
+
* **It caps what it was GIVEN.** The overflow counts from what will actually
|
|
56
|
+
* render, so `+9` never counts a mark the reader can see is missing. Drop what
|
|
57
|
+
* you cannot show before you pass it in.
|
|
58
|
+
*
|
|
59
|
+
* Pure: pass resolved labels and image urls; this fetches nothing.
|
|
60
|
+
*/
|
|
61
|
+
export interface ThumbnailStackProps {
|
|
62
|
+
items: readonly ThumbnailStackItem[];
|
|
63
|
+
/** Pictures shown before the remainder becomes `+N`. Default 3. */
|
|
64
|
+
max?: number;
|
|
65
|
+
/**
|
|
66
|
+
* A rung on the shared avatar scale — the same scale the marks in the
|
|
67
|
+
* neighbouring columns take, so a row of goods and a row of people sit on one
|
|
68
|
+
* beat. Everything inside derives from it; nothing here is a fixed pixel.
|
|
69
|
+
*/
|
|
70
|
+
size?: AvatarSize;
|
|
71
|
+
style?: StyleProp<ViewStyle>;
|
|
72
|
+
testID?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function ThumbnailStack(props: ThumbnailStackProps) {
|
|
76
|
+
const { items, max = DEFAULT_MAX, size = "md", style, testID } = props;
|
|
77
|
+
const locale = useLoticsLocale();
|
|
78
|
+
|
|
79
|
+
if (items.length === 0) return null;
|
|
80
|
+
|
|
81
|
+
const shown = items.slice(0, max);
|
|
82
|
+
const hidden = items.length - shown.length;
|
|
83
|
+
const px = AVATAR_PX[size];
|
|
84
|
+
const overlap = Math.round(px * THUMBNAIL_OVERLAP_RATIO);
|
|
85
|
+
const radius = Math.round(px * 0.2);
|
|
86
|
+
// Descending, so the FIRST picture ends up on top and the remainder lies
|
|
87
|
+
// under everything rather than covering one of the things it is counting.
|
|
88
|
+
const depth = shown.length + (hidden > 0 ? 1 : 0);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<View testID={testID} style={[{ flexDirection: "row", alignItems: "center" }, style]}>
|
|
92
|
+
{shown.map((item, index) => (
|
|
93
|
+
<View
|
|
94
|
+
key={item.id}
|
|
95
|
+
accessible
|
|
96
|
+
accessibilityLabel={item.label}
|
|
97
|
+
style={{
|
|
98
|
+
width: px,
|
|
99
|
+
height: px,
|
|
100
|
+
marginLeft: index === 0 ? 0 : -overlap,
|
|
101
|
+
// `zIndex` needs a positioned box to take effect in
|
|
102
|
+
// react-native-web; without it the marks stack in DOM order.
|
|
103
|
+
position: "relative",
|
|
104
|
+
zIndex: depth - index,
|
|
105
|
+
borderRadius: radius,
|
|
106
|
+
overflow: "hidden",
|
|
107
|
+
backgroundColor: colors.zinc["100"],
|
|
108
|
+
alignItems: "center",
|
|
109
|
+
justifyContent: "center",
|
|
110
|
+
boxShadow: MARK_RING,
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
{item.image ? (
|
|
114
|
+
<Image
|
|
115
|
+
source={{ uri: item.image }}
|
|
116
|
+
style={{ width: px, height: px }}
|
|
117
|
+
resizeMode="cover"
|
|
118
|
+
accessibilityIgnoresInvertColors
|
|
119
|
+
/>
|
|
120
|
+
) : (
|
|
121
|
+
<Icon name="package" size={Math.round(px * 0.5)} color={colors.zinc["400"]} />
|
|
122
|
+
)}
|
|
123
|
+
</View>
|
|
124
|
+
))}
|
|
125
|
+
{hidden > 0 && (
|
|
126
|
+
<View
|
|
127
|
+
accessible
|
|
128
|
+
accessibilityLabel={locale.avatar.more(hidden)}
|
|
129
|
+
style={{
|
|
130
|
+
width: px,
|
|
131
|
+
height: px,
|
|
132
|
+
marginLeft: shown.length === 0 ? 0 : -overlap,
|
|
133
|
+
position: "relative",
|
|
134
|
+
zIndex: 0,
|
|
135
|
+
borderRadius: radius,
|
|
136
|
+
backgroundColor: colors.zinc["100"],
|
|
137
|
+
alignItems: "center",
|
|
138
|
+
justifyContent: "center",
|
|
139
|
+
boxShadow: MARK_RING,
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{/* The rung's own type — a fixed size here is how a stack ends up
|
|
143
|
+
with a 14px count beside 12px marks. */}
|
|
144
|
+
<Text size={AVATAR_TEXT[size]} weight="medium" color="zinc-700" aria-hidden>
|
|
145
|
+
{`+${hidden}`}
|
|
146
|
+
</Text>
|
|
147
|
+
</View>
|
|
148
|
+
)}
|
|
149
|
+
</View>
|
|
150
|
+
);
|
|
151
|
+
}
|