@monkey-mini-app/ui 0.1.0 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- 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 +15 -1
- 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 +19 -2
- 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 +29 -6
- 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 +20 -2
- package/dist/src/blocks/radar-chart.tsx +18 -1
- 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 +12 -3
- 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 +7 -7
- package/dist/vendors/lodash.js +20 -0
- package/dist/vendors/motion.js +12 -0
- package/package.json +23 -19
- package/dist/src/products/jql-language.ts +0 -70
|
@@ -1,33 +1,405 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
5
|
+
import {
|
|
6
|
+
ArrowDown,
|
|
7
|
+
Check,
|
|
8
|
+
Copy,
|
|
9
|
+
Download,
|
|
10
|
+
Pause,
|
|
11
|
+
Play,
|
|
12
|
+
Search,
|
|
13
|
+
Terminal,
|
|
14
|
+
Trash2,
|
|
15
|
+
X,
|
|
16
|
+
} from "lucide-react";
|
|
17
|
+
|
|
18
|
+
import { useLabels } from "@monkey-mini-app/ui/i18n/context";
|
|
19
|
+
import { writeClipboard } from "@monkey-mini-app/ui/lib/clipboard";
|
|
20
|
+
import { cn } from "@monkey-mini-app/ui/lib/utils";
|
|
21
|
+
|
|
22
|
+
export type LogLevel = "info" | "warn" | "error" | "debug" | "verbose";
|
|
23
|
+
|
|
24
|
+
export type LogEntry = {
|
|
25
|
+
level: LogLevel;
|
|
26
|
+
message: string;
|
|
27
|
+
timestamp?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type LevelColors = {
|
|
31
|
+
text: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const DEFAULT_LEVEL_COLORS: Record<LogLevel, LevelColors> = {
|
|
35
|
+
error: { text: "text-rose-500 dark:text-rose-400" },
|
|
36
|
+
warn: { text: "text-amber-500 dark:text-amber-400" },
|
|
37
|
+
info: { text: "text-sky-500 dark:text-sky-400" },
|
|
38
|
+
debug: { text: "text-violet-500 dark:text-violet-400" },
|
|
39
|
+
verbose: { text: "text-zinc-400 dark:text-zinc-500" },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const LEVEL_LABELS: Record<LogLevel, string> = {
|
|
43
|
+
error: "ERR",
|
|
44
|
+
warn: "WRN",
|
|
45
|
+
info: "INF",
|
|
46
|
+
debug: "DBG",
|
|
47
|
+
verbose: "VRB",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function formatTimestampFull(ts?: string): string {
|
|
51
|
+
const d = ts ? new Date(ts) : new Date();
|
|
52
|
+
const ms = d.getMilliseconds().toString().padStart(3, "0");
|
|
53
|
+
return `${d.toLocaleTimeString("en-US", {
|
|
54
|
+
hour12: false,
|
|
55
|
+
hour: "2-digit",
|
|
56
|
+
minute: "2-digit",
|
|
57
|
+
second: "2-digit",
|
|
58
|
+
})}.${ms}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function useCopy() {
|
|
62
|
+
const [copied, setCopied] = React.useState(false);
|
|
63
|
+
|
|
64
|
+
const copy = React.useCallback(async (value: string) => {
|
|
65
|
+
if (!(await writeClipboard(value))) return;
|
|
66
|
+
setCopied(true);
|
|
67
|
+
window.setTimeout(() => setCopied(false), 1500);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
return { copied, copy };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function exportLogs(entries: LogEntry[]): void {
|
|
74
|
+
const text = entries
|
|
75
|
+
.map(
|
|
76
|
+
(e) =>
|
|
77
|
+
`[${formatTimestampFull(e.timestamp)}] [${LEVEL_LABELS[e.level]}] ${e.message}`,
|
|
78
|
+
)
|
|
79
|
+
.join("\n");
|
|
80
|
+
const blob = new Blob([text], { type: "text/plain" });
|
|
81
|
+
const url = URL.createObjectURL(blob);
|
|
82
|
+
const a = document.createElement("a");
|
|
83
|
+
a.href = url;
|
|
84
|
+
a.download = `logs-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.txt`;
|
|
85
|
+
a.click();
|
|
86
|
+
URL.revokeObjectURL(url);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function highlightSearch(text: string, query: string): React.ReactNode {
|
|
90
|
+
if (!query) return text;
|
|
91
|
+
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
92
|
+
const re = new RegExp(escaped, "gi");
|
|
93
|
+
const nodes: React.ReactNode[] = [];
|
|
94
|
+
let lastIndex = 0;
|
|
95
|
+
let match = re.exec(text);
|
|
96
|
+
while (match) {
|
|
97
|
+
if (match.index > lastIndex) {
|
|
98
|
+
nodes.push(text.slice(lastIndex, match.index));
|
|
99
|
+
}
|
|
100
|
+
nodes.push(
|
|
101
|
+
<mark
|
|
102
|
+
key={`h-${match.index}-${match[0]}`}
|
|
103
|
+
className="rounded-sm bg-amber-300/40 px-0.5 text-inherit dark:bg-amber-400/30"
|
|
104
|
+
>
|
|
105
|
+
{match[0]}
|
|
106
|
+
</mark>,
|
|
107
|
+
);
|
|
108
|
+
lastIndex = match.index + match[0].length;
|
|
109
|
+
match = re.exec(text);
|
|
110
|
+
}
|
|
111
|
+
if (lastIndex < text.length) {
|
|
112
|
+
nodes.push(text.slice(lastIndex));
|
|
113
|
+
}
|
|
114
|
+
return nodes;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function ToolbarButton({
|
|
118
|
+
onClick,
|
|
119
|
+
label,
|
|
120
|
+
active,
|
|
121
|
+
children,
|
|
122
|
+
className,
|
|
123
|
+
}: {
|
|
124
|
+
onClick: () => void;
|
|
125
|
+
label: string;
|
|
126
|
+
active?: boolean;
|
|
127
|
+
children: React.ReactNode;
|
|
128
|
+
className?: string;
|
|
129
|
+
}) {
|
|
130
|
+
return (
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={onClick}
|
|
134
|
+
aria-label={label}
|
|
135
|
+
className={cn(
|
|
136
|
+
"inline-flex size-7 items-center justify-center rounded-md transition-colors outline-none",
|
|
137
|
+
"text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
138
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
|
|
139
|
+
active && "bg-accent text-accent-foreground",
|
|
140
|
+
className,
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{children}
|
|
144
|
+
</button>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Virtualized streaming log viewer with level colors, search, and auto-scroll.
|
|
150
|
+
* @when Streaming / CLI-style log output
|
|
151
|
+
* @example
|
|
152
|
+
* <LogViewer entries={[{ level: "info", message: "boot" }, { level: "error", message: "boom" }]} />
|
|
153
|
+
* @family Realtime
|
|
154
|
+
*/
|
|
155
|
+
export function LogViewer({
|
|
156
|
+
entries,
|
|
157
|
+
title,
|
|
158
|
+
maxHeight = 320,
|
|
159
|
+
lineNumbers = true,
|
|
160
|
+
timestamps = true,
|
|
161
|
+
autoScroll = true,
|
|
162
|
+
onClear,
|
|
163
|
+
className,
|
|
164
|
+
}: {
|
|
165
|
+
entries: LogEntry[];
|
|
166
|
+
title?: string;
|
|
167
|
+
maxHeight?: number;
|
|
168
|
+
lineNumbers?: boolean;
|
|
169
|
+
timestamps?: boolean;
|
|
170
|
+
autoScroll?: boolean;
|
|
171
|
+
onClear?: () => void;
|
|
172
|
+
className?: string;
|
|
173
|
+
}) {
|
|
174
|
+
const t = useLabels("logViewer");
|
|
175
|
+
const resolvedTitle = title ?? t.title;
|
|
176
|
+
const [paused, setPaused] = React.useState(false);
|
|
177
|
+
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
178
|
+
const [searchQuery, setSearchQuery] = React.useState("");
|
|
179
|
+
const [isAtBottom, setIsAtBottom] = React.useState(true);
|
|
180
|
+
const parentRef = React.useRef<HTMLDivElement>(null);
|
|
181
|
+
const { copied, copy } = useCopy();
|
|
182
|
+
|
|
183
|
+
const filteredEntries = React.useMemo(
|
|
184
|
+
() =>
|
|
185
|
+
searchQuery
|
|
186
|
+
? entries.filter((e) =>
|
|
187
|
+
e.message.toLowerCase().includes(searchQuery.toLowerCase()),
|
|
188
|
+
)
|
|
189
|
+
: entries,
|
|
190
|
+
[entries, searchQuery],
|
|
191
|
+
);
|
|
5
192
|
|
|
6
|
-
export function LogViewer({ lines }: { lines: string[] }) {
|
|
7
|
-
const parentRef = React.useRef<HTMLDivElement>(null)
|
|
8
193
|
const virtualizer = useVirtualizer({
|
|
9
|
-
count:
|
|
194
|
+
count: filteredEntries.length,
|
|
10
195
|
getScrollElement: () => parentRef.current,
|
|
11
|
-
estimateSize: () =>
|
|
196
|
+
estimateSize: () => 24,
|
|
12
197
|
overscan: 12,
|
|
13
|
-
})
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
const scrollEnabled = autoScroll && !paused;
|
|
201
|
+
|
|
202
|
+
React.useEffect(() => {
|
|
203
|
+
if (!scrollEnabled || !isAtBottom || filteredEntries.length === 0) return;
|
|
204
|
+
virtualizer.scrollToIndex(filteredEntries.length - 1, { align: "end" });
|
|
205
|
+
}, [filteredEntries.length, scrollEnabled, isAtBottom, virtualizer]);
|
|
206
|
+
|
|
207
|
+
const handleScroll = React.useCallback(() => {
|
|
208
|
+
const el = parentRef.current;
|
|
209
|
+
if (!el) return;
|
|
210
|
+
const threshold = 40;
|
|
211
|
+
const atBottom =
|
|
212
|
+
el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
|
213
|
+
setIsAtBottom(atBottom);
|
|
214
|
+
}, []);
|
|
215
|
+
|
|
216
|
+
const scrollToBottom = React.useCallback(() => {
|
|
217
|
+
if (filteredEntries.length === 0) return;
|
|
218
|
+
virtualizer.scrollToIndex(filteredEntries.length - 1, { align: "end" });
|
|
219
|
+
setIsAtBottom(true);
|
|
220
|
+
}, [filteredEntries.length, virtualizer]);
|
|
221
|
+
|
|
222
|
+
const lineNumberWidth = Math.max(String(filteredEntries.length).length, 3);
|
|
223
|
+
|
|
224
|
+
function handleCopyAll() {
|
|
225
|
+
const text = entries
|
|
226
|
+
.map(
|
|
227
|
+
(e) =>
|
|
228
|
+
`[${formatTimestampFull(e.timestamp)}] [${LEVEL_LABELS[e.level]}] ${e.message}`,
|
|
229
|
+
)
|
|
230
|
+
.join("\n");
|
|
231
|
+
void copy(text);
|
|
232
|
+
}
|
|
233
|
+
|
|
14
234
|
return (
|
|
15
235
|
<div
|
|
16
|
-
ref={parentRef}
|
|
17
236
|
data-testid="log-viewer"
|
|
18
|
-
className=
|
|
237
|
+
className={cn(
|
|
238
|
+
"flex flex-col overflow-hidden rounded-xl border border-border/60 bg-card shadow-sm",
|
|
239
|
+
className,
|
|
240
|
+
)}
|
|
19
241
|
>
|
|
20
|
-
<div
|
|
21
|
-
|
|
242
|
+
<div className="flex items-center gap-2 border-b border-border/40 bg-muted/30 px-3 py-2">
|
|
243
|
+
<Terminal className="size-3.5 shrink-0 text-muted-foreground" />
|
|
244
|
+
<span className="flex-1 truncate text-sm font-medium text-foreground">
|
|
245
|
+
{resolvedTitle}
|
|
246
|
+
</span>
|
|
247
|
+
|
|
248
|
+
<span className="mr-1 text-[10px] tabular-nums text-muted-foreground">
|
|
249
|
+
{searchQuery
|
|
250
|
+
? t.linesFiltered(filteredEntries.length, entries.length)
|
|
251
|
+
: t.lines(filteredEntries.length)}
|
|
252
|
+
</span>
|
|
253
|
+
|
|
254
|
+
<div className="flex items-center gap-0.5">
|
|
255
|
+
<ToolbarButton
|
|
256
|
+
onClick={() => {
|
|
257
|
+
setSearchOpen(!searchOpen);
|
|
258
|
+
if (searchOpen) setSearchQuery("");
|
|
259
|
+
}}
|
|
260
|
+
label={searchOpen ? t.closeSearch : t.search}
|
|
261
|
+
active={searchOpen}
|
|
262
|
+
>
|
|
263
|
+
<Search className="size-3.5" />
|
|
264
|
+
</ToolbarButton>
|
|
265
|
+
|
|
266
|
+
<ToolbarButton
|
|
267
|
+
onClick={() => setPaused(!paused)}
|
|
268
|
+
label={paused ? t.resumeScroll : t.pauseScroll}
|
|
269
|
+
active={paused}
|
|
270
|
+
>
|
|
271
|
+
{paused ? (
|
|
272
|
+
<Play className="size-3.5" />
|
|
273
|
+
) : (
|
|
274
|
+
<Pause className="size-3.5" />
|
|
275
|
+
)}
|
|
276
|
+
</ToolbarButton>
|
|
277
|
+
|
|
278
|
+
<ToolbarButton
|
|
279
|
+
onClick={handleCopyAll}
|
|
280
|
+
label={copied ? t.copied : t.copyAll}
|
|
281
|
+
>
|
|
282
|
+
{copied ? (
|
|
283
|
+
<Check className="size-3.5 text-emerald-500" />
|
|
284
|
+
) : (
|
|
285
|
+
<Copy className="size-3.5" />
|
|
286
|
+
)}
|
|
287
|
+
</ToolbarButton>
|
|
288
|
+
|
|
289
|
+
<ToolbarButton onClick={() => exportLogs(entries)} label={t.download}>
|
|
290
|
+
<Download className="size-3.5" />
|
|
291
|
+
</ToolbarButton>
|
|
292
|
+
|
|
293
|
+
{onClear ? (
|
|
294
|
+
<ToolbarButton onClick={onClear} label={t.clear}>
|
|
295
|
+
<Trash2 className="size-3.5" />
|
|
296
|
+
</ToolbarButton>
|
|
297
|
+
) : null}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{searchOpen ? (
|
|
302
|
+
<div className="flex items-center gap-2 border-b border-border/40 bg-muted/20 px-3 py-1.5">
|
|
303
|
+
<Search className="size-3.5 shrink-0 text-muted-foreground" />
|
|
304
|
+
<input
|
|
305
|
+
type="text"
|
|
306
|
+
value={searchQuery}
|
|
307
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
308
|
+
placeholder={t.filterPlaceholder}
|
|
309
|
+
className="flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground"
|
|
310
|
+
/>
|
|
311
|
+
{searchQuery ? (
|
|
312
|
+
<button
|
|
313
|
+
type="button"
|
|
314
|
+
onClick={() => setSearchQuery("")}
|
|
315
|
+
className="inline-flex size-5 items-center justify-center rounded text-muted-foreground hover:text-foreground"
|
|
316
|
+
aria-label={t.clearSearch}
|
|
317
|
+
>
|
|
318
|
+
<X className="size-3" />
|
|
319
|
+
</button>
|
|
320
|
+
) : null}
|
|
321
|
+
</div>
|
|
322
|
+
) : null}
|
|
323
|
+
|
|
324
|
+
<div
|
|
325
|
+
ref={parentRef}
|
|
326
|
+
onScroll={handleScroll}
|
|
327
|
+
className="overflow-auto bg-card font-mono text-xs leading-relaxed [scrollbar-width:thin]"
|
|
328
|
+
style={{ maxHeight }}
|
|
329
|
+
role="log"
|
|
330
|
+
aria-live="polite"
|
|
331
|
+
aria-label={resolvedTitle}
|
|
332
|
+
>
|
|
333
|
+
{filteredEntries.length === 0 ? (
|
|
334
|
+
<div className="flex items-center justify-center py-10 text-sm text-muted-foreground">
|
|
335
|
+
{searchQuery ? t.emptySearch : t.empty}
|
|
336
|
+
</div>
|
|
337
|
+
) : (
|
|
22
338
|
<div
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
339
|
+
style={{
|
|
340
|
+
height: virtualizer.getTotalSize(),
|
|
341
|
+
position: "relative",
|
|
342
|
+
width: "100%",
|
|
343
|
+
}}
|
|
26
344
|
>
|
|
27
|
-
{
|
|
345
|
+
{virtualizer.getVirtualItems().map((item) => {
|
|
346
|
+
const entry = filteredEntries[item.index];
|
|
347
|
+
if (!entry) return null;
|
|
348
|
+
const colors = DEFAULT_LEVEL_COLORS[entry.level];
|
|
349
|
+
return (
|
|
350
|
+
<div
|
|
351
|
+
key={item.key}
|
|
352
|
+
data-index={item.index}
|
|
353
|
+
ref={virtualizer.measureElement}
|
|
354
|
+
className="absolute left-0 flex w-full gap-3 border-b border-border/20 px-3 py-1 transition-colors hover:bg-muted/30"
|
|
355
|
+
style={{
|
|
356
|
+
top: item.start,
|
|
357
|
+
height: item.size,
|
|
358
|
+
}}
|
|
359
|
+
>
|
|
360
|
+
{lineNumbers ? (
|
|
361
|
+
<span
|
|
362
|
+
className="shrink-0 select-none text-right text-muted-foreground/50"
|
|
363
|
+
style={{ width: `${lineNumberWidth}ch` }}
|
|
364
|
+
aria-hidden="true"
|
|
365
|
+
>
|
|
366
|
+
{item.index + 1}
|
|
367
|
+
</span>
|
|
368
|
+
) : null}
|
|
369
|
+
{timestamps ? (
|
|
370
|
+
<span className="shrink-0 text-muted-foreground/60">
|
|
371
|
+
{formatTimestampFull(entry.timestamp)}
|
|
372
|
+
</span>
|
|
373
|
+
) : null}
|
|
374
|
+
<span
|
|
375
|
+
className={cn(
|
|
376
|
+
"w-[3ch] shrink-0 text-right font-semibold",
|
|
377
|
+
colors.text,
|
|
378
|
+
)}
|
|
379
|
+
>
|
|
380
|
+
{LEVEL_LABELS[entry.level]}
|
|
381
|
+
</span>
|
|
382
|
+
<span className="min-w-0 flex-1 truncate whitespace-pre text-foreground/90">
|
|
383
|
+
{highlightSearch(entry.message, searchQuery)}
|
|
384
|
+
</span>
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
})}
|
|
28
388
|
</div>
|
|
29
|
-
)
|
|
389
|
+
)}
|
|
30
390
|
</div>
|
|
391
|
+
|
|
392
|
+
{!isAtBottom ? (
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={scrollToBottom}
|
|
396
|
+
className="flex w-full items-center justify-center gap-1.5 border-t border-border/40 bg-muted/30 py-1.5 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
|
397
|
+
aria-label={t.scrollLatest}
|
|
398
|
+
>
|
|
399
|
+
<ArrowDown className="size-3" />
|
|
400
|
+
{t.newBelow}
|
|
401
|
+
</button>
|
|
402
|
+
) : null}
|
|
31
403
|
</div>
|
|
32
|
-
)
|
|
404
|
+
);
|
|
33
405
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
3
5
|
import { Button } from "@monkey-mini-app/ui/components/button"
|
|
4
6
|
import { CodeEditor } from "@monkey-mini-app/ui/products/code-editor"
|
|
5
7
|
import { Markdown } from "@monkey-mini-app/ui/products/markdown"
|
|
@@ -8,6 +10,13 @@ import { useLabels } from "@monkey-mini-app/ui/i18n/context"
|
|
|
8
10
|
|
|
9
11
|
export type MarkdownEditorMode = "edit" | "split" | "preview"
|
|
10
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Markdown edit + preview toggle.
|
|
15
|
+
* @when Authoring md bodies. Read-only render → `Markdown`.
|
|
16
|
+
* @example
|
|
17
|
+
* <MarkdownEditor value={md} onChange={(v) => set(v)} />
|
|
18
|
+
* @family Rich text
|
|
19
|
+
*/
|
|
11
20
|
export function MarkdownEditor({
|
|
12
21
|
value,
|
|
13
22
|
onChange,
|
|
@@ -20,6 +29,12 @@ export function MarkdownEditor({
|
|
|
20
29
|
onModeChange?: (mode: MarkdownEditorMode) => void
|
|
21
30
|
}) {
|
|
22
31
|
const t = useLabels("markdownEditor")
|
|
32
|
+
const [currentMode, setCurrentMode] = React.useState<MarkdownEditorMode>(mode)
|
|
33
|
+
React.useEffect(() => setCurrentMode(mode), [mode])
|
|
34
|
+
const setMode = (next: MarkdownEditorMode) => {
|
|
35
|
+
setCurrentMode(next)
|
|
36
|
+
onModeChange?.(next)
|
|
37
|
+
}
|
|
23
38
|
return (
|
|
24
39
|
<div data-testid="markdown-editor" className="overflow-hidden rounded-xl border bg-card">
|
|
25
40
|
<div className="flex items-center gap-1 border-b p-1">
|
|
@@ -27,9 +42,9 @@ export function MarkdownEditor({
|
|
|
27
42
|
<Button
|
|
28
43
|
key={item}
|
|
29
44
|
size="sm"
|
|
30
|
-
variant={
|
|
45
|
+
variant={currentMode === item ? "default" : "ghost"}
|
|
31
46
|
data-testid={`markdown-mode-${item}`}
|
|
32
|
-
onClick={() =>
|
|
47
|
+
onClick={() => setMode(item)}
|
|
33
48
|
>
|
|
34
49
|
{t[item]}
|
|
35
50
|
</Button>
|
|
@@ -38,11 +53,11 @@ export function MarkdownEditor({
|
|
|
38
53
|
<div
|
|
39
54
|
className={cn(
|
|
40
55
|
"grid min-h-[280px]",
|
|
41
|
-
|
|
56
|
+
currentMode === "split" ? "md:grid-cols-2" : "grid-cols-1"
|
|
42
57
|
)}
|
|
43
58
|
>
|
|
44
|
-
{
|
|
45
|
-
<div className={cn(
|
|
59
|
+
{currentMode !== "preview" ? (
|
|
60
|
+
<div className={cn(currentMode === "split" && "border-b md:border-r md:border-b-0")}>
|
|
46
61
|
<CodeEditor
|
|
47
62
|
value={value}
|
|
48
63
|
onChange={onChange}
|
|
@@ -52,7 +67,7 @@ export function MarkdownEditor({
|
|
|
52
67
|
/>
|
|
53
68
|
</div>
|
|
54
69
|
) : null}
|
|
55
|
-
{
|
|
70
|
+
{currentMode !== "edit" ? (
|
|
56
71
|
<div className="overflow-auto p-3" data-testid="markdown-preview">
|
|
57
72
|
<Markdown>{value}</Markdown>
|
|
58
73
|
</div>
|
|
@@ -5,6 +5,13 @@ import remarkGfm from "remark-gfm"
|
|
|
5
5
|
|
|
6
6
|
import { cn } from "@monkey-mini-app/ui/lib/utils"
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Render a markdown string.
|
|
10
|
+
* @when Model output, README-ish bodies. Children must be a string.
|
|
11
|
+
* @example
|
|
12
|
+
* <Markdown>{text}</Markdown>
|
|
13
|
+
* @family Rich text
|
|
14
|
+
*/
|
|
8
15
|
export function Markdown({
|
|
9
16
|
children,
|
|
10
17
|
className,
|
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import StarterKit from "@tiptap/starter-kit"
|
|
5
|
-
import Placeholder from "@tiptap/extension-placeholder"
|
|
3
|
+
import * as React from "react"
|
|
6
4
|
import { Bold, Code, Italic, List, ListOrdered } from "lucide-react"
|
|
7
5
|
|
|
8
|
-
import {
|
|
9
|
-
import { cn } from "@monkey-mini-app/ui/lib/utils"
|
|
6
|
+
import { Button } from "@monkey-mini-app/ui/components/button"
|
|
10
7
|
import { useLabels } from "@monkey-mini-app/ui/i18n/context"
|
|
8
|
+
import { cn } from "@monkey-mini-app/ui/lib/utils"
|
|
9
|
+
|
|
10
|
+
function runCommand(command: string, value?: string) {
|
|
11
|
+
// Same primitive as react-simple-wysiwyg. Deprecated, still what lightweight
|
|
12
|
+
// browsers expose for simple WYSIWYG; no TipTap / CDN / second React.
|
|
13
|
+
document.execCommand(command, false, value)
|
|
14
|
+
}
|
|
11
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Lightweight rich-text editor (contentEditable + toolbar), same idea as
|
|
18
|
+
* `react-simple-wysiwyg`. HTML is browser-generated — sanitize server-side if needed.
|
|
19
|
+
* @family Rich text
|
|
20
|
+
* @when Notes/descriptions you store as **HTML**. Markdown → `MarkdownEditor`; code → `CodeEditor`. Sanitize server-side before rendering elsewhere.
|
|
21
|
+
*/
|
|
12
22
|
export function RichTextEditor({
|
|
13
23
|
value = "<p></p>",
|
|
14
24
|
onChange,
|
|
@@ -19,66 +29,62 @@ export function RichTextEditor({
|
|
|
19
29
|
placeholder?: string
|
|
20
30
|
}) {
|
|
21
31
|
const t = useLabels("richText")
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
32
|
+
const ref = React.useRef<HTMLDivElement>(null)
|
|
33
|
+
const ph = placeholder ?? t.placeholder
|
|
34
|
+
|
|
35
|
+
React.useEffect(() => {
|
|
36
|
+
const el = ref.current
|
|
37
|
+
if (!el) return
|
|
38
|
+
if (el.innerHTML !== value) el.innerHTML = value || ""
|
|
39
|
+
}, [value])
|
|
40
|
+
|
|
41
|
+
const emit = () => {
|
|
42
|
+
onChange?.(ref.current?.innerHTML ?? "")
|
|
43
|
+
}
|
|
34
44
|
|
|
35
|
-
|
|
45
|
+
const cmd = (command: string, arg?: string) => (e: React.MouseEvent) => {
|
|
46
|
+
e.preventDefault()
|
|
47
|
+
ref.current?.focus()
|
|
48
|
+
runCommand(command, arg)
|
|
49
|
+
emit()
|
|
50
|
+
}
|
|
36
51
|
|
|
37
52
|
return (
|
|
38
53
|
<div data-testid="rich-text-editor" className="overflow-hidden rounded-xl border bg-card">
|
|
39
54
|
<div className="flex flex-wrap gap-0.5 border-b p-1">
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
</Toggle>
|
|
56
|
-
<Toggle
|
|
57
|
-
size="sm"
|
|
58
|
-
pressed={editor.isActive("bulletList")}
|
|
59
|
-
aria-label={t.bullet}
|
|
60
|
-
onPressedChange={() => editor.chain().focus().toggleBulletList().run()}
|
|
61
|
-
>
|
|
62
|
-
<List />
|
|
63
|
-
</Toggle>
|
|
64
|
-
<Toggle
|
|
65
|
-
size="sm"
|
|
66
|
-
pressed={editor.isActive("orderedList")}
|
|
67
|
-
aria-label={t.ordered}
|
|
68
|
-
onPressedChange={() => editor.chain().focus().toggleOrderedList().run()}
|
|
69
|
-
>
|
|
70
|
-
<ListOrdered />
|
|
71
|
-
</Toggle>
|
|
72
|
-
<Toggle
|
|
73
|
-
size="sm"
|
|
74
|
-
pressed={editor.isActive("code")}
|
|
75
|
-
aria-label={t.code}
|
|
76
|
-
onPressedChange={() => editor.chain().focus().toggleCode().run()}
|
|
77
|
-
>
|
|
78
|
-
<Code />
|
|
79
|
-
</Toggle>
|
|
55
|
+
<Button type="button" size="sm" variant="ghost" aria-label={t.bold} onMouseDown={cmd("bold")}>
|
|
56
|
+
<Bold className="size-3.5" />
|
|
57
|
+
</Button>
|
|
58
|
+
<Button type="button" size="sm" variant="ghost" aria-label={t.italic} onMouseDown={cmd("italic")}>
|
|
59
|
+
<Italic className="size-3.5" />
|
|
60
|
+
</Button>
|
|
61
|
+
<Button type="button" size="sm" variant="ghost" aria-label={t.code} onMouseDown={cmd("formatBlock", "pre")}>
|
|
62
|
+
<Code className="size-3.5" />
|
|
63
|
+
</Button>
|
|
64
|
+
<Button type="button" size="sm" variant="ghost" aria-label={t.bullet} onMouseDown={cmd("insertUnorderedList")}>
|
|
65
|
+
<List className="size-3.5" />
|
|
66
|
+
</Button>
|
|
67
|
+
<Button type="button" size="sm" variant="ghost" aria-label={t.ordered} onMouseDown={cmd("insertOrderedList")}>
|
|
68
|
+
<ListOrdered className="size-3.5" />
|
|
69
|
+
</Button>
|
|
80
70
|
</div>
|
|
81
|
-
<
|
|
71
|
+
<div
|
|
72
|
+
ref={ref}
|
|
73
|
+
role="textbox"
|
|
74
|
+
aria-multiline="true"
|
|
75
|
+
aria-placeholder={ph}
|
|
76
|
+
contentEditable
|
|
77
|
+
suppressContentEditableWarning
|
|
78
|
+
data-placeholder={ph}
|
|
79
|
+
className={cn(
|
|
80
|
+
"min-h-40 px-3 py-2 text-sm outline-none",
|
|
81
|
+
"[&_ul]:list-disc [&_ul]:pl-6 [&_ol]:list-decimal [&_ol]:pl-6",
|
|
82
|
+
"[&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:px-2 [&_pre]:py-1 [&_pre]:font-mono [&_pre]:text-xs",
|
|
83
|
+
"empty:before:pointer-events-none empty:before:text-muted-foreground empty:before:content-[attr(data-placeholder)]",
|
|
84
|
+
)}
|
|
85
|
+
onInput={emit}
|
|
86
|
+
onBlur={emit}
|
|
87
|
+
/>
|
|
82
88
|
</div>
|
|
83
89
|
)
|
|
84
90
|
}
|
|
@@ -33,6 +33,13 @@ function Row({ item }: { item: SortableItem }) {
|
|
|
33
33
|
)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Drag-to-reorder list.
|
|
38
|
+
* @when Priority queues / column order. Read-only ordered events → `Timeline`.
|
|
39
|
+
* @example
|
|
40
|
+
* <SortableList items={items} onChange={(next) => set(next)} />
|
|
41
|
+
* @family Data & tables
|
|
42
|
+
*/
|
|
36
43
|
export function SortableList({
|
|
37
44
|
items,
|
|
38
45
|
onChange,
|