@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,96 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { format, isSameMonth, isSameYear, type Locale } from "date-fns"
5
+ import type { DateRange } from "react-day-picker"
6
+ import { ChevronDown } from "lucide-react"
7
+
8
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
9
+ import { useDateLocale, useLabels } from "@monkey-mini-app/ui/i18n/context"
10
+ import { Button } from "@monkey-mini-app/ui/components/button"
11
+ import { Calendar } from "@monkey-mini-app/ui/components/calendar"
12
+ import {
13
+ Popover,
14
+ PopoverContent,
15
+ PopoverTrigger,
16
+ } from "@monkey-mini-app/ui/components/popover"
17
+ import { DateChrome } from "@monkey-mini-app/ui/composites/date-chrome"
18
+
19
+ export type DateRangePickerProps = {
20
+ value?: DateRange
21
+ onChange?: (range: DateRange | undefined) => void
22
+ placeholder?: string
23
+ className?: string
24
+ }
25
+
26
+ function rangeLabel(value: DateRange, locale: Locale) {
27
+ const opts = { locale }
28
+ if (!value.from) return null
29
+ if (!value.to) return format(value.from, "yyyy, MMMM d", opts)
30
+ if (isSameYear(value.from, value.to) && isSameMonth(value.from, value.to)) {
31
+ return `${format(value.from, "yyyy, MMMM d", opts)}–${format(value.to, "d", opts)}`
32
+ }
33
+ if (isSameYear(value.from, value.to)) {
34
+ return `${format(value.from, "yyyy, MMM d", opts)} – ${format(value.to, "MMM d", opts)}`
35
+ }
36
+ return `${format(value.from, "MMM d, yyyy", opts)} – ${format(value.to, "MMM d, yyyy", opts)}`
37
+ }
38
+
39
+ /**
40
+ * Popover date range with month/year chrome.
41
+ * @when Filters needing from/to calendar days
42
+ * @example
43
+ * <DateRangePicker value={range} onChange={setRange} />
44
+ */
45
+ export function DateRangePicker({
46
+ value,
47
+ onChange,
48
+ placeholder,
49
+ className,
50
+ }: DateRangePickerProps) {
51
+ const t = useLabels("dateRangePicker")
52
+ const dateLocale = useDateLocale()
53
+ const text = placeholder ?? t.placeholder
54
+ const [month, setMonth] = React.useState(value?.from ?? new Date())
55
+ const label = value?.from ? rangeLabel(value, dateLocale) : text
56
+
57
+ React.useEffect(() => {
58
+ if (value?.from) setMonth(value.from)
59
+ }, [value?.from])
60
+
61
+ return (
62
+ <Popover>
63
+ <PopoverTrigger
64
+ render={
65
+ <Button
66
+ variant="secondary"
67
+ data-testid="date-range-picker-trigger"
68
+ className={cn(
69
+ "justify-between font-medium",
70
+ !value?.from && "text-muted-foreground",
71
+ className
72
+ )}
73
+ />
74
+ }
75
+ >
76
+ <span className="truncate">{label}</span>
77
+ <ChevronDown className="size-3.5 opacity-50" />
78
+ </PopoverTrigger>
79
+ <PopoverContent align="start" className="w-auto overflow-hidden p-0">
80
+ <DateChrome month={month} onMonthChange={setMonth}>
81
+ <Calendar
82
+ mode="range"
83
+ locale={dateLocale}
84
+ month={month}
85
+ onMonthChange={setMonth}
86
+ selected={value}
87
+ onSelect={onChange}
88
+ hideNavigation
89
+ numberOfMonths={1}
90
+ classNames={{ month_caption: "hidden", nav: "hidden" }}
91
+ />
92
+ </DateChrome>
93
+ </PopoverContent>
94
+ </Popover>
95
+ )
96
+ }
@@ -0,0 +1,115 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { format } from "date-fns"
5
+ import { CalendarIcon } from "lucide-react"
6
+
7
+ import { Button } from "@monkey-mini-app/ui/components/button"
8
+ import { Calendar } from "@monkey-mini-app/ui/components/calendar"
9
+ import {
10
+ Popover,
11
+ PopoverContent,
12
+ PopoverTrigger,
13
+ } from "@monkey-mini-app/ui/components/popover"
14
+ import { DateChrome } from "@monkey-mini-app/ui/composites/date-chrome"
15
+ import { TimeWheel } from "@monkey-mini-app/ui/composites/time-picker"
16
+ import { TimezoneSelect } from "@monkey-mini-app/ui/composites/timezone-select"
17
+ import { useDateLocale, useLabels } from "@monkey-mini-app/ui/i18n/context"
18
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
19
+
20
+ function merge(date: Date | undefined, hours: number, minutes: number): Date {
21
+ const next = new Date(date ?? new Date())
22
+ next.setHours(hours, minutes, 0, 0)
23
+ return next
24
+ }
25
+
26
+ export type DateTimePickerProps = {
27
+ value?: Date
28
+ onChange?: (date: Date | undefined) => void
29
+ timezone?: string
30
+ onTimezoneChange?: (zone: string) => void
31
+ placeholder?: string
32
+ className?: string
33
+ }
34
+
35
+ export function DateTimePicker({
36
+ value,
37
+ onChange,
38
+ timezone,
39
+ onTimezoneChange,
40
+ placeholder,
41
+ className,
42
+ }: DateTimePickerProps) {
43
+ const [open, setOpen] = React.useState(false)
44
+ const [month, setMonth] = React.useState(value ?? new Date())
45
+ const hours = value?.getHours() ?? 0
46
+ const minutes = value?.getMinutes() ?? 0
47
+ const t = useLabels("dateTimePicker")
48
+ const dateLocale = useDateLocale()
49
+ const text = placeholder ?? t.placeholder
50
+ React.useEffect(() => {
51
+ if (value) setMonth(value)
52
+ }, [value])
53
+
54
+ return (
55
+ <div className="flex flex-wrap items-center gap-2" data-testid="date-time-picker">
56
+ <Popover open={open} onOpenChange={setOpen}>
57
+ <PopoverTrigger
58
+ render={
59
+ <Button
60
+ variant="outline"
61
+ data-empty={!value}
62
+ data-testid="date-picker-trigger"
63
+ className={cn(
64
+ "w-[280px] justify-start text-left font-normal data-[empty=true]:text-muted-foreground",
65
+ className
66
+ )}
67
+ />
68
+ }
69
+ >
70
+ <CalendarIcon />
71
+ {value ? format(value, "PPP p", { locale: dateLocale }) : <span>{text}</span>}
72
+ </PopoverTrigger>
73
+ <PopoverContent className="w-auto overflow-hidden p-0">
74
+ <div className="flex">
75
+ <DateChrome
76
+ month={month}
77
+ onMonthChange={setMonth}
78
+ onToday={() => onChange?.(merge(new Date(), hours, minutes))}
79
+ >
80
+ <Calendar
81
+ mode="single"
82
+ locale={dateLocale}
83
+ month={month}
84
+ onMonthChange={setMonth}
85
+ selected={value}
86
+ hideNavigation
87
+ classNames={{ month_caption: "hidden", nav: "hidden" }}
88
+ onSelect={(date) => onChange?.(merge(date, hours, minutes))}
89
+ />
90
+ </DateChrome>
91
+ <div className="flex gap-1 border-l px-2 py-2">
92
+ <TimeWheel
93
+ testId="time-hour"
94
+ visible={7}
95
+ values={Array.from({ length: 24 }, (_, i) => i)}
96
+ value={hours}
97
+ onChange={(h) => onChange?.(merge(value, h, minutes))}
98
+ />
99
+ <TimeWheel
100
+ testId="time-minute"
101
+ visible={7}
102
+ values={Array.from({ length: 60 }, (_, i) => i)}
103
+ value={minutes}
104
+ onChange={(m) => onChange?.(merge(value, hours, m))}
105
+ />
106
+ </div>
107
+ </div>
108
+ </PopoverContent>
109
+ </Popover>
110
+ {onTimezoneChange ? (
111
+ <TimezoneSelect value={timezone} onChange={onTimezoneChange} />
112
+ ) : null}
113
+ </div>
114
+ )
115
+ }
@@ -0,0 +1,51 @@
1
+ "use client"
2
+
3
+ import { Label } from "@monkey-mini-app/ui/components/label"
4
+ import { DateRangePicker } from "@monkey-mini-app/ui/composites/date-range-picker"
5
+ import { DateTimePicker } from "@monkey-mini-app/ui/composites/date-time-picker"
6
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context"
7
+
8
+ export type DateTimeRange = { start?: Date; end?: Date }
9
+
10
+ export function DateTimeRangePicker({
11
+ value,
12
+ onChange,
13
+ allDay = false,
14
+ }: {
15
+ value?: DateTimeRange
16
+ onChange?: (value: DateTimeRange) => void
17
+ allDay?: boolean
18
+ }) {
19
+ const t = useLabels("dateTimeRange")
20
+ if (allDay) {
21
+ return (
22
+ <div data-testid="date-time-range-picker">
23
+ <DateRangePicker
24
+ value={{ from: value?.start, to: value?.end }}
25
+ onChange={(range) => onChange?.({ start: range?.from, end: range?.to })}
26
+ />
27
+ </div>
28
+ )
29
+ }
30
+
31
+ return (
32
+ <div className="flex flex-col gap-2" data-testid="date-time-range-picker">
33
+ <div className="grid gap-1">
34
+ <Label>{t.start}</Label>
35
+ <DateTimePicker
36
+ value={value?.start}
37
+ onChange={(start) => onChange?.({ start, end: value?.end })}
38
+ className="w-full"
39
+ />
40
+ </div>
41
+ <div className="grid gap-1">
42
+ <Label>{t.end}</Label>
43
+ <DateTimePicker
44
+ value={value?.end}
45
+ onChange={(end) => onChange?.({ start: value?.start, end })}
46
+ className="w-full"
47
+ />
48
+ </div>
49
+ </div>
50
+ )
51
+ }
@@ -0,0 +1,122 @@
1
+ "use client"
2
+
3
+ import { Minus, Plus } from "lucide-react"
4
+
5
+ import { Button } from "@monkey-mini-app/ui/components/button"
6
+ import { Input } from "@monkey-mini-app/ui/components/input"
7
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context"
8
+
9
+ export function parseDuration(input: string): number {
10
+ const hours = Number(/(\d+)\s*h/i.exec(input)?.[1] ?? 0)
11
+ const minutes = Number(/(\d+)\s*m/i.exec(input)?.[1] ?? 0)
12
+ if (!/[hm]/i.test(input) && /^\d+$/.test(input.trim())) {
13
+ return Number(input)
14
+ }
15
+ return hours * 60 + minutes
16
+ }
17
+
18
+ export function formatDuration(totalMinutes: number): string {
19
+ const hours = Math.floor(Math.max(0, totalMinutes) / 60)
20
+ const rest = Math.max(0, totalMinutes) % 60
21
+ if (hours && rest) return `${hours}h ${rest}m`
22
+ if (hours) return `${hours}h`
23
+ return `${rest}m`
24
+ }
25
+
26
+ function split(value?: string) {
27
+ const total = parseDuration(value ?? "0m")
28
+ return { hours: Math.floor(total / 60), minutes: total % 60 }
29
+ }
30
+
31
+ function Segment({
32
+ label,
33
+ value,
34
+ onChange,
35
+ min = 0,
36
+ max,
37
+ }: {
38
+ label: string
39
+ value: number
40
+ onChange: (value: number) => void
41
+ min?: number
42
+ max?: number
43
+ }) {
44
+ const t = useLabels("durationInput")
45
+ const clamp = (next: number) => {
46
+ if (max != null) next = Math.min(max, next)
47
+ return Math.max(min, next)
48
+ }
49
+ return (
50
+ <div className="flex items-center gap-1">
51
+ <Button
52
+ type="button"
53
+ variant="ghost"
54
+ size="icon-xs"
55
+ aria-label={t.decrease(label)}
56
+ onClick={() => onChange(clamp(value - 1))}
57
+ >
58
+ <Minus />
59
+ </Button>
60
+ <Input
61
+ inputMode="numeric"
62
+ value={String(value)}
63
+ aria-label={label}
64
+ onChange={(event) => {
65
+ const next = Number(event.target.value.replace(/\D/g, "") || 0)
66
+ onChange(clamp(next))
67
+ }}
68
+ className="h-7 w-10 border-0 bg-transparent p-0 text-center shadow-none focus-visible:ring-0"
69
+ />
70
+ <span className="text-muted-foreground w-4 text-xs">{label}</span>
71
+ <Button
72
+ type="button"
73
+ variant="ghost"
74
+ size="icon-xs"
75
+ aria-label={t.increase(label)}
76
+ onClick={() => onChange(clamp(value + 1))}
77
+ >
78
+ <Plus />
79
+ </Button>
80
+ </div>
81
+ )
82
+ }
83
+
84
+ export function DurationInput({
85
+ value,
86
+ onChange,
87
+ }: {
88
+ value?: string
89
+ onChange?: (value: string, minutes: number) => void
90
+ }) {
91
+ const t = useLabels("durationInput")
92
+ const { hours, minutes } = split(value)
93
+ const commit = (h: number, m: number) => {
94
+ let total = h * 60 + m
95
+ if (total < 0) total = 0
96
+ onChange?.(formatDuration(total), total)
97
+ }
98
+
99
+ return (
100
+ <div
101
+ data-testid="duration-input"
102
+ className="inline-flex items-center gap-1 rounded-lg border bg-background px-1 py-0.5"
103
+ >
104
+ <Segment
105
+ label={t.hours}
106
+ value={hours}
107
+ onChange={(h) => commit(h, minutes)}
108
+ />
109
+ <span className="text-muted-foreground/40">·</span>
110
+ <Segment
111
+ label={t.minutes}
112
+ value={minutes}
113
+ min={-1}
114
+ onChange={(m) => {
115
+ if (m >= 60) commit(hours + 1, 0)
116
+ else if (m < 0) commit(hours <= 0 ? 0 : hours - 1, hours <= 0 ? 0 : 59)
117
+ else commit(hours, m)
118
+ }}
119
+ />
120
+ </div>
121
+ )
122
+ }
@@ -0,0 +1,24 @@
1
+ "use client"
2
+
3
+ import { Calendar } from "@monkey-mini-app/ui/components/calendar"
4
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
5
+
6
+ export function MiniCalendar({
7
+ value,
8
+ onChange,
9
+ className,
10
+ }: {
11
+ value?: Date
12
+ onChange?: (date: Date | undefined) => void
13
+ className?: string
14
+ }) {
15
+ return (
16
+ <Calendar
17
+ mode="single"
18
+ selected={value}
19
+ onSelect={onChange}
20
+ data-testid="mini-calendar"
21
+ className={cn("rounded-xl border [--cell-size:--spacing(7)]", className)}
22
+ />
23
+ )
24
+ }
@@ -0,0 +1,53 @@
1
+ "use client"
2
+
3
+ import { Button } from "@monkey-mini-app/ui/components/button"
4
+ import { Input } from "@monkey-mini-app/ui/components/input"
5
+ import { Minus, Plus } from "lucide-react"
6
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context"
7
+
8
+ export function NumberField({
9
+ value = 0,
10
+ onChange,
11
+ min,
12
+ max,
13
+ step = 1,
14
+ }: {
15
+ value?: number
16
+ onChange?: (value: number) => void
17
+ min?: number
18
+ max?: number
19
+ step?: number
20
+ }) {
21
+ const clamp = (next: number) => {
22
+ if (min != null) next = Math.max(min, next)
23
+ if (max != null) next = Math.min(max, next)
24
+ return next
25
+ }
26
+ const t = useLabels("numberField")
27
+ return (
28
+ <div className="flex items-center gap-1" data-testid="number-field">
29
+ <Button
30
+ variant="outline"
31
+ size="icon-xs"
32
+ aria-label={t.decrease}
33
+ onClick={() => onChange?.(clamp(value - step))}
34
+ >
35
+ <Minus />
36
+ </Button>
37
+ <Input
38
+ type="number"
39
+ value={value}
40
+ onChange={(event) => onChange?.(clamp(Number(event.target.value)))}
41
+ className="w-20 text-center"
42
+ />
43
+ <Button
44
+ variant="outline"
45
+ size="icon-xs"
46
+ aria-label={t.increase}
47
+ onClick={() => onChange?.(clamp(value + step))}
48
+ >
49
+ <Plus />
50
+ </Button>
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,44 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Eye, EyeOff } 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 PasswordField({
15
+ value,
16
+ onChange,
17
+ }: {
18
+ value?: string
19
+ onChange?: (value: string) => void
20
+ }) {
21
+ const t = useLabels("passwordField")
22
+ const [visible, setVisible] = React.useState(false)
23
+ return (
24
+ <InputGroup data-testid="password-field">
25
+ <InputGroupInput
26
+ type={visible ? "text" : "password"}
27
+ value={value ?? ""}
28
+ onChange={(event) => onChange?.(event.target.value)}
29
+ autoComplete="current-password"
30
+ />
31
+ <InputGroupAddon align="inline-end">
32
+ <Button
33
+ variant="ghost"
34
+ size="icon-xs"
35
+ type="button"
36
+ aria-label={visible ? t.hide : t.show}
37
+ onClick={() => setVisible((v) => !v)}
38
+ >
39
+ {visible ? <EyeOff /> : <Eye />}
40
+ </Button>
41
+ </InputGroupAddon>
42
+ </InputGroup>
43
+ )
44
+ }
@@ -0,0 +1,22 @@
1
+ "use client"
2
+
3
+ import { Input } from "@monkey-mini-app/ui/components/input"
4
+
5
+ export function PhoneInput({
6
+ value,
7
+ onChange,
8
+ }: {
9
+ value?: string
10
+ onChange?: (value: string) => void
11
+ }) {
12
+ return (
13
+ <Input
14
+ data-testid="phone-input"
15
+ type="tel"
16
+ inputMode="tel"
17
+ placeholder="+86 138 0000 0000"
18
+ value={value ?? ""}
19
+ onChange={(event) => onChange?.(event.target.value)}
20
+ />
21
+ )
22
+ }
@@ -0,0 +1,39 @@
1
+ "use client"
2
+
3
+ import { Star } from "lucide-react"
4
+
5
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
6
+
7
+ export function Rating({
8
+ value = 0,
9
+ onChange,
10
+ max = 5,
11
+ }: {
12
+ value?: number
13
+ onChange?: (value: number) => void
14
+ max?: number
15
+ }) {
16
+ return (
17
+ <div className="flex items-center gap-0.5" data-testid="rating">
18
+ {Array.from({ length: max }, (_, index) => {
19
+ const score = index + 1
20
+ return (
21
+ <button
22
+ key={score}
23
+ type="button"
24
+ aria-label={`${score} star`}
25
+ onClick={() => onChange?.(score)}
26
+ className="text-muted-foreground hover:text-foreground"
27
+ >
28
+ <Star
29
+ className={cn(
30
+ "size-4",
31
+ score <= value && "fill-amber-400 text-amber-400"
32
+ )}
33
+ />
34
+ </button>
35
+ )
36
+ })}
37
+ </div>
38
+ )
39
+ }
@@ -0,0 +1,62 @@
1
+ "use client"
2
+
3
+ import { subDays } from "date-fns"
4
+ import type { DateRange } from "react-day-picker"
5
+
6
+ import { Button } from "@monkey-mini-app/ui/components/button"
7
+ import { DateRangePicker } from "@monkey-mini-app/ui/composites/date-range-picker"
8
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context"
9
+
10
+ export type RelativePreset = "today" | "7d" | "30d" | "sprint" | "custom"
11
+
12
+ function rangeFor(id: RelativePreset): DateRange | undefined {
13
+ const now = new Date()
14
+ if (id === "today") return { from: now, to: now }
15
+ if (id === "7d") return { from: subDays(now, 6), to: now }
16
+ if (id === "30d") return { from: subDays(now, 29), to: now }
17
+ if (id === "sprint") return { from: subDays(now, 13), to: now }
18
+ return undefined
19
+ }
20
+
21
+ export function RelativeDatePicker({
22
+ preset = "7d",
23
+ value,
24
+ onPresetChange,
25
+ onChange,
26
+ }: {
27
+ preset?: RelativePreset
28
+ value?: DateRange
29
+ onPresetChange?: (preset: RelativePreset) => void
30
+ onChange?: (range: DateRange | undefined) => void
31
+ }) {
32
+ const t = useLabels("relativeDate")
33
+ const presets: { id: RelativePreset; label: string }[] = [
34
+ { id: "today", label: t.today },
35
+ { id: "7d", label: t.last7d },
36
+ { id: "30d", label: t.last30d },
37
+ { id: "sprint", label: t.sprint },
38
+ { id: "custom", label: t.custom },
39
+ ]
40
+ return (
41
+ <div className="flex flex-col gap-2" data-testid="relative-date-picker">
42
+ <div className="flex flex-wrap gap-1">
43
+ {presets.map((item) => (
44
+ <Button
45
+ key={item.id}
46
+ size="sm"
47
+ variant={preset === item.id ? "default" : "outline"}
48
+ onClick={() => {
49
+ onPresetChange?.(item.id)
50
+ if (item.id !== "custom") onChange?.(rangeFor(item.id))
51
+ }}
52
+ >
53
+ {item.label}
54
+ </Button>
55
+ ))}
56
+ </div>
57
+ {preset === "custom" ? (
58
+ <DateRangePicker value={value} onChange={onChange} />
59
+ ) : null}
60
+ </div>
61
+ )
62
+ }
@@ -0,0 +1,49 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Search } from "lucide-react"
5
+
6
+ import {
7
+ InputGroup,
8
+ InputGroupAddon,
9
+ InputGroupInput,
10
+ } from "@monkey-mini-app/ui/components/input-group"
11
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context"
12
+
13
+ export function SearchInput({
14
+ value,
15
+ onChange,
16
+ onDebouncedChange,
17
+ delay = 250,
18
+ placeholder,
19
+ }: {
20
+ value?: string
21
+ onChange?: (value: string) => void
22
+ onDebouncedChange?: (value: string) => void
23
+ delay?: number
24
+ placeholder?: string
25
+ }) {
26
+ const t = useLabels("searchInput")
27
+ const [inner, setInner] = React.useState(value ?? "")
28
+ React.useEffect(() => setInner(value ?? ""), [value])
29
+ React.useEffect(() => {
30
+ const id = window.setTimeout(() => onDebouncedChange?.(inner), delay)
31
+ return () => window.clearTimeout(id)
32
+ }, [inner, delay, onDebouncedChange])
33
+
34
+ return (
35
+ <InputGroup className="max-w-sm" data-testid="search-input">
36
+ <InputGroupAddon>
37
+ <Search className="size-3.5" />
38
+ </InputGroupAddon>
39
+ <InputGroupInput
40
+ value={inner}
41
+ placeholder={placeholder ?? t.placeholder}
42
+ onChange={(event) => {
43
+ setInner(event.target.value)
44
+ onChange?.(event.target.value)
45
+ }}
46
+ />
47
+ </InputGroup>
48
+ )
49
+ }