panelui-native 0.32.0 → 0.35.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 (46) hide show
  1. package/README.md +2 -1
  2. package/lib/module/components/button/index.js +106 -11
  3. package/lib/module/components/button/index.js.map +1 -1
  4. package/lib/module/components/drawer/index.js +477 -0
  5. package/lib/module/components/drawer/index.js.map +1 -0
  6. package/lib/module/components/panelside/index.js +1024 -0
  7. package/lib/module/components/panelside/index.js.map +1 -0
  8. package/lib/module/components/progress/index.js +64 -24
  9. package/lib/module/components/progress/index.js.map +1 -1
  10. package/lib/module/components/select/index.js +14 -3
  11. package/lib/module/components/select/index.js.map +1 -1
  12. package/lib/module/components/swipe/index.js +537 -0
  13. package/lib/module/components/swipe/index.js.map +1 -0
  14. package/lib/module/icons/index.js +20 -0
  15. package/lib/module/icons/index.js.map +1 -1
  16. package/lib/module/index.js +4 -1
  17. package/lib/module/index.js.map +1 -1
  18. package/lib/module/native/index.js +30 -0
  19. package/lib/module/native/index.js.map +1 -1
  20. package/lib/typescript/src/components/button/index.d.ts +22 -1
  21. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  22. package/lib/typescript/src/components/drawer/index.d.ts +146 -0
  23. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -0
  24. package/lib/typescript/src/components/panelside/index.d.ts +354 -0
  25. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -0
  26. package/lib/typescript/src/components/progress/index.d.ts +25 -4
  27. package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
  28. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  29. package/lib/typescript/src/components/swipe/index.d.ts +206 -0
  30. package/lib/typescript/src/components/swipe/index.d.ts.map +1 -0
  31. package/lib/typescript/src/icons/index.d.ts +1 -0
  32. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  33. package/lib/typescript/src/index.d.ts +4 -1
  34. package/lib/typescript/src/index.d.ts.map +1 -1
  35. package/lib/typescript/src/native/index.d.ts +20 -0
  36. package/lib/typescript/src/native/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/components/button/index.tsx +140 -8
  39. package/src/components/drawer/index.tsx +662 -0
  40. package/src/components/panelside/index.tsx +1358 -0
  41. package/src/components/progress/index.tsx +93 -25
  42. package/src/components/select/index.tsx +23 -3
  43. package/src/components/swipe/index.tsx +687 -0
  44. package/src/icons/index.tsx +14 -0
  45. package/src/index.ts +42 -0
  46. package/src/native/index.ts +51 -0
@@ -52,8 +52,21 @@ export interface ProgressProps
52
52
  extends Omit<ViewProps, 'children'>,
53
53
  ProgressVariantProps {
54
54
  className?: string;
55
- /** Progress value, 0–100. Ignored when `indeterminate` is set. */
55
+ /**
56
+ * Where the work has got to, somewhere between `minValue` and `maxValue`.
57
+ * Ignored when `indeterminate` is set.
58
+ */
56
59
  value?: number;
60
+ /**
61
+ * The bottom of the range — the value at which the bar reads as empty.
62
+ * Defaults to `0`.
63
+ */
64
+ minValue?: number;
65
+ /**
66
+ * The top of the range — the value at which the bar reads as full. Defaults
67
+ * to `100`, so a bare percentage keeps working with neither bound set.
68
+ */
69
+ maxValue?: number;
57
70
  /** Show a looping animation for unknown-duration work. */
58
71
  indeterminate?: boolean;
59
72
  /** Extra classes for the moving indicator. */
@@ -75,37 +88,59 @@ export interface ProgressProps
75
88
  */
76
89
  valueLabel?: string;
77
90
  /**
78
- * Format the value label as a fraction of the whole with `Intl.NumberFormat`
79
- * — e.g. `{ style: 'currency', currency: 'USD' }`. Falls back to a rounded
80
- * percent when omitted.
91
+ * How to write the value, through `Intl.NumberFormat`. A `percent` style
92
+ * formats how far along the bar is; every other style formats the value
93
+ * itself, so `{ style: 'currency', currency: 'USD' }` against a `maxValue`
94
+ * of `2000` reads `$1,250.00` rather than a percentage of it. Falls back to
95
+ * a rounded percent when omitted.
81
96
  */
82
97
  formatOptions?: Intl.NumberFormatOptions;
83
98
  /** Extra classes for the label + value-label row. */
84
99
  headerClassName?: string;
85
100
  }
