@gryt/ui-native 0.12.0 → 0.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/README.md CHANGED
@@ -57,6 +57,25 @@ properties and no cascade, so components take a theme from context instead:
57
57
  Rendered outside a provider, components get the dark theme rather than nothing,
58
58
  which matches `@gryt/ui` shipping its dark tokens on `:root`.
59
59
 
60
+ ## Avatars are generated, not fetched
61
+
62
+ `Avatar` takes a `seed` and draws that person's owl from `@gryt/owl` — the same
63
+ generator and the same seed the web uses, so somebody looks the same in both
64
+ apps.
65
+
66
+ ```tsx
67
+ import { Avatar } from "@gryt/ui-native";
68
+ import { avatarSeed } from "@gryt/owl";
69
+
70
+ <Avatar seed={avatarSeed(member.nickname)} name={member.nickname} size="md" />;
71
+ ```
72
+
73
+ `react-native-svg` is a peer dependency for it. React Native's `Image` cannot
74
+ decode SVG from a data URI, so the markup goes to `SvgXml` instead — the web
75
+ hands the same string to an `<img>`, and that is the one place the two renderers
76
+ genuinely differ. Without a seed the component is what it was: the `source`
77
+ image if there is one, initials if there is not.
78
+
60
79
  ## Parity exceptions
61
80
 
62
81
  Kept from the first component rather than the first argument. Full 1:1 is the
@@ -77,7 +96,7 @@ goal, so the honest list is worth more than a claim of parity that isn't one.
77
96
  | `Dialog` | Android's back button stands in for Escape | It is the only hardware dismiss a phone has. `AlertDialog` disables it along with the scrim, matching Base UI suppressing outside-press and Escape. |
78
97
  | `Switch` | Drawn from tokens, not React Native's `Switch` | The platform control is green on iOS and Material on Android, and `thumbColor`/`trackColor` do not reach the whole shape. A design system that cannot colour its own switch is not one. |
79
98
  | `Select` | A list, not the platform picker | iOS gives a wheel and Android a dialog. Neither takes the Gryt palette and they look nothing like each other. |
80
- | `Checkbox` | The tick is a text glyph, not a Phosphor icon | An icon set would be this package's first runtime dependency, for one glyph, and `@phosphor-icons/react` renders SVG that React Native cannot mount without `react-native-svg`. That is a decision for whoever needs icons, not for a checkbox. |
99
+ | `Checkbox` | The tick is a text glyph, not a Phosphor icon | An icon set would be this package's first icon dependency, for one glyph. `react-native-svg` is a peer now, for the owl avatars, so mounting SVG is no longer the obstacle — but `@phosphor-icons/react` is still a web package, and picking a set is a decision for whoever needs icons rather than for a checkbox. |
81
100
  | `Tabs` | Each tab draws its own underline; `Indicator` renders nothing | Base UI slides one indicator between tabs, measured against the active one. Sliding it here means measuring every tab and animating between them, which is real work for a decoration. The underline appears rather than slides. |
82
101
  | `Tabs` | The panel has no `tabpanel` role | React Native has `tab` and `tablist` and no `tabpanel`. The panel is reachable, it just does not announce itself as belonging to the tab. |
83
102
  | `Collapsible` | Unmounts when closed instead of animating height | The web animates height, which needs the content measured first. Doable with `onLayout` and `Animated`, at the cost of a frame at the wrong size. |
@@ -3,8 +3,15 @@ export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
3
3
  export interface AvatarProps {
4
4
  name?: string;
5
5
  source?: string;
6
+ /**
7
+ * Draws this person's owl, from @gryt/owl. The same seed draws the same owl
8
+ * here as it does on the web, which is the point of the package.
9
+ *
10
+ * Pass `avatarSeed(nickname)` rather than the nickname itself.
11
+ */
12
+ seed?: string;
6
13
  size?: AvatarSize;
7
14
  style?: StyleProp<ViewStyle>;
8
15
  }
9
- export declare function Avatar({ name, source, size, style }: AvatarProps): import("react").JSX.Element;
16
+ export declare function Avatar({ name, seed, source, size, style }: AvatarProps): import("react").JSX.Element;
10
17
  //# sourceMappingURL=Avatar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../../../src/components/Avatar/Avatar.tsx"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAK3E,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAU1D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAuBD,wBAAgB,MAAM,CAAC,EAAE,IAAS,EAAE,MAAM,EAAE,IAAW,EAAE,KAAK,EAAE,EAAE,WAAW,+BA+C5E"}
