@hzblj/kara-components 0.13.2 → 0.14.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @hzblj/kara-components
2
2
 
3
+ ## 0.14.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#38](https://github.com/hzblj/kara-ui/pull/38) [`40ddb25`](https://github.com/hzblj/kara-ui/commit/40ddb254b85e95c4e86d79f5ce71c3c004598a59) Thanks [@hzblj](https://github.com/hzblj)! - `Text` takes spans and eases their colours. Nested `Text` children are runs of one paragraph, each in a `color` of its own (`<Text>Box, box. <Text color="content.accent">Box this lap</Text></Text>`), and with `animate` a colour that changes, the text's or a span's, eases to its new value on the motion tokens instead of switching: a word lit as it is said, a phrase brought forward. SwiftUI interpolates the text; on Android each span animates its colour natively. Colours keep easing with Reduce Motion on. Rebuild the native app.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [[`40ddb25`](https://github.com/hzblj/kara-ui/commit/40ddb254b85e95c4e86d79f5ce71c3c004598a59)]:
12
+ - @hzblj/kara-native@0.14.0
13
+ - @hzblj/kara-sx@0.14.0
14
+ - @hzblj/kara-theme@0.14.0
15
+
3
16
  ## 0.13.2
4
17
 
5
18
  ### Patch Changes
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+
3
+ import { Children, isValidElement } from 'react';
4
+
5
+ /** A run of the text: a string child, or a nested `Text` with a colour of its own. */
6
+
7
+ const textOf = children => Children.toArray(children).map(child => typeof child === 'string' || typeof child === 'number' ? String(child) : '').join('');
8
+
9
+ /**
10
+ * The children as runs when any of them is a nested `Text` (`type`, the platform's own component), `null` when they
11
+ * are only text. A span keeps its `color`; anything else it sets is the outer text's.
12
+ */
13
+ export const spansOf = (children, type) => {
14
+ const items = Children.toArray(children);
15
+ if (!items.some(child => /*#__PURE__*/isValidElement(child) && child.type === type)) {
16
+ return null;
17
+ }
18
+ return items.flatMap(child => {
19
+ if (typeof child === 'string' || typeof child === 'number') {
20
+ return [{
21
+ text: String(child)
22
+ }];
23
+ }
24
+ if (/*#__PURE__*/isValidElement(child) && child.type === type) {
25
+ return [{
26
+ color: child.props.color,
27
+ text: textOf(child.props.children)
28
+ }];
29
+ }
30
+ return [];
31
+ });
32
+ };
33
+
34
+ /** The colours and the text folded into one number, for SwiftUI's `.animation(_:value:)`. A 32-bit FNV-1a hash. */
35
+ export const spansKey = (color, spans, text) => {
36
+ let hash = 0x811c9dc5;
37
+ const value = spans ? spans.map(span => `${span.color ?? ''}:${span.text}`).join('|') : text;
38
+ for (const character of `${color}|${value}`) {
39
+ hash ^= character.charCodeAt(0);
40
+ hash = Math.imul(hash, 0x01000193);
41
+ }
42
+ return hash >>> 0;
43
+ };
44
+ //# sourceMappingURL=spans.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["Children","isValidElement","textOf","children","toArray","map","child","String","join","spansOf","type","items","some","flatMap","text","color","props","spansKey","spans","hash","value","span","character","charCodeAt","Math","imul"],"sourceRoot":"../../../../src","sources":["atoms/text/spans.ts"],"mappings":";;AACA,SAAQA,QAAQ,EAAEC,cAAc,QAAuB,OAAO;;AAE9D;;AAGA,MAAMC,MAAM,GAAIC,QAAmB,IACjCH,QAAQ,CAACI,OAAO,CAACD,QAAQ,CAAC,CACvBE,GAAG,CAACC,KAAK,IAAK,OAAOA,KAAK,KAAK,QAAQ,IAAI,OAAOA,KAAK,KAAK,QAAQ,GAAGC,MAAM,CAACD,KAAK,CAAC,GAAG,EAAG,CAAC,CAC3FE,IAAI,CAAC,EAAE,CAAC;;AAEb;AACA;AACA;AACA;AACA,OAAO,MAAMC,OAAO,GAAGA,CAACN,QAAmB,EAAEO,IAAa,KAAwB;EAChF,MAAMC,KAAK,GAAGX,QAAQ,CAACI,OAAO,CAACD,QAAQ,CAAC;EAExC,IAAI,CAACQ,KAAK,CAACC,IAAI,CAACN,KAAK,IAAI,aAAAL,cAAc,CAACK,KAAK,CAAC,IAAIA,KAAK,CAACI,IAAI,KAAKA,IAAI,CAAC,EAAE;IACtE,OAAO,IAAI;EACb;EAEA,OAAOC,KAAK,CAACE,OAAO,CAAWP,KAAK,IAAI;IACtC,IAAI,OAAOA,KAAK,KAAK,QAAQ,IAAI,OAAOA,KAAK,KAAK,QAAQ,EAAE;MAC1D,OAAO,CAAC;QAACQ,IAAI,EAAEP,MAAM,CAACD,KAAK;MAAC,CAAC,CAAC;IAChC;IACA,IAAI,aAAAL,cAAc,CAA6CK,KAAK,CAAC,IAAIA,KAAK,CAACI,IAAI,KAAKA,IAAI,EAAE;MAC5F,OAAO,CAAC;QAACK,KAAK,EAAET,KAAK,CAACU,KAAK,CAACD,KAAK;QAAED,IAAI,EAAEZ,MAAM,CAACI,KAAK,CAACU,KAAK,CAACb,QAAQ;MAAC,CAAC,CAAC;IACzE;IACA,OAAO,EAAE;EACX,CAAC,CAAC;AACJ,CAAC;;AAED;AACA,OAAO,MAAMc,QAAQ,GAAGA,CAACF,KAAiB,EAAEG,KAAiC,EAAEJ,IAAY,KAAa;EACtG,IAAIK,IAAI,GAAG,UAAU;EACrB,MAAMC,KAAK,GAAGF,KAAK,GAAGA,KAAK,CAACb,GAAG,CAACgB,IAAI,IAAI,GAAGA,IAAI,CAACN,KAAK,IAAI,EAAE,IAAIM,IAAI,CAACP,IAAI,EAAE,CAAC,CAACN,IAAI,CAAC,GAAG,CAAC,GAAGM,IAAI;EAE5F,KAAK,MAAMQ,SAAS,IAAI,GAAGP,KAAK,IAAIK,KAAK,EAAE,EAAE;IAC3CD,IAAI,IAAIG,SAAS,CAACC,UAAU,CAAC,CAAC,CAAC;IAC/BJ,IAAI,GAAGK,IAAI,CAACC,IAAI,CAACN,IAAI,EAAE,UAAU,CAAC;EACpC;EAEA,OAAOA,IAAI,KAAK,CAAC;AACnB,CAAC","ignoreList":[]}
@@ -2,10 +2,12 @@
2
2
 
