@monkey-mini-app/ui 0.1.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 (149) hide show
  1. package/components.json +23 -0
  2. package/dist/globals.css +2 -0
  3. package/dist/index.js +138 -0
  4. package/dist/manifest.json +11 -0
  5. package/dist/src/blocks/activity-feed.tsx +9 -0
  6. package/dist/src/blocks/app-shell.tsx +37 -0
  7. package/dist/src/blocks/attachment-gallery.tsx +30 -0
  8. package/dist/src/blocks/bar-chart.tsx +33 -0
  9. package/dist/src/blocks/comment-thread.tsx +30 -0
  10. package/dist/src/blocks/description-list.tsx +16 -0
  11. package/dist/src/blocks/detail-panel.tsx +43 -0
  12. package/dist/src/blocks/donut-chart.tsx +38 -0
  13. package/dist/src/blocks/env-badge.tsx +23 -0
  14. package/dist/src/blocks/file-tree.tsx +5 -0
  15. package/dist/src/blocks/filter-bar.tsx +21 -0
  16. package/dist/src/blocks/gauge.tsx +13 -0
  17. package/dist/src/blocks/notification-center.tsx +22 -0
  18. package/dist/src/blocks/page-header.tsx +30 -0
  19. package/dist/src/blocks/progress-ring.tsx +25 -0
  20. package/dist/src/blocks/radar-chart.tsx +31 -0
  21. package/dist/src/blocks/request-inspector.tsx +34 -0
  22. package/dist/src/blocks/run-timeline.tsx +5 -0
  23. package/dist/src/blocks/scrollspy.tsx +42 -0
  24. package/dist/src/blocks/severity-chip.tsx +27 -0
  25. package/dist/src/blocks/sparkline.tsx +19 -0
  26. package/dist/src/blocks/stat-card.tsx +49 -0
  27. package/dist/src/blocks/status-badge.tsx +39 -0
  28. package/dist/src/blocks/terminal.tsx +10 -0
  29. package/dist/src/blocks/test-step-list.tsx +19 -0
  30. package/dist/src/blocks/trend-card.tsx +51 -0
  31. package/dist/src/components/accordion.tsx +72 -0
  32. package/dist/src/components/alert-dialog.tsx +185 -0
  33. package/dist/src/components/alert.tsx +76 -0
  34. package/dist/src/components/aspect-ratio.tsx +22 -0
  35. package/dist/src/components/attachment.tsx +207 -0
  36. package/dist/src/components/avatar.tsx +107 -0
  37. package/dist/src/components/badge.tsx +52 -0
  38. package/dist/src/components/breadcrumb.tsx +125 -0
  39. package/dist/src/components/bubble.tsx +128 -0
  40. package/dist/src/components/button-group.tsx +87 -0
  41. package/dist/src/components/button.tsx +58 -0
  42. package/dist/src/components/calendar.tsx +221 -0
  43. package/dist/src/components/card.tsx +103 -0
  44. package/dist/src/components/chart.tsx +371 -0
  45. package/dist/src/components/checkbox.tsx +29 -0
  46. package/dist/src/components/collapsible.tsx +19 -0
  47. package/dist/src/components/combobox.tsx +295 -0
  48. package/dist/src/components/command.tsx +196 -0
  49. package/dist/src/components/context-menu.tsx +271 -0
  50. package/dist/src/components/dialog.tsx +158 -0
  51. package/dist/src/components/drawer.tsx +226 -0
  52. package/dist/src/components/dropdown-menu.tsx +268 -0
  53. package/dist/src/components/empty.tsx +123 -0
  54. package/dist/src/components/field.tsx +236 -0
  55. package/dist/src/components/hover-card.tsx +49 -0
  56. package/dist/src/components/input-group.tsx +158 -0
  57. package/dist/src/components/input.tsx +20 -0
  58. package/dist/src/components/item.tsx +201 -0
  59. package/dist/src/components/kbd.tsx +26 -0
  60. package/dist/src/components/label.tsx +20 -0
  61. package/dist/src/components/marker.tsx +71 -0
  62. package/dist/src/components/menubar.tsx +280 -0
  63. package/dist/src/components/message-scroller.tsx +129 -0
  64. package/dist/src/components/message.tsx +92 -0
  65. package/dist/src/components/native-select.tsx +61 -0
  66. package/dist/src/components/navigation-menu.tsx +168 -0
  67. package/dist/src/components/pagination.tsx +135 -0
  68. package/dist/src/components/popover.tsx +88 -0
  69. package/dist/src/components/progress.tsx +83 -0
  70. package/dist/src/components/questionnaire.tsx +329 -0
  71. package/dist/src/components/radio-group.tsx +36 -0
  72. package/dist/src/components/resizable.tsx +50 -0
  73. package/dist/src/components/scroll-area.tsx +52 -0
  74. package/dist/src/components/select.tsx +201 -0
  75. package/dist/src/components/separator.tsx +23 -0
  76. package/dist/src/components/sheet.tsx +136 -0
  77. package/dist/src/components/sidebar.tsx +721 -0
  78. package/dist/src/components/skeleton.tsx +13 -0
  79. package/dist/src/components/slider.tsx +52 -0
  80. package/dist/src/components/spinner.tsx +10 -0
  81. package/dist/src/components/switch.tsx +32 -0
  82. package/dist/src/components/table.tsx +114 -0
  83. package/dist/src/components/tabs.tsx +82 -0
  84. package/dist/src/components/textarea.tsx +18 -0
  85. package/dist/src/components/toast.tsx +234 -0
  86. package/dist/src/components/toggle-group.tsx +89 -0
  87. package/dist/src/components/toggle.tsx +43 -0
  88. package/dist/src/components/tooltip.tsx +66 -0
  89. package/dist/src/composites/autocomplete.tsx +55 -0
  90. package/dist/src/composites/cascader.tsx +46 -0
  91. package/dist/src/composites/color-picker.tsx +19 -0
  92. package/dist/src/composites/confirm-dialog.tsx +56 -0
  93. package/dist/src/composites/copyable.tsx +37 -0
  94. package/dist/src/composites/currency-input.tsx +31 -0
  95. package/dist/src/composites/date-chrome.tsx +157 -0
  96. package/dist/src/composites/date-picker.tsx +91 -0
  97. package/dist/src/composites/date-range-picker.tsx +96 -0
  98. package/dist/src/composites/date-time-picker.tsx +115 -0
  99. package/dist/src/composites/date-time-range-picker.tsx +51 -0
  100. package/dist/src/composites/duration-input.tsx +122 -0
  101. package/dist/src/composites/mini-calendar.tsx +24 -0
  102. package/dist/src/composites/number-field.tsx +53 -0
  103. package/dist/src/composites/password-field.tsx +44 -0
  104. package/dist/src/composites/phone-input.tsx +22 -0
  105. package/dist/src/composites/rating.tsx +39 -0
  106. package/dist/src/composites/relative-date-picker.tsx +62 -0
  107. package/dist/src/composites/search-input.tsx +49 -0
  108. package/dist/src/composites/slider-range.tsx +30 -0
  109. package/dist/src/composites/tag-input.tsx +56 -0
  110. package/dist/src/composites/time-picker.tsx +199 -0
  111. package/dist/src/composites/time-range-picker.tsx +25 -0
  112. package/dist/src/composites/timezone-select.tsx +251 -0
  113. package/dist/src/composites/transfer.tsx +52 -0
  114. package/dist/src/composites/user-picker.tsx +40 -0
  115. package/dist/src/hooks/use-mobile.ts +19 -0
  116. package/dist/src/i18n/context.tsx +72 -0
  117. package/dist/src/i18n/en.ts +168 -0
  118. package/dist/src/i18n/zh.ts +160 -0
  119. package/dist/src/index.ts +134 -0
  120. package/dist/src/lib/icons.tsx +16 -0
  121. package/dist/src/lib/illustrations.tsx +573 -0
  122. package/dist/src/lib/utils.ts +6 -0
  123. package/dist/src/products/cells.tsx +18 -0
  124. package/dist/src/products/code-block.tsx +75 -0
  125. package/dist/src/products/code-editor.tsx +49 -0
  126. package/dist/src/products/column-meta.d.ts +9 -0
  127. package/dist/src/products/data-grid.tsx +689 -0
  128. package/dist/src/products/diff-viewer.tsx +129 -0
  129. package/dist/src/products/event-calendar.tsx +845 -0
  130. package/dist/src/products/export-csv.ts +25 -0
  131. package/dist/src/products/file-dropzone.tsx +41 -0
  132. package/dist/src/products/gantt.tsx +41 -0
  133. package/dist/src/products/jira-wiki-parse.ts +197 -0
  134. package/dist/src/products/jira-wiki.tsx +285 -0
  135. package/dist/src/products/jql-input.tsx +62 -0
  136. package/dist/src/products/jql-language.ts +70 -0
  137. package/dist/src/products/json-viewer.tsx +37 -0
  138. package/dist/src/products/kanban-issue-panel.tsx +98 -0
  139. package/dist/src/products/kanban.tsx +245 -0
  140. package/dist/src/products/log-viewer.tsx +33 -0
  141. package/dist/src/products/markdown-editor.tsx +63 -0
  142. package/dist/src/products/markdown.tsx +26 -0
  143. package/dist/src/products/rich-text-editor.tsx +84 -0
  144. package/dist/src/products/sortable-list.tsx +61 -0
  145. package/dist/src/products/stepper.tsx +89 -0
  146. package/dist/src/products/timeline.tsx +32 -0
  147. package/dist/src/products/tree-view.tsx +56 -0
  148. package/dist/src/styles/globals.css +352 -0
  149. package/package.json +100 -0
