@yahoo/uds-mobile 2.23.3 → 2.24.0-beta.2

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 (163) hide show
  1. package/README.md +4 -12
  2. package/dist/bin/automated-config/dist/generated/generatedConfigs.mjs +92 -46
  3. package/dist/components/BottomSheet/BottomSheet.cjs +1 -1
  4. package/dist/components/BottomSheet/BottomSheet.js +1 -1
  5. package/dist/components/Button/buttonTheme.cjs +21 -8
  6. package/dist/components/Button/buttonTheme.d.cts +16 -4
  7. package/dist/components/Button/buttonTheme.d.cts.map +1 -1
  8. package/dist/components/Button/buttonTheme.d.ts +16 -4
  9. package/dist/components/Button/buttonTheme.d.ts.map +1 -1
  10. package/dist/components/Button/buttonTheme.js +21 -8
  11. package/dist/components/Button/buttonTheme.js.map +1 -1
  12. package/dist/components/Button.cjs +20 -7
  13. package/dist/components/Button.d.cts +7 -0
  14. package/dist/components/Button.d.cts.map +1 -1
  15. package/dist/components/Button.d.ts +7 -0
  16. package/dist/components/Button.d.ts.map +1 -1
  17. package/dist/components/Button.js +20 -7
  18. package/dist/components/Button.js.map +1 -1
  19. package/dist/components/Checkbox.cjs +30 -7
  20. package/dist/components/Checkbox.d.cts +7 -0
  21. package/dist/components/Checkbox.d.cts.map +1 -1
  22. package/dist/components/Checkbox.d.ts +7 -0
  23. package/dist/components/Checkbox.d.ts.map +1 -1
  24. package/dist/components/Checkbox.js +30 -7
  25. package/dist/components/Checkbox.js.map +1 -1
  26. package/dist/components/Chip/ChipBase.cjs +23 -9
  27. package/dist/components/Chip/ChipBase.d.cts.map +1 -1
  28. package/dist/components/Chip/ChipBase.d.ts.map +1 -1
  29. package/dist/components/Chip/ChipBase.js +23 -9
  30. package/dist/components/Chip/ChipBase.js.map +1 -1
  31. package/dist/components/Chip/ChipDismissible.cjs +3 -1
  32. package/dist/components/Chip/ChipDismissible.js +3 -1
  33. package/dist/components/Chip/ChipDismissible.js.map +1 -1
  34. package/dist/components/Chip/types.d.cts +7 -0
  35. package/dist/components/Chip/types.d.cts.map +1 -1
  36. package/dist/components/Chip/types.d.ts +7 -0
  37. package/dist/components/Chip/types.d.ts.map +1 -1
  38. package/dist/components/Icon.cjs +4 -1
  39. package/dist/components/Icon.d.cts +9 -1
  40. package/dist/components/Icon.d.cts.map +1 -1
  41. package/dist/components/Icon.d.ts +9 -1
  42. package/dist/components/Icon.d.ts.map +1 -1
  43. package/dist/components/Icon.js +4 -1
  44. package/dist/components/Icon.js.map +1 -1
  45. package/dist/components/IconButton.js.map +1 -1
  46. package/dist/components/Radio/Radio.cjs +29 -11
  47. package/dist/components/Radio/Radio.d.cts +7 -0
  48. package/dist/components/Radio/Radio.d.cts.map +1 -1
  49. package/dist/components/Radio/Radio.d.ts +7 -0
  50. package/dist/components/Radio/Radio.d.ts.map +1 -1
  51. package/dist/components/Radio/Radio.js +29 -11
  52. package/dist/components/Radio/Radio.js.map +1 -1
  53. package/dist/components/Switch.cjs +29 -6
  54. package/dist/components/Switch.d.cts +7 -0
  55. package/dist/components/Switch.d.cts.map +1 -1
  56. package/dist/components/Switch.d.ts +7 -0
  57. package/dist/components/Switch.d.ts.map +1 -1
  58. package/dist/components/Switch.js +29 -6
  59. package/dist/components/Switch.js.map +1 -1
  60. package/dist/components/Tabs/Tab.cjs +12 -2
  61. package/dist/components/Tabs/Tab.d.cts.map +1 -1
  62. package/dist/components/Tabs/Tab.d.ts.map +1 -1
  63. package/dist/components/Tabs/Tab.js +12 -2
  64. package/dist/components/Tabs/Tab.js.map +1 -1
  65. package/dist/components/Tabs/Tabs.cjs +8 -3
  66. package/dist/components/Tabs/Tabs.d.cts +9 -1
  67. package/dist/components/Tabs/Tabs.d.cts.map +1 -1
  68. package/dist/components/Tabs/Tabs.d.ts +9 -1
  69. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  70. package/dist/components/Tabs/Tabs.js +8 -3
  71. package/dist/components/Tabs/Tabs.js.map +1 -1
  72. package/dist/components/Tabs/tabsContexts.d.cts +2 -0
  73. package/dist/components/Tabs/tabsContexts.d.cts.map +1 -1
  74. package/dist/components/Tabs/tabsContexts.d.ts +2 -0
  75. package/dist/components/Tabs/tabsContexts.d.ts.map +1 -1
  76. package/dist/components/Tabs/tabsContexts.js.map +1 -1
  77. package/dist/components/Text.cjs +4 -1
  78. package/dist/components/Text.d.cts +10 -2
  79. package/dist/components/Text.d.cts.map +1 -1
  80. package/dist/components/Text.d.ts +10 -2
  81. package/dist/components/Text.d.ts.map +1 -1
  82. package/dist/components/Text.js +4 -1
  83. package/dist/components/Text.js.map +1 -1
  84. package/dist/components/UDSProvider.cjs +6 -2
  85. package/dist/components/UDSProvider.d.cts +8 -0
  86. package/dist/components/UDSProvider.d.cts.map +1 -1
  87. package/dist/components/UDSProvider.d.ts +8 -0
  88. package/dist/components/UDSProvider.d.ts.map +1 -1
  89. package/dist/components/UDSProvider.js +6 -2
  90. package/dist/components/UDSProvider.js.map +1 -1
  91. package/dist/components/internal/Overlay/index.cjs +1 -1
  92. package/dist/components/internal/Overlay/index.js +1 -1
  93. package/dist/fontScaling/FontScalingContext.cjs +35 -0
  94. package/dist/fontScaling/FontScalingContext.d.cts +38 -0
  95. package/dist/fontScaling/FontScalingContext.d.cts.map +1 -0
  96. package/dist/fontScaling/FontScalingContext.d.ts +38 -0
  97. package/dist/fontScaling/FontScalingContext.d.ts.map +1 -0
  98. package/dist/fontScaling/FontScalingContext.js +34 -0
  99. package/dist/fontScaling/FontScalingContext.js.map +1 -0
  100. package/dist/fontScaling/constants.cjs +72 -0
  101. package/dist/fontScaling/constants.d.cts +49 -0
  102. package/dist/fontScaling/constants.d.cts.map +1 -0
  103. package/dist/fontScaling/constants.d.ts +49 -0
  104. package/dist/fontScaling/constants.d.ts.map +1 -0
  105. package/dist/fontScaling/constants.js +70 -0
  106. package/dist/fontScaling/constants.js.map +1 -0
  107. package/dist/fontScaling/index.cjs +13 -0
  108. package/dist/fontScaling/index.d.cts +8 -0
  109. package/dist/fontScaling/index.d.ts +8 -0
  110. package/dist/fontScaling/index.js +6 -0
  111. package/dist/fontScaling/resolveMaxFontSizeMultiplier.cjs +38 -0
  112. package/dist/fontScaling/resolveMaxFontSizeMultiplier.d.cts +36 -0
  113. package/dist/fontScaling/resolveMaxFontSizeMultiplier.d.cts.map +1 -0
  114. package/dist/fontScaling/resolveMaxFontSizeMultiplier.d.ts +36 -0
  115. package/dist/fontScaling/resolveMaxFontSizeMultiplier.d.ts.map +1 -0
  116. package/dist/fontScaling/resolveMaxFontSizeMultiplier.js +39 -0
  117. package/dist/fontScaling/resolveMaxFontSizeMultiplier.js.map +1 -0
  118. package/dist/fontScaling/types.cjs +1 -0
  119. package/dist/fontScaling/types.d.cts +51 -0
  120. package/dist/fontScaling/types.d.cts.map +1 -0
  121. package/dist/fontScaling/types.d.ts +51 -0
  122. package/dist/fontScaling/types.d.ts.map +1 -0
  123. package/dist/fontScaling/types.js +1 -0
  124. package/dist/fontScaling/useFontScale.cjs +32 -0
  125. package/dist/fontScaling/useFontScale.d.cts +18 -0
  126. package/dist/fontScaling/useFontScale.d.cts.map +1 -0
  127. package/dist/fontScaling/useFontScale.d.ts +18 -0
  128. package/dist/fontScaling/useFontScale.d.ts.map +1 -0
  129. package/dist/fontScaling/useFontScale.js +32 -0
  130. package/dist/fontScaling/useFontScale.js.map +1 -0
  131. package/dist/fontScaling/useMaxFontSizeMultiplier.cjs +24 -0
  132. package/dist/fontScaling/useMaxFontSizeMultiplier.d.cts +16 -0
  133. package/dist/fontScaling/useMaxFontSizeMultiplier.d.cts.map +1 -0
  134. package/dist/fontScaling/useMaxFontSizeMultiplier.d.ts +16 -0
  135. package/dist/fontScaling/useMaxFontSizeMultiplier.d.ts.map +1 -0
  136. package/dist/fontScaling/useMaxFontSizeMultiplier.js +24 -0
  137. package/dist/fontScaling/useMaxFontSizeMultiplier.js.map +1 -0
  138. package/dist/jest/mocks/react-native.cjs +19 -4
  139. package/dist/jest/mocks/react-native.d.cts +8 -5
  140. package/dist/jest/mocks/react-native.d.cts.map +1 -1
  141. package/dist/jest/mocks/react-native.d.ts +8 -5
  142. package/dist/jest/mocks/react-native.d.ts.map +1 -1
  143. package/dist/jest/mocks/react-native.js +18 -5
  144. package/dist/jest/mocks/react-native.js.map +1 -1
  145. package/dist/jest/mocks/styles.cjs +15 -0
  146. package/dist/jest/mocks/styles.d.cts.map +1 -1
  147. package/dist/jest/mocks/styles.d.ts.map +1 -1
  148. package/dist/jest/mocks/styles.js +15 -0
  149. package/dist/jest/mocks/styles.js.map +1 -1
  150. package/dist/jest/setup.cjs +4 -0
  151. package/dist/jest/setup.d.cts.map +1 -1
  152. package/dist/jest/setup.d.ts.map +1 -1
  153. package/dist/jest/setup.js +4 -0
  154. package/dist/jest/setup.js.map +1 -1
  155. package/dist/portal.d.cts.map +1 -1
  156. package/dist/portal.d.ts.map +1 -1
  157. package/dist/portal.js.map +1 -1
  158. package/dist/types/dist/index.d.cts.map +1 -1
  159. package/dist/types/dist/index.d.ts.map +1 -1
  160. package/fonts/uds-icons.ttf +0 -0
  161. package/generated/styles.d.ts +5 -28
  162. package/generated/unistyles.d.ts +0 -27
  163. package/package.json +11 -1
