@monkey-mini-app/ui 0.1.0 → 0.1.4

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 (203) hide show
  1. package/README.md +58 -0
  2. package/dist/globals.css +1 -1
  3. package/dist/index.js +12 -2
  4. package/dist/manifest.json +6 -6
  5. package/dist/runtime.js +9 -0
  6. package/dist/sdk.js +158 -0
  7. package/dist/src/blocks/activity-feed.tsx +7 -0
  8. package/dist/src/blocks/app-shell.tsx +1 -0
  9. package/dist/src/blocks/attachment-gallery.tsx +7 -0
  10. package/dist/src/blocks/bar-chart.tsx +15 -1
  11. package/dist/src/blocks/comment-thread.tsx +7 -0
  12. package/dist/src/blocks/dashboard-shell.tsx +109 -0
  13. package/dist/src/blocks/description-list.tsx +7 -0
  14. package/dist/src/blocks/detail-panel.tsx +1 -0
  15. package/dist/src/blocks/donut-chart.tsx +19 -2
  16. package/dist/src/blocks/env-badge.tsx +7 -0
  17. package/dist/src/blocks/env-table.tsx +316 -0
  18. package/dist/src/blocks/file-tree.tsx +7 -0
  19. package/dist/src/blocks/filter-bar.tsx +1 -0
  20. package/dist/src/blocks/form-sheet.tsx +160 -0
  21. package/dist/src/blocks/gauge.tsx +29 -6
  22. package/dist/src/blocks/list-detail.tsx +121 -0
  23. package/dist/src/blocks/notification-center.tsx +7 -0
  24. package/dist/src/blocks/page-header.tsx +1 -0
  25. package/dist/src/blocks/progress-ring.tsx +20 -2
  26. package/dist/src/blocks/radar-chart.tsx +18 -1
  27. package/dist/src/blocks/request-inspector.tsx +7 -0
  28. package/dist/src/blocks/reveal.tsx +56 -0
  29. package/dist/src/blocks/run-timeline.tsx +7 -0
  30. package/dist/src/blocks/scrollspy.tsx +7 -0
  31. package/dist/src/blocks/settings-split.tsx +154 -0
  32. package/dist/src/blocks/severity-chip.tsx +7 -0
  33. package/dist/src/blocks/sparkline.tsx +12 -3
  34. package/dist/src/blocks/stat-card.tsx +1 -0
  35. package/dist/src/blocks/status-badge.tsx +1 -0
  36. package/dist/src/blocks/table-page.tsx +83 -0
  37. package/dist/src/blocks/terminal.tsx +21 -3
  38. package/dist/src/blocks/test-step-list.tsx +7 -0
  39. package/dist/src/blocks/trend-card.tsx +7 -0
  40. package/dist/src/blocks/wizard-shell.tsx +132 -0
  41. package/dist/src/cdn-modules.d.ts +5 -0
  42. package/dist/src/components/accordion.tsx +7 -0
  43. package/dist/src/components/alert-dialog.tsx +7 -0
  44. package/dist/src/components/alert.tsx +7 -0
  45. package/dist/src/components/aspect-ratio.tsx +4 -0
  46. package/dist/src/components/attachment.tsx +4 -0
  47. package/dist/src/components/avatar.tsx +4 -0
  48. package/dist/src/components/badge.tsx +7 -0
  49. package/dist/src/components/breadcrumb.tsx +4 -0
  50. package/dist/src/components/bubble.tsx +4 -0
  51. package/dist/src/components/button-group.tsx +6 -0
  52. package/dist/src/components/button.tsx +7 -0
  53. package/dist/src/components/calendar.tsx +4 -0
  54. package/dist/src/components/card.tsx +8 -1
  55. package/dist/src/components/chart.tsx +7 -0
  56. package/dist/src/components/checkbox.tsx +5 -0
  57. package/dist/src/components/collapsible.tsx +4 -0
  58. package/dist/src/components/combobox.tsx +4 -0
  59. package/dist/src/components/command.tsx +6 -0
  60. package/dist/src/components/context-menu.tsx +4 -0
  61. package/dist/src/components/dialog.tsx +21 -0
  62. package/dist/src/components/drawer.tsx +4 -0
  63. package/dist/src/components/dropdown-menu.tsx +7 -0
  64. package/dist/src/components/empty.tsx +7 -0
  65. package/dist/src/components/field.tsx +6 -0
  66. package/dist/src/components/hover-card.tsx +4 -0
  67. package/dist/src/components/input-group.tsx +4 -0
  68. package/dist/src/components/input.tsx +7 -0
  69. package/dist/src/components/item.tsx +4 -0
  70. package/dist/src/components/kbd.tsx +4 -0
  71. package/dist/src/components/label.tsx +7 -0
  72. package/dist/src/components/marker.tsx +4 -0
  73. package/dist/src/components/menubar.tsx +4 -0
  74. package/dist/src/components/message-scroller.tsx +4 -0
  75. package/dist/src/components/message.tsx +4 -0
  76. package/dist/src/components/native-select.tsx +4 -0
  77. package/dist/src/components/navigation-menu.tsx +4 -0
  78. package/dist/src/components/pagination.tsx +4 -0
  79. package/dist/src/components/popover.tsx +7 -0
  80. package/dist/src/components/progress.tsx +7 -0
  81. package/dist/src/components/questionnaire.tsx +4 -0
  82. package/dist/src/components/radio-group.tsx +4 -0
  83. package/dist/src/components/resizable.tsx +7 -0
  84. package/dist/src/components/scroll-area.tsx +4 -0
  85. package/dist/src/components/select.tsx +7 -0
  86. package/dist/src/components/separator.tsx +5 -0
  87. package/dist/src/components/sheet.tsx +22 -0
  88. package/dist/src/components/sidebar.tsx +4 -0
  89. package/dist/src/components/skeleton.tsx +7 -0
  90. package/dist/src/components/slider.tsx +4 -0
  91. package/dist/src/components/spinner.tsx +6 -0
  92. package/dist/src/components/switch.tsx +7 -0
  93. package/dist/src/components/table.tsx +7 -0
  94. package/dist/src/components/tabs.tsx +7 -0
  95. package/dist/src/components/textarea.tsx +7 -0
  96. package/dist/src/components/toast.tsx +7 -0
  97. package/dist/src/components/toggle-group.tsx +4 -0
  98. package/dist/src/components/toggle.tsx +4 -0
  99. package/dist/src/components/tooltip.tsx +7 -0
  100. package/dist/src/composites/autocomplete.tsx +7 -0
  101. package/dist/src/composites/cascader.tsx +7 -0
  102. package/dist/src/composites/color-picker.tsx +7 -0
  103. package/dist/src/composites/confirm-dialog.tsx +7 -0
  104. package/dist/src/composites/copyable.tsx +9 -1
  105. package/dist/src/composites/currency-input.tsx +7 -0
  106. package/dist/src/composites/date-picker.tsx +7 -0
  107. package/dist/src/composites/date-range-picker.tsx +4 -0
  108. package/dist/src/composites/date-time-picker.tsx +7 -0
  109. package/dist/src/composites/date-time-range-picker.tsx +7 -0
  110. package/dist/src/composites/duration-input.tsx +7 -0
  111. package/dist/src/composites/mini-calendar.tsx +7 -0
  112. package/dist/src/composites/number-field.tsx +7 -0
  113. package/dist/src/composites/password-field.tsx +7 -0
  114. package/dist/src/composites/phone-input.tsx +7 -0
  115. package/dist/src/composites/rating.tsx +7 -0
  116. package/dist/src/composites/relative-date-picker.tsx +7 -0
  117. package/dist/src/composites/search-input.tsx +7 -0
  118. package/dist/src/composites/slider-range.tsx +7 -0
  119. package/dist/src/composites/tag-input.tsx +7 -0
  120. package/dist/src/composites/time-picker.tsx +11 -1
  121. package/dist/src/composites/time-range-picker.tsx +7 -0
  122. package/dist/src/composites/timezone-select.tsx +7 -0
  123. package/dist/src/composites/transfer.tsx +7 -0
  124. package/dist/src/composites/user-picker.tsx +7 -0
  125. package/dist/src/hooks/use-html-dark.ts +37 -0
  126. package/dist/src/hooks/use-mobile.ts +3 -0
  127. package/dist/src/i18n/en.ts +109 -2
  128. package/dist/src/i18n/zh.ts +99 -2
  129. package/dist/src/index.ts +147 -134
  130. package/dist/src/lib/clipboard.ts +10 -0
  131. package/dist/src/lib/illustrations.tsx +2 -2
  132. package/dist/src/products/code-block.tsx +35 -18
  133. package/dist/src/products/code-editor.tsx +156 -24
  134. package/dist/src/products/commit-graph.tsx +644 -0
  135. package/dist/src/products/data-grid.tsx +2 -1
  136. package/dist/src/products/diff-viewer.tsx +104 -8
  137. package/dist/src/products/event-calendar.tsx +28 -823
  138. package/dist/src/products/file-dropzone.tsx +7 -0
  139. package/dist/src/products/full-calendar/animations.ts +38 -0
  140. package/dist/src/products/full-calendar/calendar-body.tsx +77 -0
  141. package/dist/src/products/full-calendar/constants.ts +10 -0
  142. package/dist/src/products/full-calendar/contexts/calendar-context.tsx +255 -0
  143. package/dist/src/products/full-calendar/contexts/dnd-context.tsx +157 -0
  144. package/dist/src/products/full-calendar/dialogs/add-edit-event-dialog.tsx +187 -0
  145. package/dist/src/products/full-calendar/dialogs/delete-event-dialog.tsx +39 -0
  146. package/dist/src/products/full-calendar/dialogs/event-details-dialog.tsx +100 -0
  147. package/dist/src/products/full-calendar/dialogs/events-list-dialog.tsx +87 -0
  148. package/dist/src/products/full-calendar/dnd/draggable-event.tsx +45 -0
  149. package/dist/src/products/full-calendar/dnd/droppable-area.tsx +45 -0
  150. package/dist/src/products/full-calendar/dnd/resizable-event.tsx +179 -0
  151. package/dist/src/products/full-calendar/header/calendar-header.tsx +65 -0
  152. package/dist/src/products/full-calendar/header/date-navigator.tsx +110 -0
  153. package/dist/src/products/full-calendar/header/filter.tsx +58 -0
  154. package/dist/src/products/full-calendar/header/today-button.tsx +45 -0
  155. package/dist/src/products/full-calendar/header/user-select.tsx +64 -0
  156. package/dist/src/products/full-calendar/header/view-tabs.tsx +36 -0
  157. package/dist/src/products/full-calendar/helpers.ts +453 -0
  158. package/dist/src/products/full-calendar/hooks/use-time-range-create.ts +152 -0
  159. package/dist/src/products/full-calendar/hooks.ts +70 -0
  160. package/dist/src/products/full-calendar/interfaces.ts +23 -0
  161. package/dist/src/products/full-calendar/settings/settings.tsx +80 -0
  162. package/dist/src/products/full-calendar/shims/avatar-group.tsx +16 -0
  163. package/dist/src/products/full-calendar/shims/day-picker.tsx +1 -0
  164. package/dist/src/products/full-calendar/shims/motion.tsx +97 -0
  165. package/dist/src/products/full-calendar/shims/next-themes.ts +6 -0
  166. package/dist/src/products/full-calendar/shims/re-resizable.tsx +25 -0
  167. package/dist/src/products/full-calendar/shims/sonner.ts +4 -0
  168. package/dist/src/products/full-calendar/types.ts +8 -0
  169. package/dist/src/products/full-calendar/views/agenda-view/agenda-events.tsx +140 -0
  170. package/dist/src/products/full-calendar/views/month-view/calendar-month-view.tsx +69 -0
  171. package/dist/src/products/full-calendar/views/month-view/day-cell.tsx +201 -0
  172. package/dist/src/products/full-calendar/views/month-view/event-bullet.tsx +40 -0
  173. package/dist/src/products/full-calendar/views/month-view/month-event-badge.tsx +143 -0
  174. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-day-view.tsx +295 -0
  175. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-time-line.tsx +35 -0
  176. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-week-view.tsx +253 -0
  177. package/dist/src/products/full-calendar/views/week-and-day-view/day-view-multi-day-events-row.tsx +73 -0
  178. package/dist/src/products/full-calendar/views/week-and-day-view/event-block.tsx +114 -0
  179. package/dist/src/products/full-calendar/views/week-and-day-view/render-grouped-events.tsx +49 -0
  180. package/dist/src/products/full-calendar/views/week-and-day-view/week-view-multi-day-events-row.tsx +143 -0
  181. package/dist/src/products/full-calendar/views/year-view/calendar-year-view.tsx +161 -0
  182. package/dist/src/products/gantt.tsx +7 -0
  183. package/dist/src/products/jira-wiki.tsx +1 -0
  184. package/dist/src/products/jql-input.tsx +340 -37
  185. package/dist/src/products/json-viewer.tsx +7 -0
  186. package/dist/src/products/kanban-issue-panel.tsx +7 -0
  187. package/dist/src/products/kanban.tsx +1 -0
  188. package/dist/src/products/log-viewer.tsx +390 -18
  189. package/dist/src/products/markdown-editor.tsx +21 -6
  190. package/dist/src/products/markdown.tsx +7 -0
  191. package/dist/src/products/rich-text-editor.tsx +65 -59
  192. package/dist/src/products/sortable-list.tsx +7 -0
  193. package/dist/src/products/stepper.tsx +219 -70
  194. package/dist/src/products/timeline.tsx +7 -0
  195. package/dist/src/products/tree-view.tsx +7 -0
  196. package/dist/src/sdk/app-error-boundary.tsx +240 -0
  197. package/dist/src/sdk/app-id.ts +7 -0
  198. package/dist/src/sdk/use-app.ts +126 -0
  199. package/dist/src/styles/globals.css +7 -7
  200. package/dist/vendors/lodash.js +20 -0
  201. package/dist/vendors/motion.js +12 -0
  202. package/package.json +23 -19
  203. package/dist/src/products/jql-language.ts +0 -70
