react-native-laminar 1.2.0 → 1.4.1

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.
Files changed (147) hide show
  1. package/README.md +9 -2
  2. package/lib/commonjs/hooks/use-display-units.js +1 -0
  3. package/lib/commonjs/hooks/use-display-units.js.map +1 -1
  4. package/lib/commonjs/hooks/use-inline-auto-width.js +43 -10
  5. package/lib/commonjs/hooks/use-inline-auto-width.js.map +1 -1
  6. package/lib/commonjs/hooks/use-morph-motion.js +2 -1
  7. package/lib/commonjs/hooks/use-morph-motion.js.map +1 -1
  8. package/lib/commonjs/hooks/use-morph-text-style.js +4 -0
  9. package/lib/commonjs/hooks/use-morph-text-style.js.map +1 -1
  10. package/lib/commonjs/hooks/use-numeric-lanes.js +1 -0
  11. package/lib/commonjs/hooks/use-numeric-lanes.js.map +1 -1
  12. package/lib/commonjs/hooks/use-text-glyphs.js +28 -7
  13. package/lib/commonjs/hooks/use-text-glyphs.js.map +1 -1
  14. package/lib/commonjs/index.js +54 -11
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/model/display-units.js +7 -1
  17. package/lib/commonjs/model/display-units.js.map +1 -1
  18. package/lib/commonjs/model/numeric-lanes.js +4 -0
  19. package/lib/commonjs/model/numeric-lanes.js.map +1 -1
  20. package/lib/commonjs/model/text-keys.js +4 -0
  21. package/lib/commonjs/model/text-keys.js.map +1 -1
  22. package/lib/commonjs/motion/entry-exit-builders.js +78 -1
  23. package/lib/commonjs/motion/entry-exit-builders.js.map +1 -1
  24. package/lib/commonjs/motion/preset-map.js +7 -0
  25. package/lib/commonjs/motion/preset-map.js.map +1 -1
  26. package/lib/commonjs/view/glyph-run.js +21 -2
  27. package/lib/commonjs/view/glyph-run.js.map +1 -1
  28. package/lib/commonjs/view/morph-viewport.js +4 -0
  29. package/lib/commonjs/view/morph-viewport.js.map +1 -1
  30. package/lib/commonjs/view/number-lane.js +5 -1
  31. package/lib/commonjs/view/number-lane.js.map +1 -1
  32. package/lib/commonjs/view/number-run.js +4 -0
  33. package/lib/commonjs/view/number-run.js.map +1 -1
  34. package/lib/commonjs/view/slots-run.js +258 -0
  35. package/lib/commonjs/view/slots-run.js.map +1 -0
  36. package/lib/commonjs/view/text-run.js +35 -3
  37. package/lib/commonjs/view/text-run.js.map +1 -1
  38. package/lib/module/hooks/use-display-units.js +2 -0
  39. package/lib/module/hooks/use-display-units.js.map +1 -1
  40. package/lib/module/hooks/use-inline-auto-width.js +44 -11
  41. package/lib/module/hooks/use-inline-auto-width.js.map +1 -1
  42. package/lib/module/hooks/use-morph-motion.js +2 -1
  43. package/lib/module/hooks/use-morph-motion.js.map +1 -1
  44. package/lib/module/hooks/use-morph-text-style.js +4 -0
  45. package/lib/module/hooks/use-morph-text-style.js.map +1 -1
  46. package/lib/module/hooks/use-numeric-lanes.js +1 -0
  47. package/lib/module/hooks/use-numeric-lanes.js.map +1 -1
  48. package/lib/module/hooks/use-text-glyphs.js +28 -7
  49. package/lib/module/hooks/use-text-glyphs.js.map +1 -1
  50. package/lib/module/index.js +56 -12
  51. package/lib/module/index.js.map +1 -1
  52. package/lib/module/model/display-units.js +7 -1
  53. package/lib/module/model/display-units.js.map +1 -1
  54. package/lib/module/model/numeric-lanes.js +5 -0
  55. package/lib/module/model/numeric-lanes.js.map +1 -1
  56. package/lib/module/model/text-keys.js +4 -0
  57. package/lib/module/model/text-keys.js.map +1 -1
  58. package/lib/module/motion/entry-exit-builders.js +75 -0
  59. package/lib/module/motion/entry-exit-builders.js.map +1 -1
  60. package/lib/module/motion/preset-map.js +7 -0
  61. package/lib/module/motion/preset-map.js.map +1 -1
  62. package/lib/module/view/glyph-run.js +21 -2
  63. package/lib/module/view/glyph-run.js.map +1 -1
  64. package/lib/module/view/morph-viewport.js +4 -0
  65. package/lib/module/view/morph-viewport.js.map +1 -1
  66. package/lib/module/view/number-lane.js +5 -1
  67. package/lib/module/view/number-lane.js.map +1 -1
  68. package/lib/module/view/number-run.js +4 -0
  69. package/lib/module/view/number-run.js.map +1 -1
  70. package/lib/module/view/slots-run.js +253 -0
  71. package/lib/module/view/slots-run.js.map +1 -0
  72. package/lib/module/view/text-run.js +36 -4
  73. package/lib/module/view/text-run.js.map +1 -1
  74. package/lib/typescript/commonjs/hooks/use-display-units.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts +3 -1
  76. package/lib/typescript/commonjs/hooks/use-inline-auto-width.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/hooks/use-morph-motion.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/hooks/use-morph-text-style.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/hooks/use-numeric-lanes.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/hooks/use-text-glyphs.d.ts +2 -1
  81. package/lib/typescript/commonjs/hooks/use-text-glyphs.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/index.d.ts +1 -3
  83. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/model/display-units.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/model/numeric-lanes.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/model/text-keys.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/motion/entry-exit-builders.d.ts +13 -0
  88. package/lib/typescript/commonjs/motion/entry-exit-builders.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/motion/preset-map.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/types.d.ts +12 -2
  91. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/view/glyph-run.d.ts +5 -2
  93. package/lib/typescript/commonjs/view/glyph-run.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/view/morph-viewport.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/view/number-lane.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/view/number-run.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/view/slots-run.d.ts +15 -0
  98. package/lib/typescript/commonjs/view/slots-run.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/view/text-run.d.ts +5 -1
  100. package/lib/typescript/commonjs/view/text-run.d.ts.map +1 -1
  101. package/lib/typescript/module/hooks/use-display-units.d.ts.map +1 -1
  102. package/lib/typescript/module/hooks/use-inline-auto-width.d.ts +3 -1
  103. package/lib/typescript/module/hooks/use-inline-auto-width.d.ts.map +1 -1
  104. package/lib/typescript/module/hooks/use-morph-motion.d.ts.map +1 -1
  105. package/lib/typescript/module/hooks/use-morph-text-style.d.ts.map +1 -1
  106. package/lib/typescript/module/hooks/use-numeric-lanes.d.ts.map +1 -1
  107. package/lib/typescript/module/hooks/use-text-glyphs.d.ts +2 -1
  108. package/lib/typescript/module/hooks/use-text-glyphs.d.ts.map +1 -1
  109. package/lib/typescript/module/index.d.ts +1 -3
  110. package/lib/typescript/module/index.d.ts.map +1 -1
  111. package/lib/typescript/module/model/display-units.d.ts.map +1 -1
  112. package/lib/typescript/module/model/numeric-lanes.d.ts.map +1 -1
  113. package/lib/typescript/module/model/text-keys.d.ts.map +1 -1
  114. package/lib/typescript/module/motion/entry-exit-builders.d.ts +13 -0
  115. package/lib/typescript/module/motion/entry-exit-builders.d.ts.map +1 -1
  116. package/lib/typescript/module/motion/preset-map.d.ts.map +1 -1
  117. package/lib/typescript/module/types.d.ts +12 -2
  118. package/lib/typescript/module/types.d.ts.map +1 -1
  119. package/lib/typescript/module/view/glyph-run.d.ts +5 -2
  120. package/lib/typescript/module/view/glyph-run.d.ts.map +1 -1
  121. package/lib/typescript/module/view/morph-viewport.d.ts.map +1 -1
  122. package/lib/typescript/module/view/number-lane.d.ts.map +1 -1
  123. package/lib/typescript/module/view/number-run.d.ts.map +1 -1
  124. package/lib/typescript/module/view/slots-run.d.ts +15 -0
  125. package/lib/typescript/module/view/slots-run.d.ts.map +1 -0
  126. package/lib/typescript/module/view/text-run.d.ts +5 -1
  127. package/lib/typescript/module/view/text-run.d.ts.map +1 -1
  128. package/package.json +11 -2
  129. package/src/hooks/use-display-units.ts +1 -0
  130. package/src/hooks/use-inline-auto-width.ts +55 -12
  131. package/src/hooks/use-morph-motion.ts +2 -1
  132. package/src/hooks/use-morph-text-style.ts +3 -0
  133. package/src/hooks/use-numeric-lanes.ts +1 -0
  134. package/src/hooks/use-text-glyphs.ts +48 -10
  135. package/src/index.tsx +98 -17
  136. package/src/model/display-units.ts +4 -1
  137. package/src/model/numeric-lanes.ts +3 -0
  138. package/src/model/text-keys.ts +3 -0
  139. package/src/motion/entry-exit-builders.ts +73 -0
  140. package/src/motion/preset-map.ts +4 -0
  141. package/src/types.ts +14 -2
  142. package/src/view/glyph-run.tsx +38 -13
  143. package/src/view/morph-viewport.tsx +3 -0
  144. package/src/view/number-lane.tsx +7 -2
  145. package/src/view/number-run.tsx +3 -0
  146. package/src/view/slots-run.tsx +385 -0
  147. package/src/view/text-run.tsx +77 -6
