@monkey-mini-app/ui 0.1.1 → 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.
- package/README.md +12 -6
- package/dist/globals.css +1 -1
- package/dist/index.js +12 -2
- package/dist/manifest.json +6 -6
- package/dist/runtime.js +9 -0
- package/dist/sdk.js +158 -0
- package/dist/src/blocks/activity-feed.tsx +7 -0
- package/dist/src/blocks/app-shell.tsx +1 -0
- package/dist/src/blocks/attachment-gallery.tsx +7 -0
- package/dist/src/blocks/bar-chart.tsx +11 -0
- package/dist/src/blocks/comment-thread.tsx +7 -0
- package/dist/src/blocks/dashboard-shell.tsx +109 -0
- package/dist/src/blocks/description-list.tsx +7 -0
- package/dist/src/blocks/detail-panel.tsx +1 -0
- package/dist/src/blocks/donut-chart.tsx +7 -0
- package/dist/src/blocks/env-badge.tsx +7 -0
- package/dist/src/blocks/env-table.tsx +316 -0
- package/dist/src/blocks/file-tree.tsx +7 -0
- package/dist/src/blocks/filter-bar.tsx +1 -0
- package/dist/src/blocks/form-sheet.tsx +160 -0
- package/dist/src/blocks/gauge.tsx +7 -0
- package/dist/src/blocks/list-detail.tsx +121 -0
- package/dist/src/blocks/notification-center.tsx +7 -0
- package/dist/src/blocks/page-header.tsx +1 -0
- package/dist/src/blocks/progress-ring.tsx +7 -0
- package/dist/src/blocks/radar-chart.tsx +7 -0
- package/dist/src/blocks/request-inspector.tsx +7 -0
- package/dist/src/blocks/reveal.tsx +56 -0
- package/dist/src/blocks/run-timeline.tsx +7 -0
- package/dist/src/blocks/scrollspy.tsx +7 -0
- package/dist/src/blocks/settings-split.tsx +154 -0
- package/dist/src/blocks/severity-chip.tsx +7 -0
- package/dist/src/blocks/sparkline.tsx +7 -0
- package/dist/src/blocks/stat-card.tsx +1 -0
- package/dist/src/blocks/status-badge.tsx +1 -0
- package/dist/src/blocks/table-page.tsx +83 -0
- package/dist/src/blocks/terminal.tsx +21 -3
- package/dist/src/blocks/test-step-list.tsx +7 -0
- package/dist/src/blocks/trend-card.tsx +7 -0
- package/dist/src/blocks/wizard-shell.tsx +132 -0
- package/dist/src/cdn-modules.d.ts +5 -0
- package/dist/src/components/accordion.tsx +7 -0
- package/dist/src/components/alert-dialog.tsx +7 -0
- package/dist/src/components/alert.tsx +7 -0
- package/dist/src/components/aspect-ratio.tsx +4 -0
- package/dist/src/components/attachment.tsx +4 -0
- package/dist/src/components/avatar.tsx +4 -0
- package/dist/src/components/badge.tsx +7 -0
- package/dist/src/components/breadcrumb.tsx +4 -0
- package/dist/src/components/bubble.tsx +4 -0
- package/dist/src/components/button-group.tsx +6 -0
- package/dist/src/components/button.tsx +7 -0
- package/dist/src/components/calendar.tsx +4 -0
- package/dist/src/components/card.tsx +8 -1
- package/dist/src/components/chart.tsx +7 -0
- package/dist/src/components/checkbox.tsx +5 -0
- package/dist/src/components/collapsible.tsx +4 -0
- package/dist/src/components/combobox.tsx +4 -0
- package/dist/src/components/command.tsx +6 -0
- package/dist/src/components/context-menu.tsx +4 -0
- package/dist/src/components/dialog.tsx +21 -0
- package/dist/src/components/drawer.tsx +4 -0
- package/dist/src/components/dropdown-menu.tsx +7 -0
- package/dist/src/components/empty.tsx +7 -0
- package/dist/src/components/field.tsx +6 -0
- package/dist/src/components/hover-card.tsx +4 -0
- package/dist/src/components/input-group.tsx +4 -0
- package/dist/src/components/input.tsx +7 -0
- package/dist/src/components/item.tsx +4 -0
- package/dist/src/components/kbd.tsx +4 -0
- package/dist/src/components/label.tsx +7 -0
- package/dist/src/components/marker.tsx +4 -0
- package/dist/src/components/menubar.tsx +4 -0
- package/dist/src/components/message-scroller.tsx +4 -0
- package/dist/src/components/message.tsx +4 -0
- package/dist/src/components/native-select.tsx +4 -0
- package/dist/src/components/navigation-menu.tsx +4 -0
- package/dist/src/components/pagination.tsx +4 -0
- package/dist/src/components/popover.tsx +7 -0
- package/dist/src/components/progress.tsx +7 -0
- package/dist/src/components/questionnaire.tsx +4 -0
- package/dist/src/components/radio-group.tsx +4 -0
- package/dist/src/components/resizable.tsx +7 -0
- package/dist/src/components/scroll-area.tsx +4 -0
- package/dist/src/components/select.tsx +7 -0
- package/dist/src/components/separator.tsx +5 -0
- package/dist/src/components/sheet.tsx +22 -0
- package/dist/src/components/sidebar.tsx +4 -0
- package/dist/src/components/skeleton.tsx +7 -0
- package/dist/src/components/slider.tsx +4 -0
- package/dist/src/components/spinner.tsx +6 -0
- package/dist/src/components/switch.tsx +7 -0
- package/dist/src/components/table.tsx +7 -0
- package/dist/src/components/tabs.tsx +7 -0
- package/dist/src/components/textarea.tsx +7 -0
- package/dist/src/components/toast.tsx +7 -0
- package/dist/src/components/toggle-group.tsx +4 -0
- package/dist/src/components/toggle.tsx +4 -0
- package/dist/src/components/tooltip.tsx +7 -0
- package/dist/src/composites/autocomplete.tsx +7 -0
- package/dist/src/composites/cascader.tsx +7 -0
- package/dist/src/composites/color-picker.tsx +7 -0
- package/dist/src/composites/confirm-dialog.tsx +7 -0
- package/dist/src/composites/copyable.tsx +9 -1
- package/dist/src/composites/currency-input.tsx +7 -0
- package/dist/src/composites/date-picker.tsx +7 -0
- package/dist/src/composites/date-range-picker.tsx +4 -0
- package/dist/src/composites/date-time-picker.tsx +7 -0
- package/dist/src/composites/date-time-range-picker.tsx +7 -0
- package/dist/src/composites/duration-input.tsx +7 -0
- package/dist/src/composites/mini-calendar.tsx +7 -0
- package/dist/src/composites/number-field.tsx +7 -0
- package/dist/src/composites/password-field.tsx +7 -0
- package/dist/src/composites/phone-input.tsx +7 -0
- package/dist/src/composites/rating.tsx +7 -0
- package/dist/src/composites/relative-date-picker.tsx +7 -0
- package/dist/src/composites/search-input.tsx +7 -0
- package/dist/src/composites/slider-range.tsx +7 -0
- package/dist/src/composites/tag-input.tsx +7 -0
- package/dist/src/composites/time-picker.tsx +11 -1
- package/dist/src/composites/time-range-picker.tsx +7 -0
- package/dist/src/composites/timezone-select.tsx +7 -0
- package/dist/src/composites/transfer.tsx +7 -0
- package/dist/src/composites/user-picker.tsx +7 -0
- package/dist/src/hooks/use-html-dark.ts +37 -0
- package/dist/src/hooks/use-mobile.ts +3 -0
- package/dist/src/i18n/en.ts +109 -2
- package/dist/src/i18n/zh.ts +99 -2
- package/dist/src/index.ts +147 -134
- package/dist/src/lib/clipboard.ts +10 -0
- package/dist/src/lib/illustrations.tsx +2 -2
- package/dist/src/products/code-block.tsx +35 -18
- package/dist/src/products/code-editor.tsx +156 -24
- package/dist/src/products/commit-graph.tsx +644 -0
- package/dist/src/products/data-grid.tsx +2 -1
- package/dist/src/products/diff-viewer.tsx +104 -8
- package/dist/src/products/event-calendar.tsx +28 -823
- package/dist/src/products/file-dropzone.tsx +7 -0
- package/dist/src/products/full-calendar/animations.ts +38 -0
- package/dist/src/products/full-calendar/calendar-body.tsx +77 -0
- package/dist/src/products/full-calendar/constants.ts +10 -0
- package/dist/src/products/full-calendar/contexts/calendar-context.tsx +255 -0
- package/dist/src/products/full-calendar/contexts/dnd-context.tsx +157 -0
- package/dist/src/products/full-calendar/dialogs/add-edit-event-dialog.tsx +187 -0
- package/dist/src/products/full-calendar/dialogs/delete-event-dialog.tsx +39 -0
- package/dist/src/products/full-calendar/dialogs/event-details-dialog.tsx +100 -0
- package/dist/src/products/full-calendar/dialogs/events-list-dialog.tsx +87 -0
- package/dist/src/products/full-calendar/dnd/draggable-event.tsx +45 -0
- package/dist/src/products/full-calendar/dnd/droppable-area.tsx +45 -0
- package/dist/src/products/full-calendar/dnd/resizable-event.tsx +179 -0
- package/dist/src/products/full-calendar/header/calendar-header.tsx +65 -0
- package/dist/src/products/full-calendar/header/date-navigator.tsx +110 -0
- package/dist/src/products/full-calendar/header/filter.tsx +58 -0
- package/dist/src/products/full-calendar/header/today-button.tsx +45 -0
- package/dist/src/products/full-calendar/header/user-select.tsx +64 -0
- package/dist/src/products/full-calendar/header/view-tabs.tsx +36 -0
- package/dist/src/products/full-calendar/helpers.ts +453 -0
- package/dist/src/products/full-calendar/hooks/use-time-range-create.ts +152 -0
- package/dist/src/products/full-calendar/hooks.ts +70 -0
- package/dist/src/products/full-calendar/interfaces.ts +23 -0
- package/dist/src/products/full-calendar/settings/settings.tsx +80 -0
- package/dist/src/products/full-calendar/shims/avatar-group.tsx +16 -0
- package/dist/src/products/full-calendar/shims/day-picker.tsx +1 -0
- package/dist/src/products/full-calendar/shims/motion.tsx +97 -0
- package/dist/src/products/full-calendar/shims/next-themes.ts +6 -0
- package/dist/src/products/full-calendar/shims/re-resizable.tsx +25 -0
- package/dist/src/products/full-calendar/shims/sonner.ts +4 -0
- package/dist/src/products/full-calendar/types.ts +8 -0
- package/dist/src/products/full-calendar/views/agenda-view/agenda-events.tsx +140 -0
- package/dist/src/products/full-calendar/views/month-view/calendar-month-view.tsx +69 -0
- package/dist/src/products/full-calendar/views/month-view/day-cell.tsx +201 -0
- package/dist/src/products/full-calendar/views/month-view/event-bullet.tsx +40 -0
- package/dist/src/products/full-calendar/views/month-view/month-event-badge.tsx +143 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/calendar-day-view.tsx +295 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/calendar-time-line.tsx +35 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/calendar-week-view.tsx +253 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/day-view-multi-day-events-row.tsx +73 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/event-block.tsx +114 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/render-grouped-events.tsx +49 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/week-view-multi-day-events-row.tsx +143 -0
- package/dist/src/products/full-calendar/views/year-view/calendar-year-view.tsx +161 -0
- package/dist/src/products/gantt.tsx +7 -0
- package/dist/src/products/jira-wiki.tsx +1 -0
- package/dist/src/products/jql-input.tsx +340 -37
- package/dist/src/products/json-viewer.tsx +7 -0
- package/dist/src/products/kanban-issue-panel.tsx +7 -0
- package/dist/src/products/kanban.tsx +1 -0
- package/dist/src/products/log-viewer.tsx +390 -18
- package/dist/src/products/markdown-editor.tsx +21 -6
- package/dist/src/products/markdown.tsx +7 -0
- package/dist/src/products/rich-text-editor.tsx +65 -59
- package/dist/src/products/sortable-list.tsx +7 -0
- package/dist/src/products/stepper.tsx +219 -70
- package/dist/src/products/timeline.tsx +7 -0
- package/dist/src/products/tree-view.tsx +7 -0
- package/dist/src/sdk/app-error-boundary.tsx +240 -0
- package/dist/src/sdk/app-id.ts +7 -0
- package/dist/src/sdk/use-app.ts +126 -0
- package/dist/src/styles/globals.css +1 -0
- package/dist/vendors/lodash.js +20 -0
- package/dist/vendors/motion.js +12 -0
- package/package.json +20 -19
- 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">
|
|
@@ -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
|
}: {
|
|
@@ -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 (
|