@mseep/anything-analyzer 3.6.50

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 (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,104 @@
1
+ import React, { forwardRef } from 'react'
2
+ import styles from './InputNumber.module.css'
3
+
4
+ export interface InputNumberProps {
5
+ value?: number
6
+ defaultValue?: number
7
+ onChange?: (value: number | null) => void
8
+ min?: number
9
+ max?: number
10
+ step?: number
11
+ disabled?: boolean
12
+ placeholder?: string
13
+ className?: string
14
+ style?: React.CSSProperties
15
+ }
16
+
17
+ export const InputNumber = forwardRef<HTMLInputElement, InputNumberProps>(
18
+ (
19
+ {
20
+ value: controlledValue,
21
+ defaultValue,
22
+ onChange,
23
+ min,
24
+ max,
25
+ step = 1,
26
+ disabled = false,
27
+ placeholder,
28
+ className,
29
+ style,
30
+ },
31
+ ref
32
+ ) => {
33
+ const [internalValue, setInternalValue] = React.useState<string>(
34
+ defaultValue !== undefined ? String(defaultValue) : ''
35
+ )
36
+ const displayValue =
37
+ controlledValue !== undefined ? String(controlledValue) : internalValue
38
+
39
+ const clamp = (v: number): number => {
40
+ if (min !== undefined && v < min) return min
41
+ if (max !== undefined && v > max) return max
42
+ return v
43
+ }
44
+
45
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
46
+ const raw = e.target.value
47
+ setInternalValue(raw)
48
+ if (raw === '' || raw === '-') {
49
+ onChange?.(null)
50
+ return
51
+ }
52
+ const num = parseFloat(raw)
53
+ if (!isNaN(num)) {
54
+ onChange?.(clamp(num))
55
+ }
56
+ }
57
+
58
+ const stepValue = (direction: 1 | -1) => {
59
+ const current = parseFloat(displayValue) || 0
60
+ const next = clamp(current + step * direction)
61
+ setInternalValue(String(next))
62
+ onChange?.(next)
63
+ }
64
+
65
+ return (
66
+ <div
67
+ className={`${styles.wrapper} ${disabled ? styles.disabled : ''} ${className ?? ''}`}
68
+ style={style}
69
+ >
70
+ <input
71
+ ref={ref}
72
+ type="text"
73
+ inputMode="numeric"
74
+ className={styles.input}
75
+ value={displayValue}
76
+ onChange={handleChange}
77
+ disabled={disabled}
78
+ placeholder={placeholder}
79
+ />
80
+ <div className={styles.controls}>
81
+ <button
82
+ type="button"
83
+ className={styles.stepBtn}
84
+ onClick={() => stepValue(1)}
85
+ disabled={disabled}
86
+ tabIndex={-1}
87
+ >
88
+
89
+ </button>
90
+ <button
91
+ type="button"
92
+ className={styles.stepBtn}
93
+ onClick={() => stepValue(-1)}
94
+ disabled={disabled}
95
+ tabIndex={-1}
96
+ >
97
+
98
+ </button>
99
+ </div>
100
+ </div>
101
+ )
102
+ }
103
+ )
104
+ InputNumber.displayName = 'InputNumber'
@@ -0,0 +1,83 @@
1
+ .backdrop {
2
+ position: fixed;
3
+ inset: 0;
4
+ background: rgba(0, 0, 0, 0.6);
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ z-index: var(--z-modal-backdrop);
9
+ animation: fadeIn 0.15s ease;
10
+ }
11
+
12
+ .modal {
13
+ background: var(--color-frame);
14
+ color: var(--text-secondary);
15
+ border: 1px solid var(--color-border);
16
+ border-radius: var(--radius-frame);
17
+ box-shadow: var(--shadow-overlay);
18
+ z-index: var(--z-modal);
19
+ display: flex;
20
+ flex-direction: column;
21
+ max-height: 80vh;
22
+ animation: scaleIn 0.15s ease;
23
+ }
24
+
25
+ .header {
26
+ display: flex;
27
+ align-items: center;
28
+ justify-content: space-between;
29
+ padding: var(--space-xl) var(--space-3xl);
30
+ border-bottom: 1px solid var(--color-border);
31
+ flex-shrink: 0;
32
+ }
33
+
34
+ .title {
35
+ font-size: var(--font-size-lg);
36
+ font-weight: 600;
37
+ color: var(--text-primary);
38
+ }
39
+
40
+ .closeBtn {
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ width: 24px;
45
+ height: 24px;
46
+ background: none;
47
+ border: none;
48
+ color: var(--text-muted);
49
+ cursor: pointer;
50
+ border-radius: var(--radius-badge);
51
+ transition: all var(--transition-fast);
52
+ }
53
+
54
+ .closeBtn:hover {
55
+ background: var(--color-hover);
56
+ color: var(--text-secondary);
57
+ }
58
+
59
+ .body {
60
+ padding: var(--space-xl) var(--space-3xl);
61
+ overflow-y: auto;
62
+ flex: 1;
63
+ }
64
+
65
+ .footer {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: flex-end;
69
+ gap: var(--space-md);
70
+ padding: var(--space-xl) var(--space-3xl);
71
+ border-top: 1px solid var(--color-border);
72
+ flex-shrink: 0;
73
+ }
74
+
75
+ @keyframes fadeIn {
76
+ from { opacity: 0; }
77
+ to { opacity: 1; }
78
+ }
79
+
80
+ @keyframes scaleIn {
81
+ from { opacity: 0; transform: scale(0.95); }
82
+ to { opacity: 1; transform: scale(1); }
83
+ }
@@ -0,0 +1,67 @@
1
+ import React, { useEffect, useCallback } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import styles from './Modal.module.css'
4
+
5
+ export interface ModalProps {
6
+ open: boolean
7
+ onClose: () => void
8
+ title?: React.ReactNode
9
+ footer?: React.ReactNode
10
+ width?: number | string
11
+ children: React.ReactNode
12
+ /** If true, show a close button in the header */
13
+ closable?: boolean
14
+ }
15
+
16
+ export const Modal: React.FC<ModalProps> = ({
17
+ open,
18
+ onClose,
19
+ title,
20
+ footer,
21
+ width = 520,
22
+ children,
23
+ closable = true,
24
+ }) => {
25
+ const handleKeyDown = useCallback(
26
+ (e: KeyboardEvent) => {
27
+ if (e.key === 'Escape') onClose()
28
+ },
29
+ [onClose]
30
+ )
31
+
32
+ useEffect(() => {
33
+ if (open) {
34
+ document.addEventListener('keydown', handleKeyDown)
35
+ return () => document.removeEventListener('keydown', handleKeyDown)
36
+ }
37
+ }, [open, handleKeyDown])
38
+
39
+ if (!open) return null
40
+
41
+ return createPortal(
42
+ <div className={styles.backdrop} onClick={onClose}>
43
+ <div
44
+ className={styles.modal}
45
+ style={{ width: typeof width === 'number' ? `${width}px` : width }}
46
+ onClick={(e) => e.stopPropagation()}
47
+ >
48
+ {(title || closable) && (
49
+ <div className={styles.header}>
50
+ <span className={styles.title}>{title}</span>
51
+ {closable && (
52
+ <button className={styles.closeBtn} onClick={onClose}>
53
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
54
+ <line x1="18" y1="6" x2="6" y2="18" />
55
+ <line x1="6" y1="6" x2="18" y2="18" />
56
+ </svg>
57
+ </button>
58
+ )}
59
+ </div>
60
+ )}
61
+ <div className={styles.body}>{children}</div>
62
+ {footer !== undefined && <div className={styles.footer}>{footer}</div>}
63
+ </div>
64
+ </div>,
65
+ document.body
66
+ )
67
+ }
@@ -0,0 +1,73 @@
1
+ .wrapper {
2
+ position: relative;
3
+ display: inline-flex;
4
+ }
5
+
6
+ .popover {
7
+ position: absolute;
8
+ bottom: calc(100% + 8px);
9
+ left: 50%;
10
+ transform: translateX(-50%);
11
+ background: var(--color-frame);
12
+ border: 1px solid var(--color-border);
13
+ border-radius: var(--radius-lg);
14
+ box-shadow: var(--shadow-md);
15
+ padding: var(--space-lg);
16
+ z-index: var(--z-dropdown);
17
+ min-width: 200px;
18
+ animation: popIn 0.15s ease;
19
+ }
20
+
21
+ /* Right-aligned variant — popover anchored to the right edge */
22
+ .popoverEnd {
23
+ left: auto;
24
+ right: 0;
25
+ transform: none;
26
+ }
27
+
28
+ .message {
29
+ font-size: var(--font-size-base);
30
+ color: var(--text-secondary);
31
+ margin-bottom: var(--space-lg);
32
+ line-height: var(--line-height-normal);
33
+ }
34
+
35
+ .actions {
36
+ display: flex;
37
+ justify-content: flex-end;
38
+ gap: var(--space-md);
39
+ }
40
+
41
+ /* Arrow */
42
+ .popover::after {
43
+ content: '';
44
+ position: absolute;
45
+ bottom: -5px;
46
+ left: 50%;
47
+ transform: translateX(-50%) rotate(45deg);
48
+ width: 8px;
49
+ height: 8px;
50
+ background: var(--color-frame);
51
+ border-right: 1px solid var(--color-border);
52
+ border-bottom: 1px solid var(--color-border);
53
+ }
54
+
55
+ .popoverEnd::after {
56
+ left: auto;
57
+ right: 12px;
58
+ transform: rotate(45deg);
59
+ }
60
+
61
+ @keyframes popIn {
62
+ from { opacity: 0; transform: translateX(-50%) translateY(4px); }
63
+ to { opacity: 1; transform: translateX(-50%) translateY(0); }
64
+ }
65
+
66
+ @keyframes popInEnd {
67
+ from { opacity: 0; transform: translateY(4px); }
68
+ to { opacity: 1; transform: translateY(0); }
69
+ }
70
+
71
+ .popoverEnd {
72
+ animation-name: popInEnd;
73
+ }
@@ -0,0 +1,74 @@
1
+ import React, { useState, useRef, useEffect } from 'react'
2
+ import { Button } from './Button'
3
+ import styles from './Popconfirm.module.css'
4
+
5
+ export interface PopconfirmProps {
6
+ title: string
7
+ onConfirm: () => void
8
+ onCancel?: () => void
9
+ okText?: string
10
+ cancelText?: string
11
+ /** Horizontal alignment of the popover. Default: 'center' */
12
+ align?: 'center' | 'end'
13
+ children: React.ReactNode
14
+ }
15
+
16
+ export const Popconfirm: React.FC<PopconfirmProps> = ({
17
+ title,
18
+ onConfirm,
19
+ onCancel,
20
+ okText = 'OK',
21
+ cancelText = 'Cancel',
22
+ align = 'center',
23
+ children,
24
+ }) => {
25
+ const [open, setOpen] = useState(false)
26
+ const wrapperRef = useRef<HTMLDivElement>(null)
27
+
28
+ useEffect(() => {
29
+ if (!open) return
30
+ const handleClickOutside = (e: MouseEvent) => {
31
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
32
+ setOpen(false)
33
+ }
34
+ }
35
+ document.addEventListener('mousedown', handleClickOutside)
36
+ return () => document.removeEventListener('mousedown', handleClickOutside)
37
+ }, [open])
38
+
39
+ const handleConfirm = () => {
40
+ setOpen(false)
41
+ onConfirm()
42
+ }
43
+
44
+ const handleCancel = () => {
45
+ setOpen(false)
46
+ onCancel?.()
47
+ }
48
+
49
+ return (
50
+ <div ref={wrapperRef} className={styles.wrapper}>
51
+ <div
52
+ onClick={(e: React.MouseEvent) => {
53
+ e.stopPropagation()
54
+ setOpen(!open)
55
+ }}
56
+ >
57
+ {children}
58
+ </div>
59
+ {open && (
60
+ <div className={`${styles.popover} ${align === 'end' ? styles.popoverEnd : ''}`}>
61
+ <div className={styles.message}>{title}</div>
62
+ <div className={styles.actions}>
63
+ <Button size="sm" onClick={handleCancel}>
64
+ {cancelText}
65
+ </Button>
66
+ <Button size="sm" variant="danger" onClick={handleConfirm}>
67
+ {okText}
68
+ </Button>
69
+ </div>
70
+ </div>
71
+ )}
72
+ </div>
73
+ )
74
+ }
@@ -0,0 +1,35 @@
1
+ .track {
2
+ width: 100%;
3
+ height: 4px;
4
+ background: var(--color-border);
5
+ border-radius: var(--radius-full);
6
+ overflow: hidden;
7
+ }
8
+
9
+ .bar {
10
+ height: 100%;
11
+ background: var(--color-accent);
12
+ border-radius: var(--radius-full);
13
+ transition: width 0.3s ease;
14
+ }
15
+
16
+ .bar.success {
17
+ background: var(--color-success);
18
+ }
19
+
20
+ .bar.error {
21
+ background: var(--color-error);
22
+ }
23
+
24
+ .wrapper {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: var(--space-md);
28
+ }
29
+
30
+ .percent {
31
+ font-size: var(--font-size-xs);
32
+ color: var(--text-secondary);
33
+ min-width: 36px;
34
+ text-align: right;
35
+ }
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+ import styles from './Progress.module.css'
3
+
4
+ export interface ProgressProps {
5
+ percent: number
6
+ status?: 'normal' | 'success' | 'error'
7
+ showPercent?: boolean
8
+ className?: string
9
+ }
10
+
11
+ export const Progress: React.FC<ProgressProps> = ({
12
+ percent,
13
+ status = 'normal',
14
+ showPercent = true,
15
+ className,
16
+ }) => {
17
+ const clampedPercent = Math.min(100, Math.max(0, percent))
18
+
19
+ return (
20
+ <div className={`${styles.wrapper} ${className ?? ''}`}>
21
+ <div className={styles.track}>
22
+ <div
23
+ className={`${styles.bar} ${status !== 'normal' ? styles[status] : ''}`}
24
+ style={{ width: `${clampedPercent}%` }}
25
+ />
26
+ </div>
27
+ {showPercent && <span className={styles.percent}>{Math.round(clampedPercent)}%</span>}
28
+ </div>
29
+ )
30
+ }
@@ -0,0 +1,91 @@
1
+ .wrapper {
2
+ position: relative;
3
+ display: inline-block;
4
+ width: 100%;
5
+ }
6
+
7
+ .trigger {
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: space-between;
11
+ width: 100%;
12
+ height: 28px;
13
+ padding: 0 var(--space-lg);
14
+ font-family: var(--font-sans);
15
+ font-size: var(--font-size-base);
16
+ color: var(--text-primary);
17
+ background: var(--color-surface);
18
+ border: 1px solid var(--color-border);
19
+ border-radius: var(--radius-input);
20
+ cursor: pointer;
21
+ outline: none;
22
+ transition: border-color var(--transition-normal);
23
+ user-select: none;
24
+ }
25
+
26
+ .trigger:hover {
27
+ border-color: var(--color-border-hover);
28
+ }
29
+
30
+ .trigger.open {
31
+ border-color: var(--color-accent);
32
+ box-shadow: 0 0 0 2px var(--color-focus-ring);
33
+ }
34
+
35
+ .trigger:disabled {
36
+ opacity: 0.4;
37
+ cursor: not-allowed;
38
+ }
39
+
40
+ .placeholder {
41
+ color: var(--text-muted);
42
+ }
43
+
44
+ .arrow {
45
+ color: var(--text-muted);
46
+ transition: transform var(--transition-normal);
47
+ flex-shrink: 0;
48
+ }
49
+
50
+ .arrow.up {
51
+ transform: rotate(180deg);
52
+ }
53
+
54
+ .dropdown {
55
+ position: absolute;
56
+ top: calc(100% + 4px);
57
+ left: 0;
58
+ right: 0;
59
+ max-height: 200px;
60
+ overflow-y: auto;
61
+ background: var(--color-frame);
62
+ border: 1px solid var(--color-border);
63
+ border-radius: var(--radius-input);
64
+ box-shadow: var(--shadow-md);
65
+ z-index: var(--z-dropdown);
66
+ padding: var(--space-xs) 0;
67
+ }
68
+
69
+ .option {
70
+ display: flex;
71
+ align-items: center;
72
+ padding: var(--space-sm) var(--space-lg);
73
+ font-size: var(--font-size-base);
74
+ color: var(--text-secondary);
75
+ cursor: pointer;
76
+ transition: background var(--transition-fast), color var(--transition-fast);
77
+ }
78
+
79
+ .option:hover {
80
+ background: var(--color-hover);
81
+ color: var(--text-primary);
82
+ }
83
+
84
+ .option.selected {
85
+ color: var(--color-accent);
86
+ }
87
+
88
+ .option.disabled {
89
+ opacity: 0.4;
90
+ cursor: not-allowed;
91
+ }
@@ -0,0 +1,100 @@
1
+ import React, { useState, useRef, useEffect } from 'react'
2
+ import styles from './Select.module.css'
3
+
4
+ export interface SelectOption {
5
+ label: string
6
+ value: string
7
+ disabled?: boolean
8
+ }
9
+
10
+ export interface SelectProps {
11
+ options: SelectOption[]
12
+ value?: string
13
+ defaultValue?: string
14
+ onChange?: (value: string) => void
15
+ placeholder?: string
16
+ disabled?: boolean
17
+ className?: string
18
+ style?: React.CSSProperties
19
+ }
20
+
21
+ export const Select: React.FC<SelectProps> = ({
22
+ options,
23
+ value: controlledValue,
24
+ defaultValue,
25
+ onChange,
26
+ placeholder = 'Select...',
27
+ disabled = false,
28
+ className,
29
+ style,
30
+ }) => {
31
+ const [isOpen, setIsOpen] = useState(false)
32
+ const [internalValue, setInternalValue] = useState(defaultValue ?? '')
33
+ const wrapperRef = useRef<HTMLDivElement>(null)
34
+
35
+ const value = controlledValue !== undefined ? controlledValue : internalValue
36
+ const selectedOption = options.find((o) => o.value === value)
37
+
38
+ useEffect(() => {
39
+ const handleClickOutside = (e: MouseEvent) => {
40
+ if (wrapperRef.current && !wrapperRef.current.contains(e.target as Node)) {
41
+ setIsOpen(false)
42
+ }
43
+ }
44
+ if (isOpen) {
45
+ document.addEventListener('mousedown', handleClickOutside)
46
+ }
47
+ return () => document.removeEventListener('mousedown', handleClickOutside)
48
+ }, [isOpen])
49
+
50
+ const handleSelect = (opt: SelectOption) => {
51
+ if (opt.disabled) return
52
+ if (controlledValue === undefined) {
53
+ setInternalValue(opt.value)
54
+ }
55
+ onChange?.(opt.value)
56
+ setIsOpen(false)
57
+ }
58
+
59
+ return (
60
+ <div ref={wrapperRef} className={`${styles.wrapper} ${className ?? ''}`} style={style}>
61
+ <button
62
+ type="button"
63
+ className={`${styles.trigger} ${isOpen ? styles.open : ''}`}
64
+ disabled={disabled}
65
+ onClick={() => !disabled && setIsOpen(!isOpen)}
66
+ >
67
+ {selectedOption ? (
68
+ <span>{selectedOption.label}</span>
69
+ ) : (
70
+ <span className={styles.placeholder}>{placeholder}</span>
71
+ )}
72
+ <svg
73
+ className={`${styles.arrow} ${isOpen ? styles.up : ''}`}
74
+ width="12"
75
+ height="12"
76
+ viewBox="0 0 24 24"
77
+ fill="none"
78
+ stroke="currentColor"
79
+ strokeWidth="2"
80
+ >
81
+ <polyline points="6 9 12 15 18 9" />
82
+ </svg>
83
+ </button>
84
+
85
+ {isOpen && (
86
+ <div className={styles.dropdown}>
87
+ {options.map((opt) => (
88
+ <div
89
+ key={opt.value}
90
+ className={`${styles.option} ${opt.value === value ? styles.selected : ''} ${opt.disabled ? styles.disabled : ''}`}
91
+ onClick={() => handleSelect(opt)}
92
+ >
93
+ {opt.label}
94
+ </div>
95
+ ))}
96
+ </div>
97
+ )}
98
+ </div>
99
+ )
100
+ }
@@ -0,0 +1,44 @@
1
+ .spinner {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ }
6
+
7
+ .ring {
8
+ border: 2px solid var(--color-border);
9
+ border-top-color: var(--text-secondary);
10
+ border-radius: 50%;
11
+ animation: spin 0.6s linear infinite;
12
+ }
13
+
14
+ /* Sizes */
15
+ .sm .ring {
16
+ width: 14px;
17
+ height: 14px;
18
+ }
19
+
20
+ .md .ring {
21
+ width: 20px;
22
+ height: 20px;
23
+ }
24
+
25
+ .lg .ring {
26
+ width: 32px;
27
+ height: 32px;
28
+ border-width: 3px;
29
+ }
30
+
31
+ /* Full-area spinner with overlay */
32
+ .overlay {
33
+ position: absolute;
34
+ inset: 0;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ background: rgba(10, 10, 10, 0.6);
39
+ z-index: 10;
40
+ }
41
+
42
+ @keyframes spin {
43
+ to { transform: rotate(360deg); }
44
+ }