@lotics/ui 45.8.1 → 45.9.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 CHANGED
@@ -104,12 +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 / 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).
107
+ draws Facebook / Zalo / LinkedIn / X / Google Meet as solid single-colour silhouettes a reader
108
+ scanning a feed recognises the logo before the word. The names are an allowlist, so a channel with
109
+ no mark (a phone call, an event, a website) fails to typecheck and falls back to `Icon` instead of
110
+ borrowing another company's logo. A product's official full-colour logo is NOT one of these: the
111
+ kit ships no vendor artwork, so a row that names a product passes its own image into
112
+ `SwitchButton`'s `leading` slot. → [catalog.md §Text & formatting](./docs/catalog.md).
113
113
  - **The kit's fonts/colors/icons ARE the design system** — never a custom font, icon set, or
114
114
  hand-picked palette shade; color is `solid`/`tint`/`ramp` with ONE accent per screen.
115
115
  - **Hand-typed type is off-system — and it always lands too small.** Every run of language is
package/MIGRATION.md CHANGED
@@ -4,6 +4,24 @@ Breaking changes, newest first — normally per major, plus the rare minor that
4
4
  anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
5
5
  this file exists only to move an app from one release to the next.
6
6
 
7
+ ## 45.9.0
8
+
9
+ **`BrandMark` no longer carries product logos, and `SwitchButton` takes `leading` instead of
10
+ `brand`.** `BRAND_NAMES` drops `gmail`, `outlook` and `google-drive`; the kit's marks are the
11
+ single-colour CC0 silhouettes that NAME a channel — `facebook`, `zalo`, `linkedin`, `x`,
12
+ `google-meet` — and a monochrome stand-in for a product logo reads as a different thing.
13
+
14
+ Replace `<BrandMark name="gmail" />` with your own image asset. A row that named a product with
15
+ `<SwitchButton brand="gmail" …>` passes it through the new `leading` slot instead, which takes any
16
+ node:
17
+
18
+ ```tsx
19
+ <SwitchButton leading={<Image source={{ uri: logo }} style={{ width: 20, height: 20 }} />} … />
20
+ ```
21
+
22
+ A retired name no longer typechecks, and a bundle compiled against an older kit renders nothing
23
+ where the mark was rather than throwing.
24
+
7
25
  ## 45.5.0
8
26
 
9
27
  **`InlineSelect` and `InlineMemberSelect` are a `combobox`, not a `button`.** They rest as a
package/docs/catalog.md CHANGED
@@ -484,17 +484,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
484
484
  `SubsectionHeading` instead. Pick between this and `Eyebrow` by what is LABELLED — a block or a
485
485
  value; composition.md § Eyebrow vs group lead has the table.
486
486
  - **`brand_mark`** — `BrandMark`: an OUTSIDE brand's mark, naming the channel a touchpoint
487
- happened on or the service a connection reaches — `facebook`, `zalo`, `linkedin`, `x`,
488
- `google-meet`, `gmail`, `outlook`, `google-drive`. Solid single-colour silhouettes in
489
- each brand's own hex, not the full-colour logos: a byline draws these at 14–20px and a
490
- five-colour logo turns to mud there. `size` (default 16) and `muted` (draw in the text colour).
491
- The name list is an ALLOWLIST — `BRAND_NAMES` is the array and `BrandName` derives from it, so a
492
- channel with no mark will not typecheck and the call site falls back to a generic `Icon` rather
493
- than rendering the wrong company's logo. Not for the app's
487
+ happened on — `facebook`, `zalo`, `linkedin`, `x`, `google-meet`. Solid single-colour silhouettes
488
+ in each brand's own hex, drawn from CC0 geometry. `size` (default 16) and `muted` (draw in the
489
+ text colour). The name list is an ALLOWLIST `BRAND_NAMES` is the array and `BrandName` derives
490
+ from it, so a channel with no mark will not typecheck and the call site falls back to a generic
491
+ `Icon` rather than rendering the wrong company's logo. Not for the app's
494
492
  own glyphs; that is `icon`. `x` is the one mark whose brand colour is BLACK, so it is the one