@@ -0,0 +1,160 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import type { FormEvent, ReactNode } from "react";
5
+
6
+ import {
7
+ AlertDialog,
8
+ AlertDialogCancel,
9
+ AlertDialogContent,
10
+ AlertDialogDescription,
11
+ AlertDialogAction,
12
+ AlertDialogFooter,
13
+ AlertDialogHeader,
14
+ AlertDialogTitle,
15
+ } from "@monkey-mini-app/ui/components/alert-dialog";
16
+ import {
17
+ Sheet,
18
+ SheetContent,
19
+ SheetDescription,
20
+ SheetHeader,
21
+ SheetTitle,
22
+ } from "@monkey-mini-app/ui/components/sheet";
23
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context";
24
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
25
+
26
+ export type FormSheetProps = {
27
+ open: boolean;
28
+ onOpenChange: (open: boolean) => void;
29
+ title: ReactNode;
30
+ description?: ReactNode;
31
+ /** Anything below the title: a badge, a record id, a tab strip. */
32
+ header?: ReactNode;
33
+ /** The scrolling form fields. */
34
+ children: ReactNode;
35
+ /** Action row, pinned. Put the primary control in here with `type="submit"`. */
36
+ footer?: ReactNode;
37
+ /** One submit path for the whole sheet: Enter in a field and the footer button land here. */
38
+ onSubmit?: (e: FormEvent<HTMLFormElement>) => void;
39
+ side?: "top" | "right" | "bottom" | "left";
40
+ width?: string | number;
41
+ /** Ask before throwing edits away. The app owns the check; the preset owns the prompt. */
42
+ dirty?: boolean;
43
+ className?: string;
44
+ };
45
+
46
+ /**
47
+ * Create / edit form over a list you do not want to lose.
48
+ *
49
+ * The usual build has three cracks: the header and footer scroll away with the fields (so the
50
+ * Cancel button is somewhere the user has to hunt for), Enter in a text field submits *something
51
+ * else* or nothing because there is no form around the inputs, and Escape closes a sheet full of
52
+ * unsaved edits. Here only the body is a scroller — header, form and footer are siblings inside a
53
+ * flex column — the fields and the footer share one `<form onSubmit>`, and a dirty sheet asks
54
+ * first.
55
+ *
56
+ * @when Page shape: focused create/edit over a list — new record, edit record, wizard-less config
57
+ * @example
58
+ * <FormSheet
59
+ * open={Boolean(editing)}
60
+ * onOpenChange={(o) => !o && setEditing(null)}
61
+ * title="Edit issue"
62
+ * dirty={dirty}
63
+ * onSubmit={save}
64
+ * footer={<Button type="submit">Save</Button>}
65
+ * >
66
+ * <Input name="summary" />
67
+ * </FormSheet>
68
+ * @family Layout & structure
69
+ */
70
+ export function FormSheet({
71
+ open,
72
+ onOpenChange,
73
+ title,
74
+ description,
75
+ header,
76
+ children,
77
+ footer,
78
+ onSubmit,
79
+ side = "right",
80
+ width = 480,
81
+ dirty = false,
82
+ className,
83
+ }: FormSheetProps) {
84
+ const t = useLabels("formSheet");
85
+ const [guard, setGuard] = React.useState(false);
86
+
87
+ const request = (next: boolean) => {
88
+ if (!next && dirty) {
89
+ setGuard(true);
90
+ return;
91
+ }
92
+ onOpenChange(next);
93
+ };
94
+
95
+ return (
96
+ <>
97
+ <Sheet open={open} onOpenChange={request}>
98
+ <SheetContent
99
+ side={side}
100
+ width={width}
101
+ className={cn("flex min-h-0 flex-col gap-0 p-0", className)}
102
+ data-testid="form-sheet"
103
+ >
104
+ <SheetHeader className="border-border shrink-0 border-b px-6 py-4 text-left">
105
+ <SheetTitle className="text-base">{title}</SheetTitle>
106
+ {description ? (
107
+ <SheetDescription>{description}</SheetDescription>
108
+ ) : null}
109
+ {header}
110
+ </SheetHeader>
111
+
112
+ {/* One form wraps the scroller *and* the footer, so Enter and the button are the same path. */}
113
+ <form
114
+ className="flex min-h-0 flex-1 flex-col"
115
+ onSubmit={(e) => {
116
+ e.preventDefault();
117
+ onSubmit?.(e);
118
+ }}
119
+ data-testid="form-sheet-form"
120
+ >
121
+ <div
122
+ className="min-h-0 flex-1 overflow-y-auto px-6 py-4"
123
+ data-testid="form-sheet-body"
124
+ >
125
+ {children}
126
+ </div>
127
+ {footer ? (
128
+ <div
129
+ className="border-border bg-background shrink-0 border-t px-6 py-3"
130
+ data-testid="form-sheet-footer"
131
+ >
132
+ {footer}
133
+ </div>
134
+ ) : null}
135
+ </form>
136
+ </SheetContent>
137
+ </Sheet>
138
+
139
+ <AlertDialog open={guard} onOpenChange={setGuard}>
140
+ <AlertDialogContent data-testid="form-sheet-guard">
141
+ <AlertDialogHeader>
142
+ <AlertDialogTitle>{t.discardTitle}</AlertDialogTitle>
143
+ <AlertDialogDescription>{t.discardBody}</AlertDialogDescription>
144
+ </AlertDialogHeader>
145
+ <AlertDialogFooter>
146
+ <AlertDialogCancel>{t.keepEditing}</AlertDialogCancel>
147
+ <AlertDialogAction
148
+ onClick={() => {
149
+ setGuard(false);
150
+ onOpenChange(false);
151
+ }}
152
+ >
153
+ {t.discard}
154
+ </AlertDialogAction>
155
+ </AlertDialogFooter>
156
+ </AlertDialogContent>
157
+ </AlertDialog>
158
+ </>
159
+ );
160
+ }
@@ -1,13 +1,36 @@
1
1
  import { ProgressRing } from "@monkey-mini-app/ui/blocks/progress-ring"
