@lotics/ui 45.6.0 → 45.7.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 +6 -4
- package/docs/catalog.md +22 -6
- package/package.json +1 -1
- package/src/brand_mark.tsx +35 -1
- package/src/list_item.tsx +27 -4
- package/src/switch_button.tsx +10 -2
package/AGENTS.md
CHANGED
|
@@ -104,10 +104,12 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
104
104
|
`error={overdue ? … }` is the tell: it makes a date due tomorrow read like one due in three
|
|
105
105
|
months. → [catalog.md §Dates & times](./docs/catalog.md).
|
|
106
106
|
- **A third-party channel takes `BrandMark`, our own glyphs take `Icon`.** `@lotics/ui/brand_mark`
|
|
107
|
-
draws Facebook / Zalo / LinkedIn / X / Google Meet
|
|
108
|
-
scanning a feed recognises the logo before the word. The
|
|
109
|
-
with no mark (a phone call, an event, a website) fails to
|
|
110
|
-
instead of borrowing another company's logo.
|
|
107
|
+
draws Facebook / Zalo / LinkedIn / X / Google Meet / Gmail / Outlook / Google Drive as solid
|
|
108
|
+
single-colour silhouettes — a reader scanning a feed recognises the logo before the word. The
|
|
109
|
+
names are an allowlist, so a channel with no mark (a phone call, an event, a website) fails to
|
|
110
|
+
typecheck and falls back to `Icon` instead of borrowing another company's logo. `SwitchButton`
|
|
111
|
+
takes one directly as `brand`, for a row naming an outside service.
|
|
112
|
+
→ [catalog.md §Text & formatting](./docs/catalog.md).
|
|
111
113
|
- **The kit's fonts/colors/icons ARE the design system** — never a custom font, icon set, or
|
|
112
114
|
hand-picked palette shade; color is `solid`/`tint`/`ramp` with ONE accent per screen.
|
|
113
115
|
- **Hand-typed type is off-system — and it always lands too small.** Every run of language is
|
package/docs/catalog.md
CHANGED
|
@@ -476,13 +476,17 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
476
476
|
`SubsectionHeading` instead. Pick between this and `Eyebrow` by what is LABELLED — a block or a
|
|
477
477
|
value; composition.md § Eyebrow vs group lead has the table.
|
|
478
478
|
- **`brand_mark`** — `BrandMark`: an OUTSIDE brand's mark, naming the channel a touchpoint
|
|
479
|
-
happened on — `facebook`, `zalo`, `linkedin`, `x`,
|
|
479
|
+
happened on or the service a connection reaches — `facebook`, `zalo`, `linkedin`, `x`,
|
|
480
|
+
`google-meet`, `gmail`, `outlook`, `google-drive`. Solid single-colour silhouettes in
|
|
480
481
|
each brand's own hex, not the full-colour logos: a byline draws these at 14–20px and a
|
|
481
482
|
five-colour logo turns to mud there. `size` (default 16) and `muted` (draw in the text colour).
|
|
482
|
-
The name list is an ALLOWLIST —
|
|
483
|
-
|
|
483
|
+
The name list is an ALLOWLIST — `BRAND_NAMES` is the array and `BrandName` derives from it, so a
|
|
484
|
+
channel with no mark will not typecheck and the call site falls back to a generic `Icon` rather
|
|
485
|
+
than rendering the wrong company's logo. Not for the app's
|
|
484
486
|
own glyphs; that is `icon`. `x` is the one mark whose brand colour is BLACK, so it is the one
|
|
485
|
-
that needs `muted` on a dark ground; the rest carry their own hue either way.
|
|
487
|
+
that needs `muted` on a dark ground; the rest carry their own hue either way. Where a brand's
|
|
488
|
+
official mark is a composite (Outlook's envelope-and-O), the entry carries the half that stays
|
|
489
|
+
legible small — the identifying monogram — because a shape nobody can read is not the mark.
|
|
486
490
|
- **`markdown`** — `Markdown`: the single canonical markdown renderer for chat, apps, and
|
|
487
491
|
`AgentRun`; rich GFM markdown on web with copyable tables, plain-text on
|
|
488
492
|
native; takes a markdown `children` string. **`variant`** decides whose type
|
|
@@ -1059,9 +1063,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1059
1063
|
`DatePicker` outside a FormField must be given `accessibilityLabel` explicitly; range
|
|
1060
1064
|
formats suffix each half with the locale's start/end-date names.
|
|
1061
1065
|
- **`switch_button`** — `SwitchButton`: the full-ROW toggle — a `PressableHighlight` row
|
|
1062
|
-
(optional
|
|
1066
|
+
(optional leading mark + medium title left, `Switch` pinned right) where the whole row IS the
|
|
1063
1067
|
switch (`accessibilityRole="switch"`, the inner Switch read-only). The settings-panel/menu
|
|
1064
|
-
row toggle.
|
|
1068
|
+
row toggle. The leading slot takes EITHER `icon` (an `IconName`, the app's own glyph language)
|
|
1069
|
+
or `brand` (a `BrandName`, an outside service's mark); `icon` wins if both are passed. A row
|
|
1070
|
+
naming a third-party service takes `brand` — the two sets are deliberately separate, so a
|
|
1071
|
+
connector row cannot reach for a Lotics glyph to stand in for a company's logo.
|
|
1065
1072
|
- **`use_form`** — `useForm`: THE batch draft-form state hook — `values` = `initialValues` +
|
|
1066
1073
|
an edits overlay (a revalidation refreshes untouched fields, no sync effect), `validate`
|
|
1067
1074
|
(sync/async, gates submit, editing clears the field's error), `onSubmit(values, helpers)`
|
|
@@ -1477,6 +1484,15 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1477
1484
|
when a slot is passed: `right` is routinely conditional, and an anatomy that flipped on that
|
|
1478
1485
|
would make two rows in one list press differently. Nothing to opt into — a decorative
|
|
1479
1486
|
`right` simply stops being part of the press target.
|
|
1487
|
+
**The row OWNS its `title`/`description` pair, so do not hand-roll one beside it.** Both
|
|
1488
|
+
lines are `sm`, `leading="tight"`, no gap between them — the stacked-pair law in
|
|
1489
|
+
[composition.md](./composition.md), applied by the component rather than restated at each
|
|
1490
|
+
call site. It shipped rendering that pair as PROSE (two 24px boxes plus a 2px gap = 12px
|
|
1491
|
+
between the lines, against the 6px the rule prescribes), and the height was sized around
|
|
1492
|
+
that mistake at 64, which left a correctly-sized `md` mark adrift in it; the row is 56 now.
|
|
1493
|
+
A `description` passed as a NODE keeps its own leading — the component only sets it on the
|
|
1494
|
+
string branch, because forcing a line box on somebody else's subtree re-spaces content that
|
|
1495
|
+
may not be a line of text at all.
|
|
1480
1496
|
- **`table`** — `Table` + `TableRow` + `TableCell`: the paginated high-volume register
|
|
1481
1497
|
(columns defined once, `sortLabels` localizable; rows are `PressableRow`-based).
|
|
1482
1498
|
CONTAINER-RESPONSIVE with no prop (measures itself, like `Breakdown`/`DetailTable`): when
|
package/package.json
CHANGED
package/src/brand_mark.tsx
CHANGED
|
@@ -7,7 +7,18 @@ import { Svg, Path } from "react-native-svg";
|
|
|
7
7
|
* email that is not tied to one provider. Inventing a mark for those is how a
|
|
8
8
|
* byline ends up claiming a channel the record does not carry.
|
|
9
9
|
*/
|
|
10
|
-
export
|
|
10
|
+
export const BRAND_NAMES = [
|
|
11
|
+
"facebook",
|
|
12
|
+
"zalo",
|
|
13
|
+
"linkedin",
|
|
14
|
+
"x",
|
|
15
|
+
"google-meet",
|
|
16
|
+
"gmail",
|
|
17
|
+
"outlook",
|
|
18
|
+
"google-drive",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export type BrandName = (typeof BRAND_NAMES)[number];
|
|
11
22
|
|
|
12
23
|
interface Mark {
|
|
13
24
|
/** The brand's own colour. A mark drawn in anything else is not the mark. */
|
|
@@ -52,6 +63,29 @@ const MARKS: Record<BrandName, Mark> = {
|
|
|
52
63
|
color: "#00897B",
|
|
53
64
|
path: "M5.53 2.13 0 7.75h5.53zm.398 0v5.62h7.608v3.65l5.47-4.45c-.014-1.22.031-2.25-.025-3.46-.148-1.09-1.287-1.47-2.236-1.36zM23.1 4.32c-.802.295-1.358.995-2.047 1.49-2.506 2.05-4.982 4.12-7.468 6.19 3.025 2.59 6.04 5.18 9.065 7.76 1.218.671 1.428-.814 1.328-1.64v-13a.828.828 0 0 0-.877-.825zM.038 8.15v7.7h5.53v-7.7zm13.577 8.1H6.008v5.62c3.864-.006 7.737.011 11.58-.009 1.02-.07 1.618-1.12 1.468-2.07v-2.51l-5.47-4.68v3.65zm-13.577 0c.02 1.44-.041 2.88.033 4.31.162.948 1.158 1.43 2.047 1.31h3.464v-5.62z",
|
|
54
65
|
},
|
|
66
|
+
// The envelope silhouette, in Gmail's red. The full mark is five colours
|
|
67
|
+
// meeting along the flap's diagonals — at 16px those seams alias into a
|
|
68
|
+
// brown smear, which is precisely the case the note above is about.
|
|
69
|
+
gmail: {
|
|
70
|
+
color: "#EA4335",
|
|
71
|
+
path: "M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.909 1.528-1.146C21.69 2.28 24 3.434 24 5.457z",
|
|
72
|
+
},
|
|
73
|
+
// The bare "O" monogram — an oval ring, drawn as two subpaths of OPPOSITE
|
|
74
|
+
// winding so the nonzero fill leaves the counter open. Simple Icons' Outlook
|
|
75
|
+
// path is the envelope-and-O composite, and it was tried here first: at 20px
|
|
76
|
+
// it collapses into a blue rectangle with no readable feature, and it is not
|
|
77
|
+
// much better at 64. The O alone is what identifies the brand at a glance and
|
|
78
|
+
// it is the half that survives the size.
|
|
79
|
+
outlook: {
|
|
80
|
+
color: "#0078D4",
|
|
81
|
+
path: "M12 1.2a8.7 10.8 0 1 0 0 21.6 8.7 10.8 0 1 0 0-21.6ZM12 6.6a4.1 5.4 0 1 1 0 10.8 4.1 5.4 0 1 1 0-10.8Z",
|
|
82
|
+
},
|
|
83
|
+
// Drive's triangle, drawn as one silhouette. The three-colour original is the
|
|
84
|
+
// same aliasing problem as Gmail's, and worse — its colours meet at a point.
|
|
85
|
+
"google-drive": {
|
|
86
|
+
color: "#1FA463",
|
|
87
|
+
path: "M12.01 1.485c-2.082 0-3.754.02-3.743.047.011.024 1.708 3.001 3.774 6.62l3.76 6.574h7.46c.001-.02-1.696-2.998-3.762-6.618l-3.76-6.623zm-4.28.939a5.482 5.482 0 0 0-.309.443L3.71 9.17 0 15.647l1.863 3.209a338.99 338.99 0 0 0 1.914 3.28c.04.045 7.55-13.116 7.55-13.234 0-.045-1.487-2.652-3.305-5.793zM9.97 15.463c-.639 1.099-1.943 3.361-2.9 5.03l-1.737 3.021h15.09c0-.019.848-1.484 1.884-3.259l1.882-3.222H9.97z",
|
|
88
|
+
},
|
|
55
89
|
};
|
|
56
90
|
|
|
57
91
|
export interface BrandMarkProps {
|
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: {
|
package/src/switch_button.tsx
CHANGED
|
@@ -3,16 +3,24 @@ import { Text } from "./text";
|
|
|
3
3
|
import { Switch, type SwitchProps } from "./switch";
|
|
4
4
|
import { PressableHighlight } from "./pressable_highlight";
|
|
5
5
|
import { Icon, type IconName } from "./icon";
|
|
6
|
+
import { BrandMark, type BrandName } from "./brand_mark";
|
|
6
7
|
import { CONTROL_RADIUS } from "./control_surface";
|
|
7
8
|
|
|
8
9
|
export interface SwitchButtonProps extends SwitchProps {
|
|
9
10
|
icon?: IconName;
|
|
11
|
+
/**
|
|
12
|
+
* A third-party channel's own mark, for a row that names an OUTSIDE service.
|
|
13
|
+
* Separate from `icon` because the two draw from different sets on purpose —
|
|
14
|
+
* `Icon` is the app's own glyph language, `BrandMark` a narrow allowlist of
|
|
15
|
+
* other companies' logos. `icon` wins if both are passed.
|
|
16
|
+
*/
|
|
17
|
+
brand?: BrandName;
|
|
10
18
|
title: string;
|
|
11
19
|
tooltip?: string;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
export function SwitchButton(props: SwitchButtonProps) {
|
|
15
|
-
const { title, icon, value, tooltip, onChange } = props;
|
|
23
|
+
const { title, icon, brand, value, tooltip, onChange } = props;
|
|
16
24
|
|
|
17
25
|
return (
|
|
18
26
|
<PressableHighlight
|
|
@@ -33,7 +41,7 @@ export function SwitchButton(props: SwitchButtonProps) {
|
|
|
33
41
|
aria-checked={!!value}
|
|
34
42
|
>
|
|
35
43
|
<View style={{ flexDirection: "row", gap: 8, alignItems: "center", flex: 1 }}>
|
|
36
|
-
{
|
|
44
|
+
{icon ? <Icon name={icon} size={20} /> : brand ? <BrandMark name={brand} size={20} /> : null}
|
|
37
45
|
{!!title && (
|
|
38
46
|
<Text weight="medium" userSelect="none">
|
|
39
47
|
{title}
|