panelui-native 0.28.1 → 0.32.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.
- package/README.md +22 -3
- package/lib/module/components/calendar/index.js +450 -132
- package/lib/module/components/calendar/index.js.map +1 -1
- package/lib/module/components/code-block/highlight.js +363 -0
- package/lib/module/components/code-block/highlight.js.map +1 -0
- package/lib/module/components/code-block/index.js +313 -0
- package/lib/module/components/code-block/index.js.map +1 -0
- package/lib/module/components/date-picker/index.js +8 -1
- package/lib/module/components/date-picker/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +9 -8
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +50 -4
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/menu/index.js +714 -0
- package/lib/module/components/menu/index.js.map +1 -0
- package/lib/module/components/plan/index.js +582 -0
- package/lib/module/components/plan/index.js.map +1 -0
- package/lib/module/components/popover/index.js +35 -4
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/post/index.js +992 -0
- package/lib/module/components/post/index.js.map +1 -0
- package/lib/module/components/radio-group/index.js +13 -8
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/reasoning/index.js +244 -0
- package/lib/module/components/reasoning/index.js.map +1 -0
- package/lib/module/components/response/index.js +296 -0
- package/lib/module/components/response/index.js.map +1 -0
- package/lib/module/components/response/markdown.js +463 -0
- package/lib/module/components/response/markdown.js.map +1 -0
- package/lib/module/components/select/index.js +97 -15
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/sources/index.js +221 -0
- package/lib/module/components/sources/index.js.map +1 -0
- package/lib/module/components/table/index.js +632 -0
- package/lib/module/components/table/index.js.map +1 -0
- package/lib/module/components/task/index.js +300 -0
- package/lib/module/components/task/index.js.map +1 -0
- package/lib/module/components/textarea/index.js +295 -0
- package/lib/module/components/textarea/index.js.map +1 -0
- package/lib/module/components/tooltip/index.js +179 -23
- package/lib/module/components/tooltip/index.js.map +1 -1
- package/lib/module/icons/index.js +339 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +12 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/collapse.js +112 -0
- package/lib/module/primitives/collapse.js.map +1 -0
- package/lib/module/utils/date.js +129 -22
- package/lib/module/utils/date.js.map +1 -1
- package/lib/typescript/src/components/calendar/index.d.ts +79 -5
- package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/code-block/highlight.d.ts +39 -0
- package/lib/typescript/src/components/code-block/highlight.d.ts.map +1 -0
- package/lib/typescript/src/components/code-block/index.d.ts +113 -0
- package/lib/typescript/src/components/code-block/index.d.ts.map +1 -0
- package/lib/typescript/src/components/date-picker/index.d.ts +7 -1
- package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +8 -1
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts +261 -0
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -0
- package/lib/typescript/src/components/plan/index.d.ts +238 -0
- package/lib/typescript/src/components/plan/index.d.ts.map +1 -0
- package/lib/typescript/src/components/popover/index.d.ts +35 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/post/index.d.ts +358 -0
- package/lib/typescript/src/components/post/index.d.ts.map +1 -0
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/reasoning/index.d.ts +97 -0
- package/lib/typescript/src/components/reasoning/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/index.d.ts +91 -0
- package/lib/typescript/src/components/response/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/markdown.d.ts +88 -0
- package/lib/typescript/src/components/response/markdown.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +27 -2
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sources/index.d.ts +85 -0
- package/lib/typescript/src/components/sources/index.d.ts.map +1 -0
- package/lib/typescript/src/components/table/index.d.ts +274 -0
- package/lib/typescript/src/components/table/index.d.ts.map +1 -0
- package/lib/typescript/src/components/task/index.d.ts +103 -0
- package/lib/typescript/src/components/task/index.d.ts.map +1 -0
- package/lib/typescript/src/components/textarea/index.d.ts +175 -0
- package/lib/typescript/src/components/textarea/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tooltip/index.d.ts +63 -8
- package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -1
- package/lib/typescript/src/icons/index.d.ts +54 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +14 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/collapse.d.ts +42 -0
- package/lib/typescript/src/primitives/collapse.d.ts.map +1 -0
- package/lib/typescript/src/utils/date.d.ts +49 -5
- package/lib/typescript/src/utils/date.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/calendar/index.tsx +519 -139
- package/src/components/code-block/highlight.ts +278 -0
- package/src/components/code-block/index.tsx +341 -0
- package/src/components/date-picker/index.tsx +19 -1
- package/src/components/heatmap-chart/index.tsx +9 -9
- package/src/components/map/index.tsx +53 -4
- package/src/components/menu/index.tsx +812 -0
- package/src/components/plan/index.tsx +648 -0
- package/src/components/popover/index.tsx +69 -2
- package/src/components/post/index.tsx +1116 -0
- package/src/components/radio-group/index.tsx +11 -2
- package/src/components/reasoning/index.tsx +297 -0
- package/src/components/response/index.tsx +390 -0
- package/src/components/response/markdown.ts +489 -0
- package/src/components/select/index.tsx +132 -12
- package/src/components/sources/index.tsx +278 -0
- package/src/components/table/index.tsx +747 -0
- package/src/components/task/index.tsx +329 -0
- package/src/components/textarea/index.tsx +381 -0
- package/src/components/tooltip/index.tsx +203 -20
- package/src/icons/index.tsx +264 -1
- package/src/index.ts +131 -0
- package/src/primitives/collapse.tsx +128 -0
- package/src/utils/date.ts +146 -22
- package/theme.css +112 -0
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { Portal, PortalHost, PortalProvider } from './primitives/portal';
|
|
|
21
21
|
// graceful fall back from blur to dim — as the built-in ones.
|
|
22
22
|
export { Scrim, hasBlur, type ScrimProps } from './primitives/scrim';
|
|
23
23
|
export { Text, type TextProps } from './primitives/text';
|
|
24
|
+
export { Collapse, type CollapseProps } from './primitives/collapse';
|
|
24
25
|
export {
|
|
25
26
|
KeyboardAvoider,
|
|
26
27
|
type KeyboardAvoiderProps,
|
|
@@ -253,6 +254,20 @@ export {
|
|
|
253
254
|
type TabsTriggerProps,
|
|
254
255
|
type TabsContentProps,
|
|
255
256
|
} from './components/tabs';
|
|
257
|
+
export {
|
|
258
|
+
Table,
|
|
259
|
+
type TableProps,
|
|
260
|
+
type TableFrameProps,
|
|
261
|
+
type TableHeaderProps,
|
|
262
|
+
type TableBodyProps,
|
|
263
|
+
type TableFooterProps,
|
|
264
|
+
type TableRowProps,
|
|
265
|
+
type TableHeadProps,
|
|
266
|
+
type TableCellProps,
|
|
267
|
+
type TableCaptionProps,
|
|
268
|
+
type TableEmptyProps,
|
|
269
|
+
type TableSortDirection,
|
|
270
|
+
} from './components/table';
|
|
256
271
|
export {
|
|
257
272
|
Carousel,
|
|
258
273
|
useCarouselState,
|
|
@@ -271,6 +286,8 @@ export {
|
|
|
271
286
|
export {
|
|
272
287
|
Calendar,
|
|
273
288
|
type CalendarProps,
|
|
289
|
+
type CalendarHeaderProps,
|
|
290
|
+
type CalendarNavProps,
|
|
274
291
|
type CalendarMode,
|
|
275
292
|
type CalendarCaptionLayout,
|
|
276
293
|
type CalendarDisabled,
|
|
@@ -332,6 +349,7 @@ export {
|
|
|
332
349
|
type NumberInputProps,
|
|
333
350
|
} from './components/number-input';
|
|
334
351
|
export { OtpInput, type OtpInputProps } from './components/otp-input';
|
|
352
|
+
export { Textarea, type TextareaProps } from './components/textarea';
|
|
335
353
|
export {
|
|
336
354
|
Item,
|
|
337
355
|
type ItemProps,
|
|
@@ -375,6 +393,24 @@ export {
|
|
|
375
393
|
type MarkerIconProps,
|
|
376
394
|
type MarkerContentProps,
|
|
377
395
|
} from './components/marker';
|
|
396
|
+
export {
|
|
397
|
+
Menu,
|
|
398
|
+
type MenuProps,
|
|
399
|
+
type MenuTriggerProps,
|
|
400
|
+
type MenuContentProps,
|
|
401
|
+
type MenuBackgroundProps,
|
|
402
|
+
type MenuLabelProps,
|
|
403
|
+
type MenuItemProps,
|
|
404
|
+
type MenuCheckboxItemProps,
|
|
405
|
+
type MenuRadioGroupProps,
|
|
406
|
+
type MenuRadioItemProps,
|
|
407
|
+
type MenuSeparatorProps,
|
|
408
|
+
type MenuSubProps,
|
|
409
|
+
type MenuSubTriggerProps,
|
|
410
|
+
type MenuSubContentProps,
|
|
411
|
+
type MenuItemVariant,
|
|
412
|
+
type MenuRadioIndicator,
|
|
413
|
+
} from './components/menu';
|
|
378
414
|
export {
|
|
379
415
|
MessageScroller,
|
|
380
416
|
useMessageScroller,
|
|
@@ -409,6 +445,21 @@ export {
|
|
|
409
445
|
type PopoverAlign,
|
|
410
446
|
type PopoverPresentation,
|
|
411
447
|
} from './components/popover';
|
|
448
|
+
export {
|
|
449
|
+
Post,
|
|
450
|
+
type PostProps,
|
|
451
|
+
type PostHeaderProps,
|
|
452
|
+
type PostAuthorProps,
|
|
453
|
+
type PostActionProps,
|
|
454
|
+
type PostCommunityProps,
|
|
455
|
+
type PostTitleProps,
|
|
456
|
+
type PostBodyProps,
|
|
457
|
+
type PostMediaProps,
|
|
458
|
+
type PostFooterProps,
|
|
459
|
+
type PostStatProps,
|
|
460
|
+
type PostVotesProps,
|
|
461
|
+
type PostVote,
|
|
462
|
+
} from './components/post';
|
|
412
463
|
export { Progress, type ProgressProps } from './components/progress';
|
|
413
464
|
export {
|
|
414
465
|
RadioGroup,
|
|
@@ -448,6 +499,65 @@ export {
|
|
|
448
499
|
type SignatureButtonProps,
|
|
449
500
|
} from './components/signature';
|
|
450
501
|
export { Shimmer, type ShimmerProps } from './components/shimmer';
|
|
502
|
+
export {
|
|
503
|
+
Reasoning,
|
|
504
|
+
type ReasoningProps,
|
|
505
|
+
type ReasoningTriggerProps,
|
|
506
|
+
type ReasoningContentProps,
|
|
507
|
+
} from './components/reasoning';
|
|
508
|
+
export {
|
|
509
|
+
Response,
|
|
510
|
+
type ResponseProps,
|
|
511
|
+
type ResponseComponents,
|
|
512
|
+
type ResponseBlock,
|
|
513
|
+
type ResponseInline,
|
|
514
|
+
} from './components/response';
|
|
515
|
+
export {
|
|
516
|
+
Sources,
|
|
517
|
+
type SourcesProps,
|
|
518
|
+
type SourcesTriggerProps,
|
|
519
|
+
type SourcesContentProps,
|
|
520
|
+
type SourcesSourceProps,
|
|
521
|
+
} from './components/sources';
|
|
522
|
+
export {
|
|
523
|
+
Task,
|
|
524
|
+
type TaskProps,
|
|
525
|
+
type TaskTriggerProps,
|
|
526
|
+
type TaskContentProps,
|
|
527
|
+
type TaskItemProps,
|
|
528
|
+
type TaskFileProps,
|
|
529
|
+
type TaskStatus,
|
|
530
|
+
} from './components/task';
|
|
531
|
+
export {
|
|
532
|
+
CodeBlock,
|
|
533
|
+
resolveLanguage,
|
|
534
|
+
type CodeBlockProps,
|
|
535
|
+
type CodeBlockHeaderProps,
|
|
536
|
+
type CodeBlockFilenameProps,
|
|
537
|
+
type CodeBlockLanguageProps,
|
|
538
|
+
type CodeBlockActionsProps,
|
|
539
|
+
type CodeBlockCopyButtonProps,
|
|
540
|
+
type CodeLanguage,
|
|
541
|
+
type Token,
|
|
542
|
+
type TokenKind,
|
|
543
|
+
} from './components/code-block';
|
|
544
|
+
export {
|
|
545
|
+
Plan,
|
|
546
|
+
type PlanProps,
|
|
547
|
+
type PlanHeaderProps,
|
|
548
|
+
type PlanIconProps,
|
|
549
|
+
type PlanTitleProps,
|
|
550
|
+
type PlanDescriptionProps,
|
|
551
|
+
type PlanActionProps,
|
|
552
|
+
type PlanProgressProps,
|
|
553
|
+
type PlanTriggerProps,
|
|
554
|
+
type PlanContentProps,
|
|
555
|
+
type PlanStepsProps,
|
|
556
|
+
type PlanStepProps,
|
|
557
|
+
type PlanStepStatus,
|
|
558
|
+
type PlanStepCounts,
|
|
559
|
+
type PlanFooterProps,
|
|
560
|
+
} from './components/plan';
|
|
451
561
|
export {
|
|
452
562
|
ScrollText,
|
|
453
563
|
type ScrollTextProps,
|
|
@@ -508,9 +618,12 @@ export {
|
|
|
508
618
|
type TooltipTriggerProps,
|
|
509
619
|
type TooltipContentProps,
|
|
510
620
|
type TooltipArrowProps,
|
|
621
|
+
type TooltipTitleProps,
|
|
622
|
+
type TooltipDescriptionProps,
|
|
511
623
|
type TooltipPlacement,
|
|
512
624
|
type TooltipAlign,
|
|
513
625
|
type TooltipOpenOn,
|
|
626
|
+
type TooltipVariant,
|
|
514
627
|
} from './components/tooltip';
|
|
515
628
|
export {
|
|
516
629
|
Typography,
|
|
@@ -525,7 +638,10 @@ export {
|
|
|
525
638
|
export {
|
|
526
639
|
AlertTriangleIcon,
|
|
527
640
|
AppleIcon,
|
|
641
|
+
ArrowDownIcon,
|
|
642
|
+
ArrowUpIcon,
|
|
528
643
|
ArrowUpRightIcon,
|
|
644
|
+
BadgeCheckIcon,
|
|
529
645
|
BellIcon,
|
|
530
646
|
BookmarkIcon,
|
|
531
647
|
CalendarIcon,
|
|
@@ -536,16 +652,25 @@ export {
|
|
|
536
652
|
ChevronLeftIcon,
|
|
537
653
|
ChevronRightIcon,
|
|
538
654
|
ChevronUpIcon,
|
|
655
|
+
CircleIcon,
|
|
656
|
+
CompassIcon,
|
|
657
|
+
CopyIcon,
|
|
658
|
+
CrosshairIcon,
|
|
539
659
|
DownloadIcon,
|
|
540
660
|
EllipsisIcon,
|
|
661
|
+
EyeIcon,
|
|
541
662
|
FacebookIcon,
|
|
542
663
|
FileIcon,
|
|
543
664
|
GoogleIcon,
|
|
665
|
+
HeartIcon,
|
|
544
666
|
IconColorProvider,
|
|
545
667
|
InfoIcon,
|
|
546
668
|
ImageIcon,
|
|
669
|
+
LinkIcon,
|
|
670
|
+
ListChecksIcon,
|
|
547
671
|
LockIcon,
|
|
548
672
|
MaximizeIcon,
|
|
673
|
+
MessageCircleIcon,
|
|
549
674
|
MicIcon,
|
|
550
675
|
MinusIcon,
|
|
551
676
|
MoonIcon,
|
|
@@ -557,18 +682,24 @@ export {
|
|
|
557
682
|
PlusIcon,
|
|
558
683
|
PlusSquareIcon,
|
|
559
684
|
ReceiptIcon,
|
|
685
|
+
RepeatIcon,
|
|
560
686
|
RotateCcwIcon,
|
|
687
|
+
RotateCwIcon,
|
|
561
688
|
SearchIcon,
|
|
562
689
|
SendIcon,
|
|
563
690
|
ShareNodesIcon,
|
|
564
691
|
ShieldAlertIcon,
|
|
565
692
|
ShieldCheckIcon,
|
|
693
|
+
SparklesIcon,
|
|
694
|
+
StarIcon,
|
|
566
695
|
SunIcon,
|
|
567
696
|
TrashIcon,
|
|
568
697
|
UnlockIcon,
|
|
569
698
|
XIcon,
|
|
570
699
|
useIconColor,
|
|
571
700
|
type IconProps,
|
|
701
|
+
type ToggleIconProps,
|
|
702
|
+
type BadgeCheckIconProps,
|
|
572
703
|
} from './icons';
|
|
573
704
|
|
|
574
705
|
// Hooks
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapse — a body that opens and closes by animating its own height.
|
|
3
|
+
*
|
|
4
|
+
* Height cannot be animated from `auto`, so the content is measured first and
|
|
5
|
+
* the measurement is what gets animated to. While that is happening the content
|
|
6
|
+
* is taken out of the flow, because a child of a view whose height is
|
|
7
|
+
* mid-animation reports that animated height back and the panel then settles on
|
|
8
|
+
* whatever it happened to measure.
|
|
9
|
+
*
|
|
10
|
+
* The moment the animation finishes, both go away and the content lays out
|
|
11
|
+
* normally again. That is what lets a Collapse hold another Collapse — or hold
|
|
12
|
+
* content that is still streaming in — without either of them being clipped to
|
|
13
|
+
* a number that was true a frame ago.
|
|
14
|
+
*
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <Collapse open={open}>
|
|
17
|
+
* <Text>Anything, of any height.</Text>
|
|
18
|
+
* </Collapse>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* The alternative — unmounting the body and letting a layout transition on the
|
|
22
|
+
* parent carry the change — is what `Accordion` does, and it is right when the
|
|
23
|
+
* closed state should cost nothing. This is for a body that opens and closes
|
|
24
|
+
* repeatedly and often while its content is still arriving, where remounting
|
|
25
|
+
* would throw away scroll position and restart every animation inside it.
|
|
26
|
+
*/
|
|
27
|
+
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
28
|
+
import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
|
|
29
|
+
import Animated, {
|
|
30
|
+
runOnJS,
|
|
31
|
+
useAnimatedStyle,
|
|
32
|
+
useReducedMotion,
|
|
33
|
+
useSharedValue,
|
|
34
|
+
withTiming,
|
|
35
|
+
} from 'react-native-reanimated';
|
|
36
|
+
import { cn } from '../utils/cn';
|
|
37
|
+
|
|
38
|
+
/** Long enough to read as the panel opening, short enough not to be waited on. */
|
|
39
|
+
export const COLLAPSE_DURATION = 200;
|
|
40
|
+
|
|
41
|
+
export interface CollapseProps extends Omit<ViewProps, 'children'> {
|
|
42
|
+
open: boolean;
|
|
43
|
+
/** Classes on the measured content, not on the clipping frame. */
|
|
44
|
+
className?: string;
|
|
45
|
+
duration?: number;
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function Collapse({
|
|
50
|
+
open,
|
|
51
|
+
className,
|
|
52
|
+
duration = COLLAPSE_DURATION,
|
|
53
|
+
children,
|
|
54
|
+
...props
|
|
55
|
+
}: CollapseProps) {
|
|
56
|
+
const reducedMotion = useReducedMotion();
|
|
57
|
+
const [height, setHeight] = useState(0);
|
|
58
|
+
const [animating, setAnimating] = useState(false);
|
|
59
|
+
const progress = useSharedValue(open ? 1 : 0);
|
|
60
|
+
const mounted = useRef(false);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
// The first pass is not a transition — a body that starts open should be
|
|
64
|
+
// open, not play its own entrance at nobody.
|
|
65
|
+
if (!mounted.current) {
|
|
66
|
+
mounted.current = true;
|
|
67
|
+
progress.value = open ? 1 : 0;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (reducedMotion) {
|
|
71
|
+
progress.value = open ? 1 : 0;
|
|
72
|
+
setAnimating(false);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setAnimating(true);
|
|
76
|
+
progress.value = withTiming(open ? 1 : 0, { duration }, (finished) => {
|
|
77
|
+
if (finished) runOnJS(setAnimating)(false);
|
|
78
|
+
});
|
|
79
|
+
}, [open, reducedMotion, duration, progress]);
|
|
80
|
+
|
|
81
|
+
const style = useAnimatedStyle(() => ({
|
|
82
|
+
height: progress.value * height,
|
|
83
|
+
opacity: progress.value,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
// Re-measured rather than measured once: a body whose content is still
|
|
87
|
+
// streaming in grows, and a height captured on the first frame would crop it.
|
|
88
|
+
const onLayout = (event: LayoutChangeEvent) => {
|
|
89
|
+
const next = event.nativeEvent.layout.height;
|
|
90
|
+
setHeight((current) => (Math.abs(current - next) < 1 ? current : next));
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* Once it has finished opening, the measured height and the absolute
|
|
95
|
+
* positioning are both dropped and the content lays out normally.
|
|
96
|
+
*
|
|
97
|
+
* They are only needed *while* the height is being animated to a number, and
|
|
98
|
+
* keeping them costs real bugs: a Collapse whose content is itself a Collapse
|
|
99
|
+
* measures a child that has not settled yet, and settles on that wrong
|
|
100
|
+
* number, so the inner body ends up clipped to nothing. Content that is still
|
|
101
|
+
* streaming in has the same problem against itself. In natural flow both are
|
|
102
|
+
* simply laid out, and the measurement carries on in the background against
|
|
103
|
+
* the next time it closes.
|
|
104
|
+
*/
|
|
105
|
+
const natural = open && !animating;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Animated.View
|
|
109
|
+
style={natural ? undefined : style}
|
|
110
|
+
className={natural ? undefined : 'overflow-hidden'}
|
|
111
|
+
>
|
|
112
|
+
{/*
|
|
113
|
+
The same two elements in both modes, so switching between them is a
|
|
114
|
+
style change rather than a remount — a remount would throw away the
|
|
115
|
+
state of everything inside.
|
|
116
|
+
*/}
|
|
117
|
+
<View
|
|
118
|
+
onLayout={onLayout}
|
|
119
|
+
style={natural ? undefined : { position: 'absolute', left: 0, right: 0, top: 0 }}
|
|
120
|
+
className={cn(className)}
|
|
121
|
+
{...props}
|
|
122
|
+
>
|
|
123
|
+
{children}
|
|
124
|
+
</View>
|
|
125
|
+
</Animated.View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
Collapse.displayName = 'Collapse';
|
package/src/utils/date.ts
CHANGED
|
@@ -12,6 +12,18 @@
|
|
|
12
12
|
* comparison here goes through `startOfDay`.
|
|
13
13
|
* - **Month arithmetic is done with `setMonth`, not by adding days.** The
|
|
14
14
|
* platform already knows that a month is 28, 29, 30 or 31 days long.
|
|
15
|
+
*
|
|
16
|
+
* Three more that are easy to lose and expensive to get back:
|
|
17
|
+
*
|
|
18
|
+
* - **Days are stepped with `setDate`, never with milliseconds.** Adding
|
|
19
|
+
* `n * 86400000` is wrong by an hour on the two days a year the clocks move,
|
|
20
|
+
* which is enough to land the wrong side of midnight and shift a whole grid.
|
|
21
|
+
* - **A month is normalised to the 1st before months are added.** `setMonth`
|
|
22
|
+
* on the 31st of a month whose neighbour is shorter overflows into the month
|
|
23
|
+
* after next — January 31st plus one month is March 3rd.
|
|
24
|
+
* - **Nothing is ever parsed back out of formatted text.** `Intl` is asked for
|
|
25
|
+
* the *parts* and the numbers are read from those, because the order and the
|
|
26
|
+
* separators belong to the locale and only the numbers are ours.
|
|
15
27
|
*/
|
|
16
28
|
|
|
17
29
|
/** Names of the weekdays and months, in the caller's locale. */
|
|
@@ -60,12 +72,6 @@ export function addDays(date: Date, days: number): Date {
|
|
|
60
72
|
return copy;
|
|
61
73
|
}
|
|
62
74
|
|
|
63
|
-
/** Whole days from `a` to `b`, ignoring the time of day on both. */
|
|
64
|
-
export function daysBetween(a: Date, b: Date): number {
|
|
65
|
-
const MS = 24 * 60 * 60 * 1000;
|
|
66
|
-
return Math.round((startOfDay(b).getTime() - startOfDay(a).getTime()) / MS);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
75
|
export function isBefore(a: Date, b: Date): boolean {
|
|
70
76
|
return startOfDay(a).getTime() < startOfDay(b).getTime();
|
|
71
77
|
}
|
|
@@ -82,12 +88,59 @@ export function isWithin(date: Date, from: Date, to: Date): boolean {
|
|
|
82
88
|
return time >= Math.min(start, end) && time <= Math.max(start, end);
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* `date` pulled inside `min`..`max`, at day resolution.
|
|
93
|
+
*
|
|
94
|
+
* The bound is returned at its own start of day rather than as given, so a
|
|
95
|
+
* `maxDate` that happens to carry a time of 17:30 does not hand back a value
|
|
96
|
+
* that then compares as *after* the last selectable day.
|
|
97
|
+
*/
|
|
85
98
|
export function clampDate(date: Date, min?: Date, max?: Date): Date {
|
|
86
|
-
if (min && isBefore(date, min)) return min;
|
|
87
|
-
if (max && isAfter(date, max)) return max;
|
|
99
|
+
if (min && isBefore(date, min)) return startOfDay(min);
|
|
100
|
+
if (max && isAfter(date, max)) return startOfDay(max);
|
|
88
101
|
return date;
|
|
89
102
|
}
|
|
90
103
|
|
|
104
|
+
/**
|
|
105
|
+
* A first-day-of-week index made safe.
|
|
106
|
+
*
|
|
107
|
+
* `weekStartsOn` arrives from a caller and reaches an array index by way of
|
|
108
|
+
* `(day + weekStartsOn) % 7`, where a negative or out-of-range value yields a
|
|
109
|
+
* negative index and an `undefined` weekday name. Both directions wrap.
|
|
110
|
+
*/
|
|
111
|
+
export function normalizeWeekStart(weekStartsOn: number): number {
|
|
112
|
+
if (!Number.isFinite(weekStartsOn)) return 0;
|
|
113
|
+
return ((Math.trunc(weekStartsOn) % 7) + 7) % 7;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The day the week starts on where this locale is spoken, as `Date.getDay()`
|
|
118
|
+
* counts it — 0 for Sunday.
|
|
119
|
+
*
|
|
120
|
+
* Sunday is a poor default outside North America and a handful of other
|
|
121
|
+
* regions, but it is the one the platform gives us when it knows nothing. Where
|
|
122
|
+
* `Intl` carries week data, ask it: `en-GB` and `fr-FR` start on Monday, and a
|
|
123
|
+
* calendar that says otherwise is wrong in a way its reader notices immediately.
|
|
124
|
+
*/
|
|
125
|
+
export function localeWeekStart(locale: DateLocale): number {
|
|
126
|
+
try {
|
|
127
|
+
const tag = (locale ?? 'en').split('-u-')[0] || 'en';
|
|
128
|
+
const resolved = new Intl.Locale(tag) as Intl.Locale & {
|
|
129
|
+
getWeekInfo?: () => { firstDay?: number };
|
|
130
|
+
weekInfo?: { firstDay?: number };
|
|
131
|
+
};
|
|
132
|
+
// Proposed as a method and shipped as a property first; both are in the
|
|
133
|
+
// wild, and neither is guaranteed to be there at all.
|
|
134
|
+
const info = resolved.getWeekInfo?.() ?? resolved.weekInfo;
|
|
135
|
+
const first = info?.firstDay;
|
|
136
|
+
// `Intl` counts 1 = Monday … 7 = Sunday; `Date.getDay()` counts 0 = Sunday.
|
|
137
|
+
if (typeof first === 'number' && Number.isFinite(first)) return first % 7;
|
|
138
|
+
} catch {
|
|
139
|
+
// No week data in this build.
|
|
140
|
+
}
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
91
144
|
/**
|
|
92
145
|
* The six-week grid for a month.
|
|
93
146
|
*
|
|
@@ -98,12 +151,20 @@ export function clampDate(date: Date, min?: Date, max?: Date): Date {
|
|
|
98
151
|
*
|
|
99
152
|
* The leading and trailing cells belong to the neighbouring months; whether
|
|
100
153
|
* they are drawn or left blank is the caller's decision.
|
|
154
|
+
*
|
|
155
|
+
* `system` and `locale` are what make this work for a Hijri grid, where the
|
|
156
|
+
* month it has to open on is not the Gregorian 1st.
|
|
101
157
|
*/
|
|
102
|
-
export function monthGrid(
|
|
103
|
-
|
|
158
|
+
export function monthGrid(
|
|
159
|
+
month: Date,
|
|
160
|
+
weekStartsOn: number,
|
|
161
|
+
system: 'gregory' | 'islamic' = 'gregory',
|
|
162
|
+
locale?: DateLocale
|
|
163
|
+
): Date[][] {
|
|
164
|
+
const first = startOfCalendarMonth(month, system, locale);
|
|
104
165
|
// How far back to the most recent `weekStartsOn`. The `+ 7` keeps the
|
|
105
166
|
// modulo positive when the week starts later than the first of the month.
|
|
106
|
-
const lead = (first.getDay() - weekStartsOn + 7) % 7;
|
|
167
|
+
const lead = (first.getDay() - normalizeWeekStart(weekStartsOn) + 7) % 7;
|
|
107
168
|
const start = addDays(first, -lead);
|
|
108
169
|
|
|
109
170
|
return Array.from({ length: 6 }, (_unusedWeek, week) =>
|
|
@@ -119,10 +180,49 @@ export function monthGrid(month: Date, weekStartsOn: number): Date[][] {
|
|
|
119
180
|
* locales they do not carry. Every formatter here falls back to English rather
|
|
120
181
|
* than taking the screen down over a month name.
|
|
121
182
|
*/
|
|
183
|
+
/**
|
|
184
|
+
* Formatters, kept.
|
|
185
|
+
*
|
|
186
|
+
* Building an `Intl.DateTimeFormat` is the expensive part of formatting, and a
|
|
187
|
+
* Hijri grid asks for one per cell twice over — once to decide which month the
|
|
188
|
+
* cell belongs to and once for the number in it — which is 84 constructions a
|
|
189
|
+
* render before anyone touches the year dropdown, where a century jump used to
|
|
190
|
+
* cost tens of thousands. The set of shapes asked for is tiny and fixed, so it
|
|
191
|
+
* is cached for the life of the process. A failure is cached too: a build with
|
|
192
|
+
* no data for a locale will not acquire any by being asked again.
|
|
193
|
+
*/
|
|
194
|
+
const formatters = new Map<string, Intl.DateTimeFormat | null>();
|
|
195
|
+
|
|
196
|
+
function formatter(
|
|
197
|
+
locale: DateLocale,
|
|
198
|
+
options: Intl.DateTimeFormatOptions
|
|
199
|
+
): Intl.DateTimeFormat | null {
|
|
200
|
+
// Sorted, so two call sites writing the same options in a different order
|
|
201
|
+
// share one entry rather than building two.
|
|
202
|
+
const shape = Object.keys(options)
|
|
203
|
+
.sort()
|
|
204
|
+
.map((key) => `${key}=${String(options[key as keyof Intl.DateTimeFormatOptions])}`)
|
|
205
|
+
.join(',');
|
|
206
|
+
const key = `${locale ?? ''}|${shape}`;
|
|
207
|
+
|
|
208
|
+
if (formatters.has(key)) return formatters.get(key) ?? null;
|
|
209
|
+
|
|
210
|
+
let made: Intl.DateTimeFormat | null = null;
|
|
211
|
+
try {
|
|
212
|
+
made = new Intl.DateTimeFormat(locale, options);
|
|
213
|
+
} catch {
|
|
214
|
+
made = null;
|
|
215
|
+
}
|
|
216
|
+
formatters.set(key, made);
|
|
217
|
+
return made;
|
|
218
|
+
}
|
|
219
|
+
|
|
122
220
|
function format(date: Date, locale: DateLocale, options: Intl.DateTimeFormatOptions): string | null {
|
|
123
221
|
try {
|
|
124
|
-
return
|
|
222
|
+
return formatter(locale, options)?.format(date) ?? null;
|
|
125
223
|
} catch {
|
|
224
|
+
// Constructing succeeded and formatting still failed — some builds only
|
|
225
|
+
// discover a missing calendar at format time.
|
|
126
226
|
return null;
|
|
127
227
|
}
|
|
128
228
|
}
|
|
@@ -189,7 +289,7 @@ export interface CalendarParts {
|
|
|
189
289
|
export function resolveCalendar(system: CalendarSystem, locale: DateLocale): 'gregory' | 'islamic' {
|
|
190
290
|
if (system !== 'auto') return system;
|
|
191
291
|
try {
|
|
192
|
-
const resolved =
|
|
292
|
+
const resolved = formatter(locale, {})?.resolvedOptions().calendar ?? '';
|
|
193
293
|
return resolved.startsWith('islamic') ? 'islamic' : 'gregory';
|
|
194
294
|
} catch {
|
|
195
295
|
return 'gregory';
|
|
@@ -214,11 +314,12 @@ export function calendarParts(
|
|
|
214
314
|
return { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() };
|
|
215
315
|
}
|
|
216
316
|
try {
|
|
217
|
-
const parts =
|
|
317
|
+
const parts = formatter(withCalendar(locale, 'islamic'), {
|
|
218
318
|
year: 'numeric',
|
|
219
319
|
month: 'numeric',
|
|
220
320
|
day: 'numeric',
|
|
221
|
-
})
|
|
321
|
+
})?.formatToParts(date);
|
|
322
|
+
if (!parts) throw new Error('unusable');
|
|
222
323
|
const read = (type: string) => Number(parts.find((part) => part.type === type)?.value);
|
|
223
324
|
const year = read('year');
|
|
224
325
|
const month = read('month');
|
|
@@ -246,13 +347,24 @@ export function isSameCalendarMonth(
|
|
|
246
347
|
return one.year === two.year && one.month === two.month;
|
|
247
348
|
}
|
|
248
349
|
|
|
350
|
+
/** Whether two days fall in the same year and month of the given calendar. */
|
|
351
|
+
function sameCalendarMonthParts(a: CalendarParts, b: CalendarParts): boolean {
|
|
352
|
+
return a.year === b.year && a.month === b.month;
|
|
353
|
+
}
|
|
354
|
+
|
|
249
355
|
/**
|
|
250
356
|
* The first day of the calendar month `date` falls in.
|
|
251
357
|
*
|
|
252
358
|
* Walked back a day at a time rather than computed. A Hijri month is 29 or 30
|
|
253
359
|
* days depending on the year, and the platform's own calendar data already
|
|
254
|
-
* knows which —
|
|
255
|
-
*
|
|
360
|
+
* knows which — asking it is better than shipping a table that will disagree
|
|
361
|
+
* with the device.
|
|
362
|
+
*
|
|
363
|
+
* The walk stops where the *month number changes*, not where the day number
|
|
364
|
+
* reads 1. The observational Islamic calendars the platforms ship do not always
|
|
365
|
+
* label a day 1: in 1450 AH, month 11 day 30 is followed directly by month 12
|
|
366
|
+
* day 2. Hunting for a day 1 that is never emitted walked the cursor off the
|
|
367
|
+
* end of its budget and then paged the calendar to the same month forever.
|
|
256
368
|
*/
|
|
257
369
|
export function startOfCalendarMonth(
|
|
258
370
|
date: Date,
|
|
@@ -261,9 +373,15 @@ export function startOfCalendarMonth(
|
|
|
261
373
|
): Date {
|
|
262
374
|
if (system === 'gregory') return startOfMonth(date);
|
|
263
375
|
let cursor = startOfDay(date);
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
376
|
+
const target = calendarParts(cursor, system, locale);
|
|
377
|
+
// 32, not 31: one step more than the longest month, so the loop is bounded
|
|
378
|
+
// by a real answer rather than by running out.
|
|
379
|
+
for (let step = 0; step < 32; step += 1) {
|
|
380
|
+
const previous = addDays(cursor, -1);
|
|
381
|
+
if (!sameCalendarMonthParts(calendarParts(previous, system, locale), target)) {
|
|
382
|
+
return cursor;
|
|
383
|
+
}
|
|
384
|
+
cursor = previous;
|
|
267
385
|
}
|
|
268
386
|
return cursor;
|
|
269
387
|
}
|
|
@@ -276,8 +394,13 @@ export function daysInCalendarMonth(
|
|
|
276
394
|
): number {
|
|
277
395
|
if (system === 'gregory') return endOfMonth(date).getDate();
|
|
278
396
|
const start = startOfCalendarMonth(date, system, locale);
|
|
397
|
+
const parts = calendarParts(start, system, locale);
|
|
398
|
+
// The first offset that lands outside this month is its length, whatever the
|
|
399
|
+
// day numbers along the way are called.
|
|
279
400
|
for (let length = 28; length <= 31; length += 1) {
|
|
280
|
-
if (calendarParts(addDays(start, length), system, locale)
|
|
401
|
+
if (!sameCalendarMonthParts(calendarParts(addDays(start, length), system, locale), parts)) {
|
|
402
|
+
return length;
|
|
403
|
+
}
|
|
281
404
|
}
|
|
282
405
|
return 30;
|
|
283
406
|
}
|
|
@@ -413,9 +536,10 @@ export function monthNames(locale: DateLocale, style: 'long' | 'short' = 'long')
|
|
|
413
536
|
|
|
414
537
|
/** Column headings, rotated so the first one is `weekStartsOn`. */
|
|
415
538
|
export function weekdayNames(locale: DateLocale, weekStartsOn: number): string[] {
|
|
539
|
+
const first = normalizeWeekStart(weekStartsOn);
|
|
416
540
|
// 2021-08-01 was a Sunday, so adding the index lands on each weekday in turn.
|
|
417
541
|
return Array.from({ length: 7 }, (_unused, day) => {
|
|
418
|
-
const index = (day +
|
|
542
|
+
const index = (day + first) % 7;
|
|
419
543
|
const formatted = formatGregory(new Date(2021, 7, 1 + index), locale, { weekday: 'short' });
|
|
420
544
|
return formatted ?? WEEKDAYS_EN[index]!;
|
|
421
545
|
});
|