2
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
2
3
 
3
- export function Gauge({ value, label }: { value: number; label?: string }) {
4
+ /**
5
+ * Arc gauge for one 0–100 metric.
6
+ * @when A single health/capacity number that needs a zone (SLO, disk). Series over time → `Sparkline`.
7
+ * @example
8
+ * <Gauge value={73} label="磁盘" />
9
+ * @family Chart & data
10
+ */
11
+ export function Gauge({
12
+ value,
13
+ label,
14
+ size = 112,
15
+ className,
16
+ }: {
17
+ value: number
18
+ label?: string
19
+ size?: number
20
+ className?: string
21
+ }) {
4
22
  return (
5
- <div className="flex items-center gap-2" data-testid="gauge">
6
- <ProgressRing value={value} />
7
- <div>
8
- <div className="text-sm font-medium">{value}%</div>
9
- {label ? <div className="text-muted-foreground text-xs">{label}</div> : null}
23
+ <div
24
+ className={cn("flex flex-col items-center gap-2", className)}
25
+ data-testid="gauge"
26
+ >
27
+ <div className="relative" style={{ width: size, height: size }}>
28
+ <ProgressRing value={value} className="h-full w-full" />
29
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
30
+ <span className="text-2xl font-semibold tabular-nums">{value}%</span>
31
+ </div>
10
32
  </div>
33
+ {label ? <div className="text-xs text-muted-foreground">{label}</div> : null}
11
34
  </div>
12
35
  )
13
36
  }
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import {
6
+ ResizableHandle,
7
+ ResizablePanel,
8
+ ResizablePanelGroup,
9
+ } from "@monkey-mini-app/ui/components/resizable";
10
+ import { useIsMobile } from "@monkey-mini-app/ui/hooks/use-mobile";
11
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context";
12
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
13
+
14
+ export type ListDetailProps = {
15
+ /** The collection pane: list, table, board — anything you scroll through. */
16
+ list: ReactNode;
17
+ /** The record pane. Pass `undefined` for "nothing selected" — `empty` renders instead. */
18
+ detail?: ReactNode;
19
+ /** Placeholder for the record pane while nothing is selected. */
20
+ empty?: ReactNode;
21
+ /** Search / filters / create button. Fixed above the panes; it never scrolls. */
22
+ toolbar?: ReactNode;
23
+ /** Narrow screens show one pane at a time. The app owns which one. */
24
+ mobileView?: "list" | "detail";
25
+ /** Renders a back control on the record pane below `md`. No-op on wide screens. */
26
+ onMobileBack?: () => void;
27
+ /** Collection pane share of the width, 0-100. Default 34. */
28
+ defaultSize?: number;
29
+ /** Collection pane floor, so a long record title cannot push it away. Default 18. */
30
+ minSize?: number;
31
+ className?: string;
32
+ };
33
+
34
+ /** One scroll container per pane, and nothing above it that also scrolls. */
35
+ const PANE = "h-full min-h-0 w-full overflow-y-auto";
36
+
37
+ /**
38
+ * Two-pane record browser: the list holds its place while the selected record scrolls on its own.
39
+ *
40
+ * This is the shape agents get wrong without help — a flex child with no `min-h-0` grows to fit
41
+ * its content, so the whole page scrolls, the toolbar slides away, and the two panes are never
42
+ * independent. Both panes scroll here by construction, and the record pane collapses to a
43
+ * full-width view below `md` instead of squashing.
44
+ *
45
+ * @when Page shape: click a row, then read or edit that one record beside the list — orders, tickets, contacts, log lines
46
+ * @example
47
+ * <ListDetail
48
+ * toolbar={<SearchInput />}
49
+ * list={<OrderList />}
50
+ * detail={order ? <OrderCard order={order} /> : undefined}
51
+ * empty={<div>Pick an order</div>}
52
+ * mobileView={order ? "detail" : "list"}
53
+ * onMobileBack={() => setOrder(null)}
54
+ * />
55
+ * @family Layout & structure
56
+ */
57
+ export function ListDetail({
58
+ list,
59
+ detail,
60
+ empty,
61
+ toolbar,
62
+ mobileView = "list",
63
+ onMobileBack,
64
+ defaultSize = 34,
65
+ minSize = 18,
66
+ className,
67
+ }: ListDetailProps) {
68
+ const isMobile = useIsMobile();
69
+ const t = useLabels("listDetail");
70
+
71
+ const listPane = (
72
+ <div className={PANE} data-testid="list-detail-list">
73
+ {list}
74
+ </div>
75
+ );
76
+
77
+ const detailPane = (
78
+ <div className={PANE} data-testid="list-detail-detail">
79
+ {isMobile && detail && onMobileBack ? (
80
+ <button
81
+ type="button"
82
+ onClick={onMobileBack}
83
+ className="text-muted-foreground -mb-2 px-4 pt-3 pb-1 text-sm underline-offset-4 hover:underline"
84
+ data-testid="list-detail-back"
85
+ >
86
+ {t.back}
87
+ </button>
88
+ ) : null}
89
+ {detail ?? empty ?? null}
90
+ </div>
91
+ );
92
+
93
+ return (
94
+ <div
95
+ className={cn("flex h-full min-h-0 w-full flex-col", className)}
96
+ data-testid="list-detail"
97
+ >
98
+ {toolbar ? (
99
+ <div className="shrink-0" data-testid="list-detail-toolbar">
100
+ {toolbar}
101
+ </div>
102
+ ) : null}
103
+
104
+ {isMobile ? (
105
+ <div className="min-h-0 flex-1">
106
+ {mobileView === "detail" ? detailPane : listPane}
107
+ </div>
108
+ ) : (
109
+ <ResizablePanelGroup orientation="horizontal" className="min-h-0 flex-1">
110
+ <ResizablePanel defaultSize={defaultSize} minSize={minSize}>
111
+ {listPane}
112
+ </ResizablePanel>
113
+ <ResizableHandle withHandle />
114
+ <ResizablePanel defaultSize={100 - defaultSize} minSize={30}>
115
+ {detailPane}
116
+ </ResizablePanel>
117
+ </ResizablePanelGroup>
118
+ )}
119
+ </div>
120
+ );
121
+ }
@@ -6,6 +6,13 @@ export type NotificationItem = {
6
6
  body?: string
7
7
  }
8
8
 
9
+ /**
10
+ * Grouped notification list with unread state.
11
+ * @when Inbox/alert panel. Audit log (read-only, with time) → `ActivityFeed`.
12
+ * @example
13
+ * <NotificationCenter items={[{ id: "n1", title: "构建失败", unread: true }]} />
14
+ * @family Feedback & status
15
+ */
9
16
  export function NotificationCenter({ items }: { items: NotificationItem[] }) {
10
17
  return (
11
18
  <div className="flex flex-col gap-1" data-testid="notification-center">
@@ -11,6 +11,7 @@ export type PageHeaderProps = {
11
11
  * @when Top of a page inside AppShell main
12
12
  * @example
13
13
  * <PageHeader title="QA Runs" description="Last 24h" actions={<Button>New</Button>} />
14
+ * @family Layout & structure
14
15
  */
15
16
  export function PageHeader({ title, description, actions }: PageHeaderProps) {
16
17
  return (
@@ -1,4 +1,21 @@
1
- export function ProgressRing({ value }: { value: number }) {
1
+ import { cn } from "../lib/utils"
2
+
3
+ /**
4
+ * Circular determinate progress.
5
+ * @when One percent-done value in a tight space (card corner, table cell).
6
+ * @example
7
+ * <ProgressRing value={45} size={56} />
8
+ * @family Feedback & status
9
+ */
10
+ export function ProgressRing({
11
+ value,
12
+ size,
13
+ className,
14
+ }: {
15
+ value: number
16
+ size?: number
17
+ className?: string
18
+ }) {
2
19
  const clamped = Math.min(100, Math.max(0, value))
3
20
  const r = 16
4
21
  const c = 2 * Math.PI * r
@@ -7,7 +24,8 @@ export function ProgressRing({ value }: { value: number }) {
7
24
  <svg
8
25
  data-testid="progress-ring"
9
26
  viewBox="0 0 40 40"
10
- className="size-10 -rotate-90"
27
+ style={size ? { width: size, height: size } : undefined}
28
+ className={cn("-rotate-90 size-10", className)}
11
29
  >
12
30
  <circle cx="20" cy="20" r={r} className="fill-none stroke-muted" strokeWidth="4" />
13
31
  <circle
@@ -8,18 +8,35 @@ import {
8
8
  ChartTooltipContent,
9
9
  type ChartConfig,
10
10
  } from "@monkey-mini-app/ui/components/chart"
11
+ import { cn } from "@monkey-mini-app/ui/lib/utils"
11
12
 
13
+ /**
14
+ * Multi-axis radar chart.
15
+ * @when Comparing an item across 4–8 dimensions (scores, coverage).
16
+ * @example
17
+ * <RadarChart data={[{ label: "性能", value: 70 }]} />
18
+ * @family Chart & data
19
+ */
12
20
  export function RadarChart({
13
21
  data,
14
22
  config,
15
23
  dataKey = "value",
24
+ size,
25
+ className,
16
26
  }: {
17
27
  data: { label: string; value: number }[]
18
28
  config: ChartConfig
19
29
  dataKey?: string
30
+ size?: number
31
+ className?: string
20
32
  }) {
21
33
  return (
22
- <ChartContainer config={config} className="mx-auto aspect-square h-56" data-testid="radar-chart">
34
+ <ChartContainer
35
+ config={config}
36
+ className={cn("mx-auto aspect-square", size ? "" : "w-full max-w-80", className)}
37
+ style={size ? { width: size, height: size } : undefined}
38
+ data-testid="radar-chart"
39
+ >
23
40
  <ReRadar data={data}>
24
41
  <PolarGrid />
25
42
  <PolarAngleAxis dataKey="label" />
@@ -1,6 +1,13 @@
1
1
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "@monkey-mini-app/ui/components/tabs"
2
2
  import { CodeBlock } from "@monkey-mini-app/ui/products/code-block"
3
3
 
4
+ /**
5
+ * Side-by-side HTTP request/response panel.
6
+ * @when Debugging one API call: method, url, bodies. Multiple calls streaming → `LogViewer`.
7
+ * @example
8
+ * <RequestInspector method="POST" url="/api/x" response="{\"ok\":true}" />
9
+ * @family Discovery & inspect
10
+ */
4
11
  export function RequestInspector({
5
12
  method,
6
13
  url,
@@ -0,0 +1,56 @@
1
+ import { motion, useReducedMotion } from "motion/react"
2
+
3
+ import { cn } from "../lib/utils"
4
+
5
+ /**
6
+ * Entrance reveal — fade and rise, with a per-item `delay` for a stagger.
7
+ *
8
+ * This is the kit's front door to the platform `motion` build (`/mma/vendors/motion.js`,
9
+ * one shared React). Use it for the common case and reach for
10
+ * `import { motion, AnimatePresence } from "motion/react"` when you need exit, layout,
11
+ * or gesture animation — `AnimatePresence` in particular is what keeps a removing row
12
+ * from vanishing mid-transition.
13
+ *
14
+ * @when A card, row, or section appearing on first paint or after a filter change. Not
15
+ * for a hover/press response (one property: use a Tailwind `transition`) and not for
16
+ * animating an element out (use `AnimatePresence`).
17
+ * @example
18
+ * {rows.map((r, i) => (
19
+ * <Reveal key={r.id} delay={i * 60}>
20
+ * <Card>{r.title}</Card>
21
+ * </Reveal>
22
+ * ))}
23
+ * @family Animation
24
+ */
25
+ export function Reveal({
26
+ children,
27
+ delay = 0,
28
+ distance = 8,
29
+ className,
30
+ style,
31
+ }: {
32
+ children: React.ReactNode
33
+ /** Stagger in ms — pass `i * 60` for a list. */
34
+ delay?: number
35
+ /** Rise distance in px. `0` fades in place. */
36
+ distance?: number
37
+ className?: string
38
+ style?: React.CSSProperties
39
+ }) {
40
+ // OS "reduce motion" wins over the design: no travel, no stagger, just present.
41
+ const reduce = useReducedMotion()
42
+ return (
43
+ <motion.div
44
+ data-mma-reveal=""
45
+ className={cn(className)}
46
+ style={style}
47
+ initial={reduce ? false : { opacity: 0, y: distance }}
48
+ animate={{ opacity: 1, y: 0 }}
49
+ transition={
50
+ reduce ? { duration: 0 } : { duration: 0.32, delay: delay / 1000, ease: "easeOut" }
51
+ }
52
+ >
53
+ {children}
54
+ </motion.div>
55
+ )
56
+ }
@@ -1,5 +1,12 @@
1
1
  import { Timeline, type TimelineItem } from "@monkey-mini-app/ui/products/timeline"
2
2
 
3
+ /**
4
+ * Timeline of run phases with duration + status.
5
+ * @when CI/agent/job phases on a time axis. Plain ordered events → `Timeline`.
6
+ * @example
7
+ * <RunTimeline items={[{ id: "p1", title: "checkout", status: "pass", time: "3s" }]} />
8
+ * @family Realtime
9
+ */
3
10
  export function RunTimeline({ items }: { items: TimelineItem[] }) {
4
11
  return <Timeline items={items} />
5
12
  }
@@ -2,6 +2,13 @@
2
2
 
3
3
  import * as React from "react"
4
4
 
5
+ /**
6
+ * Anchor nav that highlights the section in view.
7
+ * @when Long settings/report page with jump links.
8
+ * @example
9
+ * <Scrollspy sections={[{ id: "overview", label: "概览" }]} />
10
+ * @family Navigation
11
+ */
5
12
  export function Scrollspy({
6
13
  sections,
7
14
  }: {
@@ -0,0 +1,154 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import type { ReactNode } from "react";
5
+
6
+ import { Scrollspy } from "@monkey-mini-app/ui/blocks/scrollspy";
7
+ import { useIsMobile } from "@monkey-mini-app/ui/hooks/use-mobile";
8
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context";
9
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
10
+
11
+ export type SettingsSection = {
12
+ /** The anchor id. The preset puts it on the section **and** hands the same value to the jump
13
+ * list, so a nav that silently never highlights is not reachable from outside. */
14
+ id: string;
15
+ label: string;
16
+ /** One line under the heading — what this section is for. */
17
+ description?: string;
18
+ content: ReactNode;
19
+ };
20
+
21
+ export type SettingsSplitProps = {
22
+ /** The page's sections, in nav order. */
23
+ sections: SettingsSection[];
24
+ /** Anything above the jump list: page title, search, a plan badge. */
25
+ nav?: ReactNode;
26
+ /** Pinned to the bottom of the **content** pane — an unsaved-changes bar belongs to what it
27
+ * applies to, not to the window. */
28
+ footer?: ReactNode;
29
+ /** Jump-list width on wide screens, px. Default 224. */
30
+ navWidth?: number;
31
+ className?: string;
32
+ };
33
+
34
+ /**
35
+ * Preferences / long-form page: a jump list that always matches the sections it scrolls to.
36
+ *
37
+ * Hand-rolled, this shape fails in three quiet ways: the nav's `href="#x"` and the section's
38
+ * `id` drift apart and the highlight never moves; the whole page scrolls so the nav scrolls out
39
+ * of view with the content it navigates; and a save bar placed after the content disappears
40
+ * below the fold. Here the ids come from one array, the nav and the content are independent
41
+ * scrollers, the jump list collapses to a pinned row when the panel is narrow, and `footer` is a
42
+ * sibling of the scroller — always visible, never part of the scroll.
43
+ *
44
+ * @when Page shape: a settings page, integration config, or a long report with anchored sections
45
+ * @example
46
+ * <SettingsSplit
47
+ * sections={[
48
+ * { id: "general", label: "通用", content: <GeneralForm /> },
49
+ * { id: "hooks", label: "Webhook", description: "出站事件", content: <HooksForm /> },
50
+ * ]}
51
+ * nav={<PageHeader title="设置" />}
52
+ * footer={dirty ? <UnsavedBar onSave={save} /> : undefined}
53
+ * />
54
+ * @family Layout & structure
55
+ */
56
+ export function SettingsSplit({
57
+ sections,
58
+ nav,
59
+ footer,
60
+ navWidth = 224,
61
+ className,
62
+ }: SettingsSplitProps) {
63
+ const isNarrow = useIsMobile();
64
+ const t = useLabels("settingsSplit");
65
+
66
+ // Scrollspy re-observes whenever this array identity changes, so derive it once per section list.
67
+ const anchors = React.useMemo(
68
+ () => sections.map(({ id, label }) => ({ id, label })),
69
+ [sections],
70
+ );
71
+
72
+ const jumpList = <Scrollspy sections={anchors} />;
73
+
74
+ const content = (
75
+ <div
76
+ className="flex min-h-0 flex-1 flex-col"
77
+ data-testid="settings-split-content"
78
+ >
79
+ <div
80
+ className="min-h-0 flex-1 overflow-y-auto"
81
+ data-testid="settings-split-scroll"
82
+ >
83
+ {sections.map((s) => (
84
+ <section
85
+ key={s.id}
86
+ id={s.id}
87
+ className="border-border scroll-mt-4 border-b px-6 py-6 last:border-b-0"
88
+ >
89
+ <h2 className="text-base font-semibold tracking-tight">
90
+ {s.label}
91
+ </h2>
92
+ {s.description ? (
93
+ <p className="text-muted-foreground mt-1 text-sm">
94
+ {s.description}
95
+ </p>
96
+ ) : null}
97
+ <div className="mt-4">{s.content}</div>
98
+ </section>
99
+ ))}
100
+ </div>
101
+ {footer ? (
102
+ // Sibling of the scroller, not its last child: the bar cannot scroll out of view.
103
+ <div
104
+ className="border-border bg-background shrink-0 border-t px-6 py-3"
105
+ data-testid="settings-split-footer"
106
+ >
107
+ {footer}
108
+ </div>
109
+ ) : null}
110
+ </div>
111
+ );
112
+
113
+ if (isNarrow) {
114
+ return (
115
+ <div
116
+ className={cn("flex h-full min-h-0 w-full flex-col", className)}
117
+ data-testid="settings-split"
118
+ >
119
+ <div
120
+ className="border-border bg-background shrink-0 overflow-x-auto border-b px-4 py-2"
121
+ data-testid="settings-split-nav"
122
+ aria-label={t.nav}
123
+ >
124
+ <div className="flex min-w-max items-center gap-4">
125
+ {nav ? (
126
+ <div className="text-muted-foreground text-xs">{nav}</div>
127
+ ) : null}
128
+ {jumpList}
129
+ </div>
130
+ </div>
131
+ {content}
132
+ </div>
133
+ );
134
+ }
135
+
136
+ return (
137
+ <div
138
+ className={cn("flex h-full min-h-0 w-full", className)}
139
+ data-testid="settings-split"
140
+ data-slot="settings-split-wide"
141
+ >
142
+ <aside
143
+ className="border-border min-h-0 w-56 shrink-0 overflow-y-auto border-r px-4 py-6"
144
+ data-testid="settings-split-nav"
145
+ aria-label={t.nav}
146
+ style={{ width: navWidth }}
147
+ >
148
+ {nav ? <div className="mb-4">{nav}</div> : null}
149
+ {jumpList}
150
+ </aside>
151
+ {content}
152
+ </div>
153
+ );
154
+ }
@@ -14,6 +14,13 @@ const tone = {
14
14
 
15
15
  export type Severity = keyof typeof tone
16
16
 
17
+ /**
18
+ * Chip for severity/blocker level.
19
+ * @when P0–P4, blocker/critical columns. Run outcome → `StatusBadge`.
20
+ * @example
21
+ * <SeverityChip severity="critical" />
22
+ * @family Feedback & status
23
+ */
17
24
  export function SeverityChip({ severity }: { severity: Severity | string }) {
18
25
  return (
19
26
  <Badge