panelui-native 0.44.0 → 0.46.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 (53) hide show
  1. package/README.md +2 -1
  2. package/lib/module/components/combobox/index.js +7 -1
  3. package/lib/module/components/combobox/index.js.map +1 -1
  4. package/lib/module/components/date-time-picker/index.js +254 -0
  5. package/lib/module/components/date-time-picker/index.js.map +1 -0
  6. package/lib/module/components/grid-item/index.js +486 -0
  7. package/lib/module/components/grid-item/index.js.map +1 -0
  8. package/lib/module/components/{kpi-chart → kpi}/index.js +62 -62
  9. package/lib/module/components/kpi/index.js.map +1 -0
  10. package/lib/module/components/line-chart/index.js +92 -10
  11. package/lib/module/components/line-chart/index.js.map +1 -1
  12. package/lib/module/components/pie-chart/index.js +625 -0
  13. package/lib/module/components/pie-chart/index.js.map +1 -0
  14. package/lib/module/components/scatter-chart/index.js +1173 -0
  15. package/lib/module/components/scatter-chart/index.js.map +1 -0
  16. package/lib/module/components/tabs/index.js +275 -33
  17. package/lib/module/components/tabs/index.js.map +1 -1
  18. package/lib/module/index.js +5 -1
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/utils/chart.js +81 -0
  21. package/lib/module/utils/chart.js.map +1 -1
  22. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  23. package/lib/typescript/src/components/date-time-picker/index.d.ts +121 -0
  24. package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -0
  25. package/lib/typescript/src/components/grid-item/index.d.ts +292 -0
  26. package/lib/typescript/src/components/grid-item/index.d.ts.map +1 -0
  27. package/lib/typescript/src/components/{kpi-chart → kpi}/index.d.ts +65 -65
  28. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -0
  29. package/lib/typescript/src/components/line-chart/index.d.ts +25 -1
  30. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  31. package/lib/typescript/src/components/pie-chart/index.d.ts +245 -0
  32. package/lib/typescript/src/components/pie-chart/index.d.ts.map +1 -0
  33. package/lib/typescript/src/components/scatter-chart/index.d.ts +309 -0
  34. package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -0
  35. package/lib/typescript/src/components/tabs/index.d.ts +20 -2
  36. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  37. package/lib/typescript/src/index.d.ts +6 -2
  38. package/lib/typescript/src/index.d.ts.map +1 -1
  39. package/lib/typescript/src/utils/chart.d.ts +34 -0
  40. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/components/combobox/index.tsx +7 -1
  43. package/src/components/date-time-picker/index.tsx +375 -0
  44. package/src/components/grid-item/index.tsx +515 -0
  45. package/src/components/{kpi-chart → kpi}/index.tsx +85 -85
  46. package/src/components/line-chart/index.tsx +98 -8
  47. package/src/components/pie-chart/index.tsx +863 -0
  48. package/src/components/scatter-chart/index.tsx +1401 -0
  49. package/src/components/tabs/index.tsx +305 -35
  50. package/src/index.ts +65 -17
  51. package/src/utils/chart.ts +110 -0
  52. package/lib/module/components/kpi-chart/index.js.map +0 -1
  53. package/lib/typescript/src/components/kpi-chart/index.d.ts.map +0 -1
