panelui-native 0.43.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 (58) hide show
  1. package/README.md +2 -1
  2. package/lib/module/components/card/index.js +8 -154
  3. package/lib/module/components/card/index.js.map +1 -1
  4. package/lib/module/components/combobox/index.js +7 -1
  5. package/lib/module/components/combobox/index.js.map +1 -1
  6. package/lib/module/components/date-time-picker/index.js +254 -0
  7. package/lib/module/components/date-time-picker/index.js.map +1 -0
  8. package/lib/module/components/grid-item/index.js +486 -0
  9. package/lib/module/components/grid-item/index.js.map +1 -0
  10. package/lib/module/components/{kpi-chart → kpi}/index.js +62 -62
  11. package/lib/module/components/kpi/index.js.map +1 -0
  12. package/lib/module/components/line-chart/index.js +92 -10
  13. package/lib/module/components/line-chart/index.js.map +1 -1
  14. package/lib/module/components/pie-chart/index.js +625 -0
  15. package/lib/module/components/pie-chart/index.js.map +1 -0
  16. package/lib/module/components/scatter-chart/index.js +1173 -0
  17. package/lib/module/components/scatter-chart/index.js.map +1 -0
  18. package/lib/module/components/tabs/index.js +275 -33
  19. package/lib/module/components/tabs/index.js.map +1 -1
  20. package/lib/module/index.js +5 -1
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/utils/chart.js +81 -0
  23. package/lib/module/utils/chart.js.map +1 -1
  24. package/lib/typescript/src/components/card/index.d.ts +0 -20
  25. package/lib/typescript/src/components/card/index.d.ts.map +1 -1
  26. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  27. package/lib/typescript/src/components/date-time-picker/index.d.ts +121 -0
  28. package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -0
  29. package/lib/typescript/src/components/grid-item/index.d.ts +292 -0
  30. package/lib/typescript/src/components/grid-item/index.d.ts.map +1 -0
  31. package/lib/typescript/src/components/{kpi-chart → kpi}/index.d.ts +65 -65
  32. package/lib/typescript/src/components/kpi/index.d.ts.map +1 -0
  33. package/lib/typescript/src/components/line-chart/index.d.ts +25 -1
  34. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  35. package/lib/typescript/src/components/pie-chart/index.d.ts +245 -0
  36. package/lib/typescript/src/components/pie-chart/index.d.ts.map +1 -0
  37. package/lib/typescript/src/components/scatter-chart/index.d.ts +309 -0
  38. package/lib/typescript/src/components/scatter-chart/index.d.ts.map +1 -0
  39. package/lib/typescript/src/components/tabs/index.d.ts +20 -2
  40. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  41. package/lib/typescript/src/index.d.ts +7 -3
  42. package/lib/typescript/src/index.d.ts.map +1 -1
  43. package/lib/typescript/src/utils/chart.d.ts +34 -0
  44. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/components/card/index.tsx +7 -209
  47. package/src/components/combobox/index.tsx +7 -1
  48. package/src/components/date-time-picker/index.tsx +375 -0
  49. package/src/components/grid-item/index.tsx +515 -0
  50. package/src/components/{kpi-chart → kpi}/index.tsx +85 -85
  51. package/src/components/line-chart/index.tsx +98 -8
  52. package/src/components/pie-chart/index.tsx +863 -0
  53. package/src/components/scatter-chart/index.tsx +1401 -0
  54. package/src/components/tabs/index.tsx +305 -35
  55. package/src/index.ts +66 -18
  56. package/src/utils/chart.ts +110 -0
  57. package/lib/module/components/kpi-chart/index.js.map +0 -1
  58. package/lib/typescript/src/components/kpi-chart/index.d.ts.map +0 -1