@@ -2,11 +2,14 @@ import React from "react";
2
2
  import { type StyleProp, type TextStyle } from "react-native";
3
3
  import type { ComplexAnimationBuilder, EntryExitAnimationFunction } from "react-native-reanimated";
4
4
  import type { GlyphToken, LaminarAlign } from "../types";
5
- export declare const GlyphRun: React.MemoExoticComponent<({ glyphs, layoutTransition, enterTransition, exitTransition, align, textStyle, className, }: Readonly<{
5
+ export declare const GlyphRun: React.MemoExoticComponent<({ glyphs, layoutTransition, enterTransition, elementEnterTransition, exitTransition, elementExitTransition, leadingGap, align, textStyle, className, }: Readonly<{
6
6
  glyphs: readonly GlyphToken[];
7
- layoutTransition: ComplexAnimationBuilder;
7
+ layoutTransition?: ComplexAnimationBuilder;
8
8
  enterTransition?: EntryExitAnimationFunction;
9
+ elementEnterTransition?: EntryExitAnimationFunction;
9
10
  exitTransition?: EntryExitAnimationFunction;
11
+ elementExitTransition?: EntryExitAnimationFunction;
12
+ leadingGap?: number;
10
13
  align: LaminarAlign;
11
14
  textStyle?: StyleProp<TextStyle>;
12
15
  className?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"glyph-run.d.ts","sourceRoot":"","sources":["../../../../src/view/glyph-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EACV,uBAAuB,EACvB,0BAA0B,EAC3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAczD,eAAO,MAAM,QAAQ,0HAShB,QAAQ,CAAC;IACV,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;IAC9B,gBAAgB,EAAE,uBAAuB,CAAC;IAC1C,eAAe,CAAC,EAAE,0BAA0B,CAAC;IAC7C,cAAc,CAAC,EAAE,0BAA0B,CAAC;IAC5C,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,uBAiBH,CAAC"}
1
+ {"version":3,"file":"glyph-run.d.ts","sourceRoot":"","sources":["../../../../src/view/glyph-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EACV,uBAAuB,EACvB,0BAA0B,EAC3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAezD,eAAO,MAAM,QAAQ,qLAYhB,QAAQ,CAAC;IACV,MAAM,EAAE,SAAS,UAAU,EAAE,CAAC;IAC9B,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,eAAe,CAAC,EAAE,0BAA0B,CAAC;IAC7C,sBAAsB,CAAC,EAAE,0BAA0B,CAAC;IACpD,cAAc,CAAC,EAAE,0BAA0B,CAAC;IAC5C,qBAAqB,CAAC,EAAE,0BAA0B,CAAC;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC,uBAmCH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"morph-viewport.d.ts","sourceRoot":"","sources":["../../../../src/view/morph-viewport.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,QAAQ,MAAM,yBAAyB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AA4C7C,KAAK,kBAAkB,GAAG;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CACpC,CAAC;AAEF,eAAO,MAAM,aAAa,4HASrB,kBAAkB,uBAmCtB,CAAC"}
1
+ {"version":3,"file":"morph-viewport.d.ts","sourceRoot":"","sources":["../../../../src/view/morph-viewport.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,QAAQ,MAAM,yBAAyB,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AA4C7C,KAAK,kBAAkB,GAAG;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC;IAClF,QAAQ,CAAC,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CACpC,CAAC;AAGF,eAAO,MAAM,aAAa,4HASrB,kBAAkB,uBAqCtB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"number-lane.d.ts","sourceRoot":"","sources":["../../../../src/view/number-lane.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AACvC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIpE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAiBnE,KAAK,eAAe,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,oBAAoB,CAAC;IACzC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,UAAU,+IAYlB,eAAe,uBAiFnB,CAAC"}
1
+ {"version":3,"file":"number-lane.d.ts","sourceRoot":"","sources":["../../../../src/view/number-lane.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AACvC,OAAO,EAAQ,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIpE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAiBnE,KAAK,eAAe,GAAG;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,oBAAoB,CAAC;IACzC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAGF,eAAO,MAAM,UAAU,+IAYlB,eAAe,uBAqFnB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"number-run.d.ts","sourceRoot":"","sources":["../../../../src/view/number-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0B,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAe3D,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,SAAS,yGASjB,QAAQ,CAAC,cAAc,CAAC,uBA4C5B,CAAC"}
1
+ {"version":3,"file":"number-run.d.ts","sourceRoot":"","sources":["../../../../src/view/number-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0B,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAQ,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAe3D,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAGF,eAAO,MAAM,SAAS,yGASjB,QAAQ,CAAC,cAAc,CAAC,uBA8C5B,CAAC"}
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { type StyleProp, type TextStyle } from "react-native";
3
+ import type { LaminarAlign, MotionRecipe } from "../types";
4
+ type SlotsRunProps = {
5
+ readonly value: string;
6
+ readonly motionRecipe: MotionRecipe;
7
+ readonly align: LaminarAlign;
8
+ readonly fontSize?: number;
9
+ readonly textStyle?: StyleProp<TextStyle>;
10
+ readonly staggerMs: number;
11
+ readonly className?: string;
12
+ };
13
+ export declare const SlotsRun: React.MemoExoticComponent<({ value, motionRecipe, align, fontSize, textStyle, staggerMs, className, }: Readonly<SlotsRunProps>) => React.JSX.Element>;
14
+ export {};
15
+ //# sourceMappingURL=slots-run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"slots-run.d.ts","sourceRoot":"","sources":["../../../../src/view/slots-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqC,MAAM,OAAO,CAAC;AAC1D,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,SAAS,EAEf,MAAM,cAAc,CAAC;AAQtB,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAmQjF,KAAK,aAAa,GAAG;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAGF,eAAO,MAAM,QAAQ,yGAShB,QAAQ,CAAC,aAAa,CAAC,uBAwF3B,CAAC"}
@@ -1,13 +1,17 @@
1
1
  import React from "react";
2
+ import type { ReactNode } from "react";
2
3
  import type { StyleProp, TextStyle } from "react-native";
3
4
  import type { LaminarAlign, MotionRecipe } from "../types";
4
5
  type TextRunProps = {
5
6
  readonly value: string;
6
7
  readonly motionRecipe: MotionRecipe;
7
8
  readonly align: LaminarAlign;
9
+ readonly leading?: ReactNode;
10
+ readonly leadingKey?: string | number;
11
+ readonly leadingGap?: number;
8
12
  readonly textStyle?: StyleProp<TextStyle>;
9
13
  readonly className?: string;
10
14
  };
11
- export declare const TextRun: React.MemoExoticComponent<({ value, motionRecipe, align, textStyle, className }: TextRunProps) => React.JSX.Element>;
15
+ export declare const TextRun: React.MemoExoticComponent<({ value, motionRecipe, align, leading, leadingKey, leadingGap, textStyle, className, }: TextRunProps) => React.JSX.Element>;
12
16
  export {};
13
17
  //# sourceMappingURL=text-run.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"text-run.d.ts","sourceRoot":"","sources":["../../../../src/view/text-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwB,MAAM,OAAO,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG3D,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,eAAO,MAAM,OAAO,mFACqC,YAAY,uBA0BpE,CAAC"}
1
+ {"version":3,"file":"text-run.d.ts","sourceRoot":"","sources":["../../../../src/view/text-run.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkD,MAAM,OAAO,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAOzD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG3D,KAAK,YAAY,GAAG;IAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACtC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAGF,eAAO,MAAM,OAAO,qHAUf,YAAY,uBA8EhB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-laminar",
3
- "version": "1.2.0",
3
+ "version": "1.4.1",
4
4
  "description": "A React Native component for morphing text and numbers with character-level identity.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -21,6 +21,8 @@
21
21
  "react-native": "./src/index.tsx",
22
22
  "exports": {
23
23
  ".": {
24
+ "react-native": "./src/index.tsx",
25
+ "source": "./src/index.tsx",
24
26
  "import": {
25
27
  "types": "./lib/typescript/module/index.d.ts",
26
28
  "default": "./lib/module/index.js"
@@ -37,7 +39,14 @@
37
39
  ],
38
40
  "scripts": {
39
41
  "build": "bob build",
40
- "prepare": "bob build"
42
+ "prepare": "bob build",
43
+ "test": "jest --config jest.config.js --runInBand",
44
+ "test:watch": "jest --config jest.config.js --watch",
45
+ "test:coverage": "jest --config jest.config.js --runInBand --coverage && node scripts/check-coverage.js",
46
+ "test:ci": "jest --config jest.config.js --runInBand --ci --coverage && node scripts/check-coverage.js",
47
+ "typecheck": "tsc --project tsconfig.test.json --noEmit",
48
+ "pack:check": "npm pack --dry-run --cache ./coverage/npm-cache",
49
+ "quality": "npm run typecheck && npm run test:ci && npm run build && npm run pack:check"
41
50
  },
42
51
  "sideEffects": false,
43
52
  "peerDependencies": {
@@ -4,6 +4,7 @@ import {
4
4
  splitDisplayUnits,
5
5
  } from "../model/display-units";
6
6
 
7
+ // normalize the same units used by rendering before measurement reads them
7
8
  export const useDisplayUnits = (
8
9
  value: string,
9
10
  enabled = true
@@ -1,33 +1,36 @@
1
- import { useCallback, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import type { LayoutChangeEvent } from "react-native";
3
3
  import { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
4
4
 
5
5
  type Params = {
6
6
  enabled: boolean;
7
7
  driveToWidth: (toValue: number) => number;
8
+ measurementKey: string;
8
9
  };
9
10
 
10
- export const useInlineAutoWidth = ({ enabled, driveToWidth }: Params) => {
11
+ const WIDTH_CACHE_LIMIT = 64;
12
+
13
+ // measure each content signature once and drive later changes through one width value
14
+ export const useInlineAutoWidth = ({
15
+ enabled,
16
+ driveToWidth,
17
+ measurementKey,
18
+ }: Params) => {
11
19
  const widthValue = useSharedValue(0);
12
20
  const measuredWidthRef = useRef<number | null>(null);
13
21
  const bootstrappedRef = useRef(false);
22
+ const widthCacheRef = useRef(new Map<string, number>());
14
23
  const [hasBootstrappedWidth, setHasBootstrappedWidth] = useState(false);
15
24
 
16
- const captureLayout = useCallback(
17
- (event: LayoutChangeEvent) => {
18
- if (!enabled) {
19
- return;
20
- }
21
-
22
- const nextWidth = Math.max(0, Math.ceil(event.nativeEvent.layout.width));
23
-
25
+ // the first measurement establishes the shell without animating from zero
26
+ const applyWidth = useCallback(
27
+ (nextWidth: number) => {
24
28
  if (measuredWidthRef.current === nextWidth) {
25
29
  return;
26
30
  }
27
31
 
28
32
  measuredWidthRef.current = nextWidth;
29
33
 
30
- // snap the first width so mount does not animate from zero
31
34
  if (!bootstrappedRef.current) {
32
35
  bootstrappedRef.current = true;
33
36
  widthValue.value = nextWidth;
@@ -37,9 +40,48 @@ export const useInlineAutoWidth = ({ enabled, driveToWidth }: Params) => {
37
40
 
38
41
  widthValue.value = driveToWidth(nextWidth);
39
42
  },
40
- [driveToWidth, enabled, widthValue]
43
+ [driveToWidth, widthValue]
44
+ );
45
+
46
+ const captureLayout = useCallback(
47
+ (event: LayoutChangeEvent) => {
48
+ if (!enabled) {
49
+ return;
50
+ }
51
+
52
+ // round up so fractional native measurements cannot clip the final glyph
53
+ const nextWidth = Math.max(0, Math.ceil(event.nativeEvent.layout.width));
54
+
55
+ // delete before setting so a repeated key moves to the most recent position
56
+ widthCacheRef.current.delete(measurementKey);
57
+
58
+ if (widthCacheRef.current.size >= WIDTH_CACHE_LIMIT) {
59
+ // map insertion order gives the lru entry at the front
60
+ const oldestKey = widthCacheRef.current.keys().next().value;
61
+
62
+ if (oldestKey !== undefined) {
63
+ widthCacheRef.current.delete(oldestKey);
64
+ }
65
+ }
66
+
67
+ widthCacheRef.current.set(measurementKey, nextWidth);
68
+ applyWidth(nextWidth);
69
+ },
70
+ [applyWidth, enabled, measurementKey]
41
71
  );
42
72
 
73
+ const cachedWidth = widthCacheRef.current.get(measurementKey);
74
+
75
+ // a cached width can restore the shell before the hidden probe renders again
76
+ useEffect(() => {
77
+ if (enabled && cachedWidth !== undefined) {
78
+ // promote cache hits so active states stay available longer
79
+ widthCacheRef.current.delete(measurementKey);
80
+ widthCacheRef.current.set(measurementKey, cachedWidth);
81
+ applyWidth(cachedWidth);
82
+ }
83
+ }, [applyWidth, cachedWidth, enabled, measurementKey]);
84
+
43
85
  const animatedWidthStyle = useAnimatedStyle(
44
86
  () =>
45
87
  enabled && hasBootstrappedWidth
@@ -53,5 +95,6 @@ export const useInlineAutoWidth = ({ enabled, driveToWidth }: Params) => {
53
95
  return {
54
96
  captureLayout,
55
97
  animatedWidthStyle,
98
+ shouldMeasure: enabled && cachedWidth === undefined,
56
99
  };
57
100
  };
@@ -18,13 +18,14 @@ type MorphMotion = {
18
18
  readonly staggerMs: number;
19
19
  };
20
20
 
21
+ // resolve one motion recipe and one shared stagger unit for a render tree
21
22
  export const useMorphMotion = ({
22
23
  variant,
23
24
  animationPreset,
24
25
  animationDuration,
25
26
  stagger,
26
27
  }: Params): MorphMotion => {
27
- // numbers feel better with snappy unless you pick something else
28
+ // numeric variants need a quicker default because their lanes update often
28
29
  const resolvedPreset =
29
30
  animationPreset ?? (variant === "text" ? "default" : "snappy");
30
31
 
@@ -12,12 +12,14 @@ type MorphTextStyle = {
12
12
  readonly textStyle: StyleProp<TextStyle>;
13
13
  };
14
14
 
15
+ // compose the public style layers into the style passed to every glyph
15
16
  export const useMorphTextStyle = ({
16
17
  fontSize,
17
18
  color,
18
19
  fontStyle,
19
20
  style,
20
21
  }: Params): MorphTextStyle => {
22
+ // keep the base layer small so caller styles can override it predictably
21
23
  const baseTextStyle = useMemo(() => {
22
24
  const nextStyle: TextStyle = {
23
25
  includeFontPadding: false,
@@ -34,6 +36,7 @@ export const useMorphTextStyle = ({
34
36
  return nextStyle;
35
37
  }, [color, fontSize]);
36
38
 
39
+ // preserve the merge order used by the public style contract
37
40
  const textStyle = useMemo(
38
41
  () => [baseTextStyle, fontStyle, style],
39
42
  [baseTextStyle, fontStyle, style]
@@ -20,6 +20,7 @@ type NumericLaneSnapshot = {
20
20
  readonly leadLength: number;
21
21
  };
22
22
 
23
+ // retain numeric lane identity across value changes without storing it in state
23
24
  export const useNumericLanes = (value: string): NumericLaneSnapshot => {
24
25
  const units = useMemo(() => splitDisplayUnits(value), [value]);
25
26
  const ledgerRef = useRef<NumericLedger>({
@@ -1,4 +1,4 @@
1
- import { useMemo, useRef } from "react";
1
+ import { useMemo, useRef, type ReactNode } from "react";
2
2
  import {
3
3
  normalizeDisplayUnit,
4
4
  splitDisplayUnits,
@@ -8,28 +8,62 @@ import type { GlyphToken } from "../types";
8
8
 
9
9
  type TextGlyphLedger = {
10
10
  previousValue: string;
11
- previousUnits: readonly string[];
11
+ previousSignatures: readonly string[];
12
12
  glyphKeys: readonly string[];
13
13
  nextSeed: number;
14
14
  };
15
15
 
16
+ type TextGlyphUnit =
17
+ | { readonly kind: "text"; readonly signature: string; readonly value: string }
18
+ | { readonly kind: "element"; readonly signature: string; readonly element: ReactNode };
19
+
20
+ // build the inline token stream that lets text and leading content share one ledger
16
21
  export const useTextGlyphs = (
17
22
  value: string,
18
- namespace: string
23
+ namespace: string,
24
+ leading?: ReactNode,
25
+ leadingKey?: string | number
19
26
  ): readonly GlyphToken[] => {
20
- const units = useMemo(() => splitDisplayUnits(value), [value]);
27
+ const units = useMemo<readonly TextGlyphUnit[]>(
28
+ () => [
29
+ // the map key becomes the identity when leading content is state keyed
30
+ ...(leading
31
+ ? [
32
+ {
33
+ kind: "element",
34
+ signature: `leading:${String(leadingKey ?? "default")}`,
35
+ element: leading,
36
+ } as const,
37
+ ]
38
+ : []),
39
+ ...splitDisplayUnits(value).map((unit) => ({
40
+ kind: "text" as const,
41
+ signature: `text:${unit}`,
42
+ value: unit,
43
+ })),
44
+ ],
45
+ [leading, leadingKey, value]
46
+ );
47
+ const signatures = units.map((unit) => unit.signature);
21
48
  const ledgerRef = useRef<TextGlyphLedger>({
22
49
  previousValue: value,
23
- previousUnits: units,
50
+ previousSignatures: signatures,
24
51
  glyphKeys: units.map((_, index) => `${namespace}:c${index}`),
25
52
  nextSeed: units.length,
26
53
  });
27
54
 
28
- if (value !== ledgerRef.current.previousValue) {
55
+ // reconcile only when a token signature changes so stable glyphs keep their views
56
+ if (
57
+ value !== ledgerRef.current.previousValue ||
58
+ signatures.length !== ledgerRef.current.previousSignatures.length ||
59
+ signatures.some(
60
+ (signature, index) => signature !== ledgerRef.current.previousSignatures[index]
61
+ )
62
+ ) {
29
63
  // keep ids stable so unchanged glyphs stay mounted between updates
30
64
  const nextLedger = reconcileTextGlyphKeys(
31
- ledgerRef.current.previousUnits,
32
- units,
65
+ ledgerRef.current.previousSignatures,
66
+ signatures,
33
67
  ledgerRef.current.glyphKeys,
34
68
  ledgerRef.current.nextSeed,
35
69
  namespace
@@ -37,7 +71,7 @@ export const useTextGlyphs = (
37
71
 
38
72
  ledgerRef.current = {
39
73
  previousValue: value,
40
- previousUnits: units,
74
+ previousSignatures: signatures,
41
75
  glyphKeys: nextLedger.glyphKeys,
42
76
  nextSeed: nextLedger.nextSeed,
43
77
  };
@@ -45,11 +79,15 @@ export const useTextGlyphs = (
45
79
 
46
80
  const glyphKeys = ledgerRef.current.glyphKeys;
47
81
 
82
+ // normalize spaces at the edge where tokens become renderable glyphs
48
83
  return useMemo(
49
84
  () =>
50
85
  units.map((unit, index) => ({
51
86
  id: glyphKeys[index],
52
- value: normalizeDisplayUnit(unit),
87
+ kind: unit.kind,
88
+ ...(unit.kind === "text"
89
+ ? { value: normalizeDisplayUnit(unit.value) }
90
+ : { element: unit.element }),
53
91
  })),
54
92
  [glyphKeys, units]
55
93
  );
package/src/index.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Text } from "react-native";
2
+ import { StyleSheet, Text, View } from "react-native";
3
3
  import { useInlineAutoWidth } from "./hooks/use-inline-auto-width";
4
4
  import { useMorphMotion } from "./hooks/use-morph-motion";
5
5
  import { useMorphTextStyle } from "./hooks/use-morph-text-style";
@@ -7,11 +7,13 @@ import {
7
7
  normalizeDisplayUnit,
8
8
  splitDisplayUnits,
9
9
  } from "./model/display-units";
10
- import type { MorphingTextProps } from "./types";
10
+ import type { LaminarLeadingMap, MorphingTextProps } from "./types";
11
11
  import { MorphViewport } from "./view/morph-viewport";
12
12
  import { NumberRun } from "./view/number-run";
13
+ import { SlotsRun } from "./view/slots-run";
13
14
  import { TextRun } from "./view/text-run";
14
15
 
16
+ // choose the content variant and connect it to the shared viewport and measurement flow
15
17
  export const Laminar = React.memo(function Laminar({
16
18
  text,
17
19
  variant = "text",
@@ -19,6 +21,9 @@ export const Laminar = React.memo(function Laminar({
19
21
  color,
20
22
  align = "left",
21
23
  className,
24
+ leading,
25
+ leadingKey,
26
+ leadingGap = 0,
22
27
  style,
23
28
  containerStyle,
24
29
  fontStyle,
@@ -29,6 +34,12 @@ export const Laminar = React.memo(function Laminar({
29
34
  clipToBounds = false,
30
35
  }: Readonly<MorphingTextProps>) {
31
36
  const resolvedValue = String(text ?? "");
37
+ // resolve state keyed leading content before measurement and token reconciliation
38
+ const leadingMap = isLeadingMap(leading) ? leading : undefined;
39
+ const resolvedLeading: React.ReactNode = leadingMap
40
+ ? leadingMap[resolvedValue]
41
+ : (leading as React.ReactNode);
42
+ const resolvedLeadingKey = leadingMap ? resolvedValue : leadingKey;
32
43
  const { motionRecipe, staggerMs } = useMorphMotion({
33
44
  variant,
34
45
  animationPreset,
@@ -42,16 +53,47 @@ export const Laminar = React.memo(function Laminar({
42
53
  style,
43
54
  });
44
55
 
45
- const { captureLayout, animatedWidthStyle } = useInlineAutoWidth({
46
- enabled: autoSize,
47
- driveToWidth: motionRecipe.driveNumber,
48
- });
56
+ // invalidate the probe when text style, leading content, or leading spacing changes
57
+ const measurementKey = React.useMemo(() => {
58
+ const flattenedStyle = StyleSheet.flatten(textStyle);
59
+
60
+ return JSON.stringify([
61
+ resolvedValue,
62
+ flattenedStyle?.fontFamily,
63
+ flattenedStyle?.fontSize,
64
+ flattenedStyle?.fontStyle,
65
+ flattenedStyle?.fontWeight,
66
+ flattenedStyle?.fontVariant,
67
+ flattenedStyle?.letterSpacing,
68
+ flattenedStyle?.lineHeight,
69
+ flattenedStyle?.textTransform,
70
+ variant === "text" && Boolean(resolvedLeading),
71
+ variant === "text" ? resolvedLeadingKey : undefined,
72
+ variant === "text" ? leadingGap : 0,
73
+ ]);
74
+ }, [
75
+ leadingGap,
76
+ resolvedLeading,
77
+ resolvedLeadingKey,
78
+ resolvedValue,
79
+ textStyle,
80
+ variant,
81
+ ]);
82
+ const { captureLayout, animatedWidthStyle, shouldMeasure } =
83
+ useInlineAutoWidth({
84
+ enabled: autoSize,
85
+ driveToWidth: motionRecipe.driveNumber,
86
+ measurementKey,
87
+ });
88
+ // measurement uses the same normalized units as the visible text row
49
89
  const measuredValue = React.useMemo(() => {
50
90
  if (!autoSize) {
51
91
  return "";
52
92
  }
53
93
 
54
- return splitDisplayUnits(resolvedValue).map(normalizeDisplayUnit).join("");
94
+ return splitDisplayUnits(resolvedValue)
95
+ .map(normalizeDisplayUnit)
96
+ .join("");
55
97
  }, [autoSize, resolvedValue]);
56
98
 
57
99
  return (
@@ -62,16 +104,42 @@ export const Laminar = React.memo(function Laminar({
62
104
  containerStyle={containerStyle}
63
105
  animatedWidthStyle={animatedWidthStyle}
64
106
  measurement={
65
- <Text
66
- numberOfLines={1}
67
- onLayout={captureLayout}
68
- style={textStyle}
69
- >
70
- {measuredValue}
71
- </Text>
107
+ shouldMeasure ? (
108
+ <View
109
+ onLayout={captureLayout}
110
+ style={{ flexDirection: "row", alignSelf: "flex-start" }}
111
+ >
112
+ {variant === "text" && resolvedLeading ? (
113
+ <View
114
+ style={{
115
+ alignItems: "center",
116
+ justifyContent: "center",
117
+ marginRight: leadingGap,
118
+ }}
119
+ >
120
+ {resolvedLeading}
121
+ </View>
122
+ ) : null}
123
+ {splitDisplayUnits(measuredValue).map((unit, index) => (
124
+ <Text key={`${unit}-${index}`} style={textStyle}>
125
+ {normalizeDisplayUnit(unit)}
126
+ </Text>
127
+ ))}
128
+ </View>
129
+ ) : undefined
72
130
  }
73
131
  >
74
- {variant === "number" ? (
132
+ {variant === "slots" ? (
133
+ <SlotsRun
134
+ value={resolvedValue}
135
+ motionRecipe={motionRecipe}
136
+ align={align}
137
+ fontSize={fontSize}
138
+ textStyle={textStyle}
139
+ staggerMs={staggerMs}
140
+ className={className}
141
+ />
142
+ ) : variant === "number" ? (
75
143
  <NumberRun
76
144
  value={resolvedValue}
77
145
  motionRecipe={motionRecipe}
@@ -86,6 +154,9 @@ export const Laminar = React.memo(function Laminar({
86
154
  value={resolvedValue}
87
155
  motionRecipe={motionRecipe}
88
156
  align={align}
157
+ leading={resolvedLeading}
158
+ leadingKey={resolvedLeadingKey}
159
+ leadingGap={leadingGap}
89
160
  textStyle={textStyle}
90
161
  className={className}
91
162
  />
@@ -94,11 +165,21 @@ export const Laminar = React.memo(function Laminar({
94
165
  );
95
166
  });
96
167
 
97
- export const MorphingText = Laminar;
168
+ function isLeadingMap(
169
+ value: MorphingTextProps["leading"]
170
+ ): value is LaminarLeadingMap {
171
+ // react elements are objects too, so exclude them before treating a value as a map
172
+ return (
173
+ typeof value === "object" &&
174
+ value !== null &&
175
+ !React.isValidElement(value) &&
176
+ !Array.isArray(value)
177
+ );
178
+ }
98
179
 
99
- export default Laminar;
100
180
  export type {
101
181
  LaminarAlign,
182
+ LaminarLeadingMap,
102
183
  LaminarProps,
103
184
  MorphAnimationPresetName,
104
185
  MorphContentVariant,
@@ -4,6 +4,7 @@ const graphemeSegmenter =
4
4
  ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
5
5
  : null;
6
6
 
7
+ // segment graphemes when the runtime supports them so emoji and combining marks stay whole
7
8
  export const splitDisplayUnits = (input: string): string[] => {
8
9
  if (graphemeSegmenter) {
9
10
  return Array.from(
@@ -15,12 +16,14 @@ export const splitDisplayUnits = (input: string): string[] => {
15
16
  return Array.from(input);
16
17
  };
17
18
 
18
- // swap plain spaces so measured text keeps its width
19
+ // replace plain spaces because native text measurement can collapse them
19
20
  export const normalizeDisplayUnit = (unit: string) =>
20
21
  unit === " " ? NBSP : unit;
21
22
 
23
+ // numeric lanes only treat ascii digits as place-value columns
22
24
  export const isAsciiDigit = (unit: string) => unit >= "0" && unit <= "9";
23
25
 
26
+ // leading symbols and punctuation stay attached to the left side of a number
24
27
  export const findNumericLeadLength = (units: readonly string[]) => {
25
28
  const firstDigitIndex = units.findIndex(isAsciiDigit);
26
29
 
@@ -4,9 +4,11 @@ import {
4
4
  splitDisplayUnits,
5
5
  } from "./display-units";
6
6
 
7
+ // strip formatting before comparing values so direction follows the displayed number
7
8
  const readNumericMagnitude = (input: string) =>
8
9
  parseFloat(input.replace(/[^0-9.-]/g, "")) || 0;
9
10
 
11
+ // the sign controls which side of each digit lane enters and exits
10
12
  const deriveFlowDirection = (
11
13
  previousValue: string,
12
14
  nextValue: string
@@ -19,6 +21,7 @@ type ReconciledLaneState = {
19
21
  readonly direction: NumericFlowDirection;
20
22
  };
21
23
 
24
+ // preserve right-aligned place values while allowing prefixes to grow or shrink
22
25
  export const reconcileNumericLanes = (
23
26
  previousValue: string,
24
27
  nextValue: string,
@@ -1,3 +1,4 @@
1
+ // find the longest ordered set of shared units so edits keep nearby glyph identity
1
2
  const computeLcsPairs = (
2
3
  previousUnits: readonly string[],
3
4
  nextUnits: readonly string[]
@@ -9,6 +10,7 @@ const computeLcsPairs = (
9
10
  return [];
10
11
  }
11
12
 
13
+ // dynamic programming makes the middle edit deterministic when prefix and suffix match
12
14
  const dp = Array.from({ length: previousLength + 1 }, () =>
13
15
  new Array<number>(nextLength + 1).fill(0)
14
16
  );
@@ -54,6 +56,7 @@ type ReconciledTextGlyphState = {
54
56
  readonly nextSeed: number;
55
57
  };
56
58
 
59
+ // reuse prefix, suffix, and lcs matches before allocating keys for new glyphs
57
60
  export const reconcileTextGlyphKeys = (
58
61
  previousUnits: readonly string[],
59
62
  nextUnits: readonly string[],