panelui-native 0.79.0 → 0.80.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 (34) hide show
  1. package/lib/module/components/accordion/index.js +13 -1
  2. package/lib/module/components/accordion/index.js.map +1 -1
  3. package/lib/module/components/bottom-sheet/index.js +9 -2
  4. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  5. package/lib/module/components/panelside/index.js +527 -19
  6. package/lib/module/components/panelside/index.js.map +1 -1
  7. package/lib/module/components/qr-code/index.js +84 -15
  8. package/lib/module/components/qr-code/index.js.map +1 -1
  9. package/lib/module/components/qr-code/qr-shapes.js +202 -0
  10. package/lib/module/components/qr-code/qr-shapes.js.map +1 -0
  11. package/lib/module/components/timeline/index.js +145 -18
  12. package/lib/module/components/timeline/index.js.map +1 -1
  13. package/lib/module/index.js.map +1 -1
  14. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  15. package/lib/typescript/src/components/bottom-sheet/index.d.ts +16 -0
  16. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  17. package/lib/typescript/src/components/panelside/index.d.ts +242 -5
  18. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  19. package/lib/typescript/src/components/qr-code/index.d.ts +42 -3
  20. package/lib/typescript/src/components/qr-code/index.d.ts.map +1 -1
  21. package/lib/typescript/src/components/qr-code/qr-shapes.d.ts +64 -0
  22. package/lib/typescript/src/components/qr-code/qr-shapes.d.ts.map +1 -0
  23. package/lib/typescript/src/components/timeline/index.d.ts +33 -0
  24. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  25. package/lib/typescript/src/index.d.ts +1 -1
  26. package/lib/typescript/src/index.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/components/accordion/index.tsx +13 -0
  29. package/src/components/bottom-sheet/index.tsx +9 -2
  30. package/src/components/panelside/index.tsx +750 -28
  31. package/src/components/qr-code/index.tsx +124 -18
  32. package/src/components/qr-code/qr-shapes.ts +234 -0
  33. package/src/components/timeline/index.tsx +165 -14
  34. package/src/index.ts +3 -0
