@neko-os/ui 0.5.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/dist/NekoUI.js +12 -9
  2. package/dist/abstractions/Image.native.js +1 -1
  3. package/dist/abstractions/Image.web.js +1 -1
  4. package/dist/abstractions/WindowOverlay.js +3 -0
  5. package/dist/abstractions/WindowOverlay.native.js +21 -0
  6. package/dist/abstractions/helpers/storage.js +14 -4
  7. package/dist/abstractions/helpers/storage.native.js +9 -1
  8. package/dist/components/feedback/notifications/NotificationsHandler.js +10 -6
  9. package/dist/components/form/useNewForm.js +2 -0
  10. package/dist/components/index.js +3 -1
  11. package/dist/components/inputs/DateInput.js +10 -6
  12. package/dist/components/inputs/InputWrapper.js +11 -11
  13. package/dist/components/inputs/NumberWheelInput.js +50 -0
  14. package/dist/components/inputs/NumberWheelPicker.js +43 -0
  15. package/dist/components/inputs/SegmentedPicker.js +3 -2
  16. package/dist/components/inputs/UploadInput.js +4 -4
  17. package/dist/components/inputs/WheelPicker.js +49 -0
  18. package/dist/components/inputs/WheelPicker.native.js +88 -0
  19. package/dist/components/inputs/WheelPicker.web.js +1 -0
  20. package/dist/components/inputs/dateWheelPicker/DateWheelPicker.js +24 -0
  21. package/dist/components/inputs/dateWheelPicker/DayWheelPicker.js +48 -0
  22. package/dist/components/inputs/dateWheelPicker/MonthWheelPicker.js +19 -0
  23. package/dist/components/inputs/dateWheelPicker/QuarterWheelPicker.js +61 -0
  24. package/dist/components/inputs/dateWheelPicker/WeekWheelPicker.js +66 -0
  25. package/dist/components/inputs/dateWheelPicker/YearWheelPicker.js +35 -0
  26. package/dist/components/inputs/index.js +5 -1
  27. package/dist/components/inputs/upload/Upload.native.js +60 -52
  28. package/dist/components/inputs/upload/useUploadState.js +11 -3
  29. package/dist/components/measurements/FeetInchesInput.js +91 -0
  30. package/dist/components/measurements/LengthInput.js +32 -0
  31. package/dist/components/measurements/LengthText.js +10 -0
  32. package/dist/components/measurements/MeasurementHandler.js +26 -0
  33. package/dist/components/measurements/WeightInput.js +25 -0
  34. package/dist/components/measurements/WeightText.js +10 -0
  35. package/dist/components/measurements/helpers/detectMeasurementSystem.js +15 -0
  36. package/dist/components/measurements/helpers/detectMeasurementSystem.native.js +9 -0
  37. package/dist/components/measurements/helpers/index.js +2 -0
  38. package/dist/components/measurements/helpers/length.js +112 -0
  39. package/dist/components/measurements/helpers/weight.js +56 -0
  40. package/dist/components/measurements/index.js +9 -0
  41. package/dist/components/measurements/useLengthFormatter.js +35 -0
  42. package/dist/components/measurements/useLocalInputValue.js +32 -0
  43. package/dist/components/measurements/useWeightFormatter.js +29 -0
  44. package/dist/components/presentation/Avatar.js +3 -3
  45. package/dist/components/routing/ReturnButton.js +20 -0
  46. package/dist/components/routing/ReturnButton.native.js +20 -0
  47. package/dist/components/routing/ReturnButton.web.js +2 -0
  48. package/dist/components/routing/ReturnLink.js +25 -0
  49. package/dist/components/routing/ReturnLink.native.js +25 -0
  50. package/dist/components/routing/ReturnLink.web.js +2 -0
  51. package/dist/components/routing/RoutedStepsContent.js +21 -0
  52. package/dist/components/routing/RoutedStepsContent.native.js +94 -0
  53. package/dist/components/routing/RoutedStepsContent.web.js +3 -0
  54. package/dist/components/routing/index.js +3 -0
  55. package/dist/components/state/StatePresenter.js +1 -1
  56. package/dist/components/steps/StepsHandler.js +2 -0
  57. package/dist/components/structure/TopBar.js +18 -16
  58. package/dist/components/theme/ThemePickerDrawer.js +1 -1
  59. package/dist/helpers/compress.js +61 -0
  60. package/dist/helpers/compress.native.js +49 -0
  61. package/dist/helpers/files.js +7 -0
  62. package/dist/helpers/files.native.js +55 -0
  63. package/dist/helpers/index.js +6 -1
  64. package/dist/helpers/media.js +4 -0
  65. package/dist/helpers/media.native.js +41 -0
  66. package/dist/helpers/numbers.js +13 -0
  67. package/dist/helpers/pickAssets.js +7 -0
  68. package/dist/helpers/pickAssets.native.js +66 -0
  69. package/dist/helpers/storage.js +17 -0
  70. package/dist/i18n/I18n.js +4 -4
  71. package/dist/index.js +1 -1
  72. package/dist/modifiers/flex.js +8 -3
  73. package/dist/responsive/responsiveHooks.js +14 -0
  74. package/dist/theme/default/blackTheme.js +3 -1
  75. package/dist/theme/default/cyberpunkTheme.js +3 -1
  76. package/dist/theme/default/darkTheme.js +3 -1
  77. package/dist/theme/default/hackerTheme.js +3 -1
  78. package/dist/theme/default/lightTheme.js +3 -1
  79. package/dist/theme/default/paperTheme.js +3 -1
  80. package/package.json +2 -14
  81. package/src/NekoUI.js +16 -13
  82. package/src/abstractions/Image.native.js +1 -1
  83. package/src/abstractions/Image.web.js +1 -1
  84. package/src/abstractions/WindowOverlay.js +3 -0
  85. package/src/abstractions/WindowOverlay.native.js +21 -0
  86. package/src/abstractions/helpers/storage.js +13 -3
  87. package/src/abstractions/helpers/storage.native.js +8 -0
  88. package/src/components/feedback/notifications/NotificationsHandler.js +12 -8
  89. package/src/components/form/useNewForm.js +2 -0
  90. package/src/components/index.js +2 -0
  91. package/src/components/inputs/DateInput.js +8 -4
  92. package/src/components/inputs/InputWrapper.js +3 -3
  93. package/src/components/inputs/NumberWheelInput.js +50 -0
  94. package/src/components/inputs/NumberWheelPicker.js +43 -0
  95. package/src/components/inputs/SegmentedPicker.js +2 -1
  96. package/src/components/inputs/UploadInput.js +2 -2
  97. package/src/components/inputs/WheelPicker.js +49 -0
  98. package/src/components/inputs/WheelPicker.native.js +88 -0
  99. package/src/components/inputs/WheelPicker.web.js +1 -0
  100. package/src/components/inputs/dateWheelPicker/DateWheelPicker.js +24 -0
  101. package/src/components/inputs/dateWheelPicker/DayWheelPicker.js +48 -0
  102. package/src/components/inputs/dateWheelPicker/MonthWheelPicker.js +19 -0
  103. package/src/components/inputs/dateWheelPicker/QuarterWheelPicker.js +61 -0
  104. package/src/components/inputs/dateWheelPicker/WeekWheelPicker.js +66 -0
  105. package/src/components/inputs/dateWheelPicker/YearWheelPicker.js +35 -0
  106. package/src/components/inputs/index.js +4 -0
  107. package/src/components/inputs/upload/Upload.native.js +58 -50
  108. package/src/components/inputs/upload/useUploadState.js +11 -3
  109. package/src/components/measurements/FeetInchesInput.js +91 -0
  110. package/src/components/measurements/LengthInput.js +32 -0
  111. package/src/components/measurements/LengthText.js +10 -0
  112. package/src/components/measurements/MeasurementHandler.js +26 -0
  113. package/src/components/measurements/WeightInput.js +25 -0
  114. package/src/components/measurements/WeightText.js +10 -0
  115. package/src/components/measurements/helpers/detectMeasurementSystem.js +15 -0
  116. package/src/components/measurements/helpers/detectMeasurementSystem.native.js +9 -0
  117. package/src/components/measurements/helpers/index.js +2 -0
  118. package/src/components/measurements/helpers/length.js +112 -0
  119. package/src/components/measurements/helpers/weight.js +56 -0
  120. package/src/components/measurements/index.js +9 -0
  121. package/src/components/measurements/useLengthFormatter.js +35 -0
  122. package/src/components/measurements/useLocalInputValue.js +32 -0
  123. package/src/components/measurements/useWeightFormatter.js +29 -0
  124. package/src/components/presentation/Avatar.js +2 -2
  125. package/src/components/routing/ReturnButton.js +20 -0
  126. package/src/components/routing/ReturnButton.native.js +20 -0
  127. package/src/components/routing/ReturnButton.web.js +2 -0
  128. package/src/components/routing/ReturnLink.js +25 -0
  129. package/src/components/routing/ReturnLink.native.js +25 -0
  130. package/src/components/routing/ReturnLink.web.js +2 -0
  131. package/src/components/routing/RoutedStepsContent.js +21 -0
  132. package/src/components/routing/RoutedStepsContent.native.js +94 -0
  133. package/src/components/routing/RoutedStepsContent.web.js +3 -0
  134. package/src/components/routing/index.js +3 -0
  135. package/src/components/state/StatePresenter.js +1 -1
  136. package/src/components/steps/StepsHandler.js +2 -0
  137. package/src/components/structure/TopBar.js +16 -14
  138. package/src/components/theme/ThemePickerDrawer.js +1 -1
  139. package/src/helpers/compress.js +61 -0
  140. package/src/helpers/compress.native.js +49 -0
  141. package/src/helpers/files.js +7 -0
  142. package/src/helpers/files.native.js +55 -0
  143. package/src/helpers/index.js +6 -1
  144. package/src/helpers/media.js +4 -0
  145. package/src/helpers/media.native.js +41 -0
  146. package/src/helpers/numbers.js +13 -0
  147. package/src/helpers/pickAssets.js +7 -0
  148. package/src/helpers/pickAssets.native.js +66 -0
  149. package/src/helpers/storage.js +17 -0
  150. package/src/i18n/I18n.js +2 -2
  151. package/src/index.js +1 -1
  152. package/src/modifiers/flex.js +7 -2
  153. package/src/responsive/responsiveHooks.js +14 -0
  154. package/src/theme/default/blackTheme.js +2 -0
  155. package/src/theme/default/cyberpunkTheme.js +2 -0
  156. package/src/theme/default/darkTheme.js +2 -0
  157. package/src/theme/default/hackerTheme.js +2 -0
  158. package/src/theme/default/lightTheme.js +2 -0
  159. package/src/theme/default/paperTheme.js +2 -0
