@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,5 +1,12 @@
1
1
  import { Timeline, type TimelineItem } from "@monkey-mini-app/ui/products/timeline"
2
2
 
3
+ /**
4
+ * Chronological activity list (avatar + text + time).
5
+ * @when Read-only “who did what when” — commits, deploys, agent runs. For steps with status use `RunTimeline`.
6
+ * @example
7
+ * <ActivityFeed items={[{ id: "1", title: "部署完成", time: "10:24" }]} />
8
+ * @family Realtime
9
+ */
3
10
  export function ActivityFeed({ items }: { items: TimelineItem[] }) {
4
11
  return (
5
12
  <div data-testid="activity-feed">
@@ -9,6 +9,7 @@ import { cn } from "@monkey-mini-app/ui/lib/utils"
9
9
  * <AppShell sidebar={<nav />} header={<PageHeader title="Home" />}>
10
10
  * {children}
11
11
  * </AppShell>
12
+ * @family Layout & structure
12
13
  */
13
14
  export function AppShell({
14
15
  sidebar,
@@ -10,6 +10,13 @@ import {
10
10
 
11
11
  export type GalleryFile = { name: string; url?: string }
12
12
 
13
+ /**
14
+ * Thumbnail grid of files with open/copy.
15
+ * @when A record has N files/images and you show them as tiles, not a table.
16
+ * @example
17
+ * <AttachmentGallery files={[{ name: "log.txt", url: "https://…" }]} />
18
+ * @family Discovery & inspect
19
+ */
13
20
  export function AttachmentGallery({ files }: { files: GalleryFile[] }) {
14
21
  return (
15
22
  <AttachmentGroup data-testid="attachment-gallery">
@@ -10,6 +10,17 @@ import {
10
10
  } from "@monkey-mini-app/ui/components/chart"
11
11
  import { cn } from "@monkey-mini-app/ui/lib/utils"
12
12
 
13
+ /**
14
+ * Stacked bar chart over flat rows.
15
+ * @family Chart & data
16
+ * @when Comparing several series across categories (pass/fail per day). Single ratio → `DonutChart`; trend → `Sparkline`.
17
+ * @example
18
+ * <StackedBarChart
19
+ * data={[{ label: "09-01", ok: 12, fail: 2 }]}
20
+ * keys={["ok", "fail"]}
21
+ * config={{ ok: { label: "通过", color: "var(--primary)" }, fail: { label: "失败", color: "var(--destructive)" } }}
22
+ * />
23
+ */
13
24
  export function StackedBarChart({
14
25
  data,
15
26
  config,
@@ -2,6 +2,13 @@ import { Avatar, AvatarFallback } from "@monkey-mini-app/ui/components/avatar"
2
2
 
3
3
  export type Comment = { id: string; author: string; body: string; time?: string }
4
4
 
5
+ /**
6
+ * Nested comments with author + time.
7
+ * @when Issue/ticket discussion, review comments. Editing a comment is your job, not the component's.
8
+ * @example
9
+ * <CommentThread comments={[{ id: "c1", author: "P.Wang", text: "这里再确认下" }]} />
10
+ * @family Realtime
11
+ */
5
12
  export function CommentThread({ comments }: { comments: Comment[] }) {
6
13
  return (
7
14
  <div className="flex flex-col gap-3" data-testid="comment-thread">
@@ -0,0 +1,109 @@
1
+ "use client";
2
+
3
+ import type { ReactNode } from "react";
4
+
5
+ import { useIsMobile } from "@monkey-mini-app/ui/hooks/use-mobile";
6
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
7
+
8
+ export type DashboardShellProps = {
9
+ /** Title row / `PageHeader`. Pinned: it stays while the content scrolls under it. */
10
+ header?: ReactNode;
11
+ /** KPI strip. Pinned with the header — the point of a dashboard is that the numbers hold. */
12
+ kpis?: ReactNode;
13
+ /** Charts and tables. The scrolling body. */
14
+ main: ReactNode;
15
+ /** Activity feed / top offenders. Sits beside `main` on wide screens, stacks under it when narrow. */
16
+ aside?: ReactNode;
17
+ /** Aside width in px on wide screens. Default 320. */
18
+ asideWidth?: number;
19
+ className?: string;
20
+ };
21
+
22
+ /**
23
+ * Overview page with a pinned header + KPI strip and exactly one scrolling body.
24
+ *
25
+ * The failure this exists for: the page is a plain column, `overflow-auto` ends up on the
26
+ * outermost div, and the first chart the user scrolls past takes the header and the KPI row
27
+ * with it — a dashboard whose numbers scroll away is not a dashboard. The aside gets its own
28
+ * scroller when there is room for two, and folds under `main` into the single body scroller
29
+ * when there is not, rather than becoming a third half-height pane.
30
+ *
31
+ * @when Page shape: metrics up top, charts below, optional activity rail — ops overviews, monitor walls
32
+ * @example
33
+ * <DashboardShell
34
+ * header={<PageHeader title="值班" description="每 5 秒" />}
35
+ * kpis={<div className="grid grid-cols-4 gap-3">{cards}</div>}
36
+ * main={<Charts />}
37
+ * aside={<ActivityFeed items={items} />}
38
+ * />
39
+ * @family Layout & structure
40
+ */
41
+ export function DashboardShell({
42
+ header,
43
+ kpis,
44
+ main,
45
+ aside,
46
+ asideWidth = 320,
47
+ className,
48
+ }: DashboardShellProps) {
49
+ const isNarrow = useIsMobile();
50
+
51
+ const pinned = (
52
+ <>
53
+ {header ? (
54
+ <div className="shrink-0" data-testid="dashboard-header">
55
+ {header}
56
+ </div>
57
+ ) : null}
58
+ {kpis ? (
59
+ <div className="shrink-0" data-testid="dashboard-kpis">
60
+ {kpis}
61
+ </div>
62
+ ) : null}
63
+ </>
64
+ );
65
+
66
+ return (
67
+ <div
68
+ className={cn("flex h-full min-h-0 w-full flex-col", className)}
69
+ data-testid="dashboard-shell"
70
+ >
71
+ {pinned}
72
+
73
+ {aside && !isNarrow ? (
74
+ // Room for two panes: each scrolls on its own, neither can push the other.
75
+ <div className="flex min-h-0 flex-1" data-testid="dashboard-body">
76
+ <div
77
+ className="min-h-0 flex-1 overflow-y-auto"
78
+ data-testid="dashboard-main"
79
+ >
80
+ {main}
81
+ </div>
82
+ <div
83
+ className="border-border bg-muted/30 min-h-0 shrink-0 overflow-y-auto border-l"
84
+ data-testid="dashboard-aside"
85
+ style={{ width: asideWidth }}
86
+ >
87
+ {aside}
88
+ </div>
89
+ </div>
90
+ ) : (
91
+ // One body scroller; the aside folds to the bottom of it instead of a squeezed pane.
92
+ <div
93
+ className="min-h-0 flex-1 overflow-y-auto"
94
+ data-testid="dashboard-body"
95
+ >
96
+ <div data-testid="dashboard-main">{main}</div>
97
+ {aside ? (
98
+ <div
99
+ className="border-border border-t px-4 py-4"
100
+ data-testid="dashboard-aside"
101
+ >
102
+ {aside}
103
+ </div>
104
+ ) : null}
105
+ </div>
106
+ )}
107
+ </div>
108
+ );
109
+ }
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Label/value pairs, two columns.
3
+ * @when Detail/inspector panel of a selected row — the default “show one record” view.
4
+ * @example
5
+ * <DescriptionList items={[{ label: "状态", value: "运行中" }]} />
6
+ * @family Layout & structure
7
+ */
1
8
  export function DescriptionList({
2
9
  items,
3
10
  }: {
@@ -15,6 +15,7 @@ import {
15
15
  * @when Master-detail after row/card click
16
16
  * @example
17
17
  * <DetailPanel open={!!id} onOpenChange={() => setId(null)} title="Run #12">{body}</DetailPanel>
18
+ * @family Surface
18
19
  */
19
20
  export function DetailPanel({
20
21
  open,
@@ -13,6 +13,13 @@ import { cn } from "@monkey-mini-app/ui/lib/utils"
13
13
 
14
14
  export type Slice = { name: string; value: number; fill: string }
15
15
 
16
+ /**
17
+ * Ring chart with centre label.
18
+ * @when Part-of-whole (≤6 slices): storage usage, status split. More categories → `StackedBarChart`.
19
+ * @example
20
+ * <DonutChart data={[{ label: "OK", value: 82, color: "var(--primary)" }]} center="82%" />
21
+ * @family Chart & data
22
+ */
16
23
  export function DonutChart({
17
24
  data,
18
25
  config,
@@ -10,6 +10,13 @@ const tone: Record<string, string> = {
10
10
  local: "bg-muted text-muted-foreground",
11
11
  }
12
12
 
13
+ /**
14
+ * Coloured pill for an environment name.
15
+ * @when prd / stg / dev markers in headers and tables.
16
+ * @example
17
+ * <EnvBadge env="production" />
18
+ * @family Feedback & status
19
+ */
13
20
  export function EnvBadge({ env }: { env: string }) {
14
21
  return (
15
22
  <Badge
@@ -0,0 +1,316 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Check, ClipboardList, Copy, Eye, EyeOff } from "lucide-react";
5
+
6
+ import { useLabels } from "@monkey-mini-app/ui/i18n/context";
7
+ import { writeClipboard } from "@monkey-mini-app/ui/lib/clipboard";
8
+ import { cn } from "@monkey-mini-app/ui/lib/utils";
9
+
10
+ export type Environment =
11
+ | "production"
12
+ | "preview"
13
+ | "development"
14
+ | (string & {});
15
+
16
+ export type EnvVariable = {
17
+ /** Variable name (e.g. DATABASE_URL). */
18
+ key: string;
19
+ /** Variable value. Masked by default. */
20
+ value: string;
21
+ /** Target environment. When provided, renders as a pill. */
22
+ environment?: Environment;
23
+ /** Optional description shown below the key. */
24
+ description?: string;
25
+ };
26
+
27
+ export type EnvTableProps = Omit<
28
+ React.ComponentProps<"div">,
29
+ "children" | "title"
30
+ > & {
31
+ /** Environment variables to display. */
32
+ variables: EnvVariable[];
33
+ /** Optional heading above the table. */
34
+ title?: string;
35
+ /** Start with all values revealed. Defaults to false. */
36
+ defaultRevealed?: boolean;
37
+ };
38
+
39
+ function envBadgeColor(env: Environment): string {
40
+ switch (env) {
41
+ case "production":
42
+ return "bg-emerald-500/15 text-emerald-700 dark:text-emerald-400";
43
+ case "preview":
44
+ return "bg-sky-500/15 text-sky-700 dark:text-sky-400";
45
+ case "development":
46
+ return "bg-amber-500/15 text-amber-700 dark:text-amber-400";
47
+ default:
48
+ return "bg-muted text-muted-foreground";
49
+ }
50
+ }
51
+
52
+ function maskValue(value: string): string {
53
+ if (value.length <= 4) return "••••••••";
54
+ return `${value.slice(0, 4)}••••••••`;
55
+ }
56
+
57
+ function CopyButton({
58
+ value,
59
+ label,
60
+ className,
61
+ }: {
62
+ value: string;
63
+ label: string;
64
+ className?: string;
65
+ }) {
66
+ const [copied, setCopied] = React.useState(false);
67
+
68
+ const handleCopy = React.useCallback(() => {
69
+ void writeClipboard(value).then((ok) => {
70
+ if (!ok) return;
71
+ setCopied(true);
72
+ setTimeout(() => setCopied(false), 1500);
73
+ });
74
+ }, [value]);
75
+
76
+ return (
77
+ <button
78
+ type="button"
79
+ onClick={handleCopy}
80
+ aria-label={label}
81
+ className={cn(
82
+ "inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
83
+ className,
84
+ )}
85
+ >
86
+ {copied ? (
87
+ <Check className="size-3.5 text-emerald-500" />
88
+ ) : (
89
+ <Copy className="size-3.5" />
90
+ )}
91
+ </button>
92
+ );
93
+ }
94
+
95
+ function RevealButton({
96
+ revealed,
97
+ onToggle,
98
+ label,
99
+ }: {
100
+ revealed: boolean;
101
+ onToggle: () => void;
102
+ label: string;
103
+ }) {
104
+ return (
105
+ <button
106
+ type="button"
107
+ onClick={onToggle}
108
+ aria-label={label}
109
+ className="inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
110
+ >
111
+ {revealed ? (
112
+ <EyeOff className="size-3.5" />
113
+ ) : (
114
+ <Eye className="size-3.5" />
115
+ )}
116
+ </button>
117
+ );
118
+ }
119
+
120
+ function EnvRow({
121
+ variable,
122
+ revealed,
123
+ onToggleReveal,
124
+ }: {
125
+ variable: EnvVariable;
126
+ revealed: boolean;
127
+ onToggleReveal: () => void;
128
+ }) {
129
+ const t = useLabels("envTable");
130
+ const displayValue = revealed ? variable.value : maskValue(variable.value);
131
+
132
+ return (
133
+ <div
134
+ data-slot="env-row"
135
+ className="group flex items-start gap-3 px-3 py-2.5 sm:items-center sm:px-4"
136
+ >
137
+ <div className="flex min-w-0 flex-1 flex-col gap-1.5 sm:flex-row sm:items-center sm:gap-4">
138
+ <div className="flex shrink-0 items-center gap-2 sm:w-[220px]">
139
+ <span className="truncate font-mono text-xs font-semibold text-foreground">
140
+ {variable.key}
141
+ </span>
142
+ {variable.environment ? (
143
+ <span
144
+ className={cn(
145
+ "shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium leading-none",
146
+ envBadgeColor(variable.environment),
147
+ )}
148
+ >
149
+ {variable.environment}
150
+ </span>
151
+ ) : null}
152
+ </div>
153
+
154
+ <div className="min-w-0 flex-1">
155
+ <span
156
+ className={cn(
157
+ "break-all font-mono text-xs",
158
+ revealed ? "text-foreground" : "text-muted-foreground",
159
+ )}
160
+ >
161
+ {displayValue}
162
+ </span>
163
+ {variable.description ? (
164
+ <p className="mt-0.5 text-[11px] text-muted-foreground/70">
165
+ {variable.description}
166
+ </p>
167
+ ) : null}
168
+ </div>
169
+ </div>
170
+
171
+ <div className="flex shrink-0 items-center gap-0.5">
172
+ <RevealButton
173
+ revealed={revealed}
174
+ onToggle={onToggleReveal}
175
+ label={revealed ? t.hideKey(variable.key) : t.revealKey(variable.key)}
176
+ />
177
+ <CopyButton value={variable.value} label={t.copyValue(variable.key)} />
178
+ </div>
179
+ </div>
180
+ );
181
+ }
182
+
183
+ /**
184
+ * Read-only env var table with masked values and copy.
185
+ * @when Settings / deploy preview / secrets display
186
+ * @example
187
+ * <EnvTable variables={[{ key: "API_URL", value: "https://…", environment: "production" }]} />
188
+ * @family Data & tables
189
+ */
190
+ export function EnvTable({
191
+ variables,
192
+ title,
193
+ defaultRevealed = false,
194
+ className,
195
+ ...props
196
+ }: EnvTableProps) {
197
+ const t = useLabels("envTable");
198
+ const [revealedIndices, setRevealedIndices] = React.useState<Set<number>>(
199
+ () => (defaultRevealed ? new Set(variables.map((_, i) => i)) : new Set()),
200
+ );
201
+ const [copiedAll, setCopiedAll] = React.useState(false);
202
+
203
+ const allRevealed =
204
+ variables.length > 0 && revealedIndices.size === variables.length;
205
+
206
+ const toggleIndex = React.useCallback((index: number) => {
207
+ setRevealedIndices((prev) => {
208
+ const next = new Set(prev);
209
+ if (next.has(index)) {
210
+ next.delete(index);
211
+ } else {
212
+ next.add(index);
213
+ }
214
+ return next;
215
+ });
216
+ }, []);
217
+
218
+ const toggleAll = React.useCallback(() => {
219
+ if (allRevealed) {
220
+ setRevealedIndices(new Set());
221
+ } else {
222
+ setRevealedIndices(new Set(variables.map((_, i) => i)));
223
+ }
224
+ }, [allRevealed, variables]);
225
+
226
+ const copyAllAsEnv = React.useCallback(() => {
227
+ const text = variables.map((v) => `${v.key}=${v.value}`).join("\n");
228
+ void writeClipboard(text).then((ok) => {
229
+ if (!ok) return;
230
+ setCopiedAll(true);
231
+ setTimeout(() => setCopiedAll(false), 1500);
232
+ });
233
+ }, [variables]);
234
+
235
+ if (variables.length === 0) {
236
+ return (
237
+ <div
238
+ data-testid="env-table"
239
+ data-slot="env-table"
240
+ className={cn(
241
+ "flex items-center justify-center rounded-xl border border-border/60 bg-card py-10 text-sm text-muted-foreground shadow-sm",
242
+ className,
243
+ )}
244
+ {...props}
245
+ >
246
+ {t.empty}
247
+ </div>
248
+ );
249
+ }
250
+
251
+ return (
252
+ <div
253
+ data-testid="env-table"
254
+ data-slot="env-table"
255
+ className={cn(
256
+ "overflow-hidden rounded-xl border border-border/60 bg-card shadow-sm",
257
+ className,
258
+ )}
259
+ {...props}
260
+ >
261
+ <div className="flex items-center justify-between border-b border-border/40 px-3 py-2.5 sm:px-4">
262
+ <div className="flex items-center gap-2">
263
+ {title ? (
264
+ <h3 className="text-sm font-semibold text-foreground">{title}</h3>
265
+ ) : null}
266
+ <span className="rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
267
+ {variables.length}
268
+ </span>
269
+ </div>
270
+ <div className="flex items-center gap-1">
271
+ <button
272
+ type="button"
273
+ onClick={toggleAll}
274
+ aria-label={allRevealed ? t.hideAllValues : t.revealAllValues}
275
+ className="inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
276
+ >
277
+ {allRevealed ? (
278
+ <EyeOff className="size-3.5" />
279
+ ) : (
280
+ <Eye className="size-3.5" />
281
+ )}
282
+ <span className="hidden sm:inline">
283
+ {allRevealed ? t.hideAll : t.revealAll}
284
+ </span>
285
+ </button>
286
+ <button
287
+ type="button"
288
+ onClick={copyAllAsEnv}
289
+ aria-label={t.copyEnv}
290
+ className="inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
291
+ >
292
+ {copiedAll ? (
293
+ <Check className="size-3.5 text-emerald-500" />
294
+ ) : (
295
+ <ClipboardList className="size-3.5" />
296
+ )}
297
+ <span className="hidden sm:inline">
298
+ {copiedAll ? t.copied : t.copyEnv}
299
+ </span>
300
+ </button>
301
+ </div>
302
+ </div>
303
+
304
+ <div className="divide-y divide-border/40">
305
+ {variables.map((variable, i) => (
306
+ <EnvRow
307
+ key={`${variable.key}-${i}`}
308
+ variable={variable}
309
+ revealed={revealedIndices.has(i)}
310
+ onToggleReveal={() => toggleIndex(i)}
311
+ />
312
+ ))}
313
+ </div>
314
+ </div>
315
+ );
316
+ }
@@ -1,5 +1,12 @@
1
1
  import { TreeView, type TreeNode } from "@monkey-mini-app/ui/products/tree-view"
2
2
 
3
+ /**
4
+ * Expandable file/dir tree (icons built in).
5
+ * @when Repo/zip contents where selection matters. Plain nested list → `TreeView`.
6
+ * @example
7
+ * <FileTree nodes={[{ name: "src", children: [{ name: "main.api.ts" }] }]} />
8
+ * @family Data & tables
9
+ */
3
10
  export function FileTree({ nodes }: { nodes: TreeNode[] }) {
4
11
  return <TreeView nodes={nodes} />
5
12
  }
@@ -8,6 +8,7 @@ import type { ReactNode } from "react"
8
8
  * <DateRangePicker value={range} onChange={setRange} />
9
9
  * <SearchInput value={q} onChange={setQ} />
10
10
  * </FilterBar>
11
+ * @family Data & tables
11
12
  */
12
13
  export function FilterBar({ children }: { children: ReactNode }) {
13
14
  return (