@@ -43,8 +43,15 @@ function getSafeIconCell(fontSize) {
43
43
  const overflowBelowEm = Math.max(0, -metrics.bbox.minY);
44
44
  return Math.ceil(fontSize) + getMetricHeight(overflowAboveEm, metrics.unitsPerEm, fontSize) + getMetricHeight(overflowBelowEm, metrics.unitsPerEm, fontSize);
45
45
  }
46
- /** Safe native content metrics while retaining the token-derived target control height. */
47
- function getButtonControlMetrics(theme, size) {
46
+ /**
47
+ * Safe native content metrics while retaining the token-derived target control height.
48
+ *
49
+ * `fontScaleFactor` (the capped OS font scale, >= 1) pre-scales container
50
+ * metrics only. The returned `labelLineHeight`/`iconCellHeight` stay unscaled
51
+ * because they go into Text/Icon styles, which native multiplies by the same
52
+ * capped factor — container math must mirror that, not double-apply it.
53
+ */
54
+ function getButtonControlMetrics(theme, size, fontScaleFactor = 1) {
48
55
  const rootStyle = getLayerStyle(theme, buttonSizePath(size, "root"));
49
56
  const iconStyle = getLayerStyle(theme, buttonSizePath(size, "icon"));
50
57
  const textStyle = getLayerStyle(theme, buttonSizePath(size, "rootText"));
@@ -52,19 +59,25 @@ function getButtonControlMetrics(theme, size) {
52
59
  const iconSize = getIconSize(iconStyle);
53
60
  const fontSize = typeof textStyle.fontSize === "number" ? textStyle.fontSize : iconSize;
54
61
  const tokenLineHeight = typeof textStyle.lineHeight === "number" ? textStyle.lineHeight : fontSize;
55
- const targetContentHeight = Math.max(iconSize, tokenLineHeight);
62
+ const targetContentHeight = Math.max(iconSize, tokenLineHeight) * fontScaleFactor;
56
63
  const minHeight = Math.ceil(paddingVertical * 2 + targetContentHeight);
57
64
  const labelLineHeight = getSafeLabelLineHeight(textStyle, iconSize);
58
65
  const iconCellHeight = getSafeIconCell(iconSize);
66
+ const safeContentHeight = Math.max(labelLineHeight, iconCellHeight) * fontScaleFactor;
59
67
  return {
60
68
  minHeight,
61
69
  labelLineHeight,
62
70
  iconCellHeight,
63
- paddingVertical: Math.max(0, (minHeight - Math.max(labelLineHeight, iconCellHeight)) / 2)
71
+ paddingVertical: Math.max(0, (minHeight - safeContentHeight) / 2)
64
72
  };
65
73
  }
66
- /** Safe icon cell within the token-derived control size; explicit token height (match-button-height opt-in) wins as the target. */
67
- function getIconButtonControlMetrics(theme, size) {
74
+ /**
75
+ * Safe icon cell within the token-derived control size; explicit token height (match-button-height opt-in) wins as the target.
76
+ *
77
+ * `fontScaleFactor` behaves as in {@link getButtonControlMetrics}; an explicit
78
+ * token height stays fixed, with padding absorbing the scaled content.
79
+ */
80
+ function getIconButtonControlMetrics(theme, size, fontScaleFactor = 1) {
68
81
  const rootStyle = getLayerStyle(theme, iconButtonSizePath(size, "root"));
69
82
  const iconStyle = getLayerStyle(theme, iconButtonSizePath(size, "icon"));
70
83
  const iconSize = getIconSize(iconStyle);
@@ -72,12 +85,12 @@ function getIconButtonControlMetrics(theme, size) {
72
85
  const paddingVertical = getVerticalPadding(rootStyle);
73
86
  const explicitHeight = typeof rootStyle.height === "number" ? rootStyle.height : void 0;
74
87
  const hasExplicitHeight = SHARED_BUTTON_ICONBUTTON_SIZES.has(size) && explicitHeight !== void 0;
75
- const minHeight = hasExplicitHeight ? explicitHeight : Math.ceil(paddingVertical * 2 + Math.max(iconSize, tokenLineHeight));
88
+ const minHeight = hasExplicitHeight ? explicitHeight : Math.ceil(paddingVertical * 2 + Math.max(iconSize, tokenLineHeight) * fontScaleFactor);
76
89
  const iconCellHeight = getSafeIconCell(iconSize);
77
90
  return {
78
91
  minHeight,
79
92
  iconCellHeight,
80
- paddingVertical: Math.max(0, (minHeight - Math.max(iconSize, iconCellHeight)) / 2),
93
+ paddingVertical: Math.max(0, (minHeight - Math.max(iconSize, iconCellHeight) * fontScaleFactor) / 2),
81
94
  hasExplicitHeight
82
95
  };
83
96
  }
@@ -1 +1 @@
1
- {"version":3,"file":"buttonTheme.js","names":[],"sources":["../../../src/components/Button/buttonTheme.ts"],"sourcesContent":["import type { ButtonSize, IconButtonSize } from '@yahoo/uds-types';\nimport type { TextStyle, ViewStyle } from 'react-native';\n\nimport { fontMetrics } from '../../../generated/fontMetrics';\n\ntype ButtonTheme = {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n components: Record<string, any>;\n};\n\nconst SHARED_BUTTON_ICONBUTTON_SIZES = new Set<IconButtonSize>(['xs', 'sm', 'md', 'lg']);\n// Covers common native font boxes (roughly 1.2em) plus pixel-rounding headroom.\nconst UNKNOWN_FONT_LINE_HEIGHT_RATIO = 1.3;\n\nfunction buttonSizePath(size: ButtonSize, layer: 'root' | 'icon' | 'rootText'): string {\n return `button/size/${size}/${layer}/rest`;\n}\n\nfunction iconButtonSizePath(size: IconButtonSize, layer: 'root' | 'icon'): string {\n return `iconButton/size/${size}/${layer}/rest`;\n}\n\nfunction getLayerStyle(theme: ButtonTheme, path: string): ViewStyle | TextStyle {\n const style = theme.components[path];\n return style && typeof style === 'object' ? style : {};\n}\n\nfunction getIconSize(iconStyle: TextStyle): number {\n return (\n (typeof iconStyle.fontSize === 'number' ? iconStyle.fontSize : undefined) ??\n (typeof iconStyle.lineHeight === 'number' ? iconStyle.lineHeight : undefined) ??\n 16\n );\n}\n\nfunction getVerticalPadding(rootStyle: ViewStyle): number {\n return (\n (typeof rootStyle.paddingVertical === 'number' ? rootStyle.paddingVertical : undefined) ??\n (typeof rootStyle.padding === 'number' ? rootStyle.padding : undefined) ??\n 0\n );\n}\n\nfunction getMetricHeight(value: number, unitsPerEm: number, fontSize: number): number {\n return Math.ceil((Math.max(0, value) / unitsPerEm) * fontSize);\n}\n\nfunction getSafeLabelLineHeight(textStyle: TextStyle, fallbackFontSize: number): number {\n const fontSize = typeof textStyle.fontSize === 'number' ? textStyle.fontSize : fallbackFontSize;\n const tokenLineHeight =\n typeof textStyle.lineHeight === 'number' ? textStyle.lineHeight : fontSize;\n const metrics =\n typeof textStyle.fontFamily === 'string' ? fontMetrics[textStyle.fontFamily] : undefined;\n\n if (!metrics || metrics.unitsPerEm <= 0) {\n return Math.max(tokenLineHeight, Math.ceil(fontSize * UNKNOWN_FONT_LINE_HEIGHT_RATIO));\n }\n\n const metricsLineHeight =\n getMetricHeight(metrics.ascent, metrics.unitsPerEm, fontSize) +\n getMetricHeight(-metrics.descent, metrics.unitsPerEm, fontSize) +\n getMetricHeight(metrics.lineGap, metrics.unitsPerEm, fontSize);\n\n return Math.max(tokenLineHeight, metricsLineHeight);\n}\n\n/** Safe line box for the icon font's em square plus any bbox overflow. */\nexport function getSafeIconCell(fontSize: number): number {\n const metrics = fontMetrics['uds-icons'];\n if (!metrics || metrics.unitsPerEm <= 0) {\n return Math.ceil(fontSize * UNKNOWN_FONT_LINE_HEIGHT_RATIO);\n }\n\n const overflowAboveEm = Math.max(0, metrics.bbox.maxY - metrics.unitsPerEm);\n const overflowBelowEm = Math.max(0, -metrics.bbox.minY);\n\n return (\n Math.ceil(fontSize) +\n getMetricHeight(overflowAboveEm, metrics.unitsPerEm, fontSize) +\n getMetricHeight(overflowBelowEm, metrics.unitsPerEm, fontSize)\n );\n}\n\n/** Safe native content metrics while retaining the token-derived target control height. */\nexport function getButtonControlMetrics(\n theme: ButtonTheme,\n size: ButtonSize,\n): {\n minHeight: number;\n labelLineHeight: number;\n iconCellHeight: number;\n paddingVertical: number;\n} {\n const rootStyle = getLayerStyle(theme, buttonSizePath(size, 'root')) as ViewStyle;\n const iconStyle = getLayerStyle(theme, buttonSizePath(size, 'icon')) as TextStyle;\n const textStyle = getLayerStyle(theme, buttonSizePath(size, 'rootText')) as TextStyle;\n const paddingVertical = getVerticalPadding(rootStyle);\n const iconSize = getIconSize(iconStyle);\n const fontSize = typeof textStyle.fontSize === 'number' ? textStyle.fontSize : iconSize;\n const tokenLineHeight =\n typeof textStyle.lineHeight === 'number' ? textStyle.lineHeight : fontSize;\n const targetContentHeight = Math.max(iconSize, tokenLineHeight);\n const minHeight = Math.ceil(paddingVertical * 2 + targetContentHeight);\n const labelLineHeight = getSafeLabelLineHeight(textStyle, iconSize);\n const iconCellHeight = getSafeIconCell(iconSize);\n const safeContentHeight = Math.max(labelLineHeight, iconCellHeight);\n\n return {\n minHeight,\n labelLineHeight,\n iconCellHeight,\n paddingVertical: Math.max(0, (minHeight - safeContentHeight) / 2),\n };\n}\n\n/** Safe icon cell within the token-derived control size; explicit token height (match-button-height opt-in) wins as the target. */\nexport function getIconButtonControlMetrics(\n theme: ButtonTheme,\n size: IconButtonSize,\n): {\n minHeight: number;\n iconCellHeight: number;\n paddingVertical: number;\n hasExplicitHeight: boolean;\n} {\n const rootStyle = getLayerStyle(theme, iconButtonSizePath(size, 'root')) as ViewStyle;\n const iconStyle = getLayerStyle(theme, iconButtonSizePath(size, 'icon')) as TextStyle;\n const iconSize = getIconSize(iconStyle);\n const tokenLineHeight =\n typeof iconStyle.lineHeight === 'number' ? iconStyle.lineHeight : iconSize;\n const paddingVertical = getVerticalPadding(rootStyle);\n const explicitHeight = typeof rootStyle.height === 'number' ? rootStyle.height : undefined;\n const hasExplicitHeight =\n SHARED_BUTTON_ICONBUTTON_SIZES.has(size) && explicitHeight !== undefined;\n const minHeight = hasExplicitHeight\n ? (explicitHeight as number)\n : Math.ceil(paddingVertical * 2 + Math.max(iconSize, tokenLineHeight));\n const iconCellHeight = getSafeIconCell(iconSize);\n\n return {\n minHeight,\n iconCellHeight,\n paddingVertical: Math.max(0, (minHeight - Math.max(iconSize, iconCellHeight)) / 2),\n hasExplicitHeight,\n };\n}\n\n/** Apply the metric-safe line height after the token typography styles. */\nexport function getButtonTextStyle(textStyle: TextStyle, labelLineHeight: number): TextStyle {\n return {\n ...textStyle,\n lineHeight: labelLineHeight,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"buttonTheme.js","names":[],"sources":["../../../src/components/Button/buttonTheme.ts"],"sourcesContent":["import type { ButtonSize, IconButtonSize } from '@yahoo/uds-types';\nimport type { TextStyle, ViewStyle } from 'react-native';\n\nimport { fontMetrics } from '../../../generated/fontMetrics';\n\ntype ButtonTheme = {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n components: Record<string, any>;\n};\n\nconst SHARED_BUTTON_ICONBUTTON_SIZES = new Set<IconButtonSize>(['xs', 'sm', 'md', 'lg']);\n// Covers common native font boxes (roughly 1.2em) plus pixel-rounding headroom.\nconst UNKNOWN_FONT_LINE_HEIGHT_RATIO = 1.3;\n\nfunction buttonSizePath(size: ButtonSize, layer: 'root' | 'icon' | 'rootText'): string {\n return `button/size/${size}/${layer}/rest`;\n}\n\nfunction iconButtonSizePath(size: IconButtonSize, layer: 'root' | 'icon'): string {\n return `iconButton/size/${size}/${layer}/rest`;\n}\n\nfunction getLayerStyle(theme: ButtonTheme, path: string): ViewStyle | TextStyle {\n const style = theme.components[path];\n return style && typeof style === 'object' ? style : {};\n}\n\nfunction getIconSize(iconStyle: TextStyle): number {\n return (\n (typeof iconStyle.fontSize === 'number' ? iconStyle.fontSize : undefined) ??\n (typeof iconStyle.lineHeight === 'number' ? iconStyle.lineHeight : undefined) ??\n 16\n );\n}\n\nfunction getVerticalPadding(rootStyle: ViewStyle): number {\n return (\n (typeof rootStyle.paddingVertical === 'number' ? rootStyle.paddingVertical : undefined) ??\n (typeof rootStyle.padding === 'number' ? rootStyle.padding : undefined) ??\n 0\n );\n}\n\nfunction getMetricHeight(value: number, unitsPerEm: number, fontSize: number): number {\n return Math.ceil((Math.max(0, value) / unitsPerEm) * fontSize);\n}\n\nfunction getSafeLabelLineHeight(textStyle: TextStyle, fallbackFontSize: number): number {\n const fontSize = typeof textStyle.fontSize === 'number' ? textStyle.fontSize : fallbackFontSize;\n const tokenLineHeight =\n typeof textStyle.lineHeight === 'number' ? textStyle.lineHeight : fontSize;\n const metrics =\n typeof textStyle.fontFamily === 'string' ? fontMetrics[textStyle.fontFamily] : undefined;\n\n if (!metrics || metrics.unitsPerEm <= 0) {\n return Math.max(tokenLineHeight, Math.ceil(fontSize * UNKNOWN_FONT_LINE_HEIGHT_RATIO));\n }\n\n const metricsLineHeight =\n getMetricHeight(metrics.ascent, metrics.unitsPerEm, fontSize) +\n getMetricHeight(-metrics.descent, metrics.unitsPerEm, fontSize) +\n getMetricHeight(metrics.lineGap, metrics.unitsPerEm, fontSize);\n\n return Math.max(tokenLineHeight, metricsLineHeight);\n}\n\n/** Safe line box for the icon font's em square plus any bbox overflow. */\nexport function getSafeIconCell(fontSize: number): number {\n const metrics = fontMetrics['uds-icons'];\n if (!metrics || metrics.unitsPerEm <= 0) {\n return Math.ceil(fontSize * UNKNOWN_FONT_LINE_HEIGHT_RATIO);\n }\n\n const overflowAboveEm = Math.max(0, metrics.bbox.maxY - metrics.unitsPerEm);\n const overflowBelowEm = Math.max(0, -metrics.bbox.minY);\n\n return (\n Math.ceil(fontSize) +\n getMetricHeight(overflowAboveEm, metrics.unitsPerEm, fontSize) +\n getMetricHeight(overflowBelowEm, metrics.unitsPerEm, fontSize)\n );\n}\n\n/**\n * Safe native content metrics while retaining the token-derived target control height.\n *\n * `fontScaleFactor` (the capped OS font scale, >= 1) pre-scales container\n * metrics only. The returned `labelLineHeight`/`iconCellHeight` stay unscaled\n * because they go into Text/Icon styles, which native multiplies by the same\n * capped factor — container math must mirror that, not double-apply it.\n */\nexport function getButtonControlMetrics(\n theme: ButtonTheme,\n size: ButtonSize,\n fontScaleFactor = 1,\n): {\n minHeight: number;\n labelLineHeight: number;\n iconCellHeight: number;\n paddingVertical: number;\n} {\n const rootStyle = getLayerStyle(theme, buttonSizePath(size, 'root')) as ViewStyle;\n const iconStyle = getLayerStyle(theme, buttonSizePath(size, 'icon')) as TextStyle;\n const textStyle = getLayerStyle(theme, buttonSizePath(size, 'rootText')) as TextStyle;\n const paddingVertical = getVerticalPadding(rootStyle);\n const iconSize = getIconSize(iconStyle);\n const fontSize = typeof textStyle.fontSize === 'number' ? textStyle.fontSize : iconSize;\n const tokenLineHeight =\n typeof textStyle.lineHeight === 'number' ? textStyle.lineHeight : fontSize;\n const targetContentHeight = Math.max(iconSize, tokenLineHeight) * fontScaleFactor;\n const minHeight = Math.ceil(paddingVertical * 2 + targetContentHeight);\n const labelLineHeight = getSafeLabelLineHeight(textStyle, iconSize);\n const iconCellHeight = getSafeIconCell(iconSize);\n const safeContentHeight = Math.max(labelLineHeight, iconCellHeight) * fontScaleFactor;\n\n return {\n minHeight,\n labelLineHeight,\n iconCellHeight,\n paddingVertical: Math.max(0, (minHeight - safeContentHeight) / 2),\n };\n}\n\n/**\n * Safe icon cell within the token-derived control size; explicit token height (match-button-height opt-in) wins as the target.\n *\n * `fontScaleFactor` behaves as in {@link getButtonControlMetrics}; an explicit\n * token height stays fixed, with padding absorbing the scaled content.\n */\nexport function getIconButtonControlMetrics(\n theme: ButtonTheme,\n size: IconButtonSize,\n fontScaleFactor = 1,\n): {\n minHeight: number;\n iconCellHeight: number;\n paddingVertical: number;\n hasExplicitHeight: boolean;\n} {\n const rootStyle = getLayerStyle(theme, iconButtonSizePath(size, 'root')) as ViewStyle;\n const iconStyle = getLayerStyle(theme, iconButtonSizePath(size, 'icon')) as TextStyle;\n const iconSize = getIconSize(iconStyle);\n const tokenLineHeight =\n typeof iconStyle.lineHeight === 'number' ? iconStyle.lineHeight : iconSize;\n const paddingVertical = getVerticalPadding(rootStyle);\n const explicitHeight = typeof rootStyle.height === 'number' ? rootStyle.height : undefined;\n const hasExplicitHeight =\n SHARED_BUTTON_ICONBUTTON_SIZES.has(size) && explicitHeight !== undefined;\n const minHeight = hasExplicitHeight\n ? (explicitHeight as number)\n : Math.ceil(paddingVertical * 2 + Math.max(iconSize, tokenLineHeight) * fontScaleFactor);\n const iconCellHeight = getSafeIconCell(iconSize);\n\n return {\n minHeight,\n iconCellHeight,\n paddingVertical: Math.max(\n 0,\n (minHeight - Math.max(iconSize, iconCellHeight) * fontScaleFactor) / 2,\n ),\n hasExplicitHeight,\n };\n}\n\n/** Apply the metric-safe line height after the token typography styles. */\nexport function getButtonTextStyle(textStyle: TextStyle, labelLineHeight: number): TextStyle {\n return {\n ...textStyle,\n lineHeight: labelLineHeight,\n };\n}\n"],"mappings":"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"}
@@ -3,9 +3,11 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
4
4
  const require_index = require("../motion-tokens/dist/index.cjs");
5
5
  const require_motion = require("../motion.cjs");
6
+ const require_fontScaling_useMaxFontSizeMultiplier = require("../fontScaling/useMaxFontSizeMultiplier.cjs");
6
7
  const require_components_Button_buttonTheme = require("./Button/buttonTheme.cjs");
7
8
  const require_components_IconSlot = require("./IconSlot.cjs");
8
9
  const require_components_Text = require("./Text.cjs");
10
+ const require_fontScaling_useFontScale = require("../fontScaling/useFontScale.cjs");
9
11
  const require_components_Pressable = require("./Pressable.cjs");
10
12
  let react = require("react");
11
13
  let react_native = require("react-native");
@@ -84,10 +86,16 @@ function AnimatedIconSlot({ children, visible, iconSize, iconCellHeight, gap })
84
86
  * @see {@link IconButton} for icon-only buttons
85
87
  * @see {@link Link} for navigation actions
86
88
  */
87
- const Button = (0, react.memo)(function Button({ variant = "primary", size = "md", iconVariant = "outline", startIcon, endIcon, loading, disabled, width: _width, children, style, accessibilityLabel, accessibilityHint, disableEffects = false, onPressIn, onPressOut, ref, ...props }) {
89
+ const Button = (0, react.memo)(function Button({ variant = "primary", size = "md", iconVariant = "outline", startIcon, endIcon, loading, disabled, width: _width, children, style, accessibilityLabel, accessibilityHint, disableEffects = false, maxFontSizeMultiplier, onPressIn, onPressOut, ref, ...props }) {
88
90
  const shouldAnimate = !disableEffects;
91
+ const resolvedMaxFontSizeMultiplier = require_fontScaling_useMaxFontSizeMultiplier.useMaxFontSizeMultiplier("control", maxFontSizeMultiplier) ?? 0;
92
+ const fontScaleFactor = require_fontScaling_useFontScale.useFontScale(resolvedMaxFontSizeMultiplier);
89
93
  const { theme } = (0, react_native_unistyles.useUnistyles)();
90
- const { minHeight, labelLineHeight, iconCellHeight, paddingVertical } = (0, react.useMemo)(() => require_components_Button_buttonTheme.getButtonControlMetrics(theme, size), [theme, size]);
94
+ const { minHeight, labelLineHeight, iconCellHeight, paddingVertical } = (0, react.useMemo)(() => require_components_Button_buttonTheme.getButtonControlMetrics(theme, size, fontScaleFactor), [
95
+ theme,
96
+ size,
97
+ fontScaleFactor
98
+ ]);
91
99
  const [pressed, setPressed] = (0, react.useState)(false);
92
100
  generated_styles.buttonStyles.useVariants({
93
101
  size,
@@ -120,6 +128,7 @@ const Button = (0, react.memo)(function Button({ variant = "primary", size = "md
120
128
  const childrenNode = children && ((0, react.isValidElement)(children) ? children : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_Text.Text, {
121
129
  numberOfLines: 1,
122
130
  textAlign: "center",
131
+ maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
123
132
  style: require_components_Button_buttonTheme.getButtonTextStyle(generated_styles.buttonStyles.text, labelLineHeight),
124
133
  children
125
134
  }));
@@ -142,30 +151,34 @@ const Button = (0, react.memo)(function Button({ variant = "primary", size = "md
142
151
  const showStartIcon = !!startIcon && !loading;
143
152
  const showEndIcon = !!endIcon && !loading;
144
153
  const iconSizeToken = generated_styles.buttonStyles.icon.iconSizeToken ?? "sm";
154
+ const scaledIconSize = Math.round(iconSize * fontScaleFactor);
155
+ const scaledIconCellHeight = Math.ceil(iconCellHeight * fontScaleFactor);
145
156
  const startContent = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedIconSlot, {
146
157
  visible: showLoading || showStartIcon,
147
- iconSize,
148
- iconCellHeight,
158
+ iconSize: scaledIconSize,
159
+ iconCellHeight: scaledIconCellHeight,
149
160
  gap: buttonGap,
150
161
  children: showLoading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_native.ActivityIndicator, {
151
- size: generated_styles.buttonStyles.icon.fontSize,
162
+ size: scaledIconSize,
152
163
  color: generated_styles.buttonStyles.icon.color
153
164
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_IconSlot.IconSlot, {
154
165
  icon: startIcon,
155
166
  size: iconSizeToken,
156
167
  variant: iconVariant,
168
+ maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
157
169
  style: [generated_styles.buttonStyles.icon, { lineHeight: iconCellHeight }]
158
170
  })
159
171
  });
160
172
  const endContent = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnimatedIconSlot, {
161
173
  visible: showEndIcon,
162
- iconSize,
163
- iconCellHeight,
174
+ iconSize: scaledIconSize,
175
+ iconCellHeight: scaledIconCellHeight,
164
176
  gap: buttonGap,
165
177
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_IconSlot.IconSlot, {
166
178
  icon: endIcon,
167
179
  size: iconSizeToken,
168
180
  variant: iconVariant,
181
+ maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
169
182
  style: [generated_styles.buttonStyles.icon, { lineHeight: iconCellHeight }]
170
183
  })
171
184
  });
@@ -29,6 +29,13 @@ interface ButtonProps extends Omit<PressableProps$1, 'children' | 'disabled'> {
29
29
  * @default false
30
30
  */
31
31
  disableEffects?: boolean;
32
+ /**
33
+ * Caps how far the label and icons grow with the OS text-size setting; the
34
+ * button grows to fit the scaled content. Set null to remove the cap;
35
+ * override app-wide via UDSFontScalingProvider.
36
+ * @default 2
37
+ */
38
+ maxFontSizeMultiplier?: number | null;
32
39
  /** Ref to the underlying View */
33
40
  ref?: Ref<View>;
34
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Button.d.cts","names":[],"sources":["../../src/components/Button.tsx"],"mappings":";;;;;;;;;UAqHU,WAAA,SAAoB,IAAA,CAAK,gBAAA;;EAEjC,OAAA,GAAU,iBAAA;EAFF;EAIR,IAAA,GAAO,UAAA;;EAEP,WAAA,GAAc,WAAA;EAJJ;EAMV,SAAA,GAAY,YAAA;EAFE;EAId,OAAA,GAAU,YAAA;EAAA;EAEV,OAAA;EAWU;EATV,QAAA;EAd4B;EAgB5B,QAAA,GAAW,KAAA,CAAM,SAAA;EAhBe;;;;EAqBhC,cAAA;EAjBA;EAmBA,GAAA,GAAM,GAAA,CAAI,IAAA;AAAA;;;;;;;;;;;;;;;;;AAAI;;;;;;;;;;;;;;;;cAuCV,MAAA,EAAM,OAAA,CAAA,oBAAA,CAAA,WAAA"}
1
+ {"version":3,"file":"Button.d.cts","names":[],"sources":["../../src/components/Button.tsx"],"mappings":";;;;;;;;;UAuHU,WAAA,SAAoB,IAAA,CAAK,gBAAA;;EAEjC,OAAA,GAAU,iBAAA;EAFF;EAIR,IAAA,GAAO,UAAA;;EAEP,WAAA,GAAc,WAAA;EAJJ;EAMV,SAAA,GAAY,YAAA;EAFE;EAId,OAAA,GAAU,YAAA;EAAA;EAEV,OAAA;EAkBU;EAhBV,QAAA;EAd4B;EAgB5B,QAAA,GAAW,KAAA,CAAM,SAAA;EAhBe;;;;EAqBhC,cAAA;EAjBA;;;;;;EAwBA,qBAAA;EAlBU;EAoBV,GAAA,GAAM,GAAA,CAAI,IAAA;AAAA;;;;;;;;;;;AAAI;;;;;;;;;;;;;;;;;;;;;;cAuCV,MAAA,EAAM,OAAA,CAAA,oBAAA,CAAA,WAAA"}
@@ -29,6 +29,13 @@ interface ButtonProps extends Omit<PressableProps$1, 'children' | 'disabled'> {
29
29
  * @default false
30
30
  */
31
31
  disableEffects?: boolean;
32
+ /**
33
+ * Caps how far the label and icons grow with the OS text-size setting; the
34
+ * button grows to fit the scaled content. Set null to remove the cap;
35
+ * override app-wide via UDSFontScalingProvider.
36
+ * @default 2
37
+ */
38
+ maxFontSizeMultiplier?: number | null;
32
39
  /** Ref to the underlying View */
33
40
  ref?: Ref<View>;
34
41
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Button.d.ts","names":[],"sources":["../../src/components/Button.tsx"],"mappings":";;;;;;;;;UAqHU,WAAA,SAAoB,IAAA,CAAK,gBAAA;;EAEjC,OAAA,GAAU,iBAAA;EAFF;EAIR,IAAA,GAAO,UAAA;;EAEP,WAAA,GAAc,WAAA;EAJJ;EAMV,SAAA,GAAY,YAAA;EAFE;EAId,OAAA,GAAU,YAAA;EAAA;EAEV,OAAA;EAWU;EATV,QAAA;EAd4B;EAgB5B,QAAA,GAAW,KAAA,CAAM,SAAA;EAhBe;;;;EAqBhC,cAAA;EAjBA;EAmBA,GAAA,GAAM,GAAA,CAAI,IAAA;AAAA;;;;;;;;;;;;;;;;;AAAI;;;;;;;;;;;;;;;;cAuCV,MAAA,EAAM,OAAA,CAAA,oBAAA,CAAA,WAAA"}
1
+ {"version":3,"file":"Button.d.ts","names":[],"sources":["../../src/components/Button.tsx"],"mappings":";;;;;;;;;UAuHU,WAAA,SAAoB,IAAA,CAAK,gBAAA;;EAEjC,OAAA,GAAU,iBAAA;EAFF;EAIR,IAAA,GAAO,UAAA;;EAEP,WAAA,GAAc,WAAA;EAJJ;EAMV,SAAA,GAAY,YAAA;EAFE;EAId,OAAA,GAAU,YAAA;EAAA;EAEV,OAAA;EAkBU;EAhBV,QAAA;EAd4B;EAgB5B,QAAA,GAAW,KAAA,CAAM,SAAA;EAhBe;;;;EAqBhC,cAAA;EAjBA;;;;;;EAwBA,qBAAA;EAlBU;EAoBV,GAAA,GAAM,GAAA,CAAI,IAAA;AAAA;;;;;;;;;;;AAAI;;;;;;;;;;;;;;;;;;;;;;cAuCV,MAAA,EAAM,OAAA,CAAA,oBAAA,CAAA,WAAA"}
@@ -1,9 +1,11 @@
1
1
  /*! © 2026 Yahoo, Inc. UDS Mobile v0.0.0-development */
2
2
  import { SCALE_EFFECTS } from "../motion-tokens/dist/index.js";
3
3
  import { BUTTON_SPRING_CONFIG } from "../motion.js";
4
+ import { useMaxFontSizeMultiplier } from "../fontScaling/useMaxFontSizeMultiplier.js";
4
5
  import { getButtonControlMetrics, getButtonTextStyle } from "./Button/buttonTheme.js";
5
6
  import { IconSlot } from "./IconSlot.js";
6
7
  import { Text as Text$1 } from "./Text.js";
8
+ import { useFontScale } from "../fontScaling/useFontScale.js";
7
9
  import { AnimatedPressable } from "./Pressable.js";
8
10
  import { isValidElement, memo, useCallback, useMemo, useState } from "react";
9
11
  import { ActivityIndicator } from "react-native";
@@ -81,10 +83,16 @@ function AnimatedIconSlot({ children, visible, iconSize, iconCellHeight, gap })
81
83
  * @see {@link IconButton} for icon-only buttons
82
84
  * @see {@link Link} for navigation actions
83
85
  */
84
- const Button = memo(function Button({ variant = "primary", size = "md", iconVariant = "outline", startIcon, endIcon, loading, disabled, width: _width, children, style, accessibilityLabel, accessibilityHint, disableEffects = false, onPressIn, onPressOut, ref, ...props }) {
86
+ const Button = memo(function Button({ variant = "primary", size = "md", iconVariant = "outline", startIcon, endIcon, loading, disabled, width: _width, children, style, accessibilityLabel, accessibilityHint, disableEffects = false, maxFontSizeMultiplier, onPressIn, onPressOut, ref, ...props }) {
85
87
  const shouldAnimate = !disableEffects;
88
+ const resolvedMaxFontSizeMultiplier = useMaxFontSizeMultiplier("control", maxFontSizeMultiplier) ?? 0;
89
+ const fontScaleFactor = useFontScale(resolvedMaxFontSizeMultiplier);
86
90
  const { theme } = useUnistyles();
87
- const { minHeight, labelLineHeight, iconCellHeight, paddingVertical } = useMemo(() => getButtonControlMetrics(theme, size), [theme, size]);
91
+ const { minHeight, labelLineHeight, iconCellHeight, paddingVertical } = useMemo(() => getButtonControlMetrics(theme, size, fontScaleFactor), [
92
+ theme,
93
+ size,
94
+ fontScaleFactor
95
+ ]);
88
96
  const [pressed, setPressed] = useState(false);
89
97
  buttonStyles.useVariants({
90
98
  size,
@@ -117,6 +125,7 @@ const Button = memo(function Button({ variant = "primary", size = "md", iconVari
117
125
  const childrenNode = children && (isValidElement(children) ? children : /* @__PURE__ */ jsx(Text$1, {
118
126
  numberOfLines: 1,
119
127
  textAlign: "center",
128
+ maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
120
129
  style: getButtonTextStyle(buttonStyles.text, labelLineHeight),
121
130
  children
122
131
  }));
@@ -139,30 +148,34 @@ const Button = memo(function Button({ variant = "primary", size = "md", iconVari
139
148
  const showStartIcon = !!startIcon && !loading;
140
149
  const showEndIcon = !!endIcon && !loading;
141
150
  const iconSizeToken = buttonStyles.icon.iconSizeToken ?? "sm";
151
+ const scaledIconSize = Math.round(iconSize * fontScaleFactor);
152
+ const scaledIconCellHeight = Math.ceil(iconCellHeight * fontScaleFactor);
142
153
  const startContent = /* @__PURE__ */ jsx(AnimatedIconSlot, {
143
154
  visible: showLoading || showStartIcon,
144
- iconSize,
145
- iconCellHeight,
155
+ iconSize: scaledIconSize,
156
+ iconCellHeight: scaledIconCellHeight,
146
157
  gap: buttonGap,
147
158
  children: showLoading ? /* @__PURE__ */ jsx(ActivityIndicator, {
148
- size: buttonStyles.icon.fontSize,
159
+ size: scaledIconSize,
149
160
  color: buttonStyles.icon.color
150
161
  }) : /* @__PURE__ */ jsx(IconSlot, {
151
162
  icon: startIcon,
152
163
  size: iconSizeToken,
153
164
  variant: iconVariant,
165
+ maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
154
166
  style: [buttonStyles.icon, { lineHeight: iconCellHeight }]
155
167
  })
156
168
  });
157
169
  const endContent = /* @__PURE__ */ jsx(AnimatedIconSlot, {
158
170
  visible: showEndIcon,
159
- iconSize,
160
- iconCellHeight,
171
+ iconSize: scaledIconSize,
172
+ iconCellHeight: scaledIconCellHeight,
161
173
  gap: buttonGap,
162
174
  children: /* @__PURE__ */ jsx(IconSlot, {
163
175
  icon: endIcon,
164
176
  size: iconSizeToken,
165
177
  variant: iconVariant,
178
+ maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
166
179
  style: [buttonStyles.icon, { lineHeight: iconCellHeight }]
167
180
  })
168
181
  });
@@ -1 +1 @@
1
- {"version":3,"file":"Button.js","names":["Text"],"sources":["../../src/components/Button.tsx"],"sourcesContent":["import type { ButtonSize, ButtonVariantFlat, IconSize, IconVariant } from '@yahoo/uds-types';\nimport type { Ref } from 'react';\nimport { isValidElement, memo, useCallback, useMemo, useState } from 'react';\nimport type { View } from 'react-native';\nimport { ActivityIndicator } from 'react-native';\nimport Animated, {\n Easing,\n interpolate,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n withSpring,\n withTiming,\n} from 'react-native-reanimated';\n// eslint-disable-next-line uds/no-use-unistyles -- button control height from theme size layers\nimport { useUnistyles } from 'react-native-unistyles';\nimport { useAnimatedTheme } from 'react-native-unistyles/reanimated';\n\nimport { buttonStyles } from '../../generated/styles';\nimport { BUTTON_SPRING_CONFIG, SCALE_EFFECTS } from '../motion';\nimport { getButtonControlMetrics, getButtonTextStyle } from './Button/buttonTheme';\nimport type { IconSlotType } from './IconSlot';\nimport { IconSlot } from './IconSlot';\nimport type { PressableProps } from './Pressable';\nimport { AnimatedPressable } from './Pressable';\nimport { Text } from './Text';\n\n/* -------------------------------------------------------------------------- */\n/* Animation Helpers */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Interpolates a boxShadow string by scaling the alpha of all colors.\n * This allows smooth fade-in/out of shadows.\n */\nfunction interpolateShadowAlpha(shadow: string | undefined, alpha: number): string {\n 'worklet';\n if (!shadow) {\n return '';\n }\n if (alpha >= 1) {\n return shadow;\n }\n if (alpha <= 0) {\n return '';\n }\n\n return shadow.replace(/rgba\\(([^,]+),\\s*([^,]+),\\s*([^,]+),\\s*([^)]+)\\)/g, (_, r, g, b, a) => {\n const newAlpha = parseFloat(a) * alpha;\n return `rgba(${r}, ${g}, ${b}, ${newAlpha.toFixed(3)})`;\n });\n}\n\n/* -------------------------------------------------------------------------- */\n/* Animated Icon Slot */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Animated wrapper for icon/loading content.\n * Matches web Button icon animation: scale 0.7→1, opacity 0→1, width 0→auto\n * Uses staggered animation: opacity waits until halfway through width animation.\n */\nfunction AnimatedIconSlot({\n children,\n visible,\n iconSize,\n iconCellHeight,\n gap,\n}: {\n children: React.ReactNode;\n visible: boolean;\n iconSize: number;\n iconCellHeight: number;\n gap: number;\n}) {\n // Use useDerivedValue instead of useEffect + useSharedValue\n // This is the idiomatic Reanimated pattern for deriving animated values from React state\n const progress = useDerivedValue(\n () => withSpring(visible ? 1 : 0, BUTTON_SPRING_CONFIG),\n [visible],\n );\n\n const animatedStyle = useAnimatedStyle(() => {\n // Total width includes icon + gap when visible\n const totalWidth = iconSize + gap;\n const width = interpolate(progress.value, [0, 1], [0, totalWidth]);\n\n // Staggered animation: opacity starts at 50% of width animation\n // On enter: width expands first, then icon fades in\n // On exit: icon fades out first, then width collapses\n const opacity = interpolate(progress.value, [0.5, 1], [0, 1], 'clamp');\n const scale = interpolate(progress.value, [0.5, 1], [0.7, 1], 'clamp');\n\n return {\n width,\n opacity,\n transform: [{ scale }],\n overflow: 'hidden' as const,\n minHeight: iconCellHeight,\n justifyContent: 'center' as const,\n };\n });\n\n return <Animated.View style={animatedStyle}>{children}</Animated.View>;\n}\n\n// function LoadingIcon({ size, variant }: { size: ButtonSize, variant: ButtonVariantFlat }) {\n// const { theme } = useUnistyles();\n// const themeKey = `buttonVariant${variantToCapitalMap[variant]}IconRest` as const;\n// const iconSize = theme.components.buttonSizeLgIconRest.fontSize;\n// return <ActivityIndicator size={iconSize} color={theme.colors.text.primary} />;\n// }\n\n/* -------------------------------------------------------------------------- */\n/* Button Props */\n/* -------------------------------------------------------------------------- */\n\ninterface ButtonProps extends Omit<PressableProps, 'children' | 'disabled'> {\n /** The visual style variant of the button @default 'primary' */\n variant?: ButtonVariantFlat;\n /** The size of the button @default 'md' */\n size?: ButtonSize;\n /** The icon style variant @default 'outline' */\n iconVariant?: IconVariant;\n /** Icon displayed before the button label */\n startIcon?: IconSlotType;\n /** Icon displayed after the button label */\n endIcon?: IconSlotType;\n /** Shows a loading spinner and disables the button */\n loading?: boolean;\n /** Whether the button is disabled */\n disabled?: boolean;\n /** Button label content */\n children?: React.ReactNode;\n /**\n * Disable motion effects (scale on press, icon animations)\n * @default false\n */\n disableEffects?: boolean;\n /** Ref to the underlying View */\n ref?: Ref<View>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Button Component */\n/* -------------------------------------------------------------------------- */\n\n/**\n * **🖲️ A button element that can be used to trigger an action**\n *\n * @description\n * A button is a fundamental component used to trigger an action or event.\n * Buttons are interactive elements that users can click, tap, or otherwise\n * engage with to submit forms, open dialogues, or perform any other interaction.\n *\n * Features animated scale effect on press and smooth icon transitions matching\n * the web UDS Button behavior.\n *\n * @category Interactive\n * @platform mobile\n *\n * @example\n * ```tsx\n * import { Button } from '@yahoo/uds-mobile/Button';\n *\n * <Button onPress={() => console.log('pressed')}>Save</Button>\n * <Button variant=\"secondary\">Cancel</Button>\n * <Button startIcon=\"Add\" variant=\"brand\">Add Item</Button>\n * <Button loading>Saving...</Button>\n * ```\n *\n * @accessibility\n * - Sets `accessibilityRole=\"button\"` automatically\n * - Announces loading state to screen readers\n * - Use `accessibilityLabel` for icon-only buttons\n *\n * @see {@link IconButton} for icon-only buttons\n * @see {@link Link} for navigation actions\n */\nconst Button = memo(function Button({\n variant = 'primary',\n size = 'md',\n iconVariant = 'outline',\n startIcon,\n endIcon,\n loading,\n disabled,\n width: _width,\n children,\n style,\n accessibilityLabel,\n accessibilityHint,\n disableEffects = false,\n onPressIn,\n onPressOut,\n ref,\n ...props\n}: ButtonProps) {\n const shouldAnimate = !disableEffects;\n\n const { theme } = useUnistyles();\n const { minHeight, labelLineHeight, iconCellHeight, paddingVertical } = useMemo(\n () => getButtonControlMetrics(theme, size),\n [theme, size],\n );\n\n /* --------------------------------- State ---------------------------------- */\n const [pressed, setPressed] = useState(false);\n\n buttonStyles.useVariants({ size, variant, disabled, pressed });\n\n // Get gap and icon size from current variant styles\n const buttonGap = buttonStyles.root.gap;\n const iconSize = buttonStyles.icon.fontSize;\n\n // Get animated theme for boxShadow (useAnimatedVariantColor doesn't support non-color props)\n const animatedTheme = useAnimatedTheme();\n\n /* ------------------------------- Animation -------------------------------- */\n const scale = useSharedValue<number>(SCALE_EFFECTS.none);\n\n const handlePressIn = useCallback(\n (event: Parameters<NonNullable<PressableProps['onPressIn']>>[0]) => {\n setPressed(true);\n if (shouldAnimate) {\n scale.value = withSpring(SCALE_EFFECTS.down, BUTTON_SPRING_CONFIG);\n }\n onPressIn?.(event);\n },\n [shouldAnimate, scale, onPressIn],\n );\n\n const handlePressOut = useCallback(\n (event: Parameters<NonNullable<PressableProps['onPressOut']>>[0]) => {\n setPressed(false);\n if (shouldAnimate) {\n scale.value = withSpring(SCALE_EFFECTS.none, BUTTON_SPRING_CONFIG);\n }\n onPressOut?.(event);\n },\n [shouldAnimate, scale, onPressOut],\n );\n\n /* -------------------------------- Content --------------------------------- */\n const childrenNode =\n children &&\n (isValidElement(children) ? (\n children\n ) : (\n <Text\n numberOfLines={1}\n textAlign=\"center\"\n style={getButtonTextStyle(buttonStyles.text, labelLineHeight)}\n >\n {children}\n </Text>\n ));\n\n const a11yState = useMemo(() => ({ disabled, busy: loading }), [disabled, loading]);\n\n /* --------------------------------- Styles --------------------------------- */\n // Animate pressed state for shadow (0 = rest, 1 = pressed)\n const pressProgress = useDerivedValue(\n () => withTiming(pressed ? 1 : 0, { duration: 220, easing: Easing.bezier(0, 0, 0.2, 1) }),\n [pressed],\n );\n\n // Animate using Unistyles' variant color system + boxShadow from theme\n const animatedStyles = useAnimatedStyle(() => {\n // Get boxShadow from theme using flattened path (no camelCase conversion needed!)\n const components = animatedTheme.value.components;\n const buttonVariantPath = `button/variant/${variant}/root/pressed` as const;\n const shadowPressed = components[buttonVariantPath]?.boxShadow;\n\n return {\n transform: [{ scale: scale.value }],\n // Only animate shadow if the theme defines one for this variant\n ...(shadowPressed && {\n boxShadow: interpolateShadowAlpha(shadowPressed, pressProgress.value),\n }),\n };\n });\n\n // Determine what should be visible in start slot\n const showLoading = !!loading;\n const showStartIcon = !!startIcon && !loading;\n const showEndIcon = !!endIcon && !loading;\n\n const iconSizeToken = (buttonStyles.icon.iconSizeToken as IconSize) ?? 'sm';\n\n // Start slot: either loading spinner or start icon\n const startContent = (\n <AnimatedIconSlot\n visible={showLoading || showStartIcon}\n iconSize={iconSize}\n iconCellHeight={iconCellHeight}\n gap={buttonGap}\n >\n {showLoading ? (\n <ActivityIndicator size={buttonStyles.icon.fontSize} color={buttonStyles.icon.color} />\n ) : (\n <IconSlot\n icon={startIcon}\n size={iconSizeToken}\n variant={iconVariant}\n style={[buttonStyles.icon, { lineHeight: iconCellHeight }]}\n />\n )}\n </AnimatedIconSlot>\n );\n\n // End slot: only end icon (no loading here)\n const endContent = (\n <AnimatedIconSlot\n visible={showEndIcon}\n iconSize={iconSize}\n iconCellHeight={iconCellHeight}\n gap={buttonGap}\n >\n <IconSlot\n icon={endIcon}\n size={iconSizeToken}\n variant={iconVariant}\n style={[buttonStyles.icon, { lineHeight: iconCellHeight }]}\n />\n </AnimatedIconSlot>\n );\n\n const rootStyles = useMemo(\n () => [\n buttonStyles.root,\n { minHeight, paddingVertical },\n animatedStyles,\n typeof style === 'function' ? style({ pressed }) : style,\n ],\n [buttonStyles.root, minHeight, paddingVertical, animatedStyles, style, pressed],\n );\n\n /* --------------------------------- Render --------------------------------- */\n return (\n <AnimatedPressable\n ref={ref}\n disabled={disabled}\n onPressIn={handlePressIn}\n onPressOut={handlePressOut}\n flexDirection=\"row\"\n alignItems=\"center\"\n justifyContent=\"center\"\n overflow=\"hidden\"\n accessibilityLabel={loading ? `${accessibilityLabel ?? ''}, loading` : accessibilityLabel}\n accessibilityHint={accessibilityHint}\n accessibilityRole=\"button\"\n accessibilityState={a11yState}\n alignContent=\"center\"\n style={rootStyles}\n {...props}\n >\n {startContent}\n {childrenNode}\n {endContent}\n </AnimatedPressable>\n );\n});\n\nButton.displayName = 'Button';\n\nexport { Button, type ButtonProps };\n"],"mappings":"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"}
1
+ {"version":3,"file":"Button.js","names":["Text"],"sources":["../../src/components/Button.tsx"],"sourcesContent":["import type { ButtonSize, ButtonVariantFlat, IconSize, IconVariant } from '@yahoo/uds-types';\nimport type { Ref } from 'react';\nimport { isValidElement, memo, useCallback, useMemo, useState } from 'react';\nimport type { View } from 'react-native';\nimport { ActivityIndicator } from 'react-native';\nimport Animated, {\n Easing,\n interpolate,\n useAnimatedStyle,\n useDerivedValue,\n useSharedValue,\n withSpring,\n withTiming,\n} from 'react-native-reanimated';\n// eslint-disable-next-line uds/no-use-unistyles -- button control height from theme size layers\nimport { useUnistyles } from 'react-native-unistyles';\nimport { useAnimatedTheme } from 'react-native-unistyles/reanimated';\n\nimport { buttonStyles } from '../../generated/styles';\nimport { useFontScale } from '../fontScaling/useFontScale';\nimport { useMaxFontSizeMultiplier } from '../fontScaling/useMaxFontSizeMultiplier';\nimport { BUTTON_SPRING_CONFIG, SCALE_EFFECTS } from '../motion';\nimport { getButtonControlMetrics, getButtonTextStyle } from './Button/buttonTheme';\nimport type { IconSlotType } from './IconSlot';\nimport { IconSlot } from './IconSlot';\nimport type { PressableProps } from './Pressable';\nimport { AnimatedPressable } from './Pressable';\nimport { Text } from './Text';\n\n/* -------------------------------------------------------------------------- */\n/* Animation Helpers */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Interpolates a boxShadow string by scaling the alpha of all colors.\n * This allows smooth fade-in/out of shadows.\n */\nfunction interpolateShadowAlpha(shadow: string | undefined, alpha: number): string {\n 'worklet';\n if (!shadow) {\n return '';\n }\n if (alpha >= 1) {\n return shadow;\n }\n if (alpha <= 0) {\n return '';\n }\n\n return shadow.replace(/rgba\\(([^,]+),\\s*([^,]+),\\s*([^,]+),\\s*([^)]+)\\)/g, (_, r, g, b, a) => {\n const newAlpha = parseFloat(a) * alpha;\n return `rgba(${r}, ${g}, ${b}, ${newAlpha.toFixed(3)})`;\n });\n}\n\n/* -------------------------------------------------------------------------- */\n/* Animated Icon Slot */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Animated wrapper for icon/loading content.\n * Matches web Button icon animation: scale 0.7→1, opacity 0→1, width 0→auto\n * Uses staggered animation: opacity waits until halfway through width animation.\n */\nfunction AnimatedIconSlot({\n children,\n visible,\n iconSize,\n iconCellHeight,\n gap,\n}: {\n children: React.ReactNode;\n visible: boolean;\n iconSize: number;\n iconCellHeight: number;\n gap: number;\n}) {\n // Use useDerivedValue instead of useEffect + useSharedValue\n // This is the idiomatic Reanimated pattern for deriving animated values from React state\n const progress = useDerivedValue(\n () => withSpring(visible ? 1 : 0, BUTTON_SPRING_CONFIG),\n [visible],\n );\n\n const animatedStyle = useAnimatedStyle(() => {\n // Total width includes icon + gap when visible\n const totalWidth = iconSize + gap;\n const width = interpolate(progress.value, [0, 1], [0, totalWidth]);\n\n // Staggered animation: opacity starts at 50% of width animation\n // On enter: width expands first, then icon fades in\n // On exit: icon fades out first, then width collapses\n const opacity = interpolate(progress.value, [0.5, 1], [0, 1], 'clamp');\n const scale = interpolate(progress.value, [0.5, 1], [0.7, 1], 'clamp');\n\n return {\n width,\n opacity,\n transform: [{ scale }],\n overflow: 'hidden' as const,\n minHeight: iconCellHeight,\n justifyContent: 'center' as const,\n };\n });\n\n return <Animated.View style={animatedStyle}>{children}</Animated.View>;\n}\n\n// function LoadingIcon({ size, variant }: { size: ButtonSize, variant: ButtonVariantFlat }) {\n// const { theme } = useUnistyles();\n// const themeKey = `buttonVariant${variantToCapitalMap[variant]}IconRest` as const;\n// const iconSize = theme.components.buttonSizeLgIconRest.fontSize;\n// return <ActivityIndicator size={iconSize} color={theme.colors.text.primary} />;\n// }\n\n/* -------------------------------------------------------------------------- */\n/* Button Props */\n/* -------------------------------------------------------------------------- */\n\ninterface ButtonProps extends Omit<PressableProps, 'children' | 'disabled'> {\n /** The visual style variant of the button @default 'primary' */\n variant?: ButtonVariantFlat;\n /** The size of the button @default 'md' */\n size?: ButtonSize;\n /** The icon style variant @default 'outline' */\n iconVariant?: IconVariant;\n /** Icon displayed before the button label */\n startIcon?: IconSlotType;\n /** Icon displayed after the button label */\n endIcon?: IconSlotType;\n /** Shows a loading spinner and disables the button */\n loading?: boolean;\n /** Whether the button is disabled */\n disabled?: boolean;\n /** Button label content */\n children?: React.ReactNode;\n /**\n * Disable motion effects (scale on press, icon animations)\n * @default false\n */\n disableEffects?: boolean;\n /**\n * Caps how far the label and icons grow with the OS text-size setting; the\n * button grows to fit the scaled content. Set null to remove the cap;\n * override app-wide via UDSFontScalingProvider.\n * @default 2\n */\n maxFontSizeMultiplier?: number | null;\n /** Ref to the underlying View */\n ref?: Ref<View>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Button Component */\n/* -------------------------------------------------------------------------- */\n\n/**\n * **🖲️ A button element that can be used to trigger an action**\n *\n * @description\n * A button is a fundamental component used to trigger an action or event.\n * Buttons are interactive elements that users can click, tap, or otherwise\n * engage with to submit forms, open dialogues, or perform any other interaction.\n *\n * Features animated scale effect on press and smooth icon transitions matching\n * the web UDS Button behavior.\n *\n * @category Interactive\n * @platform mobile\n *\n * @example\n * ```tsx\n * import { Button } from '@yahoo/uds-mobile/Button';\n *\n * <Button onPress={() => console.log('pressed')}>Save</Button>\n * <Button variant=\"secondary\">Cancel</Button>\n * <Button startIcon=\"Add\" variant=\"brand\">Add Item</Button>\n * <Button loading>Saving...</Button>\n * ```\n *\n * @accessibility\n * - Sets `accessibilityRole=\"button\"` automatically\n * - Announces loading state to screen readers\n * - Use `accessibilityLabel` for icon-only buttons\n *\n * @see {@link IconButton} for icon-only buttons\n * @see {@link Link} for navigation actions\n */\nconst Button = memo(function Button({\n variant = 'primary',\n size = 'md',\n iconVariant = 'outline',\n startIcon,\n endIcon,\n loading,\n disabled,\n width: _width,\n children,\n style,\n accessibilityLabel,\n accessibilityHint,\n disableEffects = false,\n maxFontSizeMultiplier,\n onPressIn,\n onPressOut,\n ref,\n ...props\n}: ButtonProps) {\n const shouldAnimate = !disableEffects;\n\n // Inside a control, \"no cap resolved\" must force-uncap (0) rather than let\n // the label's own variant default or an inherited cap apply.\n const resolvedMaxFontSizeMultiplier =\n useMaxFontSizeMultiplier('control', maxFontSizeMultiplier) ?? 0;\n const fontScaleFactor = useFontScale(resolvedMaxFontSizeMultiplier);\n\n const { theme } = useUnistyles();\n const { minHeight, labelLineHeight, iconCellHeight, paddingVertical } = useMemo(\n () => getButtonControlMetrics(theme, size, fontScaleFactor),\n [theme, size, fontScaleFactor],\n );\n\n /* --------------------------------- State ---------------------------------- */\n const [pressed, setPressed] = useState(false);\n\n buttonStyles.useVariants({ size, variant, disabled, pressed });\n\n // Get gap and icon size from current variant styles\n const buttonGap = buttonStyles.root.gap;\n const iconSize = buttonStyles.icon.fontSize;\n\n // Get animated theme for boxShadow (useAnimatedVariantColor doesn't support non-color props)\n const animatedTheme = useAnimatedTheme();\n\n /* ------------------------------- Animation -------------------------------- */\n const scale = useSharedValue<number>(SCALE_EFFECTS.none);\n\n const handlePressIn = useCallback(\n (event: Parameters<NonNullable<PressableProps['onPressIn']>>[0]) => {\n setPressed(true);\n if (shouldAnimate) {\n scale.value = withSpring(SCALE_EFFECTS.down, BUTTON_SPRING_CONFIG);\n }\n onPressIn?.(event);\n },\n [shouldAnimate, scale, onPressIn],\n );\n\n const handlePressOut = useCallback(\n (event: Parameters<NonNullable<PressableProps['onPressOut']>>[0]) => {\n setPressed(false);\n if (shouldAnimate) {\n scale.value = withSpring(SCALE_EFFECTS.none, BUTTON_SPRING_CONFIG);\n }\n onPressOut?.(event);\n },\n [shouldAnimate, scale, onPressOut],\n );\n\n /* -------------------------------- Content --------------------------------- */\n const childrenNode =\n children &&\n (isValidElement(children) ? (\n children\n ) : (\n <Text\n numberOfLines={1}\n textAlign=\"center\"\n maxFontSizeMultiplier={resolvedMaxFontSizeMultiplier}\n style={getButtonTextStyle(buttonStyles.text, labelLineHeight)}\n >\n {children}\n </Text>\n ));\n\n const a11yState = useMemo(() => ({ disabled, busy: loading }), [disabled, loading]);\n\n /* --------------------------------- Styles --------------------------------- */\n // Animate pressed state for shadow (0 = rest, 1 = pressed)\n const pressProgress = useDerivedValue(\n () => withTiming(pressed ? 1 : 0, { duration: 220, easing: Easing.bezier(0, 0, 0.2, 1) }),\n [pressed],\n );\n\n // Animate using Unistyles' variant color system + boxShadow from theme\n const animatedStyles = useAnimatedStyle(() => {\n // Get boxShadow from theme using flattened path (no camelCase conversion needed!)\n const components = animatedTheme.value.components;\n const buttonVariantPath = `button/variant/${variant}/root/pressed` as const;\n const shadowPressed = components[buttonVariantPath]?.boxShadow;\n\n return {\n transform: [{ scale: scale.value }],\n // Only animate shadow if the theme defines one for this variant\n ...(shadowPressed && {\n boxShadow: interpolateShadowAlpha(shadowPressed, pressProgress.value),\n }),\n };\n });\n\n // Determine what should be visible in start slot\n const showLoading = !!loading;\n const showStartIcon = !!startIcon && !loading;\n const showEndIcon = !!endIcon && !loading;\n\n const iconSizeToken = (buttonStyles.icon.iconSizeToken as IconSize) ?? 'sm';\n\n // Containers pre-scale by the capped factor; icon styles stay unscaled and\n // native grows them by the same capped multiplier.\n const scaledIconSize = Math.round(iconSize * fontScaleFactor);\n const scaledIconCellHeight = Math.ceil(iconCellHeight * fontScaleFactor);\n\n // Start slot: either loading spinner or start icon\n const startContent = (\n <AnimatedIconSlot\n visible={showLoading || showStartIcon}\n iconSize={scaledIconSize}\n iconCellHeight={scaledIconCellHeight}\n gap={buttonGap}\n >\n {showLoading ? (\n <ActivityIndicator size={scaledIconSize} color={buttonStyles.icon.color} />\n ) : (\n <IconSlot\n icon={startIcon}\n size={iconSizeToken}\n variant={iconVariant}\n maxFontSizeMultiplier={resolvedMaxFontSizeMultiplier}\n style={[buttonStyles.icon, { lineHeight: iconCellHeight }]}\n />\n )}\n </AnimatedIconSlot>\n );\n\n // End slot: only end icon (no loading here)\n const endContent = (\n <AnimatedIconSlot\n visible={showEndIcon}\n iconSize={scaledIconSize}\n iconCellHeight={scaledIconCellHeight}\n gap={buttonGap}\n >\n <IconSlot\n icon={endIcon}\n size={iconSizeToken}\n variant={iconVariant}\n maxFontSizeMultiplier={resolvedMaxFontSizeMultiplier}\n style={[buttonStyles.icon, { lineHeight: iconCellHeight }]}\n />\n </AnimatedIconSlot>\n );\n\n const rootStyles = useMemo(\n () => [\n buttonStyles.root,\n { minHeight, paddingVertical },\n animatedStyles,\n typeof style === 'function' ? style({ pressed }) : style,\n ],\n [buttonStyles.root, minHeight, paddingVertical, animatedStyles, style, pressed],\n );\n\n /* --------------------------------- Render --------------------------------- */\n return (\n <AnimatedPressable\n ref={ref}\n disabled={disabled}\n onPressIn={handlePressIn}\n onPressOut={handlePressOut}\n flexDirection=\"row\"\n alignItems=\"center\"\n justifyContent=\"center\"\n overflow=\"hidden\"\n accessibilityLabel={loading ? `${accessibilityLabel ?? ''}, loading` : accessibilityLabel}\n accessibilityHint={accessibilityHint}\n accessibilityRole=\"button\"\n accessibilityState={a11yState}\n alignContent=\"center\"\n style={rootStyles}\n {...props}\n >\n {startContent}\n {childrenNode}\n {endContent}\n </AnimatedPressable>\n );\n});\n\nButton.displayName = 'Button';\n\nexport { Button, type ButtonProps };\n"],"mappings":"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"}
@@ -3,12 +3,15 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
4
4
  const require_index = require("../motion-tokens/dist/index.cjs");
5
5
  const require_motion = require("../motion.cjs");
6
+ const require_fontScaling_useMaxFontSizeMultiplier = require("../fontScaling/useMaxFontSizeMultiplier.cjs");
6
7
  const require_components_Icon = require("./Icon.cjs");
8
+ const require_fontScaling_useFontScale = require("../fontScaling/useFontScale.cjs");
7
9
  const require_components_Pressable = require("./Pressable.cjs");
8
10
  const require_components_FormLabel = require("./FormLabel.cjs");
9
11
  let react = require("react");
10
12
  let react_jsx_runtime = require("react/jsx-runtime");
11
13
  let generated_styles = require("../../generated/styles");
14
+ let _yahoo_uds_icons_tokens = require("@yahoo/uds-icons/tokens");
12
15
  let react_native_reanimated = require("react-native-reanimated");
13
16
  react_native_reanimated = require_runtime.__toESM(react_native_reanimated);
14
17
  //#region src/components/Checkbox.tsx
@@ -50,8 +53,11 @@ const VARIANT_ERROR_MAP = {
50
53
  * @see {@link Radio} for single-select options
51
54
  * @see {@link Switch} for binary toggles
52
55
  */
53
- const Checkbox = (0, react.memo)(function Checkbox({ label, labelPosition = "start", variant: variantProp = "primary", size = "md", checked: checkedProp, hasError, reduceMotion, defaultChecked, disabled, required, onChange, ref, ...viewProps }) {
56
+ const Checkbox = (0, react.memo)(function Checkbox({ label, labelPosition = "start", variant: variantProp = "primary", size = "md", checked: checkedProp, hasError, reduceMotion, defaultChecked, disabled, required, maxFontSizeMultiplier, onChange, ref, ...viewProps }) {
54
57
  const uid = `uds-checkbox-${(0, react.useId)()}`;
58
+ const resolvedMaxFontSizeMultiplier = require_fontScaling_useMaxFontSizeMultiplier.useMaxFontSizeMultiplier("control", maxFontSizeMultiplier) ?? 0;
59
+ const fontScaleFactor = require_fontScaling_useFontScale.useFontScale(resolvedMaxFontSizeMultiplier);
60
+ const scaledMarkSize = Math.round(_yahoo_uds_icons_tokens.ICON_SIZE_MAP.sm * fontScaleFactor);
55
61
  const isControlled = checkedProp !== void 0;
56
62
  const [internalChecked, setInternalChecked] = (0, react.useState)(isControlled ? checkedProp : defaultChecked ?? false);
57
63
  const [pressed, setPressed] = (0, react.useState)(false);
@@ -91,14 +97,27 @@ const Checkbox = (0, react.memo)(function Checkbox({ label, labelPosition = "sta
91
97
  pressed
92
98
  });
93
99
  const rootStyle = (0, react.useMemo)(() => [generated_styles.checkboxStyles.root, { opacity: disabled ? .5 : 1 }], [generated_styles.checkboxStyles.root, disabled]);
94
- const checkboxBoxStyle = (0, react.useMemo)(() => [
100
+ const checkboxBoxStyle = (0, react.useMemo)(() => {
101
+ const { width, height, borderRadius } = generated_styles.checkboxStyles.checkbox;
102
+ const scaledBox = fontScaleFactor !== 1 && typeof width === "number" && typeof height === "number" ? {
103
+ width: Math.round(width * fontScaleFactor),
104
+ height: Math.round(height * fontScaleFactor),
105
+ ...typeof borderRadius === "number" && { borderRadius: Math.round(borderRadius * fontScaleFactor) }
106
+ } : void 0;
107
+ return [
108
+ generated_styles.checkboxStyles.checkbox,
109
+ scaledBox,
110
+ {
111
+ alignItems: "center",
112
+ justifyContent: "center"
113
+ },
114
+ checkboxAnimatedStyle
115
+ ];
116
+ }, [
95
117
  generated_styles.checkboxStyles.checkbox,
96
- {
97
- alignItems: "center",
98
- justifyContent: "center"
99
- },
118
+ fontScaleFactor,
100
119
  checkboxAnimatedStyle
101
- ], [generated_styles.checkboxStyles.checkbox, checkboxAnimatedStyle]);
120
+ ]);
102
121
  const labelContent = (0, react.useMemo)(() => {
103
122
  if (!label) return null;
104
123
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_FormLabel.FormLabel, {
@@ -108,12 +127,14 @@ const Checkbox = (0, react.memo)(function Checkbox({ label, labelPosition = "sta
108
127
  required,
109
128
  showRequiredAsterisk: required,
110
129
  hasError,
130
+ maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
111
131
  style: generated_styles.checkboxStyles.text
112
132
  });
113
133
  }, [
114
134
  label,
115
135
  required,
116
136
  hasError,
137
+ resolvedMaxFontSizeMultiplier,
117
138
  generated_styles.checkboxStyles.text
118
139
  ]);
119
140
  const a11yState = (0, react.useMemo)(() => ({
@@ -140,6 +161,8 @@ const Checkbox = (0, react.memo)(function Checkbox({ label, labelPosition = "sta
140
161
  children: showIcon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_components_Icon.Icon, {
141
162
  name: iconName,
142
163
  size: "sm",
164
+ dangerouslySetSize: scaledMarkSize,
165
+ allowFontScaling: false,
143
166
  style: generated_styles.checkboxStyles.checkboxIcon
144
167
  })
145
168
  }), labelContent]
@@ -14,6 +14,13 @@ interface CheckboxProps extends Omit<ViewProps, 'style'>, UniversalCheckboxProps
14
14
  disabled?: boolean;
15
15
  /** Whether the checkbox is required */
16
16
  required?: boolean;
17
+ /**
18
+ * Caps how far the box, checkmark, and label grow with the OS text-size
19
+ * setting; all three scale by the same factor. Set null to remove the cap;
20
+ * override app-wide via UDSFontScalingProvider.
21
+ * @default 2
22
+ */
23
+ maxFontSizeMultiplier?: number | null;
17
24
  /** Callback fired when the checked state changes */
18
25
  onChange?: (checked: boolean) => void;
19
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Checkbox.d.cts","names":[],"sources":["../../src/components/Checkbox.tsx"],"mappings":";;;;;;;UAmBU,aAAA,SAAsB,IAAA,CAAK,SAAA,YAAqB,sBAAA;;EAExD,GAAA,GAAM,GAAA,CAAI,IAAA;EAFF;EAIR,cAAA;;EAEA,QAAA;EAJU;EAMV,QAAA;EAR8B;EAU9B,QAAA,IAAY,OAAA;AAAA;;;;;;;;;;;;;;;AAAgB;;;;;;;;;;;;;;;;;;;;cAqDxB,QAAA,EAAQ,OAAA,CAAA,oBAAA,CAAA,aAAA"}
1
+ {"version":3,"file":"Checkbox.d.cts","names":[],"sources":["../../src/components/Checkbox.tsx"],"mappings":";;;;;;;UAsBU,aAAA,SAAsB,IAAA,CAAK,SAAA,YAAqB,sBAAA;;EAExD,GAAA,GAAM,GAAA,CAAI,IAAA;EAFF;EAIR,cAAA;;EAEA,QAAA;EAJU;EAMV,QAAA;EAR8B;;;;;;EAe9B,qBAAA;EAbA;EAeA,QAAA,IAAY,OAAA;AAAA;;;;;;;;;AAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;cAqDxB,QAAA,EAAQ,OAAA,CAAA,oBAAA,CAAA,aAAA"}
@@ -14,6 +14,13 @@ interface CheckboxProps extends Omit<ViewProps, 'style'>, UniversalCheckboxProps
14
14
  disabled?: boolean;
15
15
  /** Whether the checkbox is required */
16
16
  required?: boolean;
17
+ /**
18
+ * Caps how far the box, checkmark, and label grow with the OS text-size
19
+ * setting; all three scale by the same factor. Set null to remove the cap;
20
+ * override app-wide via UDSFontScalingProvider.
21
+ * @default 2
22
+ */
23
+ maxFontSizeMultiplier?: number | null;
17
24
  /** Callback fired when the checked state changes */
18
25
  onChange?: (checked: boolean) => void;
19
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Checkbox.d.ts","names":[],"sources":["../../src/components/Checkbox.tsx"],"mappings":";;;;;;;UAmBU,aAAA,SAAsB,IAAA,CAAK,SAAA,YAAqB,sBAAA;;EAExD,GAAA,GAAM,GAAA,CAAI,IAAA;EAFF;EAIR,cAAA;;EAEA,QAAA;EAJU;EAMV,QAAA;EAR8B;EAU9B,QAAA,IAAY,OAAA;AAAA;;;;;;;;;;;;;;;AAAgB;;;;;;;;;;;;;;;;;;;;cAqDxB,QAAA,EAAQ,OAAA,CAAA,oBAAA,CAAA,aAAA"}
1
+ {"version":3,"file":"Checkbox.d.ts","names":[],"sources":["../../src/components/Checkbox.tsx"],"mappings":";;;;;;;UAsBU,aAAA,SAAsB,IAAA,CAAK,SAAA,YAAqB,sBAAA;;EAExD,GAAA,GAAM,GAAA,CAAI,IAAA;EAFF;EAIR,cAAA;;EAEA,QAAA;EAJU;EAMV,QAAA;EAR8B;;;;;;EAe9B,qBAAA;EAbA;EAeA,QAAA,IAAY,OAAA;AAAA;;;;;;;;;AAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;cAqDxB,QAAA,EAAQ,OAAA,CAAA,oBAAA,CAAA,aAAA"}
@@ -1,12 +1,15 @@
1
1
  /*! © 2026 Yahoo, Inc. UDS Mobile v0.0.0-development */
2
2
  import { SCALE_EFFECTS } from "../motion-tokens/dist/index.js";
3
3
  import { BUTTON_SPRING_CONFIG } from "../motion.js";
4
+ import { useMaxFontSizeMultiplier } from "../fontScaling/useMaxFontSizeMultiplier.js";
4
5
  import { Icon } from "./Icon.js";
6
+ import { useFontScale } from "../fontScaling/useFontScale.js";
5
7
  import { Pressable } from "./Pressable.js";
6
8
  import { FormLabel } from "./FormLabel.js";
7
9
  import { memo, useCallback, useId, useMemo, useState } from "react";
8
10
  import { jsx, jsxs } from "react/jsx-runtime";
9
11
  import { checkboxStyles } from "../../generated/styles";
12
+ import { ICON_SIZE_MAP } from "@yahoo/uds-icons/tokens";
10
13
  import Animated, { useAnimatedStyle, useDerivedValue, withSpring } from "react-native-reanimated";
11
14
  //#region src/components/Checkbox.tsx
12
15
  const VARIANT_ERROR_MAP = {
@@ -47,8 +50,11 @@ const VARIANT_ERROR_MAP = {
47
50
  * @see {@link Radio} for single-select options
48
51
  * @see {@link Switch} for binary toggles
49
52
  */
50
- const Checkbox = memo(function Checkbox({ label, labelPosition = "start", variant: variantProp = "primary", size = "md", checked: checkedProp, hasError, reduceMotion, defaultChecked, disabled, required, onChange, ref, ...viewProps }) {
53
+ const Checkbox = memo(function Checkbox({ label, labelPosition = "start", variant: variantProp = "primary", size = "md", checked: checkedProp, hasError, reduceMotion, defaultChecked, disabled, required, maxFontSizeMultiplier, onChange, ref, ...viewProps }) {
51
54
  const uid = `uds-checkbox-${useId()}`;
55
+ const resolvedMaxFontSizeMultiplier = useMaxFontSizeMultiplier("control", maxFontSizeMultiplier) ?? 0;
56
+ const fontScaleFactor = useFontScale(resolvedMaxFontSizeMultiplier);
57
+ const scaledMarkSize = Math.round(ICON_SIZE_MAP.sm * fontScaleFactor);
52
58
  const isControlled = checkedProp !== void 0;
53
59
  const [internalChecked, setInternalChecked] = useState(isControlled ? checkedProp : defaultChecked ?? false);
54
60
  const [pressed, setPressed] = useState(false);
@@ -88,14 +94,27 @@ const Checkbox = memo(function Checkbox({ label, labelPosition = "start", varian
88
94
  pressed
89
95
  });
90
96
  const rootStyle = useMemo(() => [checkboxStyles.root, { opacity: disabled ? .5 : 1 }], [checkboxStyles.root, disabled]);
91
- const checkboxBoxStyle = useMemo(() => [
97
+ const checkboxBoxStyle = useMemo(() => {
98
+ const { width, height, borderRadius } = checkboxStyles.checkbox;
99
+ const scaledBox = fontScaleFactor !== 1 && typeof width === "number" && typeof height === "number" ? {
100
+ width: Math.round(width * fontScaleFactor),
101
+ height: Math.round(height * fontScaleFactor),
102
+ ...typeof borderRadius === "number" && { borderRadius: Math.round(borderRadius * fontScaleFactor) }
103
+ } : void 0;
104
+ return [
105
+ checkboxStyles.checkbox,
106
+ scaledBox,
107
+ {
108
+ alignItems: "center",
109
+ justifyContent: "center"
110
+ },
111
+ checkboxAnimatedStyle
112
+ ];
113
+ }, [
92
114
  checkboxStyles.checkbox,
93
- {
94
- alignItems: "center",
95
- justifyContent: "center"
96
- },
115
+ fontScaleFactor,
97
116
  checkboxAnimatedStyle
98
- ], [checkboxStyles.checkbox, checkboxAnimatedStyle]);
117
+ ]);
99
118
  const labelContent = useMemo(() => {
100
119
  if (!label) return null;
101
120
  return /* @__PURE__ */ jsx(FormLabel, {
@@ -105,12 +124,14 @@ const Checkbox = memo(function Checkbox({ label, labelPosition = "start", varian
105
124
  required,
106
125
  showRequiredAsterisk: required,
107
126
  hasError,
127
+ maxFontSizeMultiplier: resolvedMaxFontSizeMultiplier,
108
128
  style: checkboxStyles.text
109
129
  });
110
130
  }, [
111
131
  label,
112
132
  required,
113
133
  hasError,
134
+ resolvedMaxFontSizeMultiplier,
114
135
  checkboxStyles.text
115
136
  ]);
116
137
  const a11yState = useMemo(() => ({
@@ -137,6 +158,8 @@ const Checkbox = memo(function Checkbox({ label, labelPosition = "start", varian
137
158
  children: showIcon && /* @__PURE__ */ jsx(Icon, {
138
159
  name: iconName,
139
160
  size: "sm",
161
+ dangerouslySetSize: scaledMarkSize,
162
+ allowFontScaling: false,
140
163
  style: checkboxStyles.checkboxIcon
141
164
  })
142
165
  }), labelContent]