3
3
  import { Text as ComposeText } from '@expo/ui/jetpack-compose';
4
4
  import { testID as testIdModifier } from '@expo/ui/jetpack-compose/modifiers';
5
- import { splitAccessibility, useAccessibilityModifiers, useSxModifiers } from '@hzblj/kara-sx';
6
- import { useAppTheme } from '@hzblj/kara-theme';
5
+ import { AnimatedText } from '@hzblj/kara-native/jetpack-compose';
6
+ import { animateConfig, splitAccessibility, useAccessibilityModifiers, useSxModifiers } from '@hzblj/kara-sx';
7
+ import { duration, easing, springPhysics, useAppTheme } from '@hzblj/kara-theme';
7
8
  import { useMemo } from 'react';
8
9
  import { useHostGuard } from "../../host-context.js";
10
+ import { spansOf } from "./spans.js";
9
11
  import { jsx as _jsx } from "react/jsx-runtime";
10
12
  const DECORATION = {
11
13
  none: 'none',
@@ -19,6 +21,41 @@ const applyTransform = (children, transform) => {
19
21
  return transform === 'uppercase' ? children.toUpperCase() : children.toLowerCase();
20
22
  };
21
23
  const decorationOf = decoration => decoration === undefined ? undefined : DECORATION[decoration];
24
+ const transformed = (text, transform) => transform === 'uppercase' ? text.toUpperCase() : transform === 'lowercase' ? text.toLowerCase() : text;
25
+
26
+ /**
27
+ * `animate` for the spans' colours, as `@hzblj/kara-sx` animates the rest on Compose: a spring token's physics, and
28
+ * `smoothOut` as a no-bounce spring over its duration; the other curves as tweens along their béziers.
29
+ */
30
+ const textAnimation = config => {
31
+ if (config.spring) {
32
+ const {
33
+ dampingRatio,
34
+ stiffness
35
+ } = springPhysics(config.spring);
36
+ return {
37
+ dampingRatio,
38
+ stiffness,
39
+ type: 'spring'
40
+ };
41
+ }
42
+ const milliseconds = duration[config.duration ?? 'fast'];
43
+ const curve = config.easing ?? 'smoothOut';
44
+ if (curve === 'smoothOut') {
45
+ const omega = 2 * Math.PI * 1000 / milliseconds;
46
+ return {
47
+ dampingRatio: 1,
48
+ stiffness: omega * omega,
49
+ type: 'spring'
50
+ };
51
+ }
52
+ return {
53
+ delayMillis: config.delay,
54
+ durationMillis: milliseconds,
55
+ easing: easing[curve],
56
+ type: 'tween'
57
+ };
58
+ };
22
59
  export const Text = ({
23
60
  align,
24
61
  children,
@@ -37,6 +74,31 @@ export const Text = ({
37
74
  const sxModifiers = useSxModifiers(sx);
38
75
  const accessibilityModifiers = useAccessibilityModifiers(accessibility);
39
76
  const modifiers = useMemo(() => [...sxModifiers, ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers], [accessibilityModifiers, sxModifiers, testID]);
77
+ const spans = useMemo(() => spansOf(children, Text), [children]);
78
+ const animate = sx.animate;
79
+
80
+ // `@expo/ui`'s text takes spans but animates none: with `animate`, kara's own Compose text eases each span's colour.
81
+ if (animate !== undefined && animate !== false) {
82
+ const runs = spans ?? [{
83
+ text: typeof children === 'string' || typeof children === 'number' ? String(children) : ''
84
+ }];
85
+ return /*#__PURE__*/_jsx(AnimatedText, {
86
+ animation: textAnimation(animateConfig(animate)),
87
+ fontFamily: style.fontFamily,
88
+ fontSize: style.fontSize,
89
+ fontWeight: style.fontWeight,
90
+ lineHeight: style.lineHeight,
91
+ maxLines: numberOfLines,
92
+ modifiers: modifiers,
93
+ spans: runs.map(span => ({
94
+ color: span.color ? theme.colorString(span.color) : undefined,
95
+ text: transformed(span.text, transform)
96
+ })),
97
+ textAlign: align,
98
+ textColor: theme.colorString(color),
99
+ textDecoration: decorationOf(decoration)
100
+ });
101
+ }
40
102
  return /*#__PURE__*/_jsx(ComposeText, {
41
103
  color: theme.colorString(color),
42
104
  maxLines: numberOfLines,
@@ -49,7 +111,10 @@ export const Text = ({
49
111
  textAlign: align,
50
112
  textDecoration: decorationOf(decoration)
51
113
  },
52
- children: applyTransform(children, transform)
114
+ children: spans ? spans.map((span, index) => /*#__PURE__*/_jsx(ComposeText, {
115
+ color: span.color ? theme.colorString(span.color) : undefined,
116
+ children: transformed(span.text, transform)
117
+ }, index)) : applyTransform(children, transform)
53
118
  });
54
119
  };
55
120
  //# sourceMappingURL=text.android.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["Text","ComposeText","testID","testIdModifier","splitAccessibility","useAccessibilityModifiers","useSxModifiers","useAppTheme","useMemo","useHostGuard","jsx","_jsx","DECORATION","none","strikethrough","underline","applyTransform","children","transform","toUpperCase","toLowerCase","decorationOf","decoration","undefined","align","color","numberOfLines","variant","props","theme","style","typography","accessibility","sx","sxModifiers","accessibilityModifiers","modifiers","colorString","maxLines","fontFamily","fontSize","fontWeight","lineHeight","textAlign","textDecoration"],"sourceRoot":"../../../../src","sources":["atoms/text/text.android.tsx"],"mappings":";;AAAA,SAAQA,IAAI,IAAIC,WAAW,QAAO,0BAA0B;AAC5D,SAAQC,MAAM,IAAIC,cAAc,QAAO,oCAAoC;AAC3E,SAAQC,kBAAkB,EAAEC,yBAAyB,EAAEC,cAAc,QAAO,gBAAgB;AAC5F,SAAQC,WAAW,QAAO,mBAAmB;AAC7C,SAAQC,OAAO,QAAO,OAAO;AAE7B,SAAQC,YAAY,QAAO,uBAAoB;AAAA,SAAAC,GAAA,IAAAC,IAAA;AAG/C,MAAMC,UAAU,GAAG;EACjBC,IAAI,EAAE,MAAM;EACZC,aAAa,EAAE,aAAa;EAC5BC,SAAS,EAAE;AACb,CAAU;AAEV,MAAMC,cAAc,GAAGA,CAACC,QAA+B,EAAEC,SAAoC,KAAK;EAChG,IAAI,CAACA,SAAS,IAAI,OAAOD,QAAQ,KAAK,QAAQ,EAAE;IAC9C,OAAOA,QAAQ;EACjB;EAEA,OAAOC,SAAS,KAAK,WAAW,GAAGD,QAAQ,CAACE,WAAW,CAAC,CAAC,GAAGF,QAAQ,CAACG,WAAW,CAAC,CAAC;AACpF,CAAC;AAED,MAAMC,YAAY,GAAIC,UAAsC,IAC1DA,UAAU,KAAKC,SAAS,GAAGA,SAAS,GAAGX,UAAU,CAACU,UAAU,CAAC;AAE/D,OAAO,MAAMtB,IAAmB,GAAGA,CAAC;EAClCwB,KAAK;EACLP,QAAQ;EACRQ,KAAK,GAAG,iBAAiB;EACzBH,UAAU;EACVI,aAAa;EACbxB,MAAM;EACNgB,SAAS;EACTS,OAAO,GAAG,MAAM;EAChB,GAAGC;AACM,CAAC,KAAK;EACfnB,YAAY,CAAC,MAAM,CAAC;EACpB,MAAMoB,KAAK,GAAGtB,WAAW,CAAC,CAAC;EAC3B,MAAMuB,KAAK,GAAGD,KAAK,CAACE,UAAU,CAACJ,OAAO,CAAC;EACvC,MAAM,CAACK,aAAa,EAAEC,EAAE,CAAC,GAAG7B,kBAAkB,CAACwB,KAAK,CAAC;EACrD,MAAMM,WAAW,GAAG5B,cAAc,CAAC2B,EAAE,CAAC;EACtC,MAAME,sBAAsB,GAAG9B,yBAAyB,CAAC2B,aAAa,CAAC;EACvE,MAAMI,SAAS,GAAG5B,OAAO,CACvB,MAAM,CAAC,GAAG0B,WAAW,EAAE,IAAIhC,MAAM,GAAG,CAACC,cAAc,CAACD,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,GAAGiC,sBAAsB,CAAC,EAC9F,CAACA,sBAAsB,EAAED,WAAW,EAAEhC,MAAM,CAC9C,CAAC;EAED,oBACES,IAAA,CAACV,WAAW;IACVwB,KAAK,EAAEI,KAAK,CAACQ,WAAW,CAACZ,KAAK,CAAE;IAChCa,QAAQ,EAAEZ,aAAc;IACxBU,SAAS,EAAEA,SAAU;IACrBN,KAAK,EAAE;MACLS,UAAU,EAAET,KAAK,CAACS,UAAU;MAC5BC,QAAQ,EAAEV,KAAK,CAACU,QAAQ;MACxBC,UAAU,EAAEX,KAAK,CAACW,UAAU;MAC5BC,UAAU,EAAEZ,KAAK,CAACY,UAAU;MAC5BC,SAAS,EAAEnB,KAAK;MAChBoB,cAAc,EAAEvB,YAAY,CAACC,UAAU;IACzC,CAAE;IAAAL,QAAA,EAEDD,cAAc,CAACC,QAAQ,EAAEC,SAAS;EAAC,CACzB,CAAC;AAElB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["Text","ComposeText","testID","testIdModifier","AnimatedText","animateConfig","splitAccessibility","useAccessibilityModifiers","useSxModifiers","duration","easing","springPhysics","useAppTheme","useMemo","useHostGuard","spansOf","jsx","_jsx","DECORATION","none","strikethrough","underline","applyTransform","children","transform","toUpperCase","toLowerCase","decorationOf","decoration","undefined","transformed","text","textAnimation","config","spring","dampingRatio","stiffness","type","milliseconds","curve","omega","Math","PI","delayMillis","delay","durationMillis","align","color","numberOfLines","variant","props","theme","style","typography","accessibility","sx","sxModifiers","accessibilityModifiers","modifiers","spans","animate","runs","String","animation","fontFamily","fontSize","fontWeight","lineHeight","maxLines","map","span","colorString","textAlign","textColor","textDecoration","index"],"sourceRoot":"../../../../src","sources":["atoms/text/text.android.tsx"],"mappings":"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","ignoreList":[]}
@@ -3,10 +3,11 @@
3
3
  import { Text as SwiftUIText } from '@expo/ui/swift-ui';
4
4
  import { foregroundStyle, lineLimit, lineSpacing, multilineTextAlignment, strikethrough, textCase, underline } from '@expo/ui/swift-ui/modifiers';
5
5
  import { scaledFont } from '@hzblj/kara-native/swift-ui';
6
- import { splitAccessibility, useAccessibilityModifiers, useSxModifiers } from '@hzblj/kara-sx';
6
+ import { splitAccessibility, textColorTransition, useAccessibilityModifiers, useSxModifiers } from '@hzblj/kara-sx';
7
7
  import { useAppTheme } from '@hzblj/kara-theme';
8
8
  import { useMemo } from 'react';
9
9
  import { useHostGuard } from "../../host-context.js";
10
+ import { spansKey, spansOf } from "./spans.js";
10
11
  import { jsx as _jsx } from "react/jsx-runtime";
11
12
  const ALIGNMENT = {
12
13
  center: 'center',
@@ -38,6 +39,10 @@ export const Text = ({
38
39
  const [accessibility, sx] = splitAccessibility(props);
39
40
  const sxModifiers = useSxModifiers(sx, FRAME_ALIGNMENT[align ?? 'left']);
40
41
  const accessibilityModifiers = useAccessibilityModifiers(accessibility);
42
+ const spans = useMemo(() => spansOf(children, Text), [children]);
43
+ const animate = sx.animate;
44
+ // With `animate`, SwiftUI interpolates the text when a colour changes: the text's own or a span's.
45
+ const transition = useMemo(() => textColorTransition(animate, spansKey(color, spans, typeof children === 'string' ? children : '')), [animate, children, color, spans]);
41
46
  const modifiers = useMemo(() => {
42
47
  const list = [
43
48
  // Not `@expo/ui`'s `font()`: for the platform's face it gives either Apple's size or a size that ignores Dynamic
@@ -69,12 +74,18 @@ export const Text = ({
69
74
  if (numberOfLines !== undefined) {
70
75
  list.push(lineLimit(numberOfLines));
71
76
  }
72
- return [...list, ...sxModifiers, ...accessibilityModifiers];
73
- }, [accessibilityModifiers, align, color, decoration, numberOfLines, style, sxModifiers, theme, transform]);
77
+ return [...list, ...transition, ...sxModifiers, ...accessibilityModifiers];
78
+ }, [accessibilityModifiers, align, color, decoration, numberOfLines, style, sxModifiers, theme, transform, transition]);
74
79
  return /*#__PURE__*/_jsx(SwiftUIText, {
75
80
  modifiers: modifiers,
76
81
  testID: testID,
77
- children: children
82
+ children: spans ? spans.map((span, index) =>
83
+ /*#__PURE__*/
84
+ // Concatenated into one SwiftUI `Text`: a span takes its colour, the font and the rest are the outer text's.
85
+ _jsx(SwiftUIText, {
86
+ modifiers: span.color ? [foregroundStyle(theme.color(span.color))] : undefined,
87
+ children: span.text
88
+ }, index)) : children
78
89
  });
79
90
  };
80
91
  //# sourceMappingURL=text.ios.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["Text","SwiftUIText","foregroundStyle","lineLimit","lineSpacing","multilineTextAlignment","strikethrough","textCase","underline","scaledFont","splitAccessibility","useAccessibilityModifiers","useSxModifiers","useAppTheme","useMemo","useHostGuard","jsx","_jsx","ALIGNMENT","center","left","right","FRAME_ALIGNMENT","align","children","color","decoration","numberOfLines","testID","transform","variant","props","theme","style","typography","accessibility","sx","sxModifiers","accessibilityModifiers","modifiers","list","family","fontFamily","size","fontSize","textStyle","scalesWith","weight","Math","max","lineHeight","push","isActive","pattern","undefined"],"sourceRoot":"../../../../src","sources":["atoms/text/text.ios.tsx"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["Text","SwiftUIText","foregroundStyle","lineLimit","lineSpacing","multilineTextAlignment","strikethrough","textCase","underline","scaledFont","splitAccessibility","textColorTransition","useAccessibilityModifiers","useSxModifiers","useAppTheme","useMemo","useHostGuard","spansKey","spansOf","jsx","_jsx","ALIGNMENT","center","left","right","FRAME_ALIGNMENT","align","children","color","decoration","numberOfLines","testID","transform","variant","props","theme","style","typography","accessibility","sx","sxModifiers","accessibilityModifiers","spans","animate","transition","modifiers","list","family","fontFamily","size","fontSize","textStyle","scalesWith","weight","Math","max","lineHeight","push","isActive","pattern","undefined","map","span","index","text"],"sourceRoot":"../../../../src","sources":["atoms/text/text.ios.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,14 @@
1
+ import type { ColorToken } from '@hzblj/kara-theme';
2
+ import { type ReactNode } from 'react';
3
+ /** A run of the text: a string child, or a nested `Text` with a colour of its own. */
4
+ export type TextSpan = {
5
+ color?: ColorToken;
6
+ text: string;
7
+ };
8
+ /**
9
+ * The children as runs when any of them is a nested `Text` (`type`, the platform's own component), `null` when they
10
+ * are only text. A span keeps its `color`; anything else it sets is the outer text's.
11
+ */
12
+ export declare const spansOf: (children: ReactNode, type: unknown) => TextSpan[] | null;
13
+ /** The colours and the text folded into one number, for SwiftUI's `.animation(_:value:)`. A 32-bit FNV-1a hash. */
14
+ export declare const spansKey: (color: ColorToken, spans: readonly TextSpan[] | null, text: string) => number;
@@ -8,6 +8,10 @@ export type TextTransform = 'lowercase' | 'uppercase';
8
8
  * SwiftUI `Text` / Compose `Text` in a theme typography variant. A native atom: outside a host, use `RnText`. It takes
9
9
  * the native `sx` surface too: padding, a frame (`wide` lines it up by `align`), a background, and the `ios` bucket's
10
10
  * `contentTransition`, `animation` and `monospacedDigit` for a number that changes in place.
11
+ *
12
+ * Nested `Text` children are spans: runs of the same text in a colour of their own, one paragraph that wraps as one.
13
+ * With `animate`, a colour that changes (the text's or a span's) eases to its new value instead of switching: a word lit
14
+ * as it is said, a phrase brought forward. SwiftUI interpolates the text, Compose animates each span's colour.
11
15
  */
12
16
  export type TextProps = ResponsiveSx & AccessibilityProps & {
13
17
  /**
@@ -15,7 +19,10 @@ export type TextProps = ResponsiveSx & AccessibilityProps & {
15
19
  * layout; Android keeps them as they are.
16
20
  */
17
21
  align?: TextAlign;
18
- /** The text. */
22
+ /**
23
+ * The text: a string, or strings and nested `Text` spans, each in a `color` of its own. A span takes only its
24
+ * colour; the variant, alignment and everything else are the outer text's.
25
+ */
19
26
  children?: ReactNode;
20
27
  /** The text's colour. `content.primary` by default. */
21
28
  color?: ColorToken;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hzblj/kara-components",
3
- "version": "0.13.2",
3
+ "version": "0.14.0",
4
4
  "description": "The atoms, molecules, organisms and templates of @hzblj/kara-ui — real SwiftUI and Jetpack Compose views through @expo/ui.",
5
5
  "author": "Jan Blazej",
6
6
  "license": "MIT",
@@ -61,9 +61,9 @@
61
61
  "dependencies": {
62
62
  "@hzblj/expo-native-icons": "^0.1.0",
63
63
  "@hzblj/expo-rich-haptics": "^0.1.0",
64
- "@hzblj/kara-native": "^0.13.2",
65
- "@hzblj/kara-sx": "^0.13.2",
66
- "@hzblj/kara-theme": "^0.13.2",
64
+ "@hzblj/kara-native": "^0.14.0",
65
+ "@hzblj/kara-sx": "^0.14.0",
66
+ "@hzblj/kara-theme": "^0.14.0",
67
67
  "sf-symbols-typescript": "^2.2.0"
68
68
  },
69
69
  "peerDependencies": {
@@ -0,0 +1,45 @@
1
+ import type {ColorToken} from '@hzblj/kara-theme'
2
+ import {Children, isValidElement, type ReactNode} from 'react'
3
+
4
+ /** A run of the text: a string child, or a nested `Text` with a colour of its own. */
5
+ export type TextSpan = {color?: ColorToken; text: string}
6
+
7
+ const textOf = (children: ReactNode): string =>
8
+ Children.toArray(children)
9
+ .map(child => (typeof child === 'string' || typeof child === 'number' ? String(child) : ''))
10
+ .join('')
11
+
12
+ /**
13
+ * The children as runs when any of them is a nested `Text` (`type`, the platform's own component), `null` when they
14
+ * are only text. A span keeps its `color`; anything else it sets is the outer text's.
15
+ */
16
+ export const spansOf = (children: ReactNode, type: unknown): TextSpan[] | null => {
17
+ const items = Children.toArray(children)
18
+
19
+ if (!items.some(child => isValidElement(child) && child.type === type)) {
20
+ return null
21
+ }
22
+
23
+ return items.flatMap<TextSpan>(child => {
24
+ if (typeof child === 'string' || typeof child === 'number') {
25
+ return [{text: String(child)}]
26
+ }
27
+ if (isValidElement<{children?: ReactNode; color?: ColorToken}>(child) && child.type === type) {
28
+ return [{color: child.props.color, text: textOf(child.props.children)}]
29
+ }
30
+ return []
31
+ })
32
+ }
33
+
34
+ /** The colours and the text folded into one number, for SwiftUI's `.animation(_:value:)`. A 32-bit FNV-1a hash. */
35
+ export const spansKey = (color: ColorToken, spans: readonly TextSpan[] | null, text: string): number => {
36
+ let hash = 0x811c9dc5
37
+ const value = spans ? spans.map(span => `${span.color ?? ''}:${span.text}`).join('|') : text
38
+
39
+ for (const character of `${color}|${value}`) {
40
+ hash ^= character.charCodeAt(0)
41
+ hash = Math.imul(hash, 0x01000193)
42
+ }
43
+
44
+ return hash >>> 0
45
+ }
@@ -1,10 +1,18 @@
1
1
  import {Text as ComposeText} from '@expo/ui/jetpack-compose'
2
2
  import {testID as testIdModifier} from '@expo/ui/jetpack-compose/modifiers'
3
- import {splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
4
- import {useAppTheme} from '@hzblj/kara-theme'
3
+ import {AnimatedText, type TextAnimation} from '@hzblj/kara-native/jetpack-compose'
4
+ import {
5
+ animateConfig,
6
+ type SxAnimateConfig,
7
+ splitAccessibility,
8
+ useAccessibilityModifiers,
9
+ useSxModifiers,
10
+ } from '@hzblj/kara-sx'
11
+ import {duration, easing, springPhysics, useAppTheme} from '@hzblj/kara-theme'
5
12
  import {useMemo} from 'react'
6
13
 
7
14
  import {useHostGuard} from '../../host-context'
15
+ import {spansOf} from './spans'
8
16
  import type {TextComponent, TextDecoration, TextProps, TextTransform} from './text.types'
9
17
 
10
18
  const DECORATION = {
@@ -24,6 +32,32 @@ const applyTransform = (children: TextProps['children'], transform: TextTransfor
24
32
  const decorationOf = (decoration: TextDecoration | undefined) =>
25
33
  decoration === undefined ? undefined : DECORATION[decoration]
26
34
 
35
+ const transformed = (text: string, transform: TextTransform | undefined) =>
36
+ transform === 'uppercase' ? text.toUpperCase() : transform === 'lowercase' ? text.toLowerCase() : text
37
+
38
+ /**
39
+ * `animate` for the spans' colours, as `@hzblj/kara-sx` animates the rest on Compose: a spring token's physics, and
40
+ * `smoothOut` as a no-bounce spring over its duration; the other curves as tweens along their béziers.
41
+ */
42
+ const textAnimation = (config: SxAnimateConfig): TextAnimation => {
43
+ if (config.spring) {
44
+ const {dampingRatio, stiffness} = springPhysics(config.spring)
45
+
46
+ return {dampingRatio, stiffness, type: 'spring'}
47
+ }
48
+
49
+ const milliseconds = duration[config.duration ?? 'fast']
50
+ const curve = config.easing ?? 'smoothOut'
51
+
52
+ if (curve === 'smoothOut') {
53
+ const omega = (2 * Math.PI * 1000) / milliseconds
54
+
55
+ return {dampingRatio: 1, stiffness: omega * omega, type: 'spring'}
56
+ }
57
+
58
+ return {delayMillis: config.delay, durationMillis: milliseconds, easing: easing[curve], type: 'tween'}
59
+ }
60
+
27
61
  export const Text: TextComponent = ({
28
62
  align,
29
63
  children,
@@ -45,6 +79,32 @@ export const Text: TextComponent = ({
45
79
  () => [...sxModifiers, ...(testID ? [testIdModifier(testID)] : []), ...accessibilityModifiers],
46
80
  [accessibilityModifiers, sxModifiers, testID]
47
81
  )
82
+ const spans = useMemo(() => spansOf(children, Text), [children])
83
+ const animate = sx.animate
84
+
85
+ // `@expo/ui`'s text takes spans but animates none: with `animate`, kara's own Compose text eases each span's colour.
86
+ if (animate !== undefined && animate !== false) {
87
+ const runs = spans ?? [{text: typeof children === 'string' || typeof children === 'number' ? String(children) : ''}]
88
+
89
+ return (
90
+ <AnimatedText
91
+ animation={textAnimation(animateConfig(animate))}
92
+ fontFamily={style.fontFamily}
93
+ fontSize={style.fontSize}
94
+ fontWeight={style.fontWeight}
95
+ lineHeight={style.lineHeight}
96
+ maxLines={numberOfLines}
97
+ modifiers={modifiers}
98
+ spans={runs.map(span => ({
99
+ color: span.color ? theme.colorString(span.color) : undefined,
100
+ text: transformed(span.text, transform),
101
+ }))}
102
+ textAlign={align}
103
+ textColor={theme.colorString(color)}
104
+ textDecoration={decorationOf(decoration)}
105
+ />
106
+ )
107
+ }
48
108
 
49
109
  return (
50
110
  <ComposeText
@@ -60,7 +120,13 @@ export const Text: TextComponent = ({
60
120
  textDecoration: decorationOf(decoration),
61
121
  }}
62
122
  >
63
- {applyTransform(children, transform)}
123
+ {spans
124
+ ? spans.map((span, index) => (
125
+ <ComposeText color={span.color ? theme.colorString(span.color) : undefined} key={index}>
126
+ {transformed(span.text, transform)}
127
+ </ComposeText>
128
+ ))
129
+ : applyTransform(children, transform)}
64
130
  </ComposeText>
65
131
  )
66
132
  }
@@ -10,11 +10,18 @@ import {
10
10
  underline,
11
11
  } from '@expo/ui/swift-ui/modifiers'
12
12
  import {scaledFont} from '@hzblj/kara-native/swift-ui'
13
- import {type FrameAlignment, splitAccessibility, useAccessibilityModifiers, useSxModifiers} from '@hzblj/kara-sx'
13
+ import {
14
+ type FrameAlignment,
15
+ splitAccessibility,
16
+ textColorTransition,
17
+ useAccessibilityModifiers,
18
+ useSxModifiers,
19
+ } from '@hzblj/kara-sx'
14
20
  import {useAppTheme} from '@hzblj/kara-theme'
15
21
  import {useMemo} from 'react'
16
22
 
17
23
  import {useHostGuard} from '../../host-context'
24
+ import {spansKey, spansOf} from './spans'
18
25
  import type {TextAlign, TextComponent, TextProps} from './text.types'
19
26
 
20
27
  const ALIGNMENT = {
@@ -48,6 +55,13 @@ export const Text: TextComponent = ({
48
55
  const [accessibility, sx] = splitAccessibility(props)
49
56
  const sxModifiers = useSxModifiers(sx, FRAME_ALIGNMENT[align ?? 'left'])
50
57
  const accessibilityModifiers = useAccessibilityModifiers(accessibility)
58
+ const spans = useMemo(() => spansOf(children, Text), [children])
59
+ const animate = sx.animate
60
+ // With `animate`, SwiftUI interpolates the text when a colour changes: the text's own or a span's.
61
+ const transition = useMemo(
62
+ () => textColorTransition(animate, spansKey(color, spans, typeof children === 'string' ? children : '')),
63
+ [animate, children, color, spans]
64
+ )
51
65
 
52
66
  const modifiers = useMemo(() => {
53
67
  const list: ModifierConfig[] = [
@@ -78,12 +92,30 @@ export const Text: TextComponent = ({
78
92
  list.push(lineLimit(numberOfLines))
79
93
  }
80
94
 
81
- return [...list, ...sxModifiers, ...accessibilityModifiers]
82
- }, [accessibilityModifiers, align, color, decoration, numberOfLines, style, sxModifiers, theme, transform])
95
+ return [...list, ...transition, ...sxModifiers, ...accessibilityModifiers]
96
+ }, [
97
+ accessibilityModifiers,
98
+ align,
99
+ color,
100
+ decoration,
101
+ numberOfLines,
102
+ style,
103
+ sxModifiers,
104
+ theme,
105
+ transform,
106
+ transition,
107
+ ])
83
108
 
84
109
  return (
85
110
  <SwiftUIText modifiers={modifiers} testID={testID}>
86
- {children}
111
+ {spans
112
+ ? spans.map((span, index) => (
113
+ // Concatenated into one SwiftUI `Text`: a span takes its colour, the font and the rest are the outer text's.
114
+ <SwiftUIText key={index} modifiers={span.color ? [foregroundStyle(theme.color(span.color))] : undefined}>
115
+ {span.text}
116
+ </SwiftUIText>
117
+ ))
118
+ : children}
87
119
  </SwiftUIText>
88
120
  )
89
121
  }
@@ -12,6 +12,10 @@ export type TextTransform = 'lowercase' | 'uppercase'
12
12
  * SwiftUI `Text` / Compose `Text` in a theme typography variant. A native atom: outside a host, use `RnText`. It takes
13
13
  * the native `sx` surface too: padding, a frame (`wide` lines it up by `align`), a background, and the `ios` bucket's
14
14
  * `contentTransition`, `animation` and `monospacedDigit` for a number that changes in place.
15
+ *
16
+ * Nested `Text` children are spans: runs of the same text in a colour of their own, one paragraph that wraps as one.
17
+ * With `animate`, a colour that changes (the text's or a span's) eases to its new value instead of switching: a word lit
18
+ * as it is said, a phrase brought forward. SwiftUI interpolates the text, Compose animates each span's colour.
15
19
  */
16
20
  export type TextProps = ResponsiveSx &
17
21
  AccessibilityProps & {
@@ -20,7 +24,10 @@ export type TextProps = ResponsiveSx &
20
24
  * layout; Android keeps them as they are.
21
25
  */
22
26
  align?: TextAlign
23
- /** The text. */
27
+ /**
28
+ * The text: a string, or strings and nested `Text` spans, each in a `color` of its own. A span takes only its
29
+ * colour; the variant, alignment and everything else are the outer text's.
30
+ */
24
31
  children?: ReactNode
25
32
  /** The text's colour. `content.primary` by default. */
26
33
  color?: ColorToken