@@ -0,0 +1,43 @@
1
+ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
5
+
6
+ const toggleVariants = cva(
7
+ "group/toggle inline-flex items-center justify-center gap-1 rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-transparent",
12
+ outline: "border border-input bg-transparent hover:bg-muted",
13
+ },
14
+ size: {
15
+ default:
16
+ "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
17
+ sm: "h-7 min-w-7 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
18
+ lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "default",
23
+ size: "default",
24
+ },
25
+ }
26
+ )
27
+
28
+ function Toggle({
29
+ className,
30
+ variant = "default",
31
+ size = "default",
32
+ ...props
33
+ }: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
34
+ return (
35
+ <TogglePrimitive
36
+ data-slot="toggle"
37
+ className={cn(toggleVariants({ variant, size, className }))}
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
43
+ export { Toggle, toggleVariants }
@@ -0,0 +1,66 @@
1
+ "use client"
2
+
3
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
4
+
5
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
6
+
7
+ function TooltipProvider({
8
+ delay = 0,
9
+ ...props
10
+ }: TooltipPrimitive.Provider.Props) {
11
+ return (
12
+ <TooltipPrimitive.Provider
13
+ data-slot="tooltip-provider"
14
+ delay={delay}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
21
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
22
+ }
23
+
24
+ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
25
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
26
+ }
27
+
28
+ function TooltipContent({
29
+ className,
30
+ side = "top",
31
+ sideOffset = 4,
32
+ align = "center",
33
+ alignOffset = 0,
34
+ children,
35
+ ...props
36
+ }: TooltipPrimitive.Popup.Props &
37
+ Pick<
38
+ TooltipPrimitive.Positioner.Props,
39
+ "align" | "alignOffset" | "side" | "sideOffset"
40
+ >) {
41
+ return (
42
+ <TooltipPrimitive.Portal>
43
+ <TooltipPrimitive.Positioner
44
+ align={align}
45
+ alignOffset={alignOffset}
46
+ side={side}
47
+ sideOffset={sideOffset}
48
+ className="isolate z-50"
49
+ >
50
+ <TooltipPrimitive.Popup
51
+ data-slot="tooltip-content"
52
+ className={cn(
53
+ "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
54
+ className
55
+ )}
56
+ {...props}
57
+ >
58
+ {children}
59
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
60
+ </TooltipPrimitive.Popup>
61
+ </TooltipPrimitive.Positioner>
62
+ </TooltipPrimitive.Portal>
63
+ )
64
+ }
65
+
66
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
@@ -0,0 +1,55 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import { Input } from "@monkey-mini-app/ui/components/input"
6
+
7
+ export function Autocomplete({
8
+ value,
9
+ onChange,
10
+ options,
11
+ placeholder,
12
+ }: {
13
+ value?: string
14
+ onChange?: (value: string) => void
15
+ options: string[]
16
+ placeholder?: string
17
+ }) {
18
+ const [open, setOpen] = React.useState(false)
19
+ const filtered = options.filter((option) =>
20
+ option.toLowerCase().includes((value ?? "").toLowerCase())
21
+ )
22
+ return (
23
+ <div className="relative" data-testid="autocomplete">
24
+ <Input
25
+ value={value ?? ""}
26
+ placeholder={placeholder}
27
+ onChange={(event) => {
28
+ onChange?.(event.target.value)
29
+ setOpen(true)
30
+ }}
31
+ onFocus={() => setOpen(true)}
32
+ onBlur={() => window.setTimeout(() => setOpen(false), 120)}
33
+ />
34
+ {open && filtered.length > 0 ? (
35
+ <ul className="absolute z-50 mt-1 max-h-48 w-full overflow-auto rounded-lg border bg-popover p-1 shadow-md">
36
+ {filtered.slice(0, 20).map((option) => (
37
+ <li key={option}>
38
+ <button
39
+ type="button"
40
+ className="w-full rounded-md px-2 py-1.5 text-left text-sm hover:bg-muted"
41
+ onMouseDown={(event) => event.preventDefault()}
42
+ onClick={() => {
43
+ onChange?.(option)
44
+ setOpen(false)
45
+ }}
46
+ >
47
+ {option}
48
+ </button>
49
+ </li>
50
+ ))}
51
+ </ul>
52
+ ) : null}
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,46 @@
1
+ "use client"
2
+
3
+ import { NativeSelect, NativeSelectOption } from "@monkey-mini-app/ui/components/native-select"
4
+
5
+ export type CascaderNode = { value: string; label: string; children?: CascaderNode[] }
6
+
7
+ export function Cascader({
8
+ options,
9
+ value = [],
10
+ onChange,
11
+ }: {
12
+ options: CascaderNode[]
13
+ value?: string[]
14
+ onChange?: (value: string[]) => void
15
+ }) {
16
+ const columns: CascaderNode[][] = []
17
+ let level = options
18
+ columns.push(level)
19
+ for (const key of value) {
20
+ const next = level.find((node) => node.value === key)?.children
21
+ if (!next?.length) break
22
+ columns.push(next)
23
+ level = next
24
+ }
25
+ return (
26
+ <div className="flex flex-wrap gap-2" data-testid="cascader">
27
+ {columns.map((nodes, index) => (
28
+ <NativeSelect
29
+ key={index}
30
+ value={value[index] ?? ""}
31
+ onChange={(event) => {
32
+ const next = event.target.value
33
+ onChange?.([...value.slice(0, index), next])
34
+ }}
35
+ >
36
+ <NativeSelectOption value="">Select</NativeSelectOption>
37
+ {nodes.map((node) => (
38
+ <NativeSelectOption key={node.value} value={node.value}>
39
+ {node.label}
40
+ </NativeSelectOption>
41
+ ))}
42
+ </NativeSelect>
43
+ ))}
44
+ </div>
45
+ )
46
+ }
@@ -0,0 +1,19 @@
1
+ "use client"
2
+
3
+ export function ColorPicker({
4
+ value = "#111111",
5
+ onChange,
6
+ }: {
7
+ value?: string
8
+ onChange?: (value: string) => void
9
+ }) {
10
+ return (
11
+ <input
12
+ data-testid="color-picker"
13
+ type="color"
14
+ value={value}
15
+ onChange={(event) => onChange?.(event.target.value)}
16
+ className="size-8 cursor-pointer rounded-md border bg-transparent p-0.5"
17
+ />
18
+ )
19
+ }
@@ -0,0 +1,56 @@
1
+ "use client"
2
+
3
+ import {
4
+ AlertDialog,
5
+ AlertDialogAction,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle,
12
+ } from "@monkey-mini-app/ui/components/alert-dialog"
13
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context"
14
+
15
+ export function ConfirmDialog({
16
+ open,
17
+ onOpenChange,
18
+ title,
19
+ description,
20
+ confirmLabel,
21
+ cancelLabel,
22
+ onConfirm,
23
+ }: {
24
+ open: boolean
25
+ onOpenChange: (open: boolean) => void
26
+ title: string
27
+ description?: string
28
+ confirmLabel?: string
29
+ cancelLabel?: string
30
+ onConfirm: () => void
31
+ }) {
32
+ const t = useLabels("confirmDialog")
33
+ return (
34
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
35
+ <AlertDialogContent data-testid="confirm-dialog">
36
+ <AlertDialogHeader>
37
+ <AlertDialogTitle>{title}</AlertDialogTitle>
38
+ {description ? (
39
+ <AlertDialogDescription>{description}</AlertDialogDescription>
40
+ ) : null}
41
+ </AlertDialogHeader>
42
+ <AlertDialogFooter>
43
+ <AlertDialogCancel>{cancelLabel ?? t.cancel}</AlertDialogCancel>
44
+ <AlertDialogAction
45
+ onClick={() => {
46
+ onConfirm()
47
+ onOpenChange(false)
48
+ }}
49
+ >
50
+ {confirmLabel ?? t.confirm}
51
+ </AlertDialogAction>
52
+ </AlertDialogFooter>
53
+ </AlertDialogContent>
54
+ </AlertDialog>
55
+ )
56
+ }
@@ -0,0 +1,37 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Check, Copy } from "lucide-react"
5
+
6
+ import { Button } from "@monkey-mini-app/ui/components/button"
7
+ import {
8
+ InputGroup,
9
+ InputGroupAddon,
10
+ InputGroupInput,
11
+ } from "@monkey-mini-app/ui/components/input-group"
12
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context"
13
+
14
+ export function Copyable({ value }: { value: string }) {
15
+ const t = useLabels("copyable")
16
+ const [copied, setCopied] = React.useState(false)
17
+ return (
18
+ <InputGroup data-testid="copyable">
19
+ <InputGroupInput readOnly value={value} />
20
+ <InputGroupAddon align="inline-end">
21
+ <Button
22
+ variant="ghost"
23
+ size="icon-xs"
24
+ type="button"
25
+ aria-label={t.copy}
26
+ onClick={async () => {
27
+ await navigator.clipboard.writeText(value)
28
+ setCopied(true)
29
+ window.setTimeout(() => setCopied(false), 1200)
30
+ }}
31
+ >
32
+ {copied ? <Check /> : <Copy />}
33
+ </Button>
34
+ </InputGroupAddon>
35
+ </InputGroup>
36
+ )
37
+ }
@@ -0,0 +1,31 @@
1
+ "use client"
2
+
3
+ import {
4
+ InputGroup,
5
+ InputGroupAddon,
6
+ InputGroupInput,
7
+ InputGroupText,
8
+ } from "@monkey-mini-app/ui/components/input-group"
9
+
10
+ export function CurrencyInput({
11
+ value,
12
+ onChange,
13
+ currency = "¥",
14
+ }: {
15
+ value?: string
16
+ onChange?: (value: string) => void
17
+ currency?: string
18
+ }) {
19
+ return (
20
+ <InputGroup className="w-40" data-testid="currency-input">
21
+ <InputGroupAddon>
22
+ <InputGroupText>{currency}</InputGroupText>
23
+ </InputGroupAddon>
24
+ <InputGroupInput
25
+ inputMode="decimal"
26
+ value={value ?? ""}
27
+ onChange={(event) => onChange?.(event.target.value.replace(/[^\d.]/g, ""))}
28
+ />
29
+ </InputGroup>
30
+ )
31
+ }
@@ -0,0 +1,157 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { addMonths, addYears, format, setMonth, setYear } from "date-fns"
5
+ import { ArrowDown, ArrowUp } from "lucide-react"
6
+
7
+ import { Button } from "@monkey-mini-app/ui/components/button"
8
+ import { useDateLocale, useLabels } from "@monkey-mini-app/ui/i18n/context"
9
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
10
+
11
+ function Stepper({
12
+ onUp,
13
+ onDown,
14
+ }: {
15
+ onUp: () => void
16
+ onDown: () => void
17
+ }) {
18
+ return (
19
+ <div className="flex items-center">
20
+ <Button type="button" variant="ghost" size="icon-xs" aria-label="Previous" onClick={onUp}>
21
+ <ArrowUp className="size-3.5" />
22
+ </Button>
23
+ <Button type="button" variant="ghost" size="icon-xs" aria-label="Next" onClick={onDown}>
24
+ <ArrowDown className="size-3.5" />
25
+ </Button>
26
+ </div>
27
+ )
28
+ }
29
+
30
+ function decadeOf(year: number) {
31
+ return Math.floor(year / 12) * 12
32
+ }
33
+
34
+ export function DateChrome({
35
+ month,
36
+ onMonthChange,
37
+ onToday,
38
+ children,
39
+ }: {
40
+ month: Date
41
+ onMonthChange: (month: Date) => void
42
+ onToday?: () => void
43
+ children: React.ReactNode
44
+ }) {
45
+ const dateLocale = useDateLocale()
46
+ const common = useLabels("common")
47
+ const year = month.getFullYear()
48
+ const [yearPane, setYearPane] = React.useState(false)
49
+ const [decadeStart, setDecadeStart] = React.useState(() => decadeOf(year))
50
+
51
+ React.useEffect(() => {
52
+ if (year < decadeStart || year >= decadeStart + 12) {
53
+ setDecadeStart(decadeOf(year))
54
+ }
55
+ }, [year, decadeStart])
56
+
57
+ return (
58
+ <div className="flex" data-testid="date-chrome">
59
+ <div className="flex min-w-[252px] flex-col p-3">
60
+ <div className="mb-2 flex items-center justify-between gap-2">
61
+ <div className="text-sm font-semibold">{format(month, "yyyy MMMM", { locale: dateLocale })}</div>
62
+ <Stepper
63
+ onUp={() => onMonthChange(addMonths(month, -1))}
64
+ onDown={() => onMonthChange(addMonths(month, 1))}
65
+ />
66
+ </div>
67
+ {children}
68
+ </div>
69
+ <div className="bg-border w-px" />
70
+ <div className="flex w-[220px] flex-col p-3">
71
+ <div className="mb-3 flex items-center justify-between">
72
+ <button
73
+ type="button"
74
+ data-testid="date-chrome-year"
75
+ className="hover:bg-muted rounded-md px-1.5 py-0.5 text-sm font-semibold"
76
+ onClick={() => setYearPane((open) => !open)}
77
+ >
78
+ {yearPane ? `${decadeStart}–${decadeStart + 11}` : year}
79
+ </button>
80
+ <Stepper
81
+ onUp={() => {
82
+ if (yearPane) setDecadeStart((start) => start - 12)
83
+ else onMonthChange(addYears(month, -1))
84
+ }}
85
+ onDown={() => {
86
+ if (yearPane) setDecadeStart((start) => start + 12)
87
+ else onMonthChange(addYears(month, 1))
88
+ }}
89
+ />
90
+ </div>
91
+ {yearPane ? (
92
+ <div className="grid grid-cols-4 gap-x-1 gap-y-2">
93
+ {Array.from({ length: 12 }, (_, index) => {
94
+ const item = decadeStart + index
95
+ const active = item === year
96
+ return (
97
+ <button
98
+ key={item}
99
+ type="button"
100
+ data-testid={`date-chrome-year-${item}`}
101
+ className={cn(
102
+ "rounded-md py-1.5 text-sm",
103
+ active
104
+ ? "bg-primary/15 font-medium text-foreground"
105
+ : "text-muted-foreground hover:bg-muted hover:text-foreground"
106
+ )}
107
+ onClick={() => {
108
+ onMonthChange(setYear(month, item))
109
+ setYearPane(false)
110
+ }}
111
+ >
112
+ {item}
113
+ </button>
114
+ )
115
+ })}
116
+ </div>
117
+ ) : (
118
+ <div className="grid grid-cols-4 gap-x-1 gap-y-2">
119
+ {Array.from({ length: 12 }, (_, index) => {
120
+ const active = month.getMonth() === index
121
+ return (
122
+ <button
123
+ key={index}
124
+ type="button"
125
+ data-testid={`date-chrome-month-${index}`}
126
+ className={cn(
127
+ "rounded-md py-1.5 text-sm",
128
+ active
129
+ ? "bg-primary/15 font-medium text-foreground"
130
+ : "text-muted-foreground hover:bg-muted hover:text-foreground"
131
+ )}
132
+ onClick={() => onMonthChange(setMonth(month, index))}
133
+ >
134
+ {format(new Date(year, index, 1), "LLL", { locale: dateLocale })}
135
+ </button>
136
+ )
137
+ })}
138
+ </div>
139
+ )}
140
+ <Button
141
+ type="button"
142
+ variant="ghost"
143
+ size="sm"
144
+ className="mt-auto self-end"
145
+ data-testid="date-chrome-today"
146
+ onClick={() => {
147
+ setYearPane(false)
148
+ onMonthChange(new Date())
149
+ onToday?.()
150
+ }}
151
+ >
152
+ {common.today}
153
+ </Button>
154
+ </div>
155
+ </div>
156
+ )
157
+ }
@@ -0,0 +1,91 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { format } from "date-fns"
5
+ import { ChevronDown } from "lucide-react"
6
+
7
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
8
+ import { useDateLocale, useLabels } from "@monkey-mini-app/ui/i18n/context"
9
+ import { Button } from "@monkey-mini-app/ui/components/button"
10
+ import { Calendar } from "@monkey-mini-app/ui/components/calendar"
11
+ import {
12
+ Popover,
13
+ PopoverContent,
14
+ PopoverTrigger,
15
+ } from "@monkey-mini-app/ui/components/popover"
16
+ import { DateChrome } from "@monkey-mini-app/ui/composites/date-chrome"
17
+
18
+ export type DatePickerProps = {
19
+ value?: Date
20
+ onChange?: (date: Date | undefined) => void
21
+ placeholder?: string
22
+ disabled?: boolean
23
+ className?: string
24
+ }
25
+
26
+ export function DatePicker({
27
+ value,
28
+ onChange,
29
+ placeholder,
30
+ disabled,
31
+ className,
32
+ }: DatePickerProps) {
33
+ const [open, setOpen] = React.useState(false)
34
+ const [month, setMonth] = React.useState(value ?? new Date())
35
+ const t = useLabels("datePicker")
36
+ const dateLocale = useDateLocale()
37
+ const text = placeholder ?? t.placeholder
38
+
39
+ React.useEffect(() => {
40
+ if (value) setMonth(value)
41
+ }, [value])
42
+
43
+ return (
44
+ <Popover open={open} onOpenChange={setOpen}>
45
+ <PopoverTrigger
46
+ disabled={disabled}
47
+ render={
48
+ <Button
49
+ variant="secondary"
50
+ data-empty={!value}
51
+ data-testid="date-picker-trigger"
52
+ className={cn(
53
+ "justify-between font-medium data-[empty=true]:text-muted-foreground",
54
+ className
55
+ )}
56
+ />
57
+ }
58
+ >
59
+ <span className="truncate">
60
+ {value ? format(value, "yyyy, MMMM d", { locale: dateLocale }) : text}
61
+ </span>
62
+ <ChevronDown className="size-3.5 opacity-50" />
63
+ </PopoverTrigger>
64
+ <PopoverContent align="start" className="w-auto overflow-hidden p-0">
65
+ <DateChrome
66
+ month={month}
67
+ onMonthChange={setMonth}
68
+ onToday={() => {
69
+ const now = new Date()
70
+ onChange?.(now)
71
+ setOpen(false)
72
+ }}
73
+ >
74
+ <Calendar
75
+ mode="single"
76
+ locale={dateLocale}
77
+ month={month}
78
+ onMonthChange={setMonth}
79
+ selected={value}
80
+ hideNavigation
81
+ classNames={{ month_caption: "hidden", nav: "hidden" }}
82
+ onSelect={(date) => {
83
+ onChange?.(date)
84
+ setOpen(false)
85
+ }}
86
+ />
87
+ </DateChrome>
88
+ </PopoverContent>
89
+ </Popover>
90
+ )
91
+ }