86
101
 
87
- function clamp(value: number) {
88
- 'worklet';
89
- if (value < 0) return 0;
90
- if (value > 100) return 100;
102
+ /** `value` held inside the range, so a stray number cannot escape the track. */
103
+ function clamp(value: number, min: number, max: number) {
104
+ if (!(value > min)) return min;
105
+ if (value > max) return max;
91
106
  return value;
92
107
  }
93
108
 
94
- /** The right-hand caption: an explicit override, an Intl fraction, or a percent. */
109
+ /**
110
+ * How far along the bar is, 0–1. An empty or inverted range has no meaningful
111
+ * position in it, so it reads as empty rather than dividing by zero.
112
+ */
113
+ function fractionOf(value: number, min: number, max: number) {
114
+ const span = max - min;
115
+ if (!(span > 0)) return 0;
116
+ return clamp((value - min) / span, 0, 1);
117
+ }
118
+
119
+ /**
120
+ * The right-hand caption: an explicit override, an `Intl` rendering, or a
121
+ * rounded percent.
122
+ *
123
+ * A `percent` style is given the fraction, because that is what a percentage
124
+ * of the range means; every other style is given the value, because a currency
125
+ * or a byte count is a quantity and not a proportion.
126
+ */
95
127
  function formatValue(
96
128
  value: number,
129
+ fraction: number,
97
130
  valueLabel?: string,
98
131
  formatOptions?: Intl.NumberFormatOptions
99
132
  ) {
100
133
  if (valueLabel != null) return valueLabel;
101
134
  if (formatOptions) {
102
135
  try {
103
- return new Intl.NumberFormat(undefined, formatOptions).format(value / 100);
136
+ return new Intl.NumberFormat(undefined, formatOptions).format(
137
+ formatOptions.style === 'percent' ? fraction : value
138
+ );
104
139
  } catch {
105
140
  // Some engines ship a partial Intl; fall through to the plain percent.
106
141
  }
107
142
  }
108
- return `${Math.round(value)}%`;
143
+ return `${Math.round(fraction * 100)}%`;
109
144
  }
110
145
 
111
146
  /**
@@ -115,6 +150,12 @@ function formatValue(
115
150
  *
116
151
  * Pass `label` or `showValueLabel` to caption the bar with a header row; the
117
152
  * bare track renders when neither is set, so existing call sites are untouched.
153
+ *
154
+ * The value is read against `minValue` / `maxValue`, which default to 0 and
155
+ * 100 — so a percentage needs neither. Set them and the bar speaks in whatever
156
+ * the work is actually counted in: bytes uploaded, seats filled, points
157
+ * scored. Nothing has to be converted to a percent on the way in, and the
158
+ * screen reader is told the real range rather than a derived one.
118
159
  */
119
160
  export const Progress = forwardRef<View, ProgressProps>(
120
161
  (
@@ -123,6 +164,8 @@ export const Progress = forwardRef<View, ProgressProps>(
123
164
  indicatorClassName,
124
165
  headerClassName,
125
166
  value = 0,
167
+ minValue = 0,
168
+ maxValue = 100,
126
169
  indeterminate = false,
127
170
  label,
128
171
  showValueLabel = false,
@@ -139,23 +182,33 @@ export const Progress = forwardRef<View, ProgressProps>(
139
182
  // Yoga mirrors the track; the bar sliding along it is a transform, so it
140
183
  // has to be turned around itself.
141
184
  const sign = useDirectionSign();
142
- const progress = useSharedValue(clamp(value) / 100);
185
+ const target = fractionOf(value, minValue, maxValue);
186
+ const progress = useSharedValue(target);
143
187
  const slide = useSharedValue(0);
144
188
 
145
- const target = clamp(value) / 100;
189
+ /**
190
+ * The loop is tied to `indeterminate` alone. Restarting it whenever the
191
+ * value moved used to leave `withRepeat` cycling from wherever the bar had
192
+ * got to, so a bar that kept receiving values while looping ended up
193
+ * sweeping a shrinking sliver of the track instead of crossing it.
194
+ */
195
+ useEffect(() => {
196
+ if (!indeterminate) return undefined;
197
+ slide.value = 0;
198
+ slide.value = withRepeat(
199
+ withTiming(1, { duration: 1100, easing: Easing.inOut(Easing.ease) }),
200
+ -1,
201
+ false
202
+ );
203
+ return () => cancelAnimation(slide);
204
+ }, [indeterminate, slide]);
146
205
 
206
+ // The fill follows the value, and picks up from wherever the loop left it
207
+ // when a bar stops being indeterminate mid-flight.
147
208
  useEffect(() => {
148
- if (indeterminate) {
149
- slide.value = withRepeat(
150
- withTiming(1, { duration: 1100, easing: Easing.inOut(Easing.ease) }),
151
- -1,
152
- false
153
- );
154
- return () => cancelAnimation(slide);
155
- }
209
+ if (indeterminate) return;
156
210
  progress.value = withSpring(target, SPRING);
157
- return undefined;
158
- }, [indeterminate, target, progress, slide]);
211
+ }, [indeterminate, target, progress]);
159
212
 
160
213
  const onLayout = (event: LayoutChangeEvent) => {
161
214
  trackWidth.value = event.nativeEvent.layout.width;
@@ -188,6 +241,12 @@ export const Progress = forwardRef<View, ProgressProps>(
188
241
  // The value label is meaningless while looping, so it is dropped there.
189
242
  const showValue = showValueLabel && !indeterminate;
190
243
  const hasHeader = label != null || showValue;
244
+ const spoken = formatValue(
245
+ clamp(value, minValue, maxValue),
246
+ target,
247
+ valueLabel,
248
+ formatOptions
249
+ );
191
250
 
192
251
  const track = (
193
252
  <View
@@ -195,7 +254,16 @@ export const Progress = forwardRef<View, ProgressProps>(
195
254
  accessibilityRole="progressbar"
196
255
  accessibilityLabel={label}
197
256
  accessibilityValue={
198
- indeterminate ? undefined : { min: 0, max: 100, now: clamp(value) }
257
+ indeterminate
258
+ ? undefined
259
+ : {
260
+ min: minValue,
261
+ max: maxValue,
262
+ now: clamp(value, minValue, maxValue),
263
+ // Without this the range is read as a bare number. `text` is
264
+ // what carries the unit — the percent sign, the currency.
265
+ text: spoken,
266
+ }
199
267
  }
200
268
  className={slots.track({ className })}
201
269
  onLayout={onLayout}
@@ -222,7 +290,7 @@ export const Progress = forwardRef<View, ProgressProps>(
222
290
  )}
223
291
  {showValue ? (
224
292
  <Text size="sm" muted className={cn(label == null && 'ms-auto')}>
225
- {formatValue(clamp(value), valueLabel, formatOptions)}
293
+ {spoken}
226
294
  </Text>
227
295
  ) : null}
228
296
  </View>
@@ -514,10 +514,20 @@ function SelectRoot({
514
514
 
515
515
  {open && overlayPosition ? (
516
516
  <Portal>
517
- {/* Full-screen catcher so a press anywhere else dismisses the list. */}
517
+ {/*
518
+ * Full-screen catcher so a press anywhere else dismisses the list.
519
+ *
520
+ * Hidden from assistive tech, and deliberately: it is a dismiss
521
+ * affordance for a pointer, and announcing it would put an unlabelled
522
+ * full-screen "button" ahead of the options in the reading order,
523
+ * where swiping through the list would land on it before the first
524
+ * one. Escaping the list is the back gesture's job, and on iOS the
525
+ * modal flag's.
526
+ */}
518
527
  <Pressable
519
- accessibilityRole="button"
520
- accessibilityLabel="Close"
528
+ accessible={false}
529
+ importantForAccessibility="no-hide-descendants"
530
+ accessibilityElementsHidden
521
531
  onPress={close}
522
532
  style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
523
533
  />
@@ -527,6 +537,16 @@ function SelectRoot({
527
537
  exiting={FadeOut.duration(120)}
528
538
  onLayout={onListLayout}
529
539
  style={overlayPosition}
540
+ /*
541
+ * The floating list is a modal layer, the same as the sheet
542
+ * presentation's is — it covers the screen with a catcher and
543
+ * takes the back button. Without this the page behind it stays
544
+ * in the accessibility tree, so a screen reader could walk out
545
+ * of the open list into content the list is covering and act on
546
+ * it. The sheet gets this from BottomSheet; the anchored list
547
+ * has to say it itself.
548
+ */
549
+ accessibilityViewIsModal
530
550
  className={slots.list()}
531
551
  >
532
552
  {search}