495
- that needs `muted` on a dark ground; the rest carry their own hue either way. Where a brand's
496
- official mark is a composite (Outlook's envelope-and-O), the entry carries the half that stays
497
- legible small the identifying monogram because a shape nobody can read is not the mark.
493
+ that needs `muted` on a dark ground; the rest carry their own hue either way. **Not a product
494
+ logo**: a monochrome stand-in for one reads as a different thing (a red "M" is not Gmail), the kit
495
+ ships no vendor artwork, and official artwork is not uniformly inlinable anyway pass your own
496
+ image to `SwitchButton`'s `leading` slot instead.
498
497
  - **`markdown`** — `Markdown`: the single canonical markdown renderer for chat, apps, and
499
498
  `AgentRun`; rich GFM markdown on web with copyable tables, plain-text on
500
499
  native; takes a markdown `children` string. **`variant`** decides whose type
@@ -1079,9 +1078,8 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1079
1078
  (optional leading mark + medium title left, `Switch` pinned right) where the whole row IS the
1080
1079
  switch (`accessibilityRole="switch"`, the inner Switch read-only). The settings-panel/menu
1081
1080
  row toggle. The leading slot takes EITHER `icon` (an `IconName`, the app's own glyph language)
1082
- or `brand` (a `BrandName`, an outside service's mark); `icon` wins if both are passed. A row
1083
- naming a third-party service takes `brand` the two sets are deliberately separate, so a
1084
- connector row cannot reach for a Lotics glyph to stand in for a company's logo.
1081
+ or `leading` (any node); `icon` wins if both are passed. A row naming an outside product passes
1082
+ its logo through `leading`, because the kit ships no vendor artwork of its own.
1085
1083
  - **`use_form`** — `useForm`: THE batch draft-form state hook — `values` = `initialValues` +
1086
1084
  an edits overlay (a revalidation refreshes untouched fields, no sync effect), `validate`
1087
1085
  (sync/async, gates submit, editing clears the field's error), `onSubmit(values, helpers)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "45.8.1",
3
+ "version": "45.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./vite": {
@@ -7,16 +7,7 @@ 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 const BRAND_NAMES = [
11
- "facebook",
12
- "zalo",
13
- "linkedin",
14
- "x",
15
- "google-meet",
16
- "gmail",
17
- "outlook",
18
- "google-drive",
19
- ] as const;
10
+ export const BRAND_NAMES = ["facebook", "zalo", "linkedin", "x", "google-meet"] as const;
20
11
 
21
12
  export type BrandName = (typeof BRAND_NAMES)[number];
22
13
 
@@ -37,6 +28,15 @@ interface Mark {
37
28
  * Path data is Simple Icons (CC0), so the geometry carries no licence of its
38
29
  * own; the marks themselves stay the trademarks of their owners and are used
39
30
  * here only to NAME the channel a touchpoint happened on.
31
+ *
32
+ * A product's OFFICIAL full-colour logo does not belong here, and the reason is
33
+ * not size — at 20px the real Gmail and Drive artwork reads perfectly well. It
34
+ * is provenance and shape: official artwork is the vendor's asset rather than
35
+ * CC0 geometry, and it is not uniformly inlinable — Gmail and Drive are flat
36
+ * multi-path files, Outlook is fourteen gradients across fifteen paths. A kit
37
+ * that carried two of them and gave up on the third would be the worst of both.
38
+ * Those live in the app's own image assets, and a row that wants one passes it
39
+ * in (`SwitchButton`'s `leading` slot).
40
40
  */
41
41
  const MARKS: Record<BrandName, Mark> = {
42
42
  facebook: {
@@ -63,29 +63,6 @@ const MARKS: Record<BrandName, Mark> = {
63
63
  color: "#00897B",
64
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",
65
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
- },
89
66
  };
90
67
 
91
68
  export interface BrandMarkProps {
@@ -112,7 +89,13 @@ export interface BrandMarkProps {
112
89
  */
113
90
  export function BrandMark(props: BrandMarkProps) {
114
91
  const { name, size = 16, muted, testID } = props;
115
- const mark = MARKS[name];
92
+ // A name outside the allowlist does not typecheck, so reaching here means a
93
+ // deployed app compiled against an older kit that still had the name. Render
94
+ // nothing rather than dereferencing undefined: a mark is decoration beside a
95
+ // label that already names the channel, and throwing takes the whole render
96
+ // tree of an app that is otherwise fine.
97
+ const mark = MARKS[name] as Mark | undefined;
98
+ if (!mark) return null;
116
99
  return (
117
100
  <Svg width={size} height={size} viewBox="0 0 24 24" testID={testID}>
118
101
  <Path d={mark.path} fill={muted ? "currentColor" : mark.color} />
@@ -1,26 +1,25 @@
1
+ import type { ReactNode } from "react";
1
2
  import { View } from "react-native";
2
3
  import { Text } from "./text";
3
4
  import { Switch, type SwitchProps } from "./switch";
4
5
  import { PressableHighlight } from "./pressable_highlight";
5
6
  import { Icon, type IconName } from "./icon";
6
- import { BrandMark, type BrandName } from "./brand_mark";
7
7
  import { CONTROL_RADIUS } from "./control_surface";
8
8
 
9
9
  export interface SwitchButtonProps extends SwitchProps {
10
10
  icon?: IconName;
11
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.
12
+ * Anything to draw in the leading slot instead of an `Icon` — a product logo
13
+ * a row names, which the caller owns because the kit does not ship other
14
+ * companies' artwork. `icon` wins if both are passed.
16
15
  */
17
- brand?: BrandName;
16
+ leading?: ReactNode;
18
17
  title: string;
19
18
  tooltip?: string;
20
19
  }
21
20
 
22
21
  export function SwitchButton(props: SwitchButtonProps) {
23
- const { title, icon, brand, value, tooltip, onChange } = props;
22
+ const { title, icon, leading, value, tooltip, onChange } = props;
24
23
 
25
24
  return (
26
25
  <PressableHighlight
@@ -41,7 +40,7 @@ export function SwitchButton(props: SwitchButtonProps) {
41
40
  aria-checked={!!value}
42
41
  >
43
42
  <View style={{ flexDirection: "row", gap: 8, alignItems: "center", flex: 1 }}>
44
- {icon ? <Icon name={icon} size={20} /> : brand ? <BrandMark name={brand} size={20} /> : null}
43
+ {icon ? <Icon name={icon} size={20} /> : (leading ?? null)}
45
44
  {!!title && (
46
45
  <Text weight="medium" userSelect="none">
47
46
  {title}