panelui-native 0.30.0 → 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 +4 -2
- package/lib/module/components/calendar/index.js +107 -24
- package/lib/module/components/calendar/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/plan/index.js +307 -21
- package/lib/module/components/plan/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/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/table/index.js +632 -0
- package/lib/module/components/table/index.js.map +1 -0
- package/lib/module/icons/index.js +200 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +4 -1
- package/lib/module/index.js.map +1 -1
- 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 +12 -3
- package/lib/typescript/src/components/calendar/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/plan/index.d.ts +121 -10
- package/lib/typescript/src/components/plan/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/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/table/index.d.ts +274 -0
- package/lib/typescript/src/components/table/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +40 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- 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 +136 -23
- package/src/components/heatmap-chart/index.tsx +9 -9
- package/src/components/map/index.tsx +53 -4
- package/src/components/plan/index.tsx +344 -21
- package/src/components/post/index.tsx +1116 -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/table/index.tsx +747 -0
- package/src/icons/index.tsx +162 -1
- package/src/index.ts +60 -0
- package/src/utils/date.ts +146 -22
package/src/icons/index.tsx
CHANGED
|
@@ -133,12 +133,18 @@ export function CalendarIcon({ size = 16, color, ...props }: IconProps) {
|
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
export function BookmarkIcon({
|
|
136
|
+
export function BookmarkIcon({
|
|
137
|
+
size = 16,
|
|
138
|
+
color,
|
|
139
|
+
filled,
|
|
140
|
+
...props
|
|
141
|
+
}: IconProps & { filled?: boolean }) {
|
|
137
142
|
const resolved = useResolvedColor(color, '#737373');
|
|
138
143
|
return (
|
|
139
144
|
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
140
145
|
<Path
|
|
141
146
|
d="m19 21-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2Z"
|
|
147
|
+
fill={filled ? resolved : 'none'}
|
|
142
148
|
stroke={resolved}
|
|
143
149
|
strokeWidth={2}
|
|
144
150
|
strokeLinecap="round"
|
|
@@ -974,3 +980,158 @@ export function CircleIcon({ size = 16, color, ...props }: IconProps) {
|
|
|
974
980
|
</Svg>
|
|
975
981
|
);
|
|
976
982
|
}
|
|
983
|
+
|
|
984
|
+
/* -------------------------------------------------------------------------- */
|
|
985
|
+
/* Social */
|
|
986
|
+
/* -------------------------------------------------------------------------- */
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* The icons a post's action row is made of.
|
|
990
|
+
*
|
|
991
|
+
* These are the only ones in the set that take `filled`, and it is not a
|
|
992
|
+
* stylistic choice: a like, a save and a vote are toggles, and the outline and
|
|
993
|
+
* the solid are the two states of one control. Drawn as two different icons
|
|
994
|
+
* they would swap shape under the finger; drawn as one that fills, the shape
|
|
995
|
+
* stays put and only the inside changes.
|
|
996
|
+
*/
|
|
997
|
+
export interface ToggleIconProps extends IconProps {
|
|
998
|
+
/** Solid rather than outlined — the on state of a like, a save, a vote. */
|
|
999
|
+
filled?: boolean;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** Upvote. Filled once the vote is cast. */
|
|
1003
|
+
export function ArrowUpIcon({ size = 16, color, filled, ...props }: ToggleIconProps) {
|
|
1004
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
1005
|
+
return (
|
|
1006
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
1007
|
+
<Path
|
|
1008
|
+
d="M9 18v-6H5l7-7 7 7h-4v6z"
|
|
1009
|
+
fill={filled ? resolved : 'none'}
|
|
1010
|
+
stroke={resolved}
|
|
1011
|
+
strokeWidth={2}
|
|
1012
|
+
strokeLinecap="round"
|
|
1013
|
+
strokeLinejoin="round"
|
|
1014
|
+
/>
|
|
1015
|
+
</Svg>
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/** Downvote. The same arrow, turned over. */
|
|
1020
|
+
export function ArrowDownIcon({ size = 16, color, filled, ...props }: ToggleIconProps) {
|
|
1021
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
1022
|
+
return (
|
|
1023
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
1024
|
+
<Path
|
|
1025
|
+
d="M9 6v6H5l7 7 7-7h-4V6z"
|
|
1026
|
+
fill={filled ? resolved : 'none'}
|
|
1027
|
+
stroke={resolved}
|
|
1028
|
+
strokeWidth={2}
|
|
1029
|
+
strokeLinecap="round"
|
|
1030
|
+
strokeLinejoin="round"
|
|
1031
|
+
/>
|
|
1032
|
+
</Svg>
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/** A like. */
|
|
1037
|
+
export function HeartIcon({ size = 16, color, filled, ...props }: ToggleIconProps) {
|
|
1038
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
1039
|
+
return (
|
|
1040
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
1041
|
+
<Path
|
|
1042
|
+
d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"
|
|
1043
|
+
fill={filled ? resolved : 'none'}
|
|
1044
|
+
stroke={resolved}
|
|
1045
|
+
strokeWidth={2}
|
|
1046
|
+
strokeLinecap="round"
|
|
1047
|
+
strokeLinejoin="round"
|
|
1048
|
+
/>
|
|
1049
|
+
</Svg>
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/** A reply, or the count of them. */
|
|
1054
|
+
export function MessageCircleIcon({ size = 16, color, filled, ...props }: ToggleIconProps) {
|
|
1055
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
1056
|
+
return (
|
|
1057
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
1058
|
+
<Path
|
|
1059
|
+
d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"
|
|
1060
|
+
fill={filled ? resolved : 'none'}
|
|
1061
|
+
stroke={resolved}
|
|
1062
|
+
strokeWidth={2}
|
|
1063
|
+
strokeLinecap="round"
|
|
1064
|
+
strokeLinejoin="round"
|
|
1065
|
+
/>
|
|
1066
|
+
</Svg>
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/** Views — how many times a thing has been seen rather than acted on. */
|
|
1071
|
+
export function EyeIcon({ size = 16, color, ...props }: IconProps) {
|
|
1072
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
1073
|
+
return (
|
|
1074
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
1075
|
+
<Path
|
|
1076
|
+
d="M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0"
|
|
1077
|
+
stroke={resolved}
|
|
1078
|
+
strokeWidth={2}
|
|
1079
|
+
strokeLinecap="round"
|
|
1080
|
+
strokeLinejoin="round"
|
|
1081
|
+
/>
|
|
1082
|
+
<Circle cx={12} cy={12} r={3} stroke={resolved} strokeWidth={2} />
|
|
1083
|
+
</Svg>
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** A repost — the same thing sent round again. */
|
|
1088
|
+
export function RepeatIcon({ size = 16, color, ...props }: IconProps) {
|
|
1089
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
1090
|
+
return (
|
|
1091
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
1092
|
+
<Path
|
|
1093
|
+
d="m2 9 3-3 3 3M5 6v10a2 2 0 0 0 2 2h6M22 15l-3 3-3-3M19 18V8a2 2 0 0 0-2-2h-6"
|
|
1094
|
+
stroke={resolved}
|
|
1095
|
+
strokeWidth={2}
|
|
1096
|
+
strokeLinecap="round"
|
|
1097
|
+
strokeLinejoin="round"
|
|
1098
|
+
/>
|
|
1099
|
+
</Svg>
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
export interface BadgeCheckIconProps extends IconProps {
|
|
1104
|
+
/** The tick's colour when the rosette is solid. Defaults to white. */
|
|
1105
|
+
checkColor?: string;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* The verified rosette.
|
|
1110
|
+
*
|
|
1111
|
+
* Solid by default, unlike everything else here. A verification mark is a
|
|
1112
|
+
* claim about the account rather than a control, and an outlined one next to
|
|
1113
|
+
* an outlined like button reads as another thing you could press.
|
|
1114
|
+
*/
|
|
1115
|
+
export function BadgeCheckIcon({
|
|
1116
|
+
size = 16,
|
|
1117
|
+
color,
|
|
1118
|
+
checkColor = '#ffffff',
|
|
1119
|
+
...props
|
|
1120
|
+
}: BadgeCheckIconProps) {
|
|
1121
|
+
const resolved = useResolvedColor(color, '#737373');
|
|
1122
|
+
return (
|
|
1123
|
+
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none" {...props}>
|
|
1124
|
+
<Path
|
|
1125
|
+
d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"
|
|
1126
|
+
fill={resolved}
|
|
1127
|
+
/>
|
|
1128
|
+
<Path
|
|
1129
|
+
d="m9 12 2 2 4-4"
|
|
1130
|
+
stroke={checkColor}
|
|
1131
|
+
strokeWidth={2.5}
|
|
1132
|
+
strokeLinecap="round"
|
|
1133
|
+
strokeLinejoin="round"
|
|
1134
|
+
/>
|
|
1135
|
+
</Svg>
|
|
1136
|
+
);
|
|
1137
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -254,6 +254,20 @@ export {
|
|
|
254
254
|
type TabsTriggerProps,
|
|
255
255
|
type TabsContentProps,
|
|
256
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';
|
|
257
271
|
export {
|
|
258
272
|
Carousel,
|
|
259
273
|
useCarouselState,
|
|
@@ -431,6 +445,21 @@ export {
|
|
|
431
445
|
type PopoverAlign,
|
|
432
446
|
type PopoverPresentation,
|
|
433
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';
|
|
434
463
|
export { Progress, type ProgressProps } from './components/progress';
|
|
435
464
|
export {
|
|
436
465
|
RadioGroup,
|
|
@@ -476,6 +505,13 @@ export {
|
|
|
476
505
|
type ReasoningTriggerProps,
|
|
477
506
|
type ReasoningContentProps,
|
|
478
507
|
} from './components/reasoning';
|
|
508
|
+
export {
|
|
509
|
+
Response,
|
|
510
|
+
type ResponseProps,
|
|
511
|
+
type ResponseComponents,
|
|
512
|
+
type ResponseBlock,
|
|
513
|
+
type ResponseInline,
|
|
514
|
+
} from './components/response';
|
|
479
515
|
export {
|
|
480
516
|
Sources,
|
|
481
517
|
type SourcesProps,
|
|
@@ -509,11 +545,17 @@ export {
|
|
|
509
545
|
Plan,
|
|
510
546
|
type PlanProps,
|
|
511
547
|
type PlanHeaderProps,
|
|
548
|
+
type PlanIconProps,
|
|
512
549
|
type PlanTitleProps,
|
|
513
550
|
type PlanDescriptionProps,
|
|
514
551
|
type PlanActionProps,
|
|
552
|
+
type PlanProgressProps,
|
|
515
553
|
type PlanTriggerProps,
|
|
516
554
|
type PlanContentProps,
|
|
555
|
+
type PlanStepsProps,
|
|
556
|
+
type PlanStepProps,
|
|
557
|
+
type PlanStepStatus,
|
|
558
|
+
type PlanStepCounts,
|
|
517
559
|
type PlanFooterProps,
|
|
518
560
|
} from './components/plan';
|
|
519
561
|
export {
|
|
@@ -596,7 +638,10 @@ export {
|
|
|
596
638
|
export {
|
|
597
639
|
AlertTriangleIcon,
|
|
598
640
|
AppleIcon,
|
|
641
|
+
ArrowDownIcon,
|
|
642
|
+
ArrowUpIcon,
|
|
599
643
|
ArrowUpRightIcon,
|
|
644
|
+
BadgeCheckIcon,
|
|
600
645
|
BellIcon,
|
|
601
646
|
BookmarkIcon,
|
|
602
647
|
CalendarIcon,
|
|
@@ -607,16 +652,25 @@ export {
|
|
|
607
652
|
ChevronLeftIcon,
|
|
608
653
|
ChevronRightIcon,
|
|
609
654
|
ChevronUpIcon,
|
|
655
|
+
CircleIcon,
|
|
656
|
+
CompassIcon,
|
|
657
|
+
CopyIcon,
|
|
658
|
+
CrosshairIcon,
|
|
610
659
|
DownloadIcon,
|
|
611
660
|
EllipsisIcon,
|
|
661
|
+
EyeIcon,
|
|
612
662
|
FacebookIcon,
|
|
613
663
|
FileIcon,
|
|
614
664
|
GoogleIcon,
|
|
665
|
+
HeartIcon,
|
|
615
666
|
IconColorProvider,
|
|
616
667
|
InfoIcon,
|
|
617
668
|
ImageIcon,
|
|
669
|
+
LinkIcon,
|
|
670
|
+
ListChecksIcon,
|
|
618
671
|
LockIcon,
|
|
619
672
|
MaximizeIcon,
|
|
673
|
+
MessageCircleIcon,
|
|
620
674
|
MicIcon,
|
|
621
675
|
MinusIcon,
|
|
622
676
|
MoonIcon,
|
|
@@ -628,18 +682,24 @@ export {
|
|
|
628
682
|
PlusIcon,
|
|
629
683
|
PlusSquareIcon,
|
|
630
684
|
ReceiptIcon,
|
|
685
|
+
RepeatIcon,
|
|
631
686
|
RotateCcwIcon,
|
|
687
|
+
RotateCwIcon,
|
|
632
688
|
SearchIcon,
|
|
633
689
|
SendIcon,
|
|
634
690
|
ShareNodesIcon,
|
|
635
691
|
ShieldAlertIcon,
|
|
636
692
|
ShieldCheckIcon,
|
|
693
|
+
SparklesIcon,
|
|
694
|
+
StarIcon,
|
|
637
695
|
SunIcon,
|
|
638
696
|
TrashIcon,
|
|
639
697
|
UnlockIcon,
|
|
640
698
|
XIcon,
|
|
641
699
|
useIconColor,
|
|
642
700
|
type IconProps,
|
|
701
|
+
type ToggleIconProps,
|
|
702
|
+
type BadgeCheckIconProps,
|
|
643
703
|
} from './icons';
|
|
644
704
|
|
|
645
705
|
// Hooks
|
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
|
});
|