@@ -0,0 +1,112 @@
1
+ const CM_TO_INCH = 0.393701
2
+ const CM_TO_FOOT = 0.0328084
3
+ const CM_PER_M = 100
4
+ const CM_PER_KM = 100000
5
+ const MI_TO_KM = 1.60934
6
+
7
+ export function cmToIn(cm) {
8
+ if (!cm) return cm
9
+ return cm * CM_TO_INCH
10
+ }
11
+
12
+ export function inToCm(inches) {
13
+ if (!inches) return inches
14
+ return inches / CM_TO_INCH
15
+ }
16
+
17
+ export function cmToFt(cm) {
18
+ if (!cm) return cm
19
+ return cm * CM_TO_FOOT
20
+ }
21
+
22
+ export function ftToCm(feet) {
23
+ if (!feet) return feet
24
+ return feet / CM_TO_FOOT
25
+ }
26
+
27
+ export function cmToFtIn(cm) {
28
+ if (!cm) return false
29
+ const totalInches = cmToIn(cm)
30
+ let feet = Math.floor(totalInches / 12)
31
+ let inches = Math.round(totalInches % 12)
32
+ if (inches === 12) {
33
+ feet += 1
34
+ inches = 0
35
+ }
36
+ return { feet, inches }
37
+ }
38
+
39
+ export function ftInToCm(value) {
40
+ if (!value) return false
41
+ const { feet, inches } = value || {}
42
+ const totalInches = (feet || 0) * 12 + (inches || 0)
43
+ return inToCm(totalInches)
44
+ }
45
+
46
+ export function cmToM(cm) {
47
+ if (!cm) return cm
48
+ return cm / CM_PER_M
49
+ }
50
+
51
+ export function mToCm(m) {
52
+ if (!m) return m
53
+ return m * CM_PER_M
54
+ }
55
+
56
+ export function mToFt(m) {
57
+ if (!m) return m
58
+ return cmToFt(m * CM_PER_M)
59
+ }
60
+
61
+ export function ftToM(ft) {
62
+ if (!ft) return ft
63
+ return ftToCm(ft) / CM_PER_M
64
+ }
65
+
66
+ export function mToFtIn(m) {
67
+ if (!m) return false
68
+ return cmToFtIn(m * CM_PER_M)
69
+ }
70
+
71
+ export function ftInToM(value) {
72
+ if (!value) return false
73
+ return ftInToCm(value) / CM_PER_M
74
+ }
75
+
76
+ export function mToIn(m) {
77
+ if (!m) return m
78
+ return cmToIn(m * CM_PER_M)
79
+ }
80
+
81
+ export function inToM(inches) {
82
+ if (!inches) return inches
83
+ return inToCm(inches) / CM_PER_M
84
+ }
85
+
86
+ export function kmToMi(km) {
87
+ if (!km) return km
88
+ return km / MI_TO_KM
89
+ }
90
+
91
+ export function miToKm(mi) {
92
+ if (!mi) return mi
93
+ return mi * MI_TO_KM
94
+ }
95
+
96
+ export const LENGTH_CONVERTERS = {
97
+ cm: {
98
+ 'ft+in': { to: cmToFtIn, from: ftInToCm },
99
+ in: { to: cmToIn, from: inToCm },
100
+ ft: { to: cmToFt, from: ftToCm },
101
+ },
102
+ m: {
103
+ ft: { to: mToFt, from: ftToM },
104
+ 'ft+in': { to: mToFtIn, from: ftInToM },
105
+ in: { to: mToIn, from: inToM },
106
+ },
107
+ km: {
108
+ mi: { to: kmToMi, from: miToKm },
109
+ },
110
+ }
111
+
112
+ export const LENGTH_IMPERIAL_DEFAULTS = { cm: 'ft+in', m: 'ft', km: 'mi' }
@@ -0,0 +1,56 @@
1
+ const KG_TO_LB = 2.20462
2
+ const OZ_PER_LB = 16
3
+ const G_PER_KG = 1000
4
+
5
+ export function kgToLbs(kg) {
6
+ if (!kg) return kg
7
+ return kg * KG_TO_LB
8
+ }
9
+
10
+ export function lbsToKg(lbs) {
11
+ if (!lbs) return lbs
12
+ return lbs / KG_TO_LB
13
+ }
14
+
15
+ export function kgToOz(kg) {
16
+ if (!kg) return kg
17
+ return kg * KG_TO_LB * OZ_PER_LB
18
+ }
19
+
20
+ export function ozToKg(oz) {
21
+ if (!oz) return oz
22
+ return oz / (KG_TO_LB * OZ_PER_LB)
23
+ }
24
+
25
+ export function gToOz(g) {
26
+ if (!g) return g
27
+ return kgToOz(g / G_PER_KG)
28
+ }
29
+
30
+ export function ozToG(oz) {
31
+ if (!oz) return oz
32
+ return ozToKg(oz) * G_PER_KG
33
+ }
34
+
35
+ export function gToLbs(g) {
36
+ if (!g) return g
37
+ return kgToLbs(g / G_PER_KG)
38
+ }
39
+
40
+ export function lbsToG(lbs) {
41
+ if (!lbs) return lbs
42
+ return lbsToKg(lbs) * G_PER_KG
43
+ }
44
+
45
+ export const WEIGHT_CONVERTERS = {
46
+ kg: {
47
+ lbs: { to: kgToLbs, from: lbsToKg },
48
+ oz: { to: kgToOz, from: ozToKg },
49
+ },
50
+ g: {
51
+ oz: { to: gToOz, from: ozToG },
52
+ lbs: { to: gToLbs, from: lbsToG },
53
+ },
54
+ }
55
+
56
+ export const WEIGHT_IMPERIAL_DEFAULTS = { kg: 'lbs', g: 'oz' }
@@ -0,0 +1,9 @@
1
+ export * from './helpers'
2
+ export * from './MeasurementHandler'
3
+ export * from './useLengthFormatter'
4
+ export * from './useWeightFormatter'
5
+ export * from './LengthText'
6
+ export * from './WeightText'
7
+ export * from './FeetInchesInput'
8
+ export * from './LengthInput'
9
+ export * from './WeightInput'
@@ -0,0 +1,35 @@
1
+ import { LENGTH_CONVERTERS, LENGTH_IMPERIAL_DEFAULTS } from './helpers/length'
2
+ import { fixedDecimals } from '../../helpers/numbers'
3
+ import { useIsImperial } from './MeasurementHandler'
4
+
5
+ export function useLengthFormatter({
6
+ measurementSystem,
7
+ metricPrecision = 'cm',
8
+ imperialPrecision,
9
+ withoutSuffix,
10
+ } = {}) {
11
+ const isImperial = useIsImperial(measurementSystem)
12
+ const impPrec = imperialPrecision || LENGTH_IMPERIAL_DEFAULTS[metricPrecision] || 'ft+in'
13
+
14
+ return (value) => {
15
+ if (!value && value !== 0) return null
16
+
17
+ if (isImperial) {
18
+ const converter = LENGTH_CONVERTERS[metricPrecision]?.[impPrec]
19
+ if (!converter) return `${fixedDecimals(value)} ${metricPrecision}`
20
+
21
+ const converted = converter.to(value)
22
+
23
+ if (impPrec === 'ft+in') {
24
+ const v = typeof converted === 'object' ? converted : { feet: 0, inches: 0 }
25
+ return `${v.feet}'${v.inches}"`
26
+ }
27
+
28
+ if (withoutSuffix) return fixedDecimals(converted)
29
+ return `${fixedDecimals(converted)} ${impPrec}`
30
+ }
31
+
32
+ if (withoutSuffix) return fixedDecimals(value)
33
+ return `${fixedDecimals(value)} ${metricPrecision}`
34
+ }
35
+ }
@@ -0,0 +1,32 @@
1
+ import React from 'react'
2
+
3
+ // Local display state for measurement inputs.
4
+ // Resyncs when the unit changes or the value is changed externally,
5
+ // without clobbering what the user is typing (their own onChange echoes back).
6
+ export function useLocalInputValue({ value, formattedValue, suffix, onChange, converter }) {
7
+ const [localValue, setLocalValue] = React.useState(formattedValue)
8
+ const [prevSuffix, setPrevSuffix] = React.useState(suffix)
9
+ const lastEmitted = React.useRef(value)
10
+
11
+ if (suffix !== prevSuffix) {
12
+ setPrevSuffix(suffix)
13
+ setLocalValue(formattedValue)
14
+ lastEmitted.current = value
15
+ } else if (value !== lastEmitted.current) {
16
+ lastEmitted.current = value
17
+ setLocalValue(formattedValue)
18
+ }
19
+
20
+ function handleChange(newValue) {
21
+ setLocalValue(newValue)
22
+ if (!newValue && newValue !== 0) {
23
+ lastEmitted.current = newValue
24
+ return onChange(newValue)
25
+ }
26
+ const converted = converter ? converter.from(newValue) : newValue
27
+ lastEmitted.current = converted
28
+ onChange(converted)
29
+ }
30
+
31
+ return [localValue, handleChange]
32
+ }
@@ -0,0 +1,29 @@
1
+ import { WEIGHT_CONVERTERS, WEIGHT_IMPERIAL_DEFAULTS } from './helpers/weight'
2
+ import { fixedDecimals } from '../../helpers/numbers'
3
+ import { useIsImperial } from './MeasurementHandler'
4
+
5
+ export function useWeightFormatter({
6
+ measurementSystem,
7
+ metricPrecision = 'kg',
8
+ imperialPrecision,
9
+ withoutSuffix,
10
+ } = {}) {
11
+ const isImperial = useIsImperial(measurementSystem)
12
+ const impPrec = imperialPrecision || WEIGHT_IMPERIAL_DEFAULTS[metricPrecision] || 'lbs'
13
+
14
+ return (value) => {
15
+ if (!value && value !== 0) return null
16
+
17
+ if (isImperial) {
18
+ const converter = WEIGHT_CONVERTERS[metricPrecision]?.[impPrec]
19
+ if (!converter) return `${fixedDecimals(value)} ${metricPrecision}`
20
+
21
+ const converted = converter.to(value)
22
+ if (withoutSuffix) return fixedDecimals(converted)
23
+ return `${fixedDecimals(converted)} ${impPrec}`
24
+ }
25
+
26
+ if (withoutSuffix) return fixedDecimals(value)
27
+ return `${fixedDecimals(value)} ${metricPrecision}`
28
+ }
29
+ }
@@ -54,7 +54,7 @@ export function Avatar(rootProps) {
54
54
  useOverflowModifier
55
55
  )([{}, rootProps])
