@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,27 @@
1
+ import React from 'react'
2
+ import styles from './Spinner.module.css'
3
+
4
+ export interface SpinnerProps {
5
+ size?: 'sm' | 'md' | 'lg'
6
+ /** If true, renders as an overlay covering the parent container */
7
+ overlay?: boolean
8
+ className?: string
9
+ }
10
+
11
+ export const Spinner: React.FC<SpinnerProps> = ({
12
+ size = 'md',
13
+ overlay = false,
14
+ className,
15
+ }) => {
16
+ const content = (
17
+ <div className={`${styles.spinner} ${styles[size]} ${className ?? ''}`}>
18
+ <div className={styles.ring} />
19
+ </div>
20
+ )
21
+
22
+ if (overlay) {
23
+ return <div className={styles.overlay}>{content}</div>
24
+ }
25
+
26
+ return content
27
+ }
@@ -0,0 +1,39 @@
1
+ .switch {
2
+ position: relative;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ width: 36px;
6
+ height: 20px;
7
+ background: var(--color-border-hover);
8
+ border-radius: var(--radius-full);
9
+ border: none;
10
+ cursor: pointer;
11
+ padding: 2px;
12
+ transition: background var(--transition-normal);
13
+ outline: none;
14
+ }
15
+
16
+ .switch:focus-visible {
17
+ box-shadow: 0 0 0 2px var(--color-focus-ring);
18
+ }
19
+
20
+ .switch.checked {
21
+ background: var(--color-success);
22
+ }
23
+
24
+ .switch:disabled {
25
+ opacity: 0.4;
26
+ cursor: not-allowed;
27
+ }
28
+
29
+ .knob {
30
+ width: 16px;
31
+ height: 16px;
32
+ background: var(--text-primary);
33
+ border-radius: var(--radius-full);
34
+ transition: transform var(--transition-normal);
35
+ }
36
+
37
+ .switch.checked .knob {
38
+ transform: translateX(16px);
39
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react'
2
+ import styles from './Switch.module.css'
3
+
4
+ export interface SwitchProps {
5
+ checked?: boolean
6
+ defaultChecked?: boolean
7
+ onChange?: (checked: boolean) => void
8
+ disabled?: boolean
9
+ className?: string
10
+ }
11
+
12
+ export const Switch: React.FC<SwitchProps> = ({
13
+ checked: controlledChecked,
14
+ defaultChecked = false,
15
+ onChange,
16
+ disabled = false,
17
+ className,
18
+ }) => {
19
+ const [internalChecked, setInternalChecked] = React.useState(defaultChecked)
20
+ const isChecked = controlledChecked !== undefined ? controlledChecked : internalChecked
21
+
22
+ const toggle = () => {
23
+ if (disabled) return
24
+ const next = !isChecked
25
+ if (controlledChecked === undefined) {
26
+ setInternalChecked(next)
27
+ }
28
+ onChange?.(next)
29
+ }
30
+
31
+ return (
32
+ <button
33
+ type="button"
34
+ role="switch"
35
+ aria-checked={isChecked}
36
+ className={`${styles.switch} ${isChecked ? styles.checked : ''} ${className ?? ''}`}
37
+ disabled={disabled}
38
+ onClick={toggle}
39
+ >
40
+ <span className={styles.knob} />
41
+ </button>
42
+ )
43
+ }
@@ -0,0 +1,76 @@
1
+ .tabs {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ }
6
+
7
+ .tabBar {
8
+ display: flex;
9
+ align-items: stretch;
10
+ height: 36px;
11
+ background: var(--color-bar);
12
+ border-bottom: 1px solid var(--color-border);
13
+ padding: 0 var(--space-lg);
14
+ gap: var(--space-lg);
15
+ flex-shrink: 0;
16
+ overflow-x: auto;
17
+ }
18
+
19
+ .tabBar::-webkit-scrollbar {
20
+ display: none;
21
+ }
22
+
23
+ .tab {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: var(--space-sm);
27
+ padding: 0;
28
+ font-family: var(--font-sans);
29
+ font-size: var(--font-size-base);
30
+ color: var(--text-muted);
31
+ background: none;
32
+ border: none;
33
+ border-bottom: 2px solid transparent;
34
+ cursor: pointer;
35
+ transition: color var(--transition-normal);
36
+ white-space: nowrap;
37
+ position: relative;
38
+ }
39
+
40
+ .tab:hover {
41
+ color: var(--text-secondary);
42
+ }
43
+
44
+ .tab.active {
45
+ color: var(--text-primary);
46
+ border-bottom-color: var(--text-primary);
47
+ }
48
+
49
+ .tabIcon {
50
+ display: flex;
51
+ align-items: center;
52
+ font-size: var(--font-size-md);
53
+ opacity: 0.7;
54
+ }
55
+
56
+ .tabBadge {
57
+ font-size: var(--font-size-2xs);
58
+ padding: 1px 5px;
59
+ border-radius: var(--radius-lg);
60
+ background: var(--color-success-bg);
61
+ color: var(--color-success);
62
+ }
63
+
64
+ .tabContent {
65
+ flex: 1;
66
+ overflow: hidden;
67
+ }
68
+
69
+ /* Extra content on the right side of tab bar */
70
+ .tabBarExtra {
71
+ margin-left: auto;
72
+ display: flex;
73
+ align-items: center;
74
+ gap: var(--space-md);
75
+ flex-shrink: 0;
76
+ }
@@ -0,0 +1,53 @@
1
+ import React from 'react'
2
+ import styles from './Tabs.module.css'
3
+
4
+ export interface TabItem {
5
+ key: string
6
+ label: React.ReactNode
7
+ icon?: React.ReactNode
8
+ badge?: string | number
9
+ children?: React.ReactNode
10
+ }
11
+
12
+ export interface TabsProps {
13
+ items: TabItem[]
14
+ activeKey: string
15
+ onChange: (key: string) => void
16
+ /** Extra content rendered at the right end of the tab bar */
17
+ extra?: React.ReactNode
18
+ className?: string
19
+ style?: React.CSSProperties
20
+ }
21
+
22
+ export const Tabs: React.FC<TabsProps> = ({
23
+ items,
24
+ activeKey,
25
+ onChange,
26
+ extra,
27
+ className,
28
+ style,
29
+ }) => {
30
+ const activeItem = items.find((i) => i.key === activeKey)
31
+
32
+ return (
33
+ <div className={`${styles.tabs} ${className ?? ''}`} style={style}>
34
+ <div className={styles.tabBar}>
35
+ {items.map((item) => (
36
+ <button
37
+ key={item.key}
38
+ className={`${styles.tab} ${item.key === activeKey ? styles.active : ''}`}
39
+ onClick={() => onChange(item.key)}
40
+ >
41
+ {item.icon && <span className={styles.tabIcon}>{item.icon}</span>}
42
+ <span>{item.label}</span>
43
+ {item.badge !== undefined && (
44
+ <span className={styles.tabBadge}>{item.badge}</span>
45
+ )}
46
+ </button>
47
+ ))}
48
+ {extra && <div className={styles.tabBarExtra}>{extra}</div>}
49
+ </div>
50
+ <div className={styles.tabContent}>{activeItem?.children}</div>
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,66 @@
1
+ .tag {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: var(--space-xs);
5
+ padding: 1px 8px;
6
+ font-family: var(--font-sans);
7
+ font-size: var(--font-size-2xs);
8
+ line-height: 1.4;
9
+ border-radius: var(--radius-badge);
10
+ white-space: nowrap;
11
+ user-select: none;
12
+ }
13
+
14
+ /* Default */
15
+ .default {
16
+ background: var(--color-surface);
17
+ color: var(--text-secondary);
18
+ border: 1px solid var(--color-border);
19
+ }
20
+
21
+ /* Semantic variants */
22
+ .success {
23
+ background: var(--color-success-bg);
24
+ color: var(--color-success);
25
+ border: 1px solid var(--color-success-border);
26
+ }
27
+
28
+ .info {
29
+ background: var(--color-info-bg);
30
+ color: var(--color-info);
31
+ border: 1px solid var(--color-info-border);
32
+ }
33
+
34
+ .warning {
35
+ background: var(--color-warning-bg);
36
+ color: var(--color-warning);
37
+ border: 1px solid var(--color-warning-border);
38
+ }
39
+
40
+ .error {
41
+ background: var(--color-error-bg);
42
+ color: var(--color-error);
43
+ border: 1px solid var(--color-error-border);
44
+ }
45
+
46
+ .purple {
47
+ background: var(--color-purple-bg);
48
+ color: var(--color-purple);
49
+ border: 1px solid rgba(168, 85, 247, 0.15);
50
+ }
51
+
52
+ .orange {
53
+ background: var(--color-orange-bg);
54
+ color: var(--color-orange);
55
+ border: 1px solid rgba(249, 115, 22, 0.15);
56
+ }
57
+
58
+ /* Clickable */
59
+ .clickable {
60
+ cursor: pointer;
61
+ transition: opacity var(--transition-fast);
62
+ }
63
+
64
+ .clickable:hover {
65
+ opacity: 0.8;
66
+ }
@@ -0,0 +1,47 @@
1
+ import React from 'react'
2
+ import styles from './Tag.module.css'
3
+
4
+ export type TagColor = 'default' | 'success' | 'info' | 'warning' | 'error' | 'purple' | 'orange'
5
+
6
+ export interface TagProps {
7
+ color?: TagColor
8
+ /** Custom inline color (hex). Overrides color variant. */
9
+ customColor?: string
10
+ onClick?: () => void
11
+ className?: string
12
+ style?: React.CSSProperties
13
+ children: React.ReactNode
14
+ }
15
+
16
+ export const Tag: React.FC<TagProps> = ({
17
+ color = 'default',
18
+ customColor,
19
+ onClick,
20
+ className,
21
+ style,
22
+ children,
23
+ }) => {
24
+ const cls = [
25
+ styles.tag,
26
+ !customColor && styles[color],
27
+ onClick && styles.clickable,
28
+ className,
29
+ ]
30
+ .filter(Boolean)
31
+ .join(' ')
32
+
33
+ const customStyle: React.CSSProperties | undefined = customColor
34
+ ? {
35
+ background: `${customColor}14`,
36
+ color: customColor,
37
+ border: `1px solid ${customColor}26`,
38
+ ...style,
39
+ }
40
+ : style
41
+
42
+ return (
43
+ <span className={cls} style={customStyle} onClick={onClick}>
44
+ {children}
45
+ </span>
46
+ )
47
+ }
@@ -0,0 +1,42 @@
1
+ .timeline {
2
+ padding: var(--space-xs) 0;
3
+ }
4
+
5
+ .item {
6
+ display: flex;
7
+ padding-bottom: var(--space-lg);
8
+ }
9
+
10
+ .item:last-child {
11
+ padding-bottom: 0;
12
+ }
13
+
14
+ .tail {
15
+ display: flex;
16
+ flex-direction: column;
17
+ align-items: center;
18
+ width: 20px;
19
+ flex-shrink: 0;
20
+ }
21
+
22
+ .dot {
23
+ width: 8px;
24
+ height: 8px;
25
+ border-radius: var(--radius-full);
26
+ border: 2px solid var(--color-info);
27
+ background: var(--color-content);
28
+ flex-shrink: 0;
29
+ }
30
+
31
+ .line {
32
+ flex: 1;
33
+ width: 1px;
34
+ background: var(--color-border);
35
+ margin-top: 4px;
36
+ }
37
+
38
+ .content {
39
+ flex: 1;
40
+ padding-left: var(--space-sm);
41
+ min-width: 0;
42
+ }
@@ -0,0 +1,29 @@
1
+ import React from 'react'
2
+ import styles from './Timeline.module.css'
3
+
4
+ interface TimelineItem {
5
+ key: string
6
+ color?: string
7
+ children: React.ReactNode
8
+ }
9
+
10
+ interface TimelineProps {
11
+ items: TimelineItem[]
12
+ className?: string
13
+ }
14
+
15
+ export const Timeline: React.FC<TimelineProps> = ({ items, className }) => {
16
+ return (
17
+ <div className={`${styles.timeline} ${className ?? ''}`}>
18
+ {items.map((item, index) => (
19
+ <div key={item.key} className={styles.item}>
20
+ <div className={styles.tail}>
21
+ <span className={styles.dot} style={{ borderColor: item.color || 'var(--color-info)' }} />
22
+ {index < items.length - 1 && <span className={styles.line} />}
23
+ </div>
24
+ <div className={styles.content}>{item.children}</div>
25
+ </div>
26
+ ))}
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,99 @@
1
+ .container {
2
+ position: fixed;
3
+ top: 52px;
4
+ right: 16px;
5
+ z-index: var(--z-toast);
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: var(--space-md);
9
+ pointer-events: none;
10
+ }
11
+
12
+ .toast {
13
+ display: flex;
14
+ align-items: center;
15
+ gap: var(--space-lg);
16
+ padding: 10px 16px;
17
+ border-radius: var(--radius-frame);
18
+ font-size: var(--font-size-sm);
19
+ backdrop-filter: blur(12px);
20
+ animation: slideIn 0.3s ease;
21
+ pointer-events: auto;
22
+ max-width: 360px;
23
+ }
24
+
25
+ .icon {
26
+ font-size: var(--font-size-lg);
27
+ flex-shrink: 0;
28
+ }
29
+
30
+ .text {
31
+ flex: 1;
32
+ line-height: var(--line-height-normal);
33
+ }
34
+
35
+ .closeBtn {
36
+ background: none;
37
+ border: none;
38
+ color: inherit;
39
+ cursor: pointer;
40
+ opacity: 0.6;
41
+ padding: 2px;
42
+ display: flex;
43
+ align-items: center;
44
+ }
45
+
46
+ .closeBtn:hover {
47
+ opacity: 1;
48
+ }
49
+
50
+ /* Variants */
51
+ .success {
52
+ background: var(--color-success-bg);
53
+ border: 1px solid var(--color-success-border);
54
+ color: var(--color-success);
55
+ }
56
+
57
+ .error {
58
+ background: var(--color-error-bg);
59
+ border: 1px solid var(--color-error-border);
60
+ color: var(--color-error);
61
+ }
62
+
63
+ .warning {
64
+ background: var(--color-warning-bg);
65
+ border: 1px solid var(--color-warning-border);
66
+ color: var(--color-warning);
67
+ }
68
+
69
+ .info {
70
+ background: var(--color-info-bg);
71
+ border: 1px solid var(--color-info-border);
72
+ color: var(--color-info);
73
+ }
74
+
75
+ @keyframes slideIn {
76
+ from {
77
+ opacity: 0;
78
+ transform: translateX(20px);
79
+ }
80
+ to {
81
+ opacity: 1;
82
+ transform: translateX(0);
83
+ }
84
+ }
85
+
86
+ @keyframes slideOut {
87
+ from {
88
+ opacity: 1;
89
+ transform: translateX(0);
90
+ }
91
+ to {
92
+ opacity: 0;
93
+ transform: translateX(20px);
94
+ }
95
+ }
96
+
97
+ .exiting {
98
+ animation: slideOut 0.2s ease forwards;
99
+ }
@@ -0,0 +1,90 @@
1
+ import React, { createContext, useContext, useState, useCallback, useRef } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import styles from './Toast.module.css'
4
+
5
+ type ToastType = 'success' | 'error' | 'warning' | 'info'
6
+
7
+ interface ToastItem {
8
+ id: number
9
+ type: ToastType
10
+ message: string
11
+ exiting?: boolean
12
+ }
13
+
14
+ interface ToastContextValue {
15
+ success: (message: string) => void
16
+ error: (message: string) => void
17
+ warning: (message: string) => void
18
+ info: (message: string) => void
19
+ }
20
+
21
+ const ToastContext = createContext<ToastContextValue | null>(null)
22
+
23
+ const TOAST_ICONS: Record<ToastType, string> = {
24
+ success: '\u2713',
25
+ error: '\u2717',
26
+ warning: '\u26A0',
27
+ info: '\u2139',
28
+ }
29
+
30
+ export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
31
+ const [toasts, setToasts] = useState<ToastItem[]>([])
32
+ const counterRef = useRef(0)
33
+
34
+ const removeToast = useCallback((id: number) => {
35
+ setToasts((prev) => prev.map((t) => (t.id === id ? { ...t, exiting: true } : t)))
36
+ setTimeout(() => {
37
+ setToasts((prev) => prev.filter((t) => t.id !== id))
38
+ }, 200)
39
+ }, [])
40
+
41
+ const addToast = useCallback(
42
+ (type: ToastType, message: string) => {
43
+ const id = ++counterRef.current
44
+ setToasts((prev) => [...prev, { id, type, message }])
45
+ setTimeout(() => removeToast(id), 3000)
46
+ },
47
+ [removeToast]
48
+ )
49
+
50
+ const contextValue: ToastContextValue = {
51
+ success: useCallback((msg: string) => addToast('success', msg), [addToast]),
52
+ error: useCallback((msg: string) => addToast('error', msg), [addToast]),
53
+ warning: useCallback((msg: string) => addToast('warning', msg), [addToast]),
54
+ info: useCallback((msg: string) => addToast('info', msg), [addToast]),
55
+ }
56
+
57
+ return (
58
+ <ToastContext.Provider value={contextValue}>
59
+ {children}
60
+ {createPortal(
61
+ <div className={styles.container}>
62
+ {toasts.map((toast) => (
63
+ <div
64
+ key={toast.id}
65
+ className={`${styles.toast} ${styles[toast.type]} ${toast.exiting ? styles.exiting : ''}`}
66
+ >
67
+ <span className={styles.icon}>{TOAST_ICONS[toast.type]}</span>
68
+ <span className={styles.text}>{toast.message}</span>
69
+ <button className={styles.closeBtn} onClick={() => removeToast(toast.id)}>
70
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
71
+ <line x1="18" y1="6" x2="6" y2="18" />
72
+ <line x1="6" y1="6" x2="18" y2="18" />
73
+ </svg>
74
+ </button>
75
+ </div>
76
+ ))}
77
+ </div>,
78
+ document.body
79
+ )}
80
+ </ToastContext.Provider>
81
+ )
82
+ }
83
+
84
+ export function useToast(): ToastContextValue {
85
+ const ctx = useContext(ToastContext)
86
+ if (!ctx) {
87
+ throw new Error('useToast must be used within a ToastProvider')
88
+ }
89
+ return ctx
90
+ }
@@ -0,0 +1,26 @@
1
+ .wrapper {
2
+ position: relative;
3
+ display: inline-flex;
4
+ }
5
+
6
+ .tooltip {
7
+ position: absolute;
8
+ bottom: calc(100% + 6px);
9
+ left: 50%;
10
+ transform: translateX(-50%);
11
+ background: var(--color-elevated);
12
+ border: 1px solid var(--color-border);
13
+ border-radius: var(--radius-button);
14
+ padding: var(--space-xs) var(--space-md);
15
+ font-size: var(--font-size-xs);
16
+ color: var(--text-secondary);
17
+ white-space: nowrap;
18
+ z-index: var(--z-tooltip);
19
+ pointer-events: none;
20
+ animation: tooltipIn 0.1s ease;
21
+ }
22
+
23
+ @keyframes tooltipIn {
24
+ from { opacity: 0; }
25
+ to { opacity: 1; }
26
+ }
@@ -0,0 +1,23 @@
1
+ import React, { useState } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import styles from './Tooltip.module.css'
4
+
5
+ export interface TooltipProps {
6
+ title: string
7
+ children: React.ReactElement
8
+ }
9
+
10
+ export const Tooltip: React.FC<TooltipProps> = ({ title, children }) => {
11
+ const [visible, setVisible] = useState(false)
12
+
13
+ return (
14
+ <div
15
+ className={styles.wrapper}
16
+ onMouseEnter={() => setVisible(true)}
17
+ onMouseLeave={() => setVisible(false)}
18
+ >
19
+ {children}
20
+ {visible && <div className={styles.tooltip}>{title}</div>}
21
+ </div>
22
+ )
23
+ }