1
+ {"version":3,"file":"Avatar.d.ts","sourceRoot":"","sources":["../../../src/components/Avatar/Avatar.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAe,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAM3E,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAU1D,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAyBD,wBAAgB,MAAM,CAAC,EAAE,IAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAW,EAAE,KAAK,EAAE,EAAE,WAAW,+BAmElF"}
@@ -1,6 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { owlAvatarSvg } from "@gryt/owl";
3
+ import { useMemo, useState } from "react";
3
4
  import { Image, View } from "react-native";
5
+ import { SvgXml } from "react-native-svg";
4
6
  import { Text } from "../../internal/Text.js";
5
7
  import { useTheme } from "../../theme/index.js";
6
8
  const SIZES = {
@@ -16,6 +18,8 @@ const SIZES = {
16
18
  * Deliberately the same arithmetic as the web: sum the code units and take the
17
19
  * remainder. Anything cleverer would give a different colour on each platform
18
20
  * for the same person, which is the one outcome to avoid.
21
+ *
22
+ * Only reached with no `source` and no `seed`. An owl brings its own colour.
19
23
  */
20
24
  function toneFor(name) {
21
25
  const tones = ["accent", "secondary", "success", "danger", "warning"];
@@ -32,11 +36,14 @@ function initials(name) {
32
36
  return parts[0].slice(0, 2).toUpperCase();
33
37
  return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
34
38
  }
35
- export function Avatar({ name = "", source, size = "md", style }) {
39
+ export function Avatar({ name = "", seed, source, size = "md", style }) {
36
40
  const theme = useTheme();
37
41
  const [failed, setFailed] = useState(false);
38
42
  const px = SIZES[size];
39
43
  const ramp = theme.scales[toneFor(name)];
44
+ // Memoised for the same reason the web one is: these render in member lists
45
+ // that repaint often and the seed never changes under a row.
46
+ const owl = useMemo(() => (seed ? owlAvatarSvg(seed) : undefined), [seed]);
40
47
  const frame = {
41
48
  width: px,
42
49
  height: px,
@@ -45,11 +52,18 @@ export function Avatar({ name = "", source, size = "md", style }) {
45
52
  justifyContent: "center",
46
53
  overflow: "hidden",
47
54
  };
48
- // A broken image URL falls back to initials rather than to a blank circle.
49
- // The web gets this from <img onerror>; here it is onError plus state.
55
+ // A broken image URL falls back to the owl, or to initials without a seed,
56
+ // rather than to a blank circle. The web gets this from <img onerror>; here
57
+ // it is onError plus state.
50
58
  if (source && !failed) {
51
59
  return (_jsx(View, { style: [frame, { backgroundColor: theme.color.surfaceRaised }, style], children: _jsx(Image, { source: { uri: source }, onError: () => setFailed(true), accessibilityLabel: name || undefined, style: { width: px, height: px } }) }));
52
60
  }
61
+ // SvgXml with the raw markup, not an Image with a data URI: React Native's
62
+ // Image cannot decode SVG, which is the one place the two renderers genuinely
63
+ // differ. The web hands the same generator's output to an <img> and it works.
64
+ if (owl) {
65
+ return (_jsx(View, { accessibilityRole: "image", accessibilityLabel: name || undefined, style: [frame, style], children: _jsx(SvgXml, { xml: owl, width: px, height: px }) }));
66
+ }
53
67
  return (_jsx(View, { accessibilityRole: "image", accessibilityLabel: name || undefined, style: [frame, { backgroundColor: ramp[2] }, style], children: _jsx(Text, { style: {
54
68
  color: ramp[10],
55
69
  fontSize: Math.round(px * 0.4),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gryt/ui-native",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Gryt's design system on React Native. Same tokens as @gryt/ui, a different renderer.",
@@ -36,10 +36,12 @@
36
36
  "react-native-reanimated": ">=4.0.0",
37
37
  "@gorhom/bottom-sheet": ">=5.2.0",
38
38
  "react-native-gesture-handler": ">=2.16.1",
39
- "react-native-safe-area-context": ">=5.0.0"
39
+ "react-native-safe-area-context": ">=5.0.0",
40
+ "react-native-svg": ">=15.0.0"
40
41
  },
41
42
  "dependencies": {
42
- "@gryt/theme": "^0.5.0"
43
+ "@gryt/owl": "^0.1.0",
44
+ "@gryt/theme": "^0.6.0"
43
45
  },
44
46
  "devDependencies": {
45
47
  "@gorhom/bottom-sheet": "^5.2.14",
@@ -49,6 +51,7 @@
49
51
  "react-native-gesture-handler": "^2.32.0",
50
52
  "react-native-reanimated": "^4.5.1",
51
53
  "react-native-safe-area-context": "^5.7.0",
54
+ "react-native-svg": "^15.14.0",
52
55
  "vitest": "^3.0.0"
53
56
  },
54
57
  "peerDependenciesMeta": {}