56
56
 
57
- let { initials, name, icon, src, invert, textProps, iconProps, iconSize, ...props } = formattedProps
57
+ let { initials, name, icon, src, invert, textProps, iconProps, iconSize, imageProps, ...props } = formattedProps
58
58
  initials = initials || getInitials(name)
59
59
 
60
60
  let content = (
@@ -69,7 +69,7 @@ export function Avatar(rootProps) {
69
69
  iconProps={{ size: iconSize, ...iconProps }}
70
70
  />
71
71
  )
72
- if (!!src) content = <Image br={0} src={src} width={sizeCode} height={sizeCode} />
72
+ if (!!src) content = <Image br={0} src={src} width={sizeCode} height={sizeCode} {...imageProps} />
73
73
 
74
74
  return (
75
75
  <AbsView className="neko-avatar" {...props}>
@@ -0,0 +1,20 @@
1
+ import { Button } from '../actions'
2
+
3
+ // Plain ReactJS (Vite/Next/CRA): react-router goBack via navigate(-1). require() in try/catch so a
4
+ // web app without react-router-dom degrades gracefully instead of failing the module load.
5
+ let useNavigate
6
+ try {
7
+ useNavigate = require('react-router-dom').useNavigate
8
+ } catch {
9
+ useNavigate = () => () => console.warn('ReturnButton: react-router-dom not installed.')
10
+ }
11
+
12
+ // A back/close Button. Defaults to a left-arrow icon that calls navigate(-1). `close` swaps to a
13
+ // close icon; `icon` overrides the icon name; `onPress` overrides goBack. Extra props pass to the
14
+ // Button (label, outline, size, color, etc.).
15
+ export function ReturnButton({ icon, close, onPress, ...props }) {
16
+ const navigate = useNavigate()
17
+ const name = icon || (close ? 'close-line' : 'arrow-left-s-line')
18
+
19
+ return <Button icon={name} onPress={onPress || (() => navigate(-1))} {...props} />
20
+ }
@@ -0,0 +1,20 @@
1
+ import { Button } from '../actions'
2
+
3
+ // react-navigation goBack. require() in try/catch so a native app without react-navigation degrades
4
+ // gracefully instead of failing the module load.
5
+ let useNavigation
6
+ try {
7
+ useNavigation = require('@react-navigation/native').useNavigation
8
+ } catch {
9
+ useNavigation = () => ({ goBack: () => console.warn('ReturnButton: @react-navigation/native not installed.') })
10
+ }
11
+
12
+ // A back/close Button. Defaults to a left-arrow icon that calls navigation.goBack(). `close` swaps to
13
+ // a close icon; `icon` overrides the icon name; `onPress` overrides goBack. Extra props pass to the
14
+ // Button (label, outline, size, color, etc.).
15
+ export function ReturnButton({ icon, close, onPress, ...props }) {
16
+ const navigation = useNavigation()
17
+ const name = icon || (close ? 'close-line' : 'arrow-left-s-line')
18
+
19
+ return <Button icon={name} onPress={onPress || (() => navigation.goBack())} {...props} />
20
+ }
@@ -0,0 +1,2 @@
1
+ // react-native-web apps use react-navigation (like native), so reuse the native implementation.
2
+ export { ReturnButton } from './ReturnButton.native'
@@ -0,0 +1,25 @@
1
+ import { Icon } from '../presentation'
2
+ import { Link } from '../actions'
3
+
4
+ // Plain ReactJS (Vite/Next/CRA): react-router goBack via navigate(-1). require() in try/catch so a
5
+ // web app without react-router-dom degrades gracefully instead of failing the module load.
6
+ let useNavigate
7
+ try {
8
+ useNavigate = require('react-router-dom').useNavigate
9
+ } catch {
10
+ useNavigate = () => () => console.warn('ReturnLink: react-router-dom not installed.')
11
+ }
12
+
13
+ // A back/close Link: a Link wrapping an Icon. Defaults to a left-arrow that calls navigate(-1).
14
+ // `close` swaps to a close icon; `icon` overrides the icon name entirely; `onPress` overrides goBack.
15
+ // Extra props pass to the Icon (size, color, etc.).
16
+ export function ReturnLink({ icon, close, onPress, ...props }) {
17
+ const navigate = useNavigate()
18
+ const name = icon || (close ? 'close-line' : 'arrow-left-s-line')
19
+
20
+ return (
21
+ <Link onPress={onPress || (() => navigate(-1))}>
22
+ <Icon name={name} {...props} />
23
+ </Link>
24
+ )
25
+ }
@@ -0,0 +1,25 @@
1
+ import { Icon } from '../presentation'
2
+ import { Link } from '../actions'
3
+
4
+ // react-navigation goBack. require() in try/catch so a native app without react-navigation degrades
5
+ // gracefully instead of failing the module load.
6
+ let useNavigation
7
+ try {
8
+ useNavigation = require('@react-navigation/native').useNavigation
9
+ } catch {
10
+ useNavigation = () => ({ goBack: () => console.warn('ReturnLink: @react-navigation/native not installed.') })
11
+ }
12
+
13
+ // A back/close Link: a Link wrapping an Icon. Defaults to a left-arrow that calls navigation.goBack().
14
+ // `close` swaps to a close icon; `icon` overrides the icon name entirely; `onPress` overrides goBack.
15
+ // Extra props pass to the Icon (size, color, etc.).
16
+ export function ReturnLink({ icon, close, onPress, ...props }) {
17
+ const navigation = useNavigation()
18
+ const name = icon || (close ? 'close-line' : 'arrow-left-s-line')
19
+
20
+ return (
21
+ <Link onPress={onPress || (() => navigation.goBack())}>
22
+ <Icon name={name} {...props} />
23
+ </Link>
24
+ )
25
+ }
@@ -0,0 +1,2 @@
1
+ // react-native-web apps use react-navigation (like native), so reuse the native implementation.
2
+ export { ReturnLink } from './ReturnLink.native'
@@ -0,0 +1,21 @@
1
+ import { ActiveStepContent } from '../steps'
2
+ import { View } from '../structure'
3
+
4
+ // Plain ReactJS (Vite/Next/CRA) fallback — no react-navigation. Renders the active step in place,
5
+ // driven by StepsHandler's activeIndex, so a wizard built with these pieces still works on plain web
6
+ // (just without native stack transitions). React Native / RNW use the .native variant.
7
+ let warned = false
8
+
9
+ // eslint-disable-next-line no-unused-vars -- absorb the native-only screenOptions so it doesn't leak onto View
10
+ export function RoutedStepsContent({ screenOptions, ...props }) {
11
+ if (!warned) {
12
+ warned = true
13
+ console.warn('RoutedStepsContent is native-only (react-navigation). Rendering steps in place on plain web.')
14
+ }
15
+
16
+ return (
17
+ <View flex {...props}>
18
+ <ActiveStepContent />
19
+ </View>
20
+ )
21
+ }
@@ -0,0 +1,94 @@
1
+ import React from 'react'
2
+
3
+ import { View } from '../structure'
4
+ import { useSteps } from '../steps'
5
+
6
+ // Native-only deps. require() inside try/catch keeps web bundlers from pulling react-navigation into
7
+ // the plain ReactJS build (which uses RoutedStepsContent.js instead).
8
+ let NavigationContainer
9
+ let NavigationIndependentTree
10
+ let StackActions
11
+ let createNativeStackNavigator
12
+ let available = true
13
+ try {
14
+ const nav = require('@react-navigation/native')
15
+ NavigationContainer = nav.NavigationContainer
16
+ NavigationIndependentTree = nav.NavigationIndependentTree
17
+ StackActions = nav.StackActions
18
+ createNativeStackNavigator = require('@react-navigation/native-stack').createNativeStackNavigator
19
+ } catch {
20
+ available = false
21
+ }
22
+
23
+ let warned = false
24
+
25
+ // Renders a step's content exactly like ActiveStepContent — raw, no wrapper. Any scroll/padding is
26
+ // the consumer's call inside their own render/renderContent.
27
+ const stepContent = (item) => {
28
+ const Content = item.render || item.renderContent || item.Content
29
+ return Content ? <Content /> : null
30
+ }
31
+
32
+ // The swapping content region — the routed equivalent of ActiveStepContent. Drop it inside any
33
+ // StepsHandler, alongside whatever fixed chrome you want. It adds NO state of its own: StepsHandler
34
+ // stays the single source of truth. This component just renders a self-contained native stack
35
+ // (independent from the host NavigationContainer) and keeps it in sync with activeIndex both ways.
36
+ export function RoutedStepsContent({ screenOptions, ...props }) {
37
+ const { items, activeIndex, moveToIndex } = useSteps()
38
+ const navRef = React.useRef(null)
39
+ const Stack = React.useRef(available ? createNativeStackNavigator() : null).current
40
+
41
+ // StepsHandler -> stack: when activeIndex changes (Next/Back buttons, StepsMenu, custom controls),
42
+ // drive the stack to the matching screen. Compare against the current TOP route (getCurrentRoute)
43
+ // and be explicit about direction: push forward, pop back. Using navigate() here is wrong — it can
44
+ // push a duplicate instead of popping. If the route already matches (e.g. a native gesture back
45
+ // already moved us), this is a no-op.
46
+ React.useEffect(() => {
47
+ const ref = navRef.current
48
+ if (!ref?.isReady?.()) return
49
+ const current = Number(ref.getCurrentRoute?.()?.name) || 0
50
+ if (activeIndex === current) return
51
+ ref.dispatch(
52
+ activeIndex > current ? StackActions.push(String(activeIndex)) : StackActions.popTo(String(activeIndex))
53
+ )
54
+ }, [activeIndex])
55
+
56
+ if (!available) {
57
+ if (!warned) {
58
+ warned = true
59
+ console.warn(
60
+ 'RoutedStepsContent requires @react-navigation/native and @react-navigation/native-stack; neither is installed.'
61
+ )
62
+ }
63
+ return null
64
+ }
65
+
66
+ // stack -> StepsHandler: native gesture / hardware back changes the route without going through
67
+ // moveToIndex. Push that index back into StepsHandler (backward never validates, matching Steps).
68
+ const handleStateChange = () => {
69
+ const index = Number(navRef.current?.getCurrentRoute?.()?.name) || 0
70
+ if (index !== activeIndex) moveToIndex(index)
71
+ }
72
+
73
+ const navigator = (
74
+ <Stack.Navigator screenOptions={{ headerShown: false, ...screenOptions }}>
75
+ {items.map((item, index) => (
76
+ <Stack.Screen key={index} name={String(index)} options={{ title: item.label }}>
77
+ {() => stepContent(item)}
78
+ </Stack.Screen>
79
+ ))}
80
+ </Stack.Navigator>
81
+ )
82
+
83
+ // Self-contained: its own navigation world, independent of the host app's NavigationContainer, so
84
+ // it never touches the host back stack (react-navigation v7+).
85
+ return (
86
+ <View flex {...props}>
87
+ <NavigationIndependentTree>
88
+ <NavigationContainer ref={navRef} onStateChange={handleStateChange}>
89
+ {navigator}
90
+ </NavigationContainer>
91
+ </NavigationIndependentTree>
92
+ </View>
93
+ )
94
+ }
@@ -0,0 +1,3 @@
1
+ // react-native-web: react-navigation runs fine here, and babel aliases react-native ->
2
+ // react-native-web, so the native implementation is correct for RNW too.
3
+ export { RoutedStepsContent } from './RoutedStepsContent.native'
@@ -0,0 +1,3 @@
1
+ export * from './RoutedStepsContent'
2
+ export * from './ReturnLink'
3
+ export * from './ReturnButton'
@@ -24,7 +24,7 @@ export function StatePresenter({
24
24
  if (error) {
25
25
  return (
26
26
  <View flex center {...props}>
27
- <Result type="error" title={errorTitle} description={errorDescription || error.message} />
27
+ <Result type="error" title={errorTitle} description={errorDescription || (typeof error === 'string' ? error : error.message)} />
28
28
  </View>
29
29
  )
30
30
  }
@@ -8,6 +8,7 @@ export function StepsHandler({ children, items, onSubmit, onValidateStep, onStep
8
8
  const [maxIndexReleased, setMaxIndexReleased] = React.useState(0)
9
9
  const [loading, setLoading] = React.useState(false)
10
10
  const activeStep = items[activeIndex]
11
+ const isFirstStep = activeIndex === 0
11
12
  const isLastStep = activeIndex === items.length - 1
12
13
 
13
14
  const moveToIndex = async (index) => {
@@ -44,6 +45,7 @@ export function StepsHandler({ children, items, onSubmit, onValidateStep, onStep
44
45
  activeStep,
45
46
  maxIndexReleased,
46
47
  loading,
48
+ isFirstStep,
47
49
  isLastStep,
48
50
  moveToNextStep,
49
51
  moveToPrevStep,
@@ -4,14 +4,24 @@ import { Text } from '../text'
4
4
  import { View } from './View'
5
5
  import { useDefaultModifier } from '../../modifiers/default'
6
6
  import { useResponsiveConverter } from '../../modifiers/responsiveConverter'
7
- import { useThemeComponentModifier } from '../../modifiers/themeComponent'
8
7
  import { useSafeAreaInsets } from '../../abstractions/helpers/useSafeAreaInsets'
8
+ import { useThemeComponentModifier } from '../../modifiers/themeComponent'
9
9
 
10
10
  const DEFAULT_PROPS = {
11
11
  borderB: 'overlayDivider',
12
+ titleProps: {
13
+ center: true,
14
+ size: 'h6',
15
+ numberOfLines: 1,
16
+ },
17
+ subtitleProps: {
18
+ center: true,
19
+ size: 'xs',
20
+ numberOfLines: 1,
21
+ },
12
22
  }
13
23
 
14
- export function TopBar({ right, left, WrapperView, children, ...rootProps }) {
24
+ export function TopBar({ title, subtitle, right, left, WrapperView, children, ...rootProps }) {
15
25
  const { top: safeTop } = useSafeAreaInsets()
16
26
 
17
27
  const [_, props] = pipe(
@@ -19,7 +29,7 @@ export function TopBar({ right, left, WrapperView, children, ...rootProps }) {
19
29
  useDefaultModifier(DEFAULT_PROPS),
20
30
  useResponsiveConverter([])
21
31
  )([{}, rootProps])
22
- let { useSafeArea = true, title, subtitle } = props
32
+ let { useSafeArea = true, titleProps, subtitleProps } = props
23
33
 
24
34
  const hasContent = !!title || !!subtitle || !!children || !!right || !!left
25
35
 
@@ -34,17 +44,9 @@ export function TopBar({ right, left, WrapperView, children, ...rootProps }) {
34
44
  </View>
35
45
 
36
46
  <View center flex={3}>
37
- {children || (
38
- <Text center h6 numberOfLines={1}>
39
- {title}
40
- </Text>
41
- )}
42
-
43
- {subtitle && (
44
- <Text center xs numberOfLines={1}>
45
- {subtitle}
46
- </Text>
47
- )}
47
+ {children || <Text {...titleProps}>{title}</Text>}
48
+
49
+ {subtitle && <Text {...subtitleProps}>{subtitle}</Text>}
48
50
  </View>
49
51
 
50
52
  <View flex={1} toRight>
@@ -3,7 +3,7 @@ import { ThemePicker } from './ThemePicker'
3
3
 
4
4
  export function ThemePickerDrawer({ open, onClose, onChange }) {
5
5
  return (
6
- <BottomDrawer open={open} onClose={onClose} maxWidth={550} snapPoints={['50%', '85%']}>
6
+ <BottomDrawer open={open} onClose={onClose} maxWidth={550} snapPoints={['60%', '85%']} useSafeArea={false}>
7
7
  <DrawerScrollView padding="md">
8
8
  <ThemePicker onChange={onChange} />
9
9
  </DrawerScrollView>