@@ -0,0 +1,375 @@
1
+ /**
2
+ * DateTimePicker — a day and a time of day, picked in one panel.
3
+ *
4
+ * ```tsx
5
+ * const [when, setWhen] = useState<Date>();
6
+ *
7
+ * <DateTimePicker value={when} onValueChange={setWhen} />
8
+ * ```
9
+ *
10
+ * ## Why it is one component and not two side by side
11
+ *
12
+ * A date field beside a time field is two decisions the reader has to make
13
+ * separately and then hold together — and the two halves can disagree, which is
14
+ * how a booking ends up on the right day at a time that has already passed. Here
15
+ * the calendar and the scale are the same panel over one `Date`, so what is on
16
+ * screen is the answer rather than two thirds of it.
17
+ *
18
+ * The layout follows from that: the calendar is the coarse choice and takes the
19
+ * room, the time sits under it behind a hairline, and one Done finishes both.
20
+ * The panel is a fixed width, which is what lets the two halves line up — a
21
+ * month grid and a time scale that measured themselves independently would be
22
+ * two boxes of slightly different widths stacked on each other.
23
+ *
24
+ * ## It does not close on the date
25
+ *
26
+ * `DatePicker` closes as soon as a single day is tapped, because at that point
27
+ * there is nothing left to say. Here there is: the day is half the value, and
28
+ * closing on it would hide the other half at the moment it became relevant. So
29
+ * the panel stays until Done, in every presentation including the popover — the
30
+ * one place `DatePicker` has no Done button at all.
31
+ *
32
+ * ## The time face
33
+ *
34
+ * `ruler` by default rather than the wheel. Under a month grid the panel is
35
+ * already tall, and the wheel is five rows of it; the ruler is one readout over
36
+ * a scale, reads at arm's length, and is the one face that fits under a calendar
37
+ * without the whole thing needing to scroll. The other two are a prop away.
38
+ *
39
+ * ## Picking the time before the day
40
+ *
41
+ * Allowed, and it means today. There has to be *some* day for a time to be a
42
+ * `Date` at all, and the day the reader is looking at is the only defensible
43
+ * guess — the alternative is refusing to emit a value until both halves have
44
+ * been touched, which is a form that silently does nothing when you use it in
45
+ * the order it did not expect.
46
+ */
47
+ import {
48
+ useCallback,
49
+ useMemo,
50
+ useState,
51
+ type ReactElement,
52
+ type ReactNode,
53
+ } from 'react';
54
+ import { View } from 'react-native';
55
+ import { CalendarIcon } from '../../icons';
56
+ import { Text } from '../../primitives/text';
57
+ import { cn } from '../../utils/cn';
58
+ import { calendarShortDate, resolveCalendar, type CalendarSystem } from '../../utils/date';
59
+ import {
60
+ clampTime,
61
+ formatTime,
62
+ roundToStep,
63
+ timeFromDate,
64
+ timeToDate,
65
+ type HourCycle,
66
+ type TimeValue,
67
+ } from '../../utils/time';
68
+ import { Button } from '../button';
69
+ import { Calendar, type CalendarCaptionLayout, type CalendarDisabled } from '../calendar';
70
+ import { Dialog } from '../dialog';
71
+ import { Popover } from '../popover';
72
+ import { TimePicker, type TimePickerLayout } from '../time-picker';
73
+
74
+ /** What a closed picker shows when nothing has been chosen yet. */
75
+ const DEFAULT_PLACEHOLDER = 'Pick a date and time';
76
+
77
+ /**
78
+ * The width both halves are laid out at.
79
+ *
80
+ * Fixed rather than measured, and shared, because that is the whole reason they
81
+ * read as one panel: a month grid and a time scale each sized to their own
82
+ * content are two boxes of slightly different widths stacked on each other, and
83
+ * the seam is visible at any size.
84
+ */
85
+ const PANEL_WIDTH = 308;
86
+
87
+ /** How a `Date` is written on the closed trigger. */
88
+ function describe(
89
+ value: Date,
90
+ system: 'gregory' | 'islamic',
91
+ hourCycle: HourCycle,
92
+ locale: string | undefined
93
+ ): string {
94
+ const day = calendarShortDate(value, system, locale);
95
+ const time = formatTime(timeFromDate(value), { hourCycle, locale });
96
+ return `${day} · ${time}`;
97
+ }
98
+
99
+ /** Where the panel is shown. `inline` renders it bare, for a Frame or a form. */
100
+ export type DateTimePickerPresentation = 'popover' | 'bottom-sheet' | 'dialog' | 'inline';
101
+
102
+ export interface DateTimePickerProps {
103
+ /** Controlled value. One `Date` carrying both halves. */
104
+ value?: Date;
105
+ /** Starting value when uncontrolled. */
106
+ defaultValue?: Date;
107
+ /**
108
+ * Fires on every change to either half, not on Done. Done closes the panel;
109
+ * it does not decide anything the caller has not already been told.
110
+ */
111
+ onValueChange?: (value: Date) => void;
112
+ /** Which face the time is picked on. `ruler` is the one that fits here. */
113
+ layout?: TimePickerLayout;
114
+ /** Anchored panel, a sheet, a dialog, or the panel with nothing around it. */
115
+ presentation?: DateTimePickerPresentation;
116
+ /** Controlled open state of the panel. */
117
+ open?: boolean;
118
+ onOpenChange?: (open: boolean) => void;
119
+ /** `12` shows a meridiem, `24` does not. The value is 24-hour either way. */
120
+ hourCycle?: HourCycle;
121
+ /** Minutes between one selectable time and the next. */
122
+ minuteStep?: number;
123
+ /** Earliest selectable time of day, inclusive. */
124
+ minTime?: TimeValue;
125
+ /** Latest selectable time of day, inclusive. */
126
+ maxTime?: TimeValue;
127
+ /** What the trigger reads when nothing has been chosen. */
128
+ placeholder?: string;
129
+ /** Override how the chosen value is written on the trigger. */
130
+ format?: (value: Date) => string;
131
+ /** Stop the trigger opening it, and the panel from being used. */
132
+ disabled?: boolean;
133
+ /** Days that cannot be picked: a list, a span, or a rule. */
134
+ disabledDates?: CalendarDisabled;
135
+ /** Earliest selectable day. */
136
+ minDate?: Date;
137
+ /** Latest selectable day. */
138
+ maxDate?: Date;
139
+ /** `dropdown` swaps the month caption for month and year pickers. */
140
+ captionLayout?: CalendarCaptionLayout;
141
+ /** `0` is Sunday. */
142
+ weekStartsOn?: number;
143
+ /** BCP 47 tag for the month names, the time and the trigger's own text. */
144
+ locale?: string;
145
+ /** Which calendar the months and day numbers are counted in. */
146
+ calendar?: CalendarSystem;
147
+ /** Label on the button that closes the panel. */
148
+ doneLabel?: string;
149
+ className?: string;
150
+ /**
151
+ * A trigger of your own. Given one, it is cloned with an `onPress` that opens
152
+ * the panel — so a field row or an icon button can stand in for the default
153
+ * button without this component knowing what either looks like.
154
+ *
155
+ * Ignored by `presentation="inline"`, which has no trigger.
156
+ */
157
+ children?: ReactElement<{ onPress?: () => void }> | ReactNode;
158
+ }
159
+
160
+ function DateTimePickerRoot({
161
+ value: valueProp,
162
+ defaultValue,
163
+ onValueChange,
164
+ layout = 'ruler',
165
+ presentation = 'popover',
166
+ open: openProp,
167
+ onOpenChange,
168
+ hourCycle = 12,
169
+ minuteStep,
170
+ minTime,
171
+ maxTime,
172
+ placeholder = DEFAULT_PLACEHOLDER,
173
+ format,
174
+ disabled = false,
175
+ disabledDates,
176
+ minDate,
177
+ maxDate,
178
+ captionLayout = 'label',
179
+ weekStartsOn = 0,
180
+ locale,
181
+ calendar = 'gregory',
182
+ doneLabel = 'Done',
183
+ className,
184
+ children,
185
+ }: DateTimePickerProps) {
186
+ // Resolved here as well as in the grid, because the trigger's text has to be
187
+ // written in the same calendar the cells were tapped in.
188
+ const system = useMemo(() => resolveCalendar(calendar, locale), [calendar, locale]);
189
+
190
+ const [internalValue, setInternalValue] = useState<Date | undefined>(defaultValue);
191
+ const [internalOpen, setInternalOpen] = useState(false);
192
+
193
+ const isValueControlled = valueProp !== undefined;
194
+ const isOpenControlled = openProp !== undefined;
195
+ const value = isValueControlled ? valueProp : internalValue;
196
+ const open = isOpenControlled ? openProp : internalOpen;
197
+
198
+ const setOpen = useCallback(
199
+ (next: boolean) => {
200
+ if (!isOpenControlled) setInternalOpen(next);
201
+ onOpenChange?.(next);
202
+ },
203
+ [isOpenControlled, onOpenChange]
204
+ );
205
+
206
+ const commit = useCallback(
207
+ (next: Date) => {
208
+ if (!isValueControlled) setInternalValue(next);
209
+ onValueChange?.(next);
210
+ },
211
+ [isValueControlled, onValueChange]
212
+ );
213
+
214
+ /*
215
+ * The time half, kept separately as well as inside the value.
216
+ *
217
+ * The value cannot hold it on its own: before a day is picked there is no
218
+ * `Date` to put a time in, and a time chosen first would be dropped on the
219
+ * way to the calendar. Held here, the two halves can be filled in either
220
+ * order and neither one loses the other.
221
+ */
222
+ const [draftTime, setDraftTime] = useState<TimeValue | undefined>(
223
+ defaultValue ? timeFromDate(defaultValue) : undefined
224
+ );
225
+ const time = value ? timeFromDate(value) : draftTime;
226
+
227
+ /** The time a day picked on its own is given: the top of the current hour. */
228
+ const fallbackTime = useMemo(
229
+ () =>
230
+ clampTime(
231
+ roundToStep({ hour: new Date().getHours(), minute: 0 }, minuteStep ?? 15),
232
+ minTime,
233
+ maxTime
234
+ ),
235
+ [minuteStep, minTime, maxTime]
236
+ );
237
+
238
+ const handleDay = useCallback(
239
+ (day: Date | undefined) => {
240
+ if (!day) return;
241
+ commit(timeToDate(time ?? fallbackTime, day));
242
+ },
243
+ [commit, time, fallbackTime]
244
+ );
245
+
246
+ const handleTime = useCallback(
247
+ (next: TimeValue) => {
248
+ setDraftTime(next);
249
+ // No day yet means today: a time is not a `Date` without one, and the day
250
+ // the reader is looking at is the only defensible guess.
251
+ commit(timeToDate(next, value ?? new Date()));
252
+ },
253
+ [commit, value]
254
+ );
255
+
256
+ const label = useMemo(() => {
257
+ if (!value) return null;
258
+ if (format) return format(value);
259
+ return describe(value, system, hourCycle, locale);
260
+ }, [format, value, system, hourCycle, locale]);
261
+
262
+ const panel = (
263
+ <View
264
+ style={{ width: PANEL_WIDTH, maxWidth: '100%' }}
265
+ className={cn('self-center', presentation === 'inline' ? className : undefined)}
266
+ >
267
+ {/*
268
+ Unbordered: the panel around it already draws one, and a card inside a
269
+ card is a seam. The calendar frames itself only when it is standing on
270
+ a page.
271
+ */}
272
+ <Calendar
273
+ bordered={false}
274
+ mode="single"
275
+ selected={value}
276
+ onSelect={handleDay}
277
+ disabled={disabledDates}
278
+ minDate={minDate}
279
+ maxDate={maxDate}
280
+ startMonth={minDate}
281
+ endMonth={maxDate}
282
+ captionLayout={captionLayout}
283
+ weekStartsOn={weekStartsOn}
284
+ locale={locale}
285
+ calendar={calendar}
286
+ />
287
+ {/* A hairline, not a gap. The two halves are one answer, and space alone
288
+ between them reads as two controls that happen to be stacked. */}
289
+ <View className="mt-1 border-t border-border pt-3">
290
+ <TimePicker
291
+ presentation="inline"
292
+ layout={layout}
293
+ value={time}
294
+ onValueChange={handleTime}
295
+ hourCycle={hourCycle}
296
+ minuteStep={minuteStep}
297
+ minTime={minTime}
298
+ maxTime={maxTime}
299
+ locale={locale}
300
+ disabled={disabled}
301
+ />
302
+ </View>
303
+ </View>
304
+ );
305
+
306
+ if (presentation === 'inline') return panel;
307
+
308
+ const trigger = (
309
+ children ?? (
310
+ <Button
311
+ variant="outline"
312
+ disabled={disabled}
313
+ className={cn('justify-start gap-2', className)}
314
+ >
315
+ <CalendarIcon size={16} />
316
+ <Text className={label ? undefined : 'text-muted-foreground'}>
317
+ {label ?? placeholder}
318
+ </Text>
319
+ </Button>
320
+ )
321
+ ) as ReactElement<{ onPress?: () => void }>;
322
+
323
+ if (presentation === 'dialog') {
324
+ return (
325
+ <Dialog open={open} onOpenChange={setOpen}>
326
+ <Dialog.Trigger>{trigger}</Dialog.Trigger>
327
+ {/* Blurred rather than dimmed: a dialog is what you reach for when the
328
+ appointment *is* the decision on the screen. */}
329
+ <Dialog.Content blur className="items-center gap-0 p-4">
330
+ {panel}
331
+ <Dialog.Close>
332
+ <Button className="mt-3 w-full">{doneLabel}</Button>
333
+ </Dialog.Close>
334
+ </Dialog.Content>
335
+ </Dialog>
336
+ );
337
+ }
338
+
339
+ const isSheet = presentation === 'bottom-sheet';
340
+
341
+ return (
342
+ <Popover
343
+ open={open}
344
+ onOpenChange={setOpen}
345
+ presentation={isSheet ? 'bottom-sheet' : 'popover'}
346
+ >
347
+ <Popover.Trigger>{trigger}</Popover.Trigger>
348
+ {/* The padding is on the inner view, not the panel: in sheet mode a
349
+ className on the panel is merged into the sheet's own padding and
350
+ would replace it. */}
351
+ <Popover.Content width={isSheet ? 'full' : 'content-fit'}>
352
+ <View className={isSheet ? 'w-full pb-2' : 'p-3'}>
353
+ {panel}
354
+ {/*
355
+ A Done button in the popover too, which `DatePicker` does not have.
356
+ There it is unnecessary — a single date finishes itself, and the
357
+ panel closes on the tap. Here the date is half the value, so
358
+ something has to say when both halves are settled, and tapping
359
+ outside is not that: it is how a popover is *abandoned*.
360
+ */}
361
+ <Popover.Close>
362
+ <Button className={cn('mt-3 w-full self-center', !isSheet && 'max-w-[308px]')}>
363
+ {doneLabel}
364
+ </Button>
365
+ </Popover.Close>
366
+ </View>
367
+ </Popover.Content>
368
+ </Popover>
369
+ );
370
+ }
371
+ DateTimePickerRoot.displayName = 'DateTimePicker';
372
+
373
+ export const DateTimePicker = Object.assign(DateTimePickerRoot, {
374
+ Trigger: Popover.Trigger,
375
+ });