@@ -49,6 +49,7 @@ import {
49
49
  createContext,
50
50
  forwardRef,
51
51
  isValidElement,
52
+ useCallback,
52
53
  useContext,
53
54
  useMemo,
54
55
  type ReactElement,
@@ -94,10 +95,27 @@ const AnimatedText = Animated.createAnimatedComponent(Text);
94
95
  function useColumnDetent(
95
96
  scrollX: SharedValue<number>,
96
97
  offsets: number[],
97
- enabled: boolean
98
+ haptics: boolean,
99
+ onColumnChange?: (index: number) => void
98
100
  ) {
99
101
  const nearest = useSharedValue(0);
100
102
 
103
+ /*
104
+ * A stable function for `runOnJS`. The prop itself is a fresh closure on
105
+ * every render, and handing that to the UI thread re-serialises it on every
106
+ * frame the reaction is rebuilt.
107
+ */
108
+ const report = useCallback(
109
+ (index: number) => {
110
+ onColumnChange?.(index);
111
+ },
112
+ [onColumnChange]
113
+ );
114
+
115
+ // Nothing to compute unless somebody is listening. The reaction runs on
116
+ // every scroll frame, so a timeline that wants neither pays nothing.
117
+ const enabled = haptics || onColumnChange !== undefined;
118
+
101
119
  useAnimatedReaction(
102
120
  () => {
103
121
  if (!enabled || offsets.length === 0) return 0;
@@ -115,7 +133,8 @@ function useColumnDetent(
115
133
  (index, previous) => {
116
134
  if (!enabled || previous === null || index === previous) return;
117
135
  nearest.value = index;
118
- runOnJS(selectionTick)();
136
+ if (haptics) runOnJS(selectionTick)();
137
+ if (onColumnChange !== undefined) runOnJS(report)(index);
119
138
  }
120
139
  );
121
140
  }
@@ -184,10 +203,18 @@ const TimelineColumnOffsetContext = createContext<number | null>(null);
184
203
  *
185
204
  * The rail has to land on exactly the same line in every column, and the
186
205
  * simplest way to guarantee that is to make the band above it a fixed height
187
- * rather than to position the rail against measured content. So the aside is
188
- * `HORIZONTAL_RAIL_TOP` tall in every column, the rail is drawn once across the
189
- * whole track at that offset, and each column's tick is pulled up by half its
190
- * own height to sit centred on it.
206
+ * rather than to position the rail against measured content.
207
+ *
208
+ * So the *column* reserves it, as padding, and the rail is drawn once across
209
+ * the whole track at that offset. Each column's tick is the first thing after
210
+ * the padding and is pulled up by half its own height to straddle the line.
211
+ *
212
+ * `Timeline.Aside` fills that band with a negative margin equal to its own
213
+ * height: it draws inside the reserved strip and takes no flow height of its
214
+ * own. That is what makes it optional. Reserved by the aside instead, a column
215
+ * without one put its tick at the top of the column and 64 points above the
216
+ * rail — which is not a mistake anyone makes twice, but is also not a mistake
217
+ * the component should let anyone make once.
191
218
  */
192
219
  const HORIZONTAL_RAIL_TOP = 64;
193
220
  const HORIZONTAL_TICK_HEIGHT = 10;
@@ -239,6 +266,25 @@ const timelineVariants = tv({
239
266
  stats: 'mt-2 flex-row gap-6 rounded-xl border border-border px-3.5 py-2.5',
240
267
  statLabel: 'text-xs text-muted-foreground',
241
268
  statValue: 'text-sm font-medium text-foreground',
269
+ /*
270
+ * The masthead over a horizontal rail: what the run of columns is, said
271
+ * once, above them.
272
+ *
273
+ * `gap-0` between the two lines and tight leading on both, because they
274
+ * are one thing rather than two — a label and the name it introduces. Set
275
+ * a step apart with a normal paragraph gap they read as a small heading
276
+ * followed by a bigger unrelated one, which is what the pair looked like
277
+ * before.
278
+ *
279
+ * The label is a size below the title and muted rather than the same size
280
+ * in a lighter weight: at the same size the eye reads the grey one first
281
+ * because it is on the left of the taller one, and the name — the part
282
+ * anybody is actually looking for — arrives second.
283
+ */
284
+ masthead: 'gap-2',
285
+ mastheadMedia: 'flex-row items-center',
286
+ mastheadLabel: 'text-lg font-medium leading-tight text-muted-foreground',
287
+ mastheadTitle: 'text-2xl font-semibold leading-tight text-foreground',
242
288
  },
243
289
  variants: {
244
290
  variant: {
@@ -280,10 +326,25 @@ const timelineVariants = tv({
280
326
  vertical: {},
281
327
  horizontal: {
282
328
  item: 'w-auto shrink-0 flex-col gap-0 pr-6',
283
- aside: 'w-auto items-start justify-end gap-1 pb-2 pt-0',
329
+ aside: 'w-auto items-start justify-end gap-0 pb-2 pt-0',
330
+ // Reserved by the column, not by whatever it happens to contain.
284
331
  rail: 'w-full items-start',
285
332
  body: 'w-auto flex-none pb-0 pt-5',
286
333
  title: 'text-sm',
334
+ /*
335
+ * A column's date is its name, not a caption on it. At `text-xs` next
336
+ * to a `text-xs` meta line the two were a two-line grey block and the
337
+ * year — the only thing distinguishing one column from the next — had
338
+ * to be looked for. The meta line stays small above it and the pair
339
+ * closes up, so they read as one label with an eyebrow rather than as
340
+ * two.
341
+ *
342
+ * Colour is left alone: `useColumnInk` animates it per column so the
343
+ * one you have landed on is the one in full ink, and a colour in the
344
+ * class would be overridden by that anyway.
345
+ */
346
+ date: 'text-sm font-semibold leading-tight',
347
+ meta: 'text-[11px] font-medium leading-tight',
287
348
  },
288
349
  },
289
350
  },
@@ -416,6 +477,24 @@ export interface TimelineProps extends ViewProps {
416
477
  * whether this one is worth feeling is the caller's call.
417
478
  */
418
479
  haptics?: boolean;
480
+ /**
481
+ * Horizontal only: which column is at the reading edge, reported as it
482
+ * changes.
483
+ *
484
+ * For anything outside the rail that belongs to the column being read — a
485
+ * masthead naming it, a caption, a picture. Without it that block can only
486
+ * show the same thing for the whole run, which makes a swipe through ten
487
+ * columns a swipe under one unchanging heading.
488
+ *
489
+ * The index is the column's position among the rendered items, not its
490
+ * `step`: `step` is the progress value and may be sparse or repeated, so it
491
+ * cannot address a column.
492
+ *
493
+ * It fires on the crossing, not per frame — the reading edge passing from
494
+ * one column to the next — so it is a state update per column rather than
495
+ * per scroll event.
496
+ */
497
+ onColumnChange?: (index: number) => void;
419
498
  children?: ReactNode;
420
499
  }
421
500
 
@@ -428,6 +507,7 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
428
507
  orientation = 'vertical',
429
508
  snap = true,
430
509
  haptics = false,
510
+ onColumnChange,
431
511
  children,
432
512
  ...props
433
513
  },
@@ -484,7 +564,7 @@ const TimelineRoot = forwardRef<View, TimelineProps>(
484
564
  scrollX.value = event.contentOffset.x;
485
565
  });
486
566
 
487
- useColumnDetent(scrollX, offsets, horizontal && snap && haptics);
567
+ useColumnDetent(scrollX, offsets, horizontal && snap && haptics, horizontal ? onColumnChange : undefined);
488
568
 
489
569
  if (!horizontal) {
490
570
  return (
@@ -777,7 +857,13 @@ const TimelineItem = forwardRef<View, TimelineItemProps>(
777
857
  ref={ref}
778
858
  className={item({ className })}
779
859
  {...props}
780
- style={[style, { width: columnWidth }, columnStyle]}
860
+ style={[
861
+ style,
862
+ // The band above the rail, reserved whether or not this column
863
+ // writes a `Timeline.Aside` into it.
864
+ { width: columnWidth, paddingTop: HORIZONTAL_RAIL_TOP },
865
+ columnStyle,
866
+ ]}
781
867
  >
782
868
  {virtualized && !last ? (
783
869
  <View
@@ -817,14 +903,15 @@ const TimelineAside = forwardRef<View, ViewProps & { className?: string }>(
817
903
  ref={ref}
818
904
  className={aside({ className })}
819
905
  /*
820
- * Horizontal: this band's height is where the rail lands, so it is
821
- * fixed rather than sized to whatever the column happens to put in it.
822
- * A column with a longer label would otherwise push its own tick below
823
- * everybody else's and the rail would stop being a line.
906
+ * Horizontal: this draws *into* the band the column has already
907
+ * reserved, rather than being the thing that reserves it — hence the
908
+ * negative margin cancelling its own height. A column with a longer
909
+ * label therefore cannot push its own tick below everybody else's, and
910
+ * a column with no aside at all still has its tick on the rail.
824
911
  */
825
912
  style={
826
913
  orientation === 'horizontal'
827
- ? [{ height: HORIZONTAL_RAIL_TOP }, style]
914
+ ? [{ height: HORIZONTAL_RAIL_TOP, marginTop: -HORIZONTAL_RAIL_TOP }, style]
828
915
  : style
829
916
  }
830
917
  {...props}
@@ -1104,10 +1191,74 @@ const TimelineStat = forwardRef<View, TimelineStatProps>(
1104
1191
  );
1105
1192
  TimelineStat.displayName = 'Timeline.Stat';
1106
1193
 
1194
+ export interface TimelineMastheadProps extends ViewProps {
1195
+ className?: string;
1196
+ /**
1197
+ * What sits above the two lines — a logo pair, an avatar stack, a single
1198
+ * mark. Anything; the slot only lays it out in a row.
1199
+ */
1200
+ media?: ReactNode;
1201
+ /** The small line: what kind of thing the run below is. */
1202
+ label?: string;
1203
+ /** The name of it, in the size the eye lands on first. */
1204
+ title?: string;
1205
+ /** Anything else, below the title. */
1206
+ children?: ReactNode;
1207
+ }
1208
+
1209
+ /**
1210
+ * The block above a horizontal rail, saying what the run of columns is.
1211
+ *
1212
+ * Media, then a small label, then the name — the order a reader takes them in
1213
+ * anyway, and the reason it is a part rather than two `Text`s at every call
1214
+ * site: the pair has to be typeset as one unit, and a masthead written by hand
1215
+ * is a heading above another heading.
1216
+ *
1217
+ * ```tsx
1218
+ * <Timeline.Masthead
1219
+ * media={
1220
+ * <Avatar.Group size="sm">
1221
+ * <Avatar fallback="A"><AppleIcon size={18} /></Avatar>
1222
+ * </Avatar.Group>
1223
+ * }
1224
+ * label="Built for"
1225
+ * title="iOS and Android"
1226
+ * />
1227
+ *
1228
+ * <Timeline orientation="horizontal">…</Timeline>
1229
+ * ```
1230
+ *
1231
+ * It sits outside the `Timeline`, above it, because it belongs to the whole
1232
+ * run rather than to any column in it — and a column is the only thing a
1233
+ * horizontal `Timeline` lays out.
1234
+ */
1235
+ const TimelineMasthead = forwardRef<View, TimelineMastheadProps>(
1236
+ ({ className, media, label, title, children, ...props }, ref) => {
1237
+ const { masthead, mastheadMedia, mastheadLabel, mastheadTitle } = timelineVariants({
1238
+ orientation: 'horizontal',
1239
+ });
1240
+
1241
+ return (
1242
+ <View ref={ref} className={masthead({ className })} {...props}>
1243
+ {media ? <View className={mastheadMedia()}>{media}</View> : null}
1244
+ {label !== undefined || title !== undefined ? (
1245
+ <View>
1246
+ {label !== undefined ? <Text className={mastheadLabel()}>{label}</Text> : null}
1247
+ {title !== undefined ? <Text className={mastheadTitle()}>{title}</Text> : null}
1248
+ </View>
1249
+ ) : null}
1250
+ {children}
1251
+ </View>
1252
+ );
1253
+ }
1254
+ );
1255
+ TimelineMasthead.displayName = 'Timeline.Masthead';
1256
+
1107
1257
  export const Timeline = Object.assign(TimelineRoot, {
1108
1258
  List: TimelineList,
1109
1259
  Item: TimelineItem,
1110
1260
  Aside: TimelineAside,
1261
+ Masthead: TimelineMasthead,
1111
1262
  Indicator: TimelineIndicator,
1112
1263
  Content: TimelineContent,
1113
1264
  Header: TimelineHeader,
package/src/index.ts CHANGED
@@ -884,6 +884,9 @@ export {
884
884
  type QRCodeContentProps,
885
885
  type QRCodeSize,
886
886
  type QRCodePresentation,
887
+ type QRCodeModuleShape,
888
+ type QRCodeEyeFrameShape,
889
+ type QRCodeEyeBallShape,
887
890
  type ErrorCorrectionLevel,
888
891
  } from './components/qr-code';
889
892
  export {