@monkey-mini-app/ui 0.1.1 → 0.1.5

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 +12 -6
  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 +11 -0
  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 +7 -0
  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 +7 -0
  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 +7 -0
  26. package/dist/src/blocks/radar-chart.tsx +7 -0
  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 +7 -0
  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 +1 -0
  200. package/dist/vendors/lodash.js +20 -0
  201. package/dist/vendors/motion.js +12 -0
  202. package/package.json +20 -19
  203. package/dist/src/products/jql-language.ts +0 -70
@@ -1,89 +1,238 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import { Check } from "lucide-react"
3
+ import * as React from "react";
4
+ import { Check } from "lucide-react";
4
5
 
5
- import { cn } from "@monkey-mini-app/ui/lib/utils"
6
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
6
7
 
7
- export type Step = { id: string; title: string; description?: string }
8
+ export type StepStatus = "default" | "active" | "completed";
8
9
 
9
- export function Stepper({
10
- steps,
11
- current,
12
- orientation = "vertical",
13
- onStepClick,
14
- }: {
15
- steps: Step[]
16
- current: number
17
- orientation?: "vertical" | "horizontal"
18
- onStepClick?: (index: number) => void
19
- }) {
20
- const horizontal = orientation === "horizontal"
10
+ export interface StepperProps {
11
+ /** StepperItem elements. */
12
+ children: React.ReactNode;
13
+ /** Layout direction. Defaults to "vertical". */
14
+ orientation?: "vertical" | "horizontal";
15
+ className?: string;
16
+ }
17
+
18
+ export interface StepperItemProps {
19
+ /** Step heading text. */
20
+ title: string;
21
+ /** Optional description below the title. */
22
+ description?: string;
23
+ /** Override the auto-incremented step number. */
24
+ step?: number;
25
+ /** Visual status of this step. Defaults to "default". */
26
+ status?: StepStatus;
27
+ /** Custom icon to replace the step number circle. */
28
+ icon?: React.ReactNode;
29
+ /** Content rendered below the step header (code blocks, text, etc.). */
30
+ children?: React.ReactNode;
31
+ className?: string;
32
+ }
33
+
34
+ /**
35
+ * One step inside `Stepper` (title, description, status).
36
+ * @family Feedback & status
37
+ * @when As a child of `Stepper` only — status is `default | active | completed` (lowercase).
38
+ */
39
+ export function StepperItem({
40
+ title,
41
+ description,
42
+ step,
43
+ status = "default",
44
+ icon,
45
+ children,
46
+ className,
47
+ }: StepperItemProps) {
48
+ const stepNumber = step ?? 1;
21
49
 
22
50
  return (
23
- <ol
24
- data-testid="stepper"
25
- data-orientation={orientation}
26
- className={cn(horizontal ? "flex items-start gap-0" : "flex flex-col gap-3")}
51
+ <div
52
+ data-slot="stepper-item"
53
+ data-status={status}
54
+ className={cn("group/step relative", className)}
27
55
  >
28
- {steps.map((step, index) => {
29
- const state =
30
- index < current ? "done" : index === current ? "current" : "todo"
31
- const clickable = Boolean(onStepClick)
32
- return (
33
- <li
34
- key={step.id}
56
+ <div className="flex gap-3">
57
+ <div className="flex shrink-0 flex-col items-center self-stretch">
58
+ <div
35
59
  className={cn(
36
- horizontal
37
- ? "flex min-w-0 flex-1 flex-col items-center text-center"
38
- : "flex gap-3"
60
+ "relative z-10 flex size-8 shrink-0 items-center justify-center rounded-full border-2 text-sm font-semibold transition-colors",
61
+ status === "completed" &&
62
+ "border-primary bg-primary text-primary-foreground",
63
+ status === "active" &&
64
+ "border-primary bg-primary/10 text-primary",
65
+ status === "default" &&
66
+ "border-border bg-background text-muted-foreground"
39
67
  )}
40
68
  >
41
- <div
42
- className={cn(
43
- "flex items-center",
44
- horizontal ? "w-full justify-center" : "flex-col"
45
- )}
46
- >
47
- {horizontal && index > 0 ? (
48
- <span
69
+ {icon ? (
70
+ icon
71
+ ) : status === "completed" ? (
72
+ <Check className="size-3.5" aria-hidden="true" />
73
+ ) : (
74
+ <span className="text-xs">{stepNumber}</span>
75
+ )}
76
+ </div>
77
+
78
+ <div
79
+ className={cn(
80
+ "w-px min-h-4 grow",
81
+ status === "completed"
82
+ ? "bg-primary"
83
+ : "bg-border dark:bg-muted-foreground/25",
84
+ "group-last/step:hidden"
85
+ )}
86
+ />
87
+ </div>
88
+
89
+ <div className="flex min-w-0 flex-col gap-1 pb-6 pt-1 group-last/step:pb-0">
90
+ <div className="flex flex-col gap-0.5">
91
+ <h3 className="text-sm font-semibold leading-tight text-foreground">
92
+ {title}
93
+ </h3>
94
+ {description ? (
95
+ <p className="text-sm text-muted-foreground">{description}</p>
96
+ ) : null}
97
+ </div>
98
+ {children ? <div className="mt-2">{children}</div> : null}
99
+ </div>
100
+ </div>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ function VerticalStepper({
106
+ children,
107
+ className,
108
+ }: {
109
+ children: React.ReactNode;
110
+ className?: string;
111
+ }) {
112
+ const items = React.Children.toArray(children).filter(React.isValidElement);
113
+
114
+ return (
115
+ <div
116
+ data-testid="stepper"
117
+ data-slot="stepper"
118
+ data-orientation="vertical"
119
+ className={cn("flex flex-col", className)}
120
+ >
121
+ {items.map((child, index) => {
122
+ if (!React.isValidElement<StepperItemProps>(child)) return child;
123
+ return React.cloneElement(child, {
124
+ step: child.props.step ?? index + 1,
125
+ key: child.key ?? index,
126
+ });
127
+ })}
128
+ </div>
129
+ );
130
+ }
131
+
132
+ function HorizontalStepper({
133
+ children,
134
+ className,
135
+ }: {
136
+ children: React.ReactNode;
137
+ className?: string;
138
+ }) {
139
+ const items = React.Children.toArray(children).filter(React.isValidElement);
140
+
141
+ return (
142
+ <div
143
+ data-testid="stepper"
144
+ data-slot="stepper"
145
+ data-orientation="horizontal"
146
+ className={cn("flex flex-col gap-4", className)}
147
+ >
148
+ <div className="flex items-center">
149
+ {items.map((child, index) => {
150
+ if (!React.isValidElement<StepperItemProps>(child)) return null;
151
+ const { status = "default", icon, step: stepProp } = child.props;
152
+ const stepNumber = stepProp ?? index + 1;
153
+ const isLast = index === items.length - 1;
154
+
155
+ return (
156
+ <React.Fragment key={child.key ?? index}>
157
+ <div className="flex flex-col items-center gap-1.5">
158
+ <div
49
159
  className={cn(
50
- "mr-2 h-px min-w-4 flex-1",
51
- index <= current ? "bg-primary" : "bg-border"
160
+ "flex size-8 shrink-0 items-center justify-center rounded-full border-2 text-sm font-semibold transition-colors",
161
+ status === "completed" &&
162
+ "border-primary bg-primary text-primary-foreground",
163
+ status === "active" &&
164
+ "border-primary bg-primary/10 text-primary",
165
+ status === "default" &&
166
+ "border-border bg-background text-muted-foreground"
52
167
  )}
53
- />
54
- ) : null}
55
- <button
56
- type="button"
57
- disabled={!clickable}
58
- onClick={() => onStepClick?.(index)}
59
- className={cn(
60
- "flex size-7 shrink-0 items-center justify-center rounded-full text-xs font-medium",
61
- state === "done" && "bg-primary text-primary-foreground",
62
- state === "current" && "border-2 border-primary text-primary",
63
- state === "todo" && "border text-muted-foreground",
64
- clickable && "cursor-pointer"
65
- )}
66
- >
67
- {state === "done" ? <Check className="size-3.5" /> : index + 1}
68
- </button>
69
- {horizontal && index < steps.length - 1 ? (
168
+ >
169
+ {icon ? (
170
+ icon
171
+ ) : status === "completed" ? (
172
+ <Check className="size-3.5" aria-hidden="true" />
173
+ ) : (
174
+ <span className="text-xs">{stepNumber}</span>
175
+ )}
176
+ </div>
70
177
  <span
71
178
  className={cn(
72
- "ml-2 h-px min-w-4 flex-1",
73
- index < current ? "bg-primary" : "bg-border"
179
+ "max-w-[8rem] text-center text-xs font-medium",
180
+ status === "default"
181
+ ? "text-muted-foreground"
182
+ : "text-foreground"
183
+ )}
184
+ >
185
+ {child.props.title}
186
+ </span>
187
+ </div>
188
+ {!isLast ? (
189
+ <div
190
+ className={cn(
191
+ "mb-5 h-px flex-1",
192
+ status === "completed" ? "bg-primary" : "bg-border"
74
193
  )}
75
194
  />
76
195
  ) : null}
77
- </div>
78
- <div className={cn(horizontal ? "mt-2 px-1" : "")}>
79
- <div className="text-sm font-medium">{step.title}</div>
80
- {step.description ? (
81
- <p className="text-muted-foreground text-xs">{step.description}</p>
82
- ) : null}
83
- </div>
84
- </li>
85
- )
196
+ </React.Fragment>
197
+ );
198
+ })}
199
+ </div>
200
+
201
+ {items.map((child, index) => {
202
+ if (!React.isValidElement<StepperItemProps>(child)) return null;
203
+ const { status = "default", description, children: stepChildren } =
204
+ child.props;
205
+ if (status !== "active") return null;
206
+
207
+ return (
208
+ <div key={child.key ?? index} className="flex flex-col gap-1.5">
209
+ {description ? (
210
+ <p className="text-sm text-muted-foreground">{description}</p>
211
+ ) : null}
212
+ {stepChildren ? <div>{stepChildren}</div> : null}
213
+ </div>
214
+ );
86
215
  })}
87
- </ol>
88
- )
216
+ </div>
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Numbered steps with status; children are `StepperItem`.
222
+ * @family Feedback & status
223
+ * @when Wizard/progress of an ordered process. Time axis → `RunTimeline`.
224
+ * @example
225
+ * <Stepper><StepperItem title="上传" status="pass" /><StepperItem title="校验" status="running" /></Stepper>
226
+ */
227
+ export function Stepper({
228
+ children,
229
+ orientation = "vertical",
230
+ className,
231
+ }: StepperProps) {
232
+ if (orientation === "horizontal") {
233
+ return (
234
+ <HorizontalStepper className={className}>{children}</HorizontalStepper>
235
+ );
236
+ }
237
+ return <VerticalStepper className={className}>{children}</VerticalStepper>;
89
238
  }
@@ -7,6 +7,13 @@ export type TimelineItem = {
7
7
  time?: string
8
8
  }
9
9
 
10
+ /**
11
+ * Vertical ordered events.
12
+ * @when Statuses/history without durations. Durations → `RunTimeline`.
13
+ * @example
14
+ * <Timeline items={[{ id: "1", title: "已创建", time: "09:00" }]} />
15
+ * @family Realtime
16
+ */
10
17
  export function Timeline({ items }: { items: TimelineItem[] }) {
11
18
  return (
12
19
  <ol className="relative space-y-4 border-l pl-4" data-testid="timeline">
@@ -45,6 +45,13 @@ function Node({
45
45
  )
46
46
  }
47
47
 
48
+ /**
49
+ * Generic expandable node list.
50
+ * @when Any hierarchy without file semantics. Files → `FileTree`.
51
+ * @example
52
+ * <TreeView nodes={[{ id: "a", label: "华东" }]} />
53
+ * @family Data & tables
54
+ */
48
55
  export function TreeView({ nodes }: { nodes: TreeNode[] }) {
49
56
  return (
50
57
  <div data-testid="tree-view">
@@ -0,0 +1,240 @@
1
+ import * as React from "react";
2
+
3
+ import { useLabels } from "../i18n/context";
4
+ import { AppIdContext } from "./app-id";
5
+
6
+ /** Where a captured failure came from — drives the hint the human sees. */
7
+ export type AppErrorKind = "render" | "module" | "uncaught" | "async";
8
+
9
+ export type ReportedAppError = {
10
+ kind: AppErrorKind;
11
+ message: string;
12
+ file?: string;
13
+ line?: number;
14
+ column?: number;
15
+ stack?: string;
16
+ componentStack?: string;
17
+ };
18
+
19
+ /**
20
+ * Hand an error to the host so `mini_app_errors` can see it.
21
+ *
22
+ * The app iframe is same-origin with the host, but the panel embedding it is not — so the
23
+ * only channel that works in both directions is the iframe POSTing to its own host. This
24
+ * is deliberately fire-and-forget: a diagnostic path must never be able to break the app
25
+ * that is reporting through it.
26
+ */
27
+ export function reportAppError(appId: string, err: ReportedAppError): void {
28
+ if (!appId) return;
29
+ try {
30
+ const body = JSON.stringify(err);
31
+ const url = `/api/app/${encodeURIComponent(appId)}/errors`;
32
+ // sendBeacon survives the unmount that a crashing app often triggers right after.
33
+ if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
34
+ const sent = navigator.sendBeacon(url, new Blob([body], { type: "application/json" }));
35
+ if (sent) return;
36
+ }
37
+ void fetch(url, {
38
+ method: "POST",
39
+ headers: { "content-type": "application/json" },
40
+ body,
41
+ keepalive: true,
42
+ }).catch(() => {});
43
+ } catch {
44
+ /* never throw from a failure handler */
45
+ }
46
+ }
47
+
48
+ const HINT_KEY: Record<AppErrorKind, "renderHint" | "moduleHint" | "uncaughtHint" | "asyncHint"> = {
49
+ render: "renderHint",
50
+ module: "moduleHint",
51
+ uncaught: "uncaughtHint",
52
+ async: "asyncHint",
53
+ };
54
+
55
+ function ErrorCard(props: {
56
+ appId: string;
57
+ kind: AppErrorKind;
58
+ message: string;
59
+ stack?: string;
60
+ componentStack?: string;
61
+ onRetry: () => void;
62
+ }) {
63
+ const t = useLabels("appError");
64
+ const [open, setOpen] = React.useState(false);
65
+ const detail = [props.componentStack, props.stack].filter(Boolean).join("\n");
66
+
67
+ return (
68
+ <div
69
+ role="alert"
70
+ style={{
71
+ boxSizing: "border-box",
72
+ margin: "24px",
73
+ padding: "16px 18px",
74
+ borderRadius: 12,
75
+ border: "1px solid var(--destructive,#dc2626)",
76
+ background: "var(--card,#fff)",
77
+ color: "var(--foreground,#111)",
78
+ font: "13px/1.6 var(--font-sans,ui-sans-serif,system-ui,sans-serif)",
79
+ maxWidth: 720,
80
+ }}
81
+ >
82
+ <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
83
+ <span
84
+ style={{
85
+ width: 8,
86
+ height: 8,
87
+ borderRadius: "50%",
88
+ background: "var(--destructive,#dc2626)",
89
+ flex: "0 0 auto",
90
+ }}
91
+ />
92
+ <strong style={{ fontSize: 14 }}>{t.title}</strong>
93
+ <code
94
+ style={{
95
+ marginLeft: "auto",
96
+ padding: "1px 6px",
97
+ borderRadius: 6,
98
+ background: "var(--muted,#f3f4f6)",
99
+ color: "var(--muted-foreground,#6b7280)",
100
+ fontSize: 11,
101
+ }}
102
+ >
103
+ {props.appId}
104
+ </code>
105
+ </div>
106
+
107
+ <p style={{ margin: "0 0 8px", color: "var(--muted-foreground,#6b7280)" }}>{t[HINT_KEY[props.kind]]}</p>
108
+
109
+ <pre
110
+ style={{
111
+ margin: 0,
112
+ padding: "10px 12px",
113
+ borderRadius: 8,
114
+ background: "var(--muted,#f3f4f6)",
115
+ whiteSpace: "pre-wrap",
116
+ wordBreak: "break-word",
117
+ fontFamily: "var(--font-mono,ui-monospace,SFMono-Regular,monospace)",
118
+ fontSize: 12,
119
+ }}
120
+ >
121
+ {props.message}
122
+ </pre>
123
+
124
+ <div style={{ display: "flex", gap: 8, alignItems: "center", marginTop: 12 }}>
125
+ <button
126
+ type="button"
127
+ onClick={props.onRetry}
128
+ style={{
129
+ padding: "5px 12px",
130
+ borderRadius: 8,
131
+ border: "1px solid var(--border,#e5e7eb)",
132
+ background: "var(--primary,#2563eb)",
133
+ color: "var(--primary-foreground,#fff)",
134
+ font: "inherit",
135
+ cursor: "pointer",
136
+ }}
137
+ >
138
+ {t.retry}
139
+ </button>
140
+ {detail ? (
141
+ <button
142
+ type="button"
143
+ onClick={() => setOpen((v) => !v)}
144
+ style={{
145
+ padding: "5px 12px",
146
+ borderRadius: 8,
147
+ border: "1px solid var(--border,#e5e7eb)",
148
+ background: "transparent",
149
+ color: "var(--foreground,#111)",
150
+ font: "inherit",
151
+ cursor: "pointer",
152
+ }}
153
+ >
154
+ {open ? t.hideDetails : t.details}
155
+ </button>
156
+ ) : null}
157
+ <span style={{ marginLeft: "auto", fontSize: 11, color: "var(--muted-foreground,#6b7280)" }}>
158
+ {t.reported}
159
+ </span>
160
+ </div>
161
+
162
+ {open && detail ? (
163
+ <pre
164
+ style={{
165
+ margin: "10px 0 0",
166
+ padding: "10px 12px",
167
+ borderRadius: 8,
168
+ background: "var(--muted,#f3f4f6)",
169
+ color: "var(--muted-foreground,#6b7280)",
170
+ whiteSpace: "pre-wrap",
171
+ wordBreak: "break-word",
172
+ maxHeight: 260,
173
+ overflow: "auto",
174
+ fontFamily: "var(--font-mono,ui-monospace,SFMono-Regular,monospace)",
175
+ fontSize: 11,
176
+ }}
177
+ >
178
+ {detail}
179
+ </pre>
180
+ ) : null}
181
+ </div>
182
+ );
183
+ }
184
+
185
+ type BoundaryProps = { children: React.ReactNode };
186
+ /** Never `null`: React types `state` as `Readonly<S>`, and a nullable S is not a valid JSX element type. */
187
+ type BoundaryState = { error?: Error; componentStack?: string };
188
+
189
+ class AppErrorBoundaryClass extends React.Component<
190
+ BoundaryProps & { appId: string },
191
+ BoundaryState
192
+ > {
193
+ /** Required: without an initializer `this.state` is null and the first render throws. */
194
+ state: BoundaryState = {};
195
+
196
+ static getDerivedStateFromError(error: Error): BoundaryState {
197
+ return { error };
198
+ }
199
+
200
+ componentDidCatch(error: Error, info: { componentStack?: string }): void {
201
+ const componentStack = typeof info?.componentStack === "string" ? info.componentStack : undefined;
202
+ this.setState({ componentStack });
203
+ reportAppError(this.props.appId, {
204
+ kind: "render",
205
+ message: error?.message || String(error),
206
+ stack: error?.stack,
207
+ componentStack,
208
+ });
209
+ }
210
+
211
+ render(): React.ReactNode {
212
+ const error = this.state.error;
213
+ if (error) {
214
+ return (
215
+ <ErrorCard
216
+ appId={this.props.appId}
217
+ kind="render"
218
+ message={error.message || String(error)}
219
+ stack={error.stack}
220
+ componentStack={this.state.componentStack}
221
+ onRetry={() => this.setState({ error: undefined, componentStack: undefined })}
222
+ />
223
+ );
224
+ }
225
+ return this.props.children;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Runtime crash guard for a mini-app UI. The host injects this around the app entry when
231
+ * it compiles `ui.tsx`, so authors never have to add it — and an app that throws during
232
+ * render shows a real message instead of a blank iframe.
233
+ *
234
+ * @when Not used from mini-app code: it is already wrapped around your `ui.tsx` by the host.
235
+ * @family Feedback & status
236
+ */
237
+ export function AppErrorBoundary(props: { children: React.ReactNode }) {
238
+ const appId = React.useContext(AppIdContext);
239
+ return <AppErrorBoundaryClass appId={appId} key={appId} {...props} />;
240
+ }
@@ -0,0 +1,7 @@
1
+ import { createContext } from "react";
2
+
3
+ /**
4
+ * App id injected by `<AppRuntime>`. Its own module so the error boundary can read it
5
+ * without importing `use-app` (which would make the two mutually dependent).
6
+ */
7
+ export const AppIdContext = createContext("");