@@ -0,0 +1,515 @@
1
+ /**
2
+ * GridItem — a tile in a bento grid, and the grid that lays it out.
3
+ *
4
+ * ```tsx
5
+ * <GridItem.Group columns={2} gap={12}>
6
+ * <GridItem colSpan={2}>
7
+ * <GridItem.Media variant="icon"><ZapIcon /></GridItem.Media>
8
+ * <GridItem.Title>Deploys</GridItem.Title>
9
+ * <GridItem.Value>1,284</GridItem.Value>
10
+ * <GridItem.Description>this week</GridItem.Description>
11
+ * </GridItem>
12
+ * <GridItem rowSpan={2}>…</GridItem>
13
+ * <GridItem>…</GridItem>
14
+ * </GridItem.Group>
15
+ * ```
16
+ *
17
+ * ## Why the group places the tiles
18
+ *
19
+ * `Item` next door is a row, and a list of rows is a column of views — the
20
+ * layout falls out of the flexbox and nothing has to be measured. A bento is
21
+ * not that. Its whole idea is that tiles are different sizes and the grid still
22
+ * lines up: a wide tile and the two square ones beside it share a left edge, a
23
+ * tall one runs past the tile next to it and the next tile fills in underneath.
24
+ *
25
+ * A wrapping flex row cannot do the last of those. Wrapping puts everything
26
+ * that did not fit on a *new line*, so nothing ever tucks under a tall tile,
27
+ * and `rowSpan` in a wrapping row is a prop that quietly does nothing.
28
+ *
29
+ * So the group measures itself, walks its children into the first free cell
30
+ * that fits each one — row by row, the way a grid places anything it is not
31
+ * told where to put — and positions them absolutely. Every tile is a whole
32
+ * number of cells, which is what makes the edges line up, and the group's own
33
+ * height is the number of rows it ended up needing.
34
+ *
35
+ * The consequence worth knowing: **a tile's height is its cells, not its
36
+ * content**. That is the right way round for a bento — a grid of boxes that
37
+ * each grew to fit its own text is not a grid — but it does mean long text
38
+ * needs `numberOfLines`, a smaller `size`, or a taller `rowSpan`.
39
+ *
40
+ * ## Nesting
41
+ *
42
+ * A group inside a tile is a group like any other, and it measures the cell it
43
+ * was put in. That is how a bento gets a sub-rhythm — two small tiles stacked
44
+ * inside one cell of the outer grid — without the outer grid needing a notion
45
+ * of half a row.
46
+ */
47
+ import {
48
+ Children,
49
+ createContext,
50
+ forwardRef,
51
+ isValidElement,
52
+ useContext,
53
+ useMemo,
54
+ useState,
55
+ type ReactNode,
56
+ } from 'react';
57
+ import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
58
+ import { tv, type VariantProps } from 'tailwind-variants';
59
+ import {
60
+ AnimatedPressable,
61
+ type AnimatedPressableProps,
62
+ } from '../../primitives/animated-pressable';
63
+ import { Text, type TextProps, textChildren } from '../../primitives/text';
64
+ import { cn } from '../../utils/cn';
65
+
66
+ type GridItemSize = 'default' | 'sm';
67
+
68
+ const gridItemVariants = tv({
69
+ slots: {
70
+ // Full-bleed inside its cell: the group has already decided how big the
71
+ // tile is, and a tile that sized itself would break the grid it is in.
72
+ root: 'h-full w-full overflow-hidden rounded-2xl',
73
+ title: 'font-medium text-foreground',
74
+ value: 'font-semibold text-foreground',
75
+ description: 'text-muted-foreground',
76
+ },
77
+ variants: {
78
+ variant: {
79
+ /** A card in a tray — the tile that reads as a tile. */
80
+ default: { root: 'border border-border bg-card' },
81
+ /** Outline only, for a grid over a coloured or patterned page. */
82
+ outline: { root: 'border border-border' },
83
+ /** Filled and unbordered, for a quieter tile among louder ones. */
84
+ muted: { root: 'bg-muted' },
85
+ /** Nothing at all: the tile is whatever is put inside it. */
86
+ plain: { root: '' },
87
+ },
88
+ size: {
89
+ default: { root: 'gap-2 p-4', title: 'text-sm', value: 'text-2xl', description: 'text-xs' },
90
+ sm: { root: 'gap-1.5 p-3', title: 'text-xs', value: 'text-xl', description: 'text-xs' },
91
+ },
92
+ disabled: {
93
+ true: { root: 'opacity-[0.64]' },
94
+ },
95
+ },
96
+ defaultVariants: {
97
+ variant: 'default',
98
+ size: 'default',
99
+ },
100
+ });
101
+
102
+ const mediaVariants = tv({
103
+ base: 'shrink-0 items-center justify-center',
104
+ variants: {
105
+ variant: {
106
+ /** No box — for an Avatar or anything that styles itself. */
107
+ default: '',
108
+ /** Rounded square tile sized for an icon. */
109
+ icon: 'rounded-lg border border-border bg-muted',
110
+ /** Clipped frame for an image or thumbnail. */
111
+ image: 'overflow-hidden rounded-lg bg-muted',
112
+ },
113
+ size: {
114
+ default: '',
115
+ sm: '',
116
+ },
117
+ },
118
+ compoundVariants: [
119
+ { variant: 'icon', size: 'default', class: 'h-9 w-9' },
120
+ { variant: 'icon', size: 'sm', class: 'h-7 w-7' },
121
+ { variant: 'image', size: 'default', class: 'h-11 w-11' },
122
+ { variant: 'image', size: 'sm', class: 'h-9 w-9' },
123
+ ],
124
+ defaultVariants: {
125
+ variant: 'default',
126
+ size: 'default',
127
+ },
128
+ });
129
+
130
+ /** Density, set once on the group and read by every part inside a tile. */
131
+ const GridItemContext = createContext<{ size: GridItemSize }>({ size: 'default' });
132
+
133
+ export interface GridItemGroupProps extends ViewProps {
134
+ className?: string;
135
+ /** How many tracks wide the grid is. */
136
+ columns?: number;
137
+ /** Gutter between tiles, in points — both ways. */
138
+ gap?: number;
139
+ /**
140
+ * The shape of one cell, as width ÷ height. `1` is square; below one the
141
+ * cells are taller than they are wide. Ignored when `rowHeight` is given.
142
+ */
143
+ aspect?: number;
144
+ /** Cell height in points, when the grid should not be driven by its width. */
145
+ rowHeight?: number;
146
+ /** Density for every tile in the grid. Set here rather than on each one. */
147
+ size?: GridItemSize;
148
+ children?: ReactNode;
149
+ }
150
+
151
+ /** One tile's place in the grid, in cells. */
152
+ interface Placement {
153
+ row: number;
154
+ column: number;
155
+ colSpan: number;
156
+ rowSpan: number;
157
+ }
158
+
159
+ /**
160
+ * The first cell a tile of this size fits in, scanning row by row.
161
+ *
162
+ * The same rule a grid uses for anything it is not told where to put: take the
163
+ * earliest place it fits, never move anything already placed, and open a new
164
+ * row when nothing on the existing ones will do. Row-major, so the order the
165
+ * tiles are written in is the order they are read in — which is the order a
166
+ * screen reader will walk them in too, since that is the child order.
167
+ */
168
+ function findSpot(
169
+ occupied: boolean[][],
170
+ columns: number,
171
+ colSpan: number,
172
+ rowSpan: number
173
+ ): { row: number; column: number } {
174
+ for (let row = 0; ; row += 1) {
175
+ for (let column = 0; column + colSpan <= columns; column += 1) {
176
+ let free = true;
177
+ for (let y = row; y < row + rowSpan && free; y += 1) {
178
+ for (let x = column; x < column + colSpan; x += 1) {
179
+ if (occupied[y]?.[x]) {
180
+ free = false;
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ if (free) return { row, column };
186
+ }
187
+ }
188
+ }
189
+
190
+ /**
191
+ * The grid. Measures its own width, places its tiles, and stands as tall as the
192
+ * rows they ended up needing.
193
+ */
194
+ const GridItemGroup = forwardRef<View, GridItemGroupProps>(
195
+ (
196
+ {
197
+ className,
198
+ columns = 2,
199
+ gap = 12,
200
+ aspect = 1,
201
+ rowHeight,
202
+ size = 'default',
203
+ children,
204
+ ...props
205
+ },
206
+ ref
207
+ ) => {
208
+ const [width, setWidth] = useState(0);
209
+
210
+ const tracks = Math.max(1, Math.floor(columns));
211
+ const track = (width - gap * (tracks - 1)) / tracks;
212
+ const cell = rowHeight ?? track / (aspect || 1);
213
+
214
+ const { placements, rows } = useMemo(() => {
215
+ const occupied: boolean[][] = [];
216
+ const result: Placement[] = [];
217
+ let used = 0;
218
+
219
+ Children.forEach(children, (child) => {
220
+ if (!isValidElement(child)) return;
221
+ const spans = child.props as { colSpan?: number; rowSpan?: number };
222
+ const colSpan = Math.max(1, Math.min(Math.floor(spans.colSpan ?? 1), tracks));
223
+ const rowSpan = Math.max(1, Math.floor(spans.rowSpan ?? 1));
224
+
225
+ const { row, column } = findSpot(occupied, tracks, colSpan, rowSpan);
226
+ for (let y = row; y < row + rowSpan; y += 1) {
227
+ const line = (occupied[y] ??= []);
228
+ for (let x = column; x < column + colSpan; x += 1) line[x] = true;
229
+ }
230
+
231
+ result.push({ row, column, colSpan, rowSpan });
232
+ used = Math.max(used, row + rowSpan);
233
+ });
234
+
235
+ return { placements: result, rows: used };
236
+ }, [children, tracks]);
237
+
238
+ const onLayout = (event: LayoutChangeEvent) => {
239
+ const next = event.nativeEvent.layout.width;
240
+ if (Math.abs(next - width) > 0.5) setWidth(next);
241
+ props.onLayout?.(event);
242
+ };
243
+
244
+ const height = rows > 0 ? rows * cell + gap * (rows - 1) : 0;
245
+
246
+ let index = -1;
247
+
248
+ return (
249
+ <GridItemContext.Provider value={{ size }}>
250
+ <View
251
+ ref={ref}
252
+ onLayout={onLayout}
253
+ accessibilityRole="list"
254
+ style={width > 0 ? { height } : undefined}
255
+ className={cn('w-full', className)}
256
+ {...props}
257
+ >
258
+ {/* Nothing is drawn until the width is known: every tile is placed
259
+ from it, and one frame of tiles piled on the origin is worse than
260
+ one frame of an empty box. */}
261
+ {width > 0
262
+ ? Children.map(children, (child) => {
263
+ if (!isValidElement(child)) return null;
264
+ index += 1;
265
+ const place = placements[index];
266
+ if (!place) return null;
267
+ return (
268
+ <View
269
+ style={{
270
+ position: 'absolute',
271
+ left: place.column * (track + gap),
272
+ top: place.row * (cell + gap),
273
+ width: place.colSpan * track + (place.colSpan - 1) * gap,
274
+ height: place.rowSpan * cell + (place.rowSpan - 1) * gap,
275
+ }}
276
+ >
277
+ {child}
278
+ </View>
279
+ );
280
+ })
281
+ : null}
282
+ </View>
283
+ </GridItemContext.Provider>
284
+ );
285
+ }
286
+ );
287
+ GridItemGroup.displayName = 'GridItem.Group';
288
+
289
+ export interface GridItemProps
290
+ extends Omit<AnimatedPressableProps, 'children' | 'disabled'>,
291
+ Omit<VariantProps<typeof gridItemVariants>, 'disabled' | 'size'> {
292
+ className?: string;
293
+ disabled?: boolean;
294
+ /**
295
+ * How many tracks wide the tile is. Clamped to the group's column count, so
296
+ * a tile asking for three columns of a two-column grid is two wide rather
297
+ * than overflowing it.
298
+ *
299
+ * Read by `GridItem.Group`, which does the placing — a tile does not size
300
+ * itself, because a tile that sized itself would not be in a grid.
301
+ */
302
+ colSpan?: number;
303
+ /** How many rows tall it is. Also read by the group. */
304
+ rowSpan?: number;
305
+ children?: ReactNode;
306
+ }
307
+
308
+ /**
309
+ * One tile.
310
+ *
311
+ * Renders as a pressable when given `onPress`, and as a plain view otherwise,
312
+ * so a tile that is only showing a number does not announce itself as a button.
313
+ */
314
+ const GridItemRoot = forwardRef<View, GridItemProps>(
315
+ (
316
+ {
317
+ className,
318
+ variant,
319
+ disabled,
320
+ children,
321
+ onPress,
322
+ // Declared so they are part of the tile's API and typed at the call site,
323
+ // read by the group, and deliberately not applied here.
324
+ colSpan: _colSpan,
325
+ rowSpan: _rowSpan,
326
+ ...props
327
+ },
328
+ ref
329
+ ) => {
330
+ const { size } = useContext(GridItemContext);
331
+ const { root } = gridItemVariants({ variant, size, disabled: !!disabled });
332
+
333
+ if (!onPress) {
334
+ return (
335
+ <View
336
+ ref={ref}
337
+ accessibilityState={{ disabled: !!disabled }}
338
+ className={root({ className })}
339
+ {...(props as ViewProps)}
340
+ >
341
+ {textChildren(children)}
342
+ </View>
343
+ );
344
+ }
345
+
346
+ return (
347
+ <AnimatedPressable
348
+ ref={ref}
349
+ accessibilityRole="button"
350
+ accessibilityState={{ disabled: !!disabled }}
351
+ disabled={disabled}
352
+ onPress={onPress}
353
+ className={root({ className })}
354
+ {...props}
355
+ >
356
+ {textChildren(children)}
357
+ </AnimatedPressable>
358
+ );
359
+ }
360
+ );
361
+ GridItemRoot.displayName = 'GridItem';
362
+
363
+ export interface GridItemBackgroundProps extends ViewProps {
364
+ className?: string;
365
+ children?: ReactNode;
366
+ }
367
+
368
+ /**
369
+ * The layer behind the tile's content — a chart, a gradient, an image, a
370
+ * pattern.
371
+ *
372
+ * Absolutely filling the tile and taking no touches, so it can be written
373
+ * anywhere among the children without moving anything. It is what makes a bento
374
+ * grid read as a bento grid rather than as a wall of stat cards, and it is the
375
+ * one part that is meant to be cropped: the tile clips it.
376
+ */
377
+ const GridItemBackground = forwardRef<View, GridItemBackgroundProps>(
378
+ ({ className, children, ...props }, ref) => (
379
+ <View
380
+ ref={ref}
381
+ pointerEvents="none"
382
+ style={{ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 }}
383
+ className={cn('overflow-hidden', className)}
384
+ {...props}
385
+ >
386
+ {textChildren(children)}
387
+ </View>
388
+ )
389
+ );
390
+ GridItemBackground.displayName = 'GridItem.Background';
391
+
392
+ export interface GridItemMediaProps
393
+ extends ViewProps,
394
+ VariantProps<typeof mediaVariants> {
395
+ className?: string;
396
+ children?: ReactNode;
397
+ }
398
+
399
+ /** Leading slot: an icon tile, a thumbnail, or an avatar passed through. */
400
+ const GridItemMedia = forwardRef<View, GridItemMediaProps>(
401
+ ({ className, variant, size, children, ...props }, ref) => {
402
+ const grid = useContext(GridItemContext);
403
+
404
+ return (
405
+ <View
406
+ ref={ref}
407
+ className={mediaVariants({ variant, size: size ?? grid.size, className })}
408
+ {...props}
409
+ >
410
+ {textChildren(children)}
411
+ </View>
412
+ );
413
+ }
414
+ );
415
+ GridItemMedia.displayName = 'GridItem.Media';
416
+
417
+ export interface GridItemTitleProps extends TextProps {
418
+ className?: string;
419
+ }
420
+
421
+ /** What the tile is of. Quiet, because the number under it is the message. */
422
+ const GridItemTitle = forwardRef<React.ElementRef<typeof Text>, GridItemTitleProps>(
423
+ ({ className, ...props }, ref) => {
424
+ const { size } = useContext(GridItemContext);
425
+ const { title } = gridItemVariants({ size });
426
+
427
+ return <Text ref={ref} className={title({ className })} {...props} />;
428
+ }
429
+ );
430
+ GridItemTitle.displayName = 'GridItem.Title';
431
+
432
+ export interface GridItemValueProps extends TextProps {
433
+ className?: string;
434
+ }
435
+
436
+ /** The figure. The largest thing on the tile, and the reason it is there. */
437
+ const GridItemValue = forwardRef<React.ElementRef<typeof Text>, GridItemValueProps>(
438
+ ({ className, ...props }, ref) => {
439
+ const { size } = useContext(GridItemContext);
440
+ const { value } = gridItemVariants({ size });
441
+
442
+ return <Text ref={ref} className={value({ className })} {...props} />;
443
+ }
444
+ );
445
+ GridItemValue.displayName = 'GridItem.Value';
446
+
447
+ export interface GridItemDescriptionProps extends TextProps {
448
+ className?: string;
449
+ }
450
+
451
+ const GridItemDescription = forwardRef<
452
+ React.ElementRef<typeof Text>,
453
+ GridItemDescriptionProps
454
+ >(({ className, ...props }, ref) => {
455
+ const { size } = useContext(GridItemContext);
456
+ const { description } = gridItemVariants({ size });
457
+
458
+ return <Text ref={ref} className={description({ className })} {...props} />;
459
+ });
460
+ GridItemDescription.displayName = 'GridItem.Description';
461
+
462
+ export interface GridItemFooterProps extends ViewProps {
463
+ className?: string;
464
+ children?: ReactNode;
465
+ }
466
+
467
+ /**
468
+ * The strip along the bottom of the tile.
469
+ *
470
+ * `mt-auto` rather than `absolute`: a tile's height is fixed by its cells, so
471
+ * pushing the footer down with the space left over pins it to the bottom
472
+ * without taking it out of the layout the rest of the tile is sharing.
473
+ */
474
+ const GridItemFooter = forwardRef<View, GridItemFooterProps>(
475
+ ({ className, children, ...props }, ref) => (
476
+ <View
477
+ ref={ref}
478
+ className={cn('mt-auto w-full flex-row items-center gap-2', className)}
479
+ {...props}
480
+ >
481
+ {textChildren(children)}
482
+ </View>
483
+ )
484
+ );
485
+ GridItemFooter.displayName = 'GridItem.Footer';
486
+
487
+ export interface GridItemActionsProps extends ViewProps {
488
+ className?: string;
489
+ children?: ReactNode;
490
+ }
491
+
492
+ /** Trailing slot: buttons, a chip, a chevron. */
493
+ const GridItemActions = forwardRef<View, GridItemActionsProps>(
494
+ ({ className, children, ...props }, ref) => (
495
+ <View
496
+ ref={ref}
497
+ className={cn('shrink-0 flex-row items-center gap-1.5', className)}
498
+ {...props}
499
+ >
500
+ {textChildren(children)}
501
+ </View>
502
+ )
503
+ );
504
+ GridItemActions.displayName = 'GridItem.Actions';
505
+
506
+ export const GridItem = Object.assign(GridItemRoot, {
507
+ Group: GridItemGroup,
508
+ Background: GridItemBackground,
509
+ Media: GridItemMedia,
510
+ Title: GridItemTitle,
511
+ Value: GridItemValue,
512
+ Description: GridItemDescription,
513
+ Footer: GridItemFooter,
514
+ Actions: GridItemActions,
515
+ });