dsh-tool-stats 0.3.0 → 0.4.1

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.
@@ -17,7 +17,7 @@ export function apply(ctx) {
17
17
  ctx.slots.inject('settings.section', () => ctx.slots.register({
18
18
  name: 'settings.section',
19
19
  id: 'tool-stats',
20
- order: 43,
20
+ order: 42,
21
21
  label: () => ctx.locale.bind(NS)('nav'),
22
22
  locale: NS,
23
23
  }, ToolStatsPanel));
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Shared UI component kit for dsh plugin panels.
3
+ *
4
+ * Self-contained: imports nothing but React. All styling is inline + CSS custom
5
+ * properties so the components follow the host theme. A single <style> tag injects
6
+ * keyframes the first time any component mounts.
7
+ *
8
+ * @module client/ui
9
+ */
10
+ import type { ButtonHTMLAttributes, CSSProperties, InputHTMLAttributes, ReactNode, SelectHTMLAttributes } from 'react';
11
+ export declare function Modal({ title, onClose, children, footer, width, }: {
12
+ title: string;
13
+ onClose: () => void;
14
+ children: ReactNode;
15
+ footer?: ReactNode;
16
+ width?: number;
17
+ }): ReactNode;
18
+ export declare function ConfirmDialog({ title, message, confirmLabel, cancelLabel, onConfirm, onClose, danger, }: {
19
+ title: string;
20
+ message: string;
21
+ confirmLabel: string;
22
+ cancelLabel?: string;
23
+ onConfirm: () => void;
24
+ onClose: () => void;
25
+ danger?: boolean;
26
+ }): ReactNode;
27
+ export declare function Button({ variant, size, children, style, ...rest }: {
28
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
29
+ size?: 'sm' | 'md';
30
+ children: ReactNode;
31
+ style?: CSSProperties;
32
+ } & ButtonHTMLAttributes<HTMLButtonElement>): ReactNode;
33
+ export declare function Card({ title, icon, children, actions, style, padding, }: {
34
+ title?: string;
35
+ icon?: string;
36
+ children: ReactNode;
37
+ actions?: ReactNode;
38
+ style?: CSSProperties;
39
+ padding?: number;
40
+ }): ReactNode;
41
+ export declare function StatCard({ value, label, color, unit, }: {
42
+ value: string | number;
43
+ label: string;
44
+ color?: string;
45
+ unit?: string;
46
+ }): ReactNode;
47
+ export declare function Badge({ children, color, }: {
48
+ children: ReactNode;
49
+ color?: 'default' | 'success' | 'error' | 'warning' | 'info';
50
+ }): ReactNode;
51
+ export declare function Input({ style, ...rest }: {
52
+ style?: CSSProperties;
53
+ } & InputHTMLAttributes<HTMLInputElement>): ReactNode;
54
+ export declare function Select({ style, children, ...rest }: {
55
+ style?: CSSProperties;
56
+ children: ReactNode;
57
+ } & SelectHTMLAttributes<HTMLSelectElement>): ReactNode;
58
+ export declare function Textarea({ style, ...rest }: {
59
+ style?: CSSProperties;
60
+ } & React.TextareaHTMLAttributes<HTMLTextAreaElement>): ReactNode;
61
+ interface ToastItem {
62
+ id: number;
63
+ type: 'success' | 'error' | 'info';
64
+ message: string;
65
+ }
66
+ export declare function ToastProvider({ children }: {
67
+ children: ReactNode;
68
+ }): ReactNode;
69
+ export declare function useToast(): (type: ToastItem['type'], message: string) => void;
70
+ export declare function Spinner({ size }: {
71
+ size?: number;
72
+ }): ReactNode;
73
+ export declare function EmptyState({ icon, message, }: {
74
+ icon?: string;
75
+ message: string;
76
+ }): ReactNode;
77
+ export declare function SectionTitle({ icon, children, actions, }: {
78
+ icon?: string;
79
+ children: ReactNode;
80
+ actions?: ReactNode;
81
+ }): ReactNode;
82
+ export declare const tableStyles: {
83
+ table: {
84
+ borderCollapse: "collapse";
85
+ width: "100%";
86
+ };
87
+ th: {
88
+ textAlign: "left";
89
+ padding: string;
90
+ fontWeight: 600;
91
+ fontSize: 12;
92
+ color: string;
93
+ borderBottom: string;
94
+ };
95
+ td: {
96
+ padding: string;
97
+ fontSize: 13;
98
+ borderBottom: string;
99
+ };
100
+ clickRow: {
101
+ cursor: "pointer";
102
+ };
103
+ };
104
+ export declare function CodeBlock({ children, maxHeight, style, }: {
105
+ children: string;
106
+ maxHeight?: number;
107
+ style?: CSSProperties;
108
+ }): ReactNode;
109
+ export declare function ProgressBar({ pct, color, height, }: {
110
+ pct: number;
111
+ color?: string;
112
+ height?: number;
113
+ }): ReactNode;
114
+ export declare function Field({ label, children, hint, }: {
115
+ label: string;
116
+ children: ReactNode;
117
+ hint?: string;
118
+ }): ReactNode;
119
+ export declare function usePanel<P>(path: string): {
120
+ payload: P | null;
121
+ error: string | null;
122
+ reload: () => void;
123
+ };
124
+ export {};
@@ -0,0 +1,388 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Shared UI component kit for dsh plugin panels.
4
+ *
5
+ * Self-contained: imports nothing but React. All styling is inline + CSS custom
6
+ * properties so the components follow the host theme. A single <style> tag injects
7
+ * keyframes the first time any component mounts.
8
+ *
9
+ * @module client/ui
10
+ */
11
+ import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
12
+ // ─── Theme tokens ──────────────────────────────────────────────
13
+ const T = {
14
+ accent: 'var(--accent, #4B8BBE)',
15
+ accentHover: 'var(--accent-hover, #3a6f9e)',
16
+ error: 'var(--error, #e53935)',
17
+ success: 'var(--success, #2e7d32)',
18
+ warning: 'var(--warning, #ed6c02)',
19
+ bg: 'var(--bg-secondary, rgba(128,128,128,0.04))',
20
+ bgHover: 'var(--bg-tertiary, rgba(128,128,128,0.08))',
21
+ border: 'var(--border, rgba(128,128,128,0.2))',
22
+ text: 'var(--text-primary, inherit)',
23
+ muted: 'var(--text-secondary, rgba(128,128,128,0.65))',
24
+ surface: 'var(--bg-primary, #fff)',
25
+ };
26
+ // ─── Style injection (once) ────────────────────────────────────
27
+ let injected = false;
28
+ function ensureStyles() {
29
+ if (injected || typeof document === 'undefined')
30
+ return;
31
+ injected = true;
32
+ const el = document.createElement('style');
33
+ el.textContent = `
34
+ @keyframes dsh-fade-in{from{opacity:0}to{opacity:1}}
35
+ @keyframes dsh-slide-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
36
+ @keyframes dsh-spin{to{transform:rotate(360deg)}}
37
+ .dsh-modal-overlay{animation:dsh-fade-in .15s ease}
38
+ .dsh-modal-body{animation:dsh-slide-up .2s ease}
39
+ .dsh-toast{animation:dsh-slide-up .2s ease}
40
+ .dsh-spin{animation:dsh-spin .6s linear infinite}
41
+ `;
42
+ document.head.appendChild(el);
43
+ }
44
+ // ─── Modal ─────────────────────────────────────────────────────
45
+ export function Modal({ title, onClose, children, footer, width = 520, }) {
46
+ useEffect(() => {
47
+ ensureStyles();
48
+ const handler = (e) => {
49
+ if (e.key === 'Escape')
50
+ onClose();
51
+ };
52
+ document.addEventListener('keydown', handler);
53
+ return () => document.removeEventListener('keydown', handler);
54
+ }, [onClose]);
55
+ return (_jsx("div", { className: "dsh-modal-overlay", style: {
56
+ position: 'fixed',
57
+ inset: 0,
58
+ zIndex: 1000,
59
+ display: 'flex',
60
+ alignItems: 'center',
61
+ justifyContent: 'center',
62
+ background: 'rgba(0,0,0,0.45)',
63
+ backdropFilter: 'blur(3px)',
64
+ WebkitBackdropFilter: 'blur(3px)',
65
+ }, onClick: onClose, children: _jsxs("div", { className: "dsh-modal-body", style: {
66
+ background: T.surface,
67
+ borderRadius: 14,
68
+ boxShadow: '0 12px 40px rgba(0,0,0,0.25)',
69
+ width: `min(92vw, ${width}px)`,
70
+ maxHeight: '85vh',
71
+ display: 'flex',
72
+ flexDirection: 'column',
73
+ overflow: 'hidden',
74
+ }, onClick: (e) => e.stopPropagation(), children: [_jsxs("div", { style: {
75
+ display: 'flex',
76
+ alignItems: 'center',
77
+ justifyContent: 'space-between',
78
+ padding: '16px 22px',
79
+ borderBottom: `1px solid ${T.border}`,
80
+ }, children: [_jsx("h3", { style: { margin: 0, fontSize: 15, fontWeight: 600 }, children: title }), _jsx("button", { type: "button", onClick: onClose, style: {
81
+ background: 'none',
82
+ border: 'none',
83
+ cursor: 'pointer',
84
+ fontSize: 20,
85
+ color: T.muted,
86
+ padding: '0 4px',
87
+ lineHeight: 1,
88
+ borderRadius: 4,
89
+ }, "aria-label": "Close", children: "\u00D7" })] }), _jsx("div", { style: { padding: 22, overflow: 'auto', flex: 1 }, children: children }), footer !== undefined && (_jsx("div", { style: {
90
+ padding: '14px 22px',
91
+ borderTop: `1px solid ${T.border}`,
92
+ display: 'flex',
93
+ justifyContent: 'flex-end',
94
+ gap: 10,
95
+ background: T.bg,
96
+ }, children: footer }))] }) }));
97
+ }
98
+ // ─── ConfirmDialog ─────────────────────────────────────────────
99
+ export function ConfirmDialog({ title, message, confirmLabel, cancelLabel = 'Cancel', onConfirm, onClose, danger, }) {
100
+ return (_jsx(Modal, { title: title, onClose: onClose, width: 420, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", onClick: onClose, children: cancelLabel }), _jsx(Button, { variant: danger === true ? 'danger' : 'primary', onClick: () => {
101
+ onConfirm();
102
+ onClose();
103
+ }, children: confirmLabel })] }), children: _jsx("p", { style: { margin: 0, fontSize: 14, lineHeight: 1.6 }, children: message }) }));
104
+ }
105
+ // ─── Button ────────────────────────────────────────────────────
106
+ export function Button({ variant = 'secondary', size = 'md', children, style, ...rest }) {
107
+ const base = {
108
+ cursor: 'pointer',
109
+ borderRadius: 7,
110
+ fontWeight: 500,
111
+ transition: 'all 0.15s ease',
112
+ border: 'none',
113
+ display: 'inline-flex',
114
+ alignItems: 'center',
115
+ gap: 4,
116
+ fontSize: size === 'sm' ? 12 : 13,
117
+ padding: size === 'sm' ? '5px 12px' : '8px 18px',
118
+ fontFamily: 'inherit',
119
+ };
120
+ const variants = {
121
+ primary: { background: T.accent, color: '#fff' },
122
+ secondary: {
123
+ background: T.bg,
124
+ color: T.text,
125
+ border: `1px solid ${T.border}`,
126
+ },
127
+ danger: {
128
+ background: 'transparent',
129
+ color: T.error,
130
+ border: `1px solid ${T.error}`,
131
+ },
132
+ ghost: { background: 'transparent', color: T.muted, border: 'none' },
133
+ };
134
+ return (_jsx("button", { type: "button", style: { ...base, ...variants[variant], ...style }, ...rest, children: children }));
135
+ }
136
+ // ─── Card ──────────────────────────────────────────────────────
137
+ export function Card({ title, icon, children, actions, style, padding = 14, }) {
138
+ return (_jsxs("div", { style: {
139
+ borderRadius: 10,
140
+ border: `1px solid ${T.border}`,
141
+ background: T.bg,
142
+ overflow: 'hidden',
143
+ ...style,
144
+ }, children: [(title !== undefined || actions !== undefined) && (_jsxs("div", { style: {
145
+ display: 'flex',
146
+ alignItems: 'center',
147
+ justifyContent: 'space-between',
148
+ padding: '12px 16px',
149
+ borderBottom: title !== undefined ? `1px solid ${T.border}` : 'none',
150
+ }, children: [_jsxs("span", { style: { fontSize: 13, fontWeight: 600, display: 'flex', alignItems: 'center', gap: 6 }, children: [icon !== undefined && _jsx("span", { children: icon }), title] }), actions] })), _jsx("div", { style: { padding }, children: children })] }));
151
+ }
152
+ // ─── StatCard ──────────────────────────────────────────────────
153
+ export function StatCard({ value, label, color, unit, }) {
154
+ return (_jsxs("div", { style: {
155
+ flex: 1,
156
+ minWidth: 90,
157
+ textAlign: 'center',
158
+ padding: '14px 10px',
159
+ borderRadius: 10,
160
+ border: `1px solid ${T.border}`,
161
+ background: T.bg,
162
+ }, children: [_jsxs("div", { style: { fontSize: 24, fontWeight: 700, color: color ?? T.text, lineHeight: 1.2 }, children: [value, unit !== undefined && (_jsx("span", { style: { fontSize: 13, fontWeight: 500, opacity: 0.7, marginLeft: 2 }, children: unit }))] }), _jsx("div", { style: { fontSize: 11, color: T.muted, marginTop: 4 }, children: label })] }));
163
+ }
164
+ // ─── Badge ─────────────────────────────────────────────────────
165
+ export function Badge({ children, color = 'default', }) {
166
+ const colors = {
167
+ default: { bg: 'rgba(128,128,128,0.12)', fg: T.muted },
168
+ success: { bg: 'rgba(46,125,50,0.12)', fg: T.success },
169
+ error: { bg: 'rgba(229,57,53,0.12)', fg: T.error },
170
+ warning: { bg: 'rgba(245,124,0,0.12)', fg: T.warning },
171
+ info: { bg: 'rgba(75,139,190,0.12)', fg: T.accent },
172
+ };
173
+ const c = colors[color];
174
+ return (_jsx("span", { style: {
175
+ display: 'inline-block',
176
+ padding: '2px 9px',
177
+ borderRadius: 10,
178
+ fontSize: 11,
179
+ fontWeight: 600,
180
+ background: c.bg,
181
+ color: c.fg,
182
+ }, children: children }));
183
+ }
184
+ // ─── Input ─────────────────────────────────────────────────────
185
+ export function Input({ style, ...rest }) {
186
+ return (_jsx("input", { ...rest, style: {
187
+ fontSize: 13,
188
+ padding: '8px 12px',
189
+ borderRadius: 7,
190
+ border: `1px solid ${T.border}`,
191
+ background: T.surface,
192
+ color: T.text,
193
+ width: '100%',
194
+ boxSizing: 'border-box',
195
+ fontFamily: 'inherit',
196
+ outline: 'none',
197
+ transition: 'border-color 0.15s',
198
+ ...style,
199
+ } }));
200
+ }
201
+ // ─── Select ────────────────────────────────────────────────────
202
+ export function Select({ style, children, ...rest }) {
203
+ return (_jsx("select", { ...rest, style: {
204
+ fontSize: 13,
205
+ padding: '8px 12px',
206
+ borderRadius: 7,
207
+ border: `1px solid ${T.border}`,
208
+ background: T.surface,
209
+ color: T.text,
210
+ cursor: 'pointer',
211
+ fontFamily: 'inherit',
212
+ outline: 'none',
213
+ ...style,
214
+ }, children: children }));
215
+ }
216
+ // ─── Textarea ──────────────────────────────────────────────────
217
+ export function Textarea({ style, ...rest }) {
218
+ return (_jsx("textarea", { ...rest, style: {
219
+ fontSize: 13,
220
+ padding: '8px 12px',
221
+ borderRadius: 7,
222
+ border: `1px solid ${T.border}`,
223
+ background: T.surface,
224
+ color: T.text,
225
+ width: '100%',
226
+ boxSizing: 'border-box',
227
+ fontFamily: 'inherit',
228
+ outline: 'none',
229
+ resize: 'vertical',
230
+ lineHeight: 1.5,
231
+ ...style,
232
+ } }));
233
+ }
234
+ const ToastContext = createContext(() => { });
235
+ export function ToastProvider({ children }) {
236
+ const [toasts, setToasts] = useState([]);
237
+ const show = useCallback((type, message) => {
238
+ const id = Date.now() + Math.random();
239
+ setToasts((t) => [...t, { id, type, message }]);
240
+ setTimeout(() => setToasts((t) => t.filter((x) => x.id !== id)), 3500);
241
+ }, []);
242
+ return (_jsxs(ToastContext.Provider, { value: show, children: [children, toasts.length > 0 && (_jsx("div", { style: {
243
+ position: 'fixed',
244
+ bottom: 24,
245
+ right: 24,
246
+ zIndex: 2000,
247
+ display: 'flex',
248
+ flexDirection: 'column',
249
+ gap: 8,
250
+ }, children: toasts.map((t) => (_jsx("div", { className: "dsh-toast", style: {
251
+ padding: '12px 18px',
252
+ borderRadius: 8,
253
+ fontSize: 13,
254
+ fontWeight: 500,
255
+ boxShadow: '0 6px 20px rgba(0,0,0,0.2)',
256
+ color: '#fff',
257
+ background: t.type === 'success'
258
+ ? T.success
259
+ : t.type === 'error'
260
+ ? T.error
261
+ : T.accent,
262
+ maxWidth: 360,
263
+ }, children: t.message }, t.id))) }))] }));
264
+ }
265
+ export function useToast() {
266
+ return useContext(ToastContext);
267
+ }
268
+ // ─── Spinner ───────────────────────────────────────────────────
269
+ export function Spinner({ size = 16 }) {
270
+ ensureStyles();
271
+ return (_jsx("span", { className: "dsh-spin", style: {
272
+ display: 'inline-block',
273
+ width: size,
274
+ height: size,
275
+ border: `2px solid ${T.border}`,
276
+ borderTopColor: T.accent,
277
+ borderRadius: '50%',
278
+ } }));
279
+ }
280
+ // ─── EmptyState ────────────────────────────────────────────────
281
+ export function EmptyState({ icon, message, }) {
282
+ return (_jsxs("div", { style: {
283
+ textAlign: 'center',
284
+ padding: '36px 16px',
285
+ color: T.muted,
286
+ }, children: [icon !== undefined && (_jsx("div", { style: { fontSize: 36, marginBottom: 10, opacity: 0.4 }, children: icon })), _jsx("div", { style: { fontSize: 13 }, children: message })] }));
287
+ }
288
+ // ─── SectionTitle ──────────────────────────────────────────────
289
+ export function SectionTitle({ icon, children, actions, }) {
290
+ return (_jsxs("div", { style: {
291
+ display: 'flex',
292
+ alignItems: 'center',
293
+ justifyContent: 'space-between',
294
+ marginTop: 6,
295
+ marginBottom: 8,
296
+ }, children: [_jsxs("span", { style: {
297
+ fontSize: 14,
298
+ fontWeight: 600,
299
+ display: 'flex',
300
+ alignItems: 'center',
301
+ gap: 6,
302
+ }, children: [icon !== undefined && _jsx("span", { children: icon }), children] }), actions] }));
303
+ }
304
+ // ─── Table helpers ─────────────────────────────────────────────
305
+ export const tableStyles = {
306
+ table: {
307
+ borderCollapse: 'collapse',
308
+ width: '100%',
309
+ },
310
+ th: {
311
+ textAlign: 'left',
312
+ padding: '8px 12px 8px 0',
313
+ fontWeight: 600,
314
+ fontSize: 12,
315
+ color: T.muted,
316
+ borderBottom: `1px solid ${T.border}`,
317
+ },
318
+ td: {
319
+ padding: '8px 12px 8px 0',
320
+ fontSize: 13,
321
+ borderBottom: '1px solid rgba(128,128,128,0.08)',
322
+ },
323
+ clickRow: { cursor: 'pointer' },
324
+ };
325
+ // ─── CodeBlock ─────────────────────────────────────────────────
326
+ export function CodeBlock({ children, maxHeight = 200, style, }) {
327
+ return (_jsx("pre", { style: {
328
+ whiteSpace: 'pre-wrap',
329
+ wordBreak: 'break-word',
330
+ fontSize: 12,
331
+ maxHeight,
332
+ overflow: 'auto',
333
+ padding: 10,
334
+ borderRadius: 7,
335
+ background: 'rgba(128,128,128,0.06)',
336
+ border: `1px solid ${T.border}`,
337
+ margin: 0,
338
+ ...style,
339
+ }, children: children }));
340
+ }
341
+ // ─── ProgressBar ───────────────────────────────────────────────
342
+ export function ProgressBar({ pct, color, height = 18, }) {
343
+ return (_jsx("div", { style: {
344
+ width: '100%',
345
+ height,
346
+ background: 'rgba(128,128,128,0.12)',
347
+ borderRadius: 5,
348
+ overflow: 'hidden',
349
+ position: 'relative',
350
+ }, children: _jsx("div", { style: {
351
+ width: `${Math.min(pct, 100)}%`,
352
+ height: '100%',
353
+ background: color ?? T.accent,
354
+ borderRadius: 5,
355
+ transition: 'width 0.3s ease',
356
+ } }) }));
357
+ }
358
+ // ─── Field label ───────────────────────────────────────────────
359
+ export function Field({ label, children, hint, }) {
360
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 4 }, children: [_jsx("label", { style: { fontSize: 12, fontWeight: 600, color: T.text }, children: label }), children, hint !== undefined && (_jsx("span", { style: { fontSize: 11, color: T.muted }, children: hint }))] }));
361
+ }
362
+ // ─── usePanel hook (shared fetch logic) ────────────────────────
363
+ export function usePanel(path) {
364
+ const [payload, setPayload] = useState(null);
365
+ const [error, setError] = useState(null);
366
+ const [tick, setTick] = useState(0);
367
+ const reload = useCallback(() => setTick((v) => v + 1), []);
368
+ useEffect(() => {
369
+ const c = new AbortController();
370
+ setError(null);
371
+ fetch(path, { signal: c.signal })
372
+ .then(async (r) => {
373
+ if (!r.ok)
374
+ throw new Error(String(r.status));
375
+ return r.json();
376
+ })
377
+ .then((p) => {
378
+ if (!c.signal.aborted)
379
+ setPayload(p);
380
+ })
381
+ .catch((e) => {
382
+ if (!c.signal.aborted)
383
+ setPayload(null), setError(e instanceof Error ? e.message : String(e));
384
+ });
385
+ return () => c.abort();
386
+ }, [tick, path]);
387
+ return useMemo(() => ({ payload, error, reload }), [payload, error, reload]);
388
+ }
@@ -1,24 +1,10 @@
1
1
  /**
2
- * Pure rendering half of the toolStats page.
3
- *
4
- * Separate from `index.tsx` so a static render can assert in Node what the page draws —
5
- * the shipped bundle is a loader factory only a browser can run. Every user-visible
6
- * string comes from the `t` seat the renderer binds from this plugin's namespace, so the
7
- * page follows the UI language; no copy is hardcoded here.
8
- *
2
+ * Pure rendering half of the toolStats page. Uses shared UI kit.
9
3
  * @module client/view
10
4
  */
11
- import type { PanelPayload } from '../types.js';
5
+ import type { ReactNode } from 'react';
12
6
  export type Translate = (key: string, params?: Record<string, unknown>) => string;
13
7
  export interface PanelProps {
14
8
  t: Translate;
15
9
  }
16
- interface PanelState {
17
- payload: PanelPayload | null;
18
- error: string | null;
19
- }
20
- export declare function usePanel(): PanelState & {
21
- reload: () => void;
22
- };
23
- export declare function ToolStatsPanel({ t }: PanelProps): import("react").JSX.Element;
24
- export {};
10
+ export declare function ToolStatsPanel({ t }: PanelProps): ReactNode;