@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
@@ -0,0 +1,644 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps, CSSProperties, ReactNode } from "react";
4
+ import type { UiMessages } from "@monkey-mini-app/ui/i18n/en";
5
+
6
+ import {
7
+ Avatar,
8
+ AvatarFallback,
9
+ AvatarImage,
10
+ } from "@monkey-mini-app/ui/components/avatar";
11
+ import {
12
+ Popover,
13
+ PopoverContent,
14
+ PopoverTrigger,
15
+ } from "@monkey-mini-app/ui/components/popover";
16
+ import { useDateLocale, useLabels } from "@monkey-mini-app/ui/i18n/context";
17
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
18
+
19
+ export type CommitAuthor = {
20
+ name: string;
21
+ avatarUrl?: string;
22
+ };
23
+
24
+ export type Commit = {
25
+ /** Commit hash (full or abbreviated). */
26
+ hash: string;
27
+ /** Commit message (first line). */
28
+ message: string;
29
+ /** Commit author. */
30
+ author: CommitAuthor;
31
+ /** ISO date string or Date object. */
32
+ date: string | Date;
33
+ /** Parent commit hashes. Empty for root commits. Two parents = merge commit. */
34
+ parents: string[];
35
+ /** Branch or ref label (e.g. "main", "feat/auth"). */
36
+ refs?: string[];
37
+ /** Tag label (e.g. "v1.0.0"). */
38
+ tag?: string;
39
+ };
40
+
41
+ export type CommitGraphProps = Omit<ComponentProps<"div">, "children"> & {
42
+ /** Commits in topological order (newest first). Each commit includes parent hashes. */
43
+ commits: Commit[];
44
+ /** Number of hash characters to display. @default 7 */
45
+ truncateHash?: number;
46
+ /** Pixel width per rail column. @default 24 */
47
+ railWidth?: number;
48
+ };
49
+
50
+ const RAIL_COLORS = [
51
+ "#3b82f6",
52
+ "#22c55e",
53
+ "#f59e0b",
54
+ "#ef4444",
55
+ "#8b5cf6",
56
+ "#ec4899",
57
+ "#06b6d4",
58
+ "#f97316",
59
+ ];
60
+
61
+ function color(rail: number): string {
62
+ return RAIL_COLORS[rail % RAIL_COLORS.length] ?? "#3b82f6";
63
+ }
64
+
65
+ function initials(name: string): string {
66
+ return name
67
+ .split(/\s+/)
68
+ .map((w) => w[0] ?? "")
69
+ .join("")
70
+ .toUpperCase()
71
+ .slice(0, 2);
72
+ }
73
+
74
+ // Graph layout computation
75
+
76
+ type GraphRow = {
77
+ commit: Commit;
78
+ rail: number;
79
+ rails: (string | null)[];
80
+ edges: Edge[];
81
+ };
82
+
83
+ type Edge = {
84
+ fromRail: number;
85
+ toRail: number;
86
+ color: string;
87
+ type: "straight" | "merge-in" | "fork-out";
88
+ };
89
+
90
+ function computeLayout(commits: Commit[]): GraphRow[] {
91
+ const rows: GraphRow[] = [];
92
+ // Active rails: each slot holds the hash of the commit it's "waiting for"
93
+ const rails: (string | null)[] = [];
94
+
95
+ for (const commit of commits) {
96
+ const hash = commit.hash;
97
+
98
+ // Find which rail this commit occupies (if any rail is waiting for it)
99
+ let commitRail = rails.indexOf(hash);
100
+
101
+ if (commitRail === -1) {
102
+ // New branch — find first empty slot or append
103
+ const emptySlot = rails.indexOf(null);
104
+ if (emptySlot !== -1) {
105
+ commitRail = emptySlot;
106
+ rails[commitRail] = hash;
107
+ } else {
108
+ commitRail = rails.length;
109
+ rails.push(hash);
110
+ }
111
+ }
112
+
113
+ const commitColor = color(commitRail);
114
+ const edges: Edge[] = [];
115
+
116
+ // Draw straight lines for all other active rails (pass-through)
117
+ for (let r = 0; r < rails.length; r++) {
118
+ if (r !== commitRail && rails[r] !== null) {
119
+ edges.push({
120
+ fromRail: r,
121
+ toRail: r,
122
+ color: color(r),
123
+ type: "straight",
124
+ });
125
+ }
126
+ }
127
+
128
+ // Clear this rail — the commit has been rendered
129
+ rails[commitRail] = null;
130
+
131
+ // Process parents
132
+ const parents = commit.parents;
133
+ const firstParent = parents[0];
134
+ if (firstParent !== undefined) {
135
+ // First parent continues on the same rail
136
+ const existingRail = rails.indexOf(firstParent);
137
+ if (existingRail !== -1) {
138
+ // Parent already expected on another rail — merge line
139
+ edges.push({
140
+ fromRail: commitRail,
141
+ toRail: existingRail,
142
+ color: commitColor,
143
+ type: "merge-in",
144
+ });
145
+ } else {
146
+ // Parent takes this commit's rail
147
+ rails[commitRail] = firstParent;
148
+ edges.push({
149
+ fromRail: commitRail,
150
+ toRail: commitRail,
151
+ color: commitColor,
152
+ type: "straight",
153
+ });
154
+ }
155
+ }
156
+
157
+ // Second+ parents (merge sources)
158
+ for (let p = 1; p < parents.length; p++) {
159
+ const parentHash = parents[p];
160
+ if (parentHash === undefined) continue;
161
+ const existingRail = rails.indexOf(parentHash);
162
+ if (existingRail !== -1) {
163
+ // Already on a rail — draw merge line from that rail
164
+ edges.push({
165
+ fromRail: existingRail,
166
+ toRail: commitRail,
167
+ color: color(existingRail),
168
+ type: "merge-in",
169
+ });
170
+ } else {
171
+ // Needs a new rail — fork out
172
+ const emptySlot = rails.indexOf(null);
173
+ const newRail = emptySlot !== -1 ? emptySlot : rails.length;
174
+ if (newRail >= rails.length) rails.push(null);
175
+ rails[newRail] = parentHash;
176
+ edges.push({
177
+ fromRail: commitRail,
178
+ toRail: newRail,
179
+ color: color(newRail),
180
+ type: "fork-out",
181
+ });
182
+ }
183
+ }
184
+
185
+ // Trim trailing nulls
186
+ while (rails.length > 0 && rails[rails.length - 1] === null) {
187
+ rails.pop();
188
+ }
189
+
190
+ rows.push({
191
+ commit,
192
+ rail: commitRail,
193
+ rails: [...rails],
194
+ edges,
195
+ });
196
+ }
197
+
198
+ return rows;
199
+ }
200
+
201
+ // SVG rendering for rails
202
+
203
+ const ROW_HEIGHT = 40;
204
+
205
+ function RailsSVG({
206
+ row,
207
+ prevRow,
208
+ railWidth,
209
+ maxRails,
210
+ }: {
211
+ row: GraphRow;
212
+ prevRow: GraphRow | null;
213
+ railWidth: number;
214
+ maxRails: number;
215
+ }) {
216
+ const w = maxRails * railWidth;
217
+ const h = ROW_HEIGHT;
218
+ const cy = h / 2;
219
+
220
+ function rx(rail: number) {
221
+ return rail * railWidth + railWidth / 2;
222
+ }
223
+
224
+ const commitX = rx(row.rail);
225
+
226
+ // Collect which rails are active above this row (from previous row's post-state)
227
+ const activeAbove = new Set<number>();
228
+ if (prevRow) {
229
+ for (let r = 0; r < prevRow.rails.length; r++) {
230
+ if (prevRow.rails[r] !== null) activeAbove.add(r);
231
+ }
232
+ }
233
+
234
+ // Collect which rails are active below this row
235
+ const activeBelow = new Set<number>();
236
+ for (let r = 0; r < row.rails.length; r++) {
237
+ if (row.rails[r] !== null) activeBelow.add(r);
238
+ }
239
+
240
+ return (
241
+ <svg
242
+ width={w}
243
+ height={h}
244
+ viewBox={`0 0 ${w} ${h}`}
245
+ className="shrink-0"
246
+ aria-hidden="true"
247
+ >
248
+ {/* Pass-through rails */}
249
+ {Array.from(activeAbove).map((r) => {
250
+ if (r === row.rail) return null;
251
+ if (!activeBelow.has(r)) return null;
252
+ const x = rx(r);
253
+ return (
254
+ <line
255
+ key={`pt-${r}`}
256
+ x1={x}
257
+ y1={0}
258
+ x2={x}
259
+ y2={h}
260
+ stroke={color(r)}
261
+ strokeWidth={2}
262
+ strokeOpacity={0.6}
263
+ />
264
+ );
265
+ })}
266
+
267
+ {/* Commit rail: incoming line (top to dot) */}
268
+ {activeAbove.has(row.rail) && (
269
+ <line
270
+ x1={commitX}
271
+ y1={0}
272
+ x2={commitX}
273
+ y2={cy}
274
+ stroke={color(row.rail)}
275
+ strokeWidth={2}
276
+ strokeOpacity={0.6}
277
+ />
278
+ )}
279
+
280
+ {/* Commit rail: outgoing line (dot to bottom) */}
281
+ {activeBelow.has(row.rail) && (
282
+ <line
283
+ x1={commitX}
284
+ y1={cy}
285
+ x2={commitX}
286
+ y2={h}
287
+ stroke={color(row.rail)}
288
+ strokeWidth={2}
289
+ strokeOpacity={0.6}
290
+ />
291
+ )}
292
+
293
+ {/* Fork-out curves */}
294
+ {row.edges
295
+ .filter((e) => e.type === "fork-out")
296
+ .map((edge) => (
297
+ <path
298
+ key={`f-${edge.fromRail}-${edge.toRail}`}
299
+ d={`M${rx(edge.fromRail)},${cy} C${rx(edge.fromRail)},${h} ${rx(edge.toRail)},${cy} ${rx(edge.toRail)},${h}`}
300
+ stroke={edge.color}
301
+ strokeWidth={2}
302
+ strokeOpacity={0.6}
303
+ fill="none"
304
+ />
305
+ ))}
306
+
307
+ {/* Merge curves */}
308
+ {row.edges
309
+ .filter((e) => e.type === "merge-in")
310
+ .map((edge) => {
311
+ const isOutgoing = edge.fromRail === row.rail;
312
+ const x1 = rx(edge.fromRail);
313
+ const x2 = rx(edge.toRail);
314
+ const d = isOutgoing
315
+ ? `M${x1},${cy} C${x1},${h} ${x2},${cy} ${x2},${h}`
316
+ : `M${x1},${0} C${x1},${cy} ${x2},${0} ${x2},${cy}`;
317
+ return (
318
+ <path
319
+ key={`m-${edge.fromRail}-${edge.toRail}-${isOutgoing ? "out" : "in"}`}
320
+ d={d}
321
+ stroke={edge.color}
322
+ strokeWidth={2}
323
+ strokeOpacity={0.6}
324
+ fill="none"
325
+ />
326
+ );
327
+ })}
328
+
329
+ {/* Rails that terminate here */}
330
+ {Array.from(activeAbove).map((r) => {
331
+ if (r === row.rail) return null;
332
+ if (activeBelow.has(r)) return null;
333
+ const x = rx(r);
334
+ return (
335
+ <line
336
+ key={`end-${r}`}
337
+ x1={x}
338
+ y1={0}
339
+ x2={x}
340
+ y2={cy}
341
+ stroke={color(r)}
342
+ strokeWidth={2}
343
+ strokeOpacity={0.6}
344
+ />
345
+ );
346
+ })}
347
+
348
+ {/* Commit dot */}
349
+ <circle
350
+ cx={commitX}
351
+ cy={cy}
352
+ r={5}
353
+ fill={color(row.rail)}
354
+ stroke="var(--color-background)"
355
+ strokeWidth={2}
356
+ />
357
+ </svg>
358
+ );
359
+ }
360
+
361
+ function AuthorAvatar({
362
+ author,
363
+ className,
364
+ fallbackClassName,
365
+ }: {
366
+ author: CommitAuthor;
367
+ className?: string;
368
+ fallbackClassName?: string;
369
+ }) {
370
+ return (
371
+ <Avatar className={cn("size-4", className)}>
372
+ {author.avatarUrl ? <AvatarImage src={author.avatarUrl} alt="" /> : null}
373
+ <AvatarFallback className={cn("text-[8px] font-bold", fallbackClassName)}>
374
+ {initials(author.name)}
375
+ </AvatarFallback>
376
+ </Avatar>
377
+ );
378
+ }
379
+
380
+ function formatDate(
381
+ date: string | Date,
382
+ t: UiMessages["commitGraph"],
383
+ locale: string,
384
+ ): string {
385
+ const d = typeof date === "string" ? new Date(date) : date;
386
+ const now = new Date();
387
+ const diffMs = now.getTime() - d.getTime();
388
+ const diffMins = Math.floor(diffMs / 60_000);
389
+ const diffHours = Math.floor(diffMs / 3_600_000);
390
+ const diffDays = Math.floor(diffMs / 86_400_000);
391
+
392
+ if (diffMins < 1) return t.justNow;
393
+ if (diffMins < 60) return t.minutesAgo(diffMins);
394
+ if (diffHours < 24) return t.hoursAgo(diffHours);
395
+ if (diffDays < 7) return t.daysAgo(diffDays);
396
+
397
+ return d.toLocaleDateString(locale, {
398
+ month: "short",
399
+ day: "numeric",
400
+ year: d.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
401
+ });
402
+ }
403
+
404
+ function formatFullDate(date: string | Date, locale: string): string {
405
+ const d = typeof date === "string" ? new Date(date) : date;
406
+ return d.toLocaleDateString(locale, {
407
+ weekday: "short",
408
+ month: "short",
409
+ day: "numeric",
410
+ year: "numeric",
411
+ hour: "numeric",
412
+ minute: "2-digit",
413
+ });
414
+ }
415
+
416
+ function CommitDetail({
417
+ commit,
418
+ hashLength,
419
+ railColor,
420
+ children,
421
+ className,
422
+ style,
423
+ }: {
424
+ commit: Commit;
425
+ hashLength: number;
426
+ railColor: string;
427
+ children: ReactNode;
428
+ className?: string;
429
+ style?: CSSProperties;
430
+ }) {
431
+ const t = useLabels("commitGraph");
432
+ const dateLocale = useDateLocale();
433
+ const locale = dateLocale.code ?? "en-US";
434
+
435
+ return (
436
+ <Popover>
437
+ <PopoverTrigger
438
+ type="button"
439
+ data-slot="commit-entry"
440
+ className={className}
441
+ style={style}
442
+ >
443
+ {children}
444
+ </PopoverTrigger>
445
+ <PopoverContent side="right" sideOffset={8} className="w-80 p-3">
446
+ <div className="flex flex-col gap-2">
447
+ <p className="text-sm leading-snug font-medium">{commit.message}</p>
448
+ <div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
449
+ <span className="inline-flex items-center gap-1.5">
450
+ <AuthorAvatar
451
+ author={commit.author}
452
+ className="size-3.5"
453
+ fallbackClassName="text-[7px]"
454
+ />
455
+ {commit.author.name}
456
+ </span>
457
+ <span className="text-border">·</span>
458
+ <code className="rounded bg-muted px-1 py-0.5 font-mono text-[10px]">
459
+ {commit.hash.slice(0, hashLength)}
460
+ </code>
461
+ </div>
462
+ <div className="text-[11px] text-muted-foreground">
463
+ {formatFullDate(commit.date, locale)}
464
+ </div>
465
+ {(commit.refs || commit.tag) && (
466
+ <div className="flex flex-wrap gap-1">
467
+ {commit.refs?.map((ref) => (
468
+ <span
469
+ key={ref}
470
+ className="inline-flex items-center gap-1 rounded-md border border-border/60 bg-muted/50 px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground"
471
+ >
472
+ {ref}
473
+ </span>
474
+ ))}
475
+ {commit.tag && (
476
+ <span
477
+ className="inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-medium"
478
+ style={{
479
+ backgroundColor: `${railColor}20`,
480
+ color: railColor,
481
+ }}
482
+ >
483
+ {commit.tag}
484
+ </span>
485
+ )}
486
+ </div>
487
+ )}
488
+ {commit.parents.length > 0 && (
489
+ <div className="text-[10px] text-muted-foreground/60">
490
+ {commit.parents.length === 1 ? t.parent : t.parents}:{" "}
491
+ {commit.parents.map((p) => p.slice(0, hashLength)).join(", ")}
492
+ </div>
493
+ )}
494
+ </div>
495
+ </PopoverContent>
496
+ </Popover>
497
+ );
498
+ }
499
+
500
+ /**
501
+ * Topological git commit graph (forks / merges).
502
+ * @when App history, release notes, git timeline views
503
+ * @example
504
+ * <CommitGraph commits={[{ hash, message, author, date, parents }]} />
505
+ * @family Chart & data
506
+ */
507
+ export function CommitGraph({
508
+ commits,
509
+ truncateHash = 7,
510
+ railWidth = 24,
511
+ className,
512
+ ...props
513
+ }: CommitGraphProps) {
514
+ const t = useLabels("commitGraph");
515
+ const dateLocale = useDateLocale();
516
+ const locale = dateLocale.code ?? "en-US";
517
+
518
+ // Simple mode: if no commit has parents, infer a linear topology
519
+ const hasTopology = commits.some((c) => c.parents && c.parents.length > 0);
520
+ const resolvedCommits = hasTopology
521
+ ? commits
522
+ : commits.map((c, i) => {
523
+ const next = commits[i + 1];
524
+ return {
525
+ ...c,
526
+ parents: next ? [next.hash] : [],
527
+ };
528
+ });
529
+
530
+ if (resolvedCommits.length === 0) {
531
+ return (
532
+ <div
533
+ data-slot="commit-graph"
534
+ data-testid="commit-graph"
535
+ className={cn(
536
+ "flex items-center justify-center rounded-xl border border-border/60 bg-card py-10 text-sm text-muted-foreground shadow-sm",
537
+ className,
538
+ )}
539
+ {...props}
540
+ >
541
+ {t.empty}
542
+ </div>
543
+ );
544
+ }
545
+
546
+ const rows = computeLayout(resolvedCommits);
547
+ const maxRails = Math.max(
548
+ ...rows.map((r) =>
549
+ Math.max(
550
+ r.rail + 1,
551
+ r.rails.length,
552
+ ...r.edges.map((e) => Math.max(e.fromRail, e.toRail) + 1),
553
+ ),
554
+ ),
555
+ );
556
+ const svgWidth = maxRails * railWidth;
557
+
558
+ return (
559
+ <div
560
+ data-slot="commit-graph"
561
+ data-testid="commit-graph"
562
+ className={cn(
563
+ "overflow-hidden rounded-xl border border-border/60 bg-card shadow-sm",
564
+ className,
565
+ )}
566
+ {...props}
567
+ >
568
+ <div className="overflow-x-auto">
569
+ {rows.map((row, i) => {
570
+ const prevRow = i > 0 ? (rows[i - 1] ?? null) : null;
571
+ return (
572
+ <CommitDetail
573
+ key={row.commit.hash}
574
+ commit={row.commit}
575
+ hashLength={truncateHash}
576
+ railColor={color(row.rail)}
577
+ className="flex w-full items-center gap-0 border-b border-border/30 transition-colors last:border-b-0 hover:bg-muted/30 focus-visible:bg-muted/30 focus-visible:outline-none"
578
+ style={{ height: ROW_HEIGHT }}
579
+ >
580
+ {/* Rails */}
581
+ <div style={{ width: svgWidth }} className="shrink-0">
582
+ <RailsSVG
583
+ row={row}
584
+ prevRow={prevRow}
585
+ railWidth={railWidth}
586
+ maxRails={maxRails}
587
+ />
588
+ </div>
589
+
590
+ {/* Refs */}
591
+ <div className="flex shrink-0 items-center gap-1 px-2">
592
+ {row.commit.refs?.map((ref) => (
593
+ <span
594
+ key={ref}
595
+ className="inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px] leading-none font-semibold"
596
+ style={{
597
+ borderColor: `${color(row.rail)}40`,
598
+ backgroundColor: `${color(row.rail)}10`,
599
+ color: color(row.rail),
600
+ }}
601
+ >
602
+ {ref}
603
+ </span>
604
+ ))}
605
+ {row.commit.tag && (
606
+ <span
607
+ className="inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] leading-none font-semibold"
608
+ style={{
609
+ backgroundColor: `${color(row.rail)}20`,
610
+ color: color(row.rail),
611
+ }}
612
+ >
613
+ {row.commit.tag}
614
+ </span>
615
+ )}
616
+ </div>
617
+
618
+ {/* Message */}
619
+ <p className="min-w-0 flex-1 truncate px-2 text-left text-sm text-foreground/80">
620
+ {row.commit.message}
621
+ </p>
622
+
623
+ {/* Meta */}
624
+ <div className="flex shrink-0 items-center gap-3 px-3">
625
+ <code className="font-mono text-[11px] text-muted-foreground/60">
626
+ {row.commit.hash.slice(0, truncateHash)}
627
+ </code>
628
+ <span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
629
+ <AuthorAvatar author={row.commit.author} />
630
+ <span className="hidden sm:inline">
631
+ {row.commit.author.name}
632
+ </span>
633
+ </span>
634
+ <span className="text-[11px] text-muted-foreground/50">
635
+ {formatDate(row.commit.date, t, locale)}
636
+ </span>
637
+ </div>
638
+ </CommitDetail>
639
+ );
640
+ })}
641
+ </div>
642
+ </div>
643
+ );
644
+ }
@@ -352,7 +352,8 @@ function GridToolbar<TData>({
352
352
  * pageIndex={page}
353
353
  * onPageIndexChange={setPage}
354
354
  * />
355
- * // “回到第 1 页”按钮:setPage(0);结构性重载也可用 key 重挂
355
+ * // “back to page 1 button: setPage(0); for a structural reload you can also remount via key
356
+ * @family Data & tables
356
357
  */
357
358
  export function DataGrid<TData>({
358
359
  columns,