@antigenic-oss/paint 0.1.0

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 (158) hide show
  1. package/LICENSE +178 -0
  2. package/NOTICE +4 -0
  3. package/README.md +180 -0
  4. package/bin/paint.js +266 -0
  5. package/next-env.d.ts +6 -0
  6. package/next.config.ts +19 -0
  7. package/package.json +81 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/dev-editor-inspector.js +1872 -0
  10. package/src/app/api/claude/analyze/route.ts +319 -0
  11. package/src/app/api/claude/apply/route.ts +185 -0
  12. package/src/app/api/claude/pick-folder/route.ts +64 -0
  13. package/src/app/api/claude/scan/route.ts +221 -0
  14. package/src/app/api/claude/status/route.ts +55 -0
  15. package/src/app/api/project/scan/route.ts +634 -0
  16. package/src/app/api/project-scan/css-variables/route.ts +238 -0
  17. package/src/app/api/project-scan/route.ts +40 -0
  18. package/src/app/api/project-scan/tailwind-config/route.ts +172 -0
  19. package/src/app/api/proxy/[[...path]]/route.ts +2400 -0
  20. package/src/app/docs/DocsClient.tsx +322 -0
  21. package/src/app/docs/layout.tsx +7 -0
  22. package/src/app/docs/page.tsx +855 -0
  23. package/src/app/globals.css +176 -0
  24. package/src/app/layout.tsx +19 -0
  25. package/src/app/page.tsx +46 -0
  26. package/src/bridge/api-handlers.ts +885 -0
  27. package/src/bridge/proxy-handler.ts +329 -0
  28. package/src/bridge/server.ts +113 -0
  29. package/src/components/BreakpointTabs.tsx +72 -0
  30. package/src/components/ChangeSummaryModal.tsx +267 -0
  31. package/src/components/ConnectModal.tsx +994 -0
  32. package/src/components/Editor.tsx +90 -0
  33. package/src/components/PageSelector.tsx +208 -0
  34. package/src/components/PreviewFrame.tsx +299 -0
  35. package/src/components/ProjectFolderBanner.tsx +91 -0
  36. package/src/components/ResponsiveToolbar.tsx +222 -0
  37. package/src/components/TargetSelector.tsx +243 -0
  38. package/src/components/TopBar.tsx +315 -0
  39. package/src/components/common/CollapsibleSection.tsx +36 -0
  40. package/src/components/common/ColorPicker.tsx +920 -0
  41. package/src/components/common/EditablePre.tsx +136 -0
  42. package/src/components/common/ErrorBoundary.tsx +65 -0
  43. package/src/components/common/ResizablePanel.tsx +83 -0
  44. package/src/components/common/ScanAnimation.tsx +76 -0
  45. package/src/components/common/ToastContainer.tsx +97 -0
  46. package/src/components/common/UnitInput.tsx +77 -0
  47. package/src/components/common/VariableColorPicker.tsx +622 -0
  48. package/src/components/left-panel/AddElementPanel.tsx +237 -0
  49. package/src/components/left-panel/ComponentsPanel.tsx +609 -0
  50. package/src/components/left-panel/IconSidebar.tsx +99 -0
  51. package/src/components/left-panel/LayerNode.tsx +874 -0
  52. package/src/components/left-panel/LayerSearch.tsx +23 -0
  53. package/src/components/left-panel/LayersPanel.tsx +52 -0
  54. package/src/components/left-panel/LeftPanel.tsx +122 -0
  55. package/src/components/left-panel/PagesPanel.tsx +114 -0
  56. package/src/components/left-panel/icons.tsx +162 -0
  57. package/src/components/left-panel/terminal/ScanOverlay.tsx +66 -0
  58. package/src/components/left-panel/terminal/TerminalPanel.tsx +217 -0
  59. package/src/components/right-panel/ElementLogBox.tsx +248 -0
  60. package/src/components/right-panel/PanelTabs.tsx +83 -0
  61. package/src/components/right-panel/RightPanel.tsx +41 -0
  62. package/src/components/right-panel/changes/AiScanResultPanel.tsx +285 -0
  63. package/src/components/right-panel/changes/ChangeEntry.tsx +59 -0
  64. package/src/components/right-panel/changes/ChangelogActions.tsx +105 -0
  65. package/src/components/right-panel/changes/ChangesPanel.tsx +1474 -0
  66. package/src/components/right-panel/claude/ApplyConfirmModal.tsx +376 -0
  67. package/src/components/right-panel/claude/ClaudeErrorState.tsx +125 -0
  68. package/src/components/right-panel/claude/ClaudeIntegrationPanel.tsx +482 -0
  69. package/src/components/right-panel/claude/ClaudeProgressIndicator.tsx +76 -0
  70. package/src/components/right-panel/claude/DiffCard.tsx +130 -0
  71. package/src/components/right-panel/claude/DiffViewer.tsx +54 -0
  72. package/src/components/right-panel/claude/ProjectRootSelector.tsx +275 -0
  73. package/src/components/right-panel/claude/ResultsSummary.tsx +119 -0
  74. package/src/components/right-panel/claude/SetupFlow.tsx +315 -0
  75. package/src/components/right-panel/console/ConsolePanel.tsx +209 -0
  76. package/src/components/right-panel/design/AppearanceSection.tsx +703 -0
  77. package/src/components/right-panel/design/BackgroundSection.tsx +516 -0
  78. package/src/components/right-panel/design/BorderSection.tsx +161 -0
  79. package/src/components/right-panel/design/CSSRawView.tsx +412 -0
  80. package/src/components/right-panel/design/DesignCSSTabToggle.tsx +51 -0
  81. package/src/components/right-panel/design/DesignPanel.tsx +275 -0
  82. package/src/components/right-panel/design/ElementBreadcrumb.tsx +51 -0
  83. package/src/components/right-panel/design/GradientEditor.tsx +726 -0
  84. package/src/components/right-panel/design/LayoutSection.tsx +1948 -0
  85. package/src/components/right-panel/design/PositionSection.tsx +865 -0
  86. package/src/components/right-panel/design/PropertiesSection.tsx +86 -0
  87. package/src/components/right-panel/design/SVGSection.tsx +361 -0
  88. package/src/components/right-panel/design/ShadowBlurSection.tsx +227 -0
  89. package/src/components/right-panel/design/SizeSection.tsx +183 -0
  90. package/src/components/right-panel/design/TextSection.tsx +719 -0
  91. package/src/components/right-panel/design/icons.tsx +948 -0
  92. package/src/components/right-panel/design/inputs/BoxModelPreview.tsx +467 -0
  93. package/src/components/right-panel/design/inputs/ColorInput.tsx +43 -0
  94. package/src/components/right-panel/design/inputs/CompactInput.tsx +333 -0
  95. package/src/components/right-panel/design/inputs/DraggableLabel.tsx +118 -0
  96. package/src/components/right-panel/design/inputs/IconToggleGroup.tsx +54 -0
  97. package/src/components/right-panel/design/inputs/LinkedInputPair.tsx +174 -0
  98. package/src/components/right-panel/design/inputs/SectionHeader.tsx +79 -0
  99. package/src/components/right-panel/variables/VariablesPanel.tsx +388 -0
  100. package/src/hooks/useBridge.ts +95 -0
  101. package/src/hooks/useChangeTracker.ts +563 -0
  102. package/src/hooks/useClaudeAPI.ts +118 -0
  103. package/src/hooks/useDOMTree.ts +25 -0
  104. package/src/hooks/useKeyboardShortcuts.ts +76 -0
  105. package/src/hooks/usePostMessage.ts +589 -0
  106. package/src/hooks/useProjectScan.ts +204 -0
  107. package/src/hooks/useResizable.ts +20 -0
  108. package/src/hooks/useSelectedElement.ts +51 -0
  109. package/src/hooks/useTargetUrl.ts +81 -0
  110. package/src/inspector/DOMTraverser.ts +71 -0
  111. package/src/inspector/ElementSelector.ts +23 -0
  112. package/src/inspector/HoverHighlighter.ts +54 -0
  113. package/src/inspector/SelectionHighlighter.ts +27 -0
  114. package/src/inspector/StyleExtractor.ts +19 -0
  115. package/src/inspector/inspector.ts +17 -0
  116. package/src/inspector/messaging.ts +30 -0
  117. package/src/lib/apiBase.ts +15 -0
  118. package/src/lib/classifyElement.ts +430 -0
  119. package/src/lib/claude-bin.ts +197 -0
  120. package/src/lib/claude-stream.ts +158 -0
  121. package/src/lib/clientProjectScanner.ts +344 -0
  122. package/src/lib/componentMatcher.ts +156 -0
  123. package/src/lib/constants.ts +573 -0
  124. package/src/lib/cssVariableUtils.ts +409 -0
  125. package/src/lib/diffParser.ts +206 -0
  126. package/src/lib/folderPicker.ts +84 -0
  127. package/src/lib/gradientParser.ts +160 -0
  128. package/src/lib/projectScanner.ts +355 -0
  129. package/src/lib/promptBuilder.ts +402 -0
  130. package/src/lib/shadowParser.ts +124 -0
  131. package/src/lib/tailwindClassParser.ts +248 -0
  132. package/src/lib/textShadowUtils.ts +106 -0
  133. package/src/lib/utils.ts +299 -0
  134. package/src/lib/validatePath.ts +40 -0
  135. package/src/proxy.ts +92 -0
  136. package/src/server/terminal-server.ts +104 -0
  137. package/src/store/changeSlice.ts +288 -0
  138. package/src/store/claudeSlice.ts +222 -0
  139. package/src/store/componentSlice.ts +90 -0
  140. package/src/store/consoleSlice.ts +51 -0
  141. package/src/store/cssVariableSlice.ts +94 -0
  142. package/src/store/elementSlice.ts +78 -0
  143. package/src/store/index.ts +35 -0
  144. package/src/store/terminalSlice.ts +30 -0
  145. package/src/store/treeSlice.ts +69 -0
  146. package/src/store/uiSlice.ts +327 -0
  147. package/src/types/changelog.ts +49 -0
  148. package/src/types/claude.ts +131 -0
  149. package/src/types/component.ts +49 -0
  150. package/src/types/cssVariables.ts +18 -0
  151. package/src/types/element.ts +21 -0
  152. package/src/types/file-system-access.d.ts +27 -0
  153. package/src/types/gradient.ts +12 -0
  154. package/src/types/messages.ts +392 -0
  155. package/src/types/shadow.ts +8 -0
  156. package/src/types/tree.ts +9 -0
  157. package/tsconfig.json +42 -0
  158. package/tsconfig.server.json +12 -0
@@ -0,0 +1,90 @@
1
+ import type { StateCreator } from 'zustand'
2
+ import type { DetectedComponent, CreatedComponent } from '@/types/component'
3
+
4
+ export interface ComponentSlice {
5
+ detectedComponents: DetectedComponent[]
6
+ selectedComponentPath: string | null
7
+ componentSearchQuery: string
8
+ createdComponents: Record<string, CreatedComponent>
9
+
10
+ setDetectedComponents: (components: DetectedComponent[]) => void
11
+ setSelectedComponentPath: (path: string | null) => void
12
+ setComponentSearchQuery: (query: string) => void
13
+ addCreatedComponent: (component: CreatedComponent) => void
14
+ removeCreatedComponent: (selectorPath: string) => void
15
+ updateComponentVariantActiveIndex: (
16
+ selectorPath: string,
17
+ groupIndex: number,
18
+ optionIndex: number,
19
+ ) => void
20
+ clearComponents: () => void
21
+ }
22
+
23
+ export const createComponentSlice: StateCreator<
24
+ ComponentSlice,
25
+ [],
26
+ [],
27
+ ComponentSlice
28
+ > = (set) => ({
29
+ detectedComponents: [],
30
+ selectedComponentPath: null,
31
+ componentSearchQuery: '',
32
+ createdComponents: {},
33
+
34
+ setDetectedComponents: (components) => {
35
+ set({ detectedComponents: components })
36
+ },
37
+
38
+ setSelectedComponentPath: (path) => {
39
+ set({ selectedComponentPath: path })
40
+ },
41
+
42
+ setComponentSearchQuery: (query) => {
43
+ set({ componentSearchQuery: query })
44
+ },
45
+
46
+ addCreatedComponent: (component) => {
47
+ set((state) => ({
48
+ createdComponents: {
49
+ ...state.createdComponents,
50
+ [component.selectorPath]: component,
51
+ },
52
+ }))
53
+ },
54
+
55
+ removeCreatedComponent: (selectorPath) => {
56
+ set((state) => {
57
+ const next = { ...state.createdComponents }
58
+ delete next[selectorPath]
59
+ return { createdComponents: next }
60
+ })
61
+ },
62
+
63
+ updateComponentVariantActiveIndex: (
64
+ selectorPath,
65
+ groupIndex,
66
+ optionIndex,
67
+ ) => {
68
+ set((state) => ({
69
+ detectedComponents: state.detectedComponents.map((comp) => {
70
+ if (comp.selectorPath !== selectorPath) return comp
71
+ return {
72
+ ...comp,
73
+ variants: comp.variants.map((group, gi) => {
74
+ if (gi !== groupIndex) return group
75
+ return { ...group, activeIndex: optionIndex }
76
+ }),
77
+ }
78
+ }),
79
+ }))
80
+ },
81
+
82
+ clearComponents: () => {
83
+ set({
84
+ detectedComponents: [],
85
+ selectedComponentPath: null,
86
+ componentSearchQuery: '',
87
+ createdComponents: {},
88
+ })
89
+ },
90
+ })
@@ -0,0 +1,51 @@
1
+ import type { StateCreator } from 'zustand'
2
+ import type { ConsoleLevel } from '@/types/messages'
3
+
4
+ const MAX_CONSOLE_ENTRIES = 200
5
+
6
+ export interface ConsoleEntry {
7
+ id: number
8
+ level: ConsoleLevel
9
+ args: string[]
10
+ timestamp: number
11
+ source?: string
12
+ line?: number
13
+ column?: number
14
+ }
15
+
16
+ export interface ConsoleSlice {
17
+ consoleEntries: ConsoleEntry[]
18
+ consoleErrorCount: number
19
+
20
+ addConsoleEntry: (payload: Omit<ConsoleEntry, 'id'>) => void
21
+ clearConsole: () => void
22
+ }
23
+
24
+ let nextConsoleId = 1
25
+
26
+ export const createConsoleSlice: StateCreator<
27
+ ConsoleSlice,
28
+ [],
29
+ [],
30
+ ConsoleSlice
31
+ > = (set) => ({
32
+ consoleEntries: [],
33
+ consoleErrorCount: 0,
34
+
35
+ addConsoleEntry: (payload) => {
36
+ const entry: ConsoleEntry = { ...payload, id: nextConsoleId++ }
37
+ set((state) => {
38
+ const entries =
39
+ state.consoleEntries.length >= MAX_CONSOLE_ENTRIES
40
+ ? [...state.consoleEntries.slice(1), entry]
41
+ : [...state.consoleEntries, entry]
42
+ return {
43
+ consoleEntries: entries,
44
+ consoleErrorCount:
45
+ state.consoleErrorCount + (payload.level === 'error' ? 1 : 0),
46
+ }
47
+ })
48
+ },
49
+
50
+ clearConsole: () => set({ consoleEntries: [], consoleErrorCount: 0 }),
51
+ })
@@ -0,0 +1,94 @@
1
+ import type { StateCreator } from 'zustand'
2
+ import type {
3
+ CSSVariableDefinition,
4
+ CSSVariableFamily,
5
+ } from '@/types/cssVariables'
6
+ import { groupVariablesIntoFamilies } from '@/lib/cssVariableUtils'
7
+ import type { TailwindColorClass } from '@/lib/tailwindClassParser'
8
+
9
+ export type TailwindClassMapEntry = TailwindColorClass & {
10
+ variableName: string | null
11
+ }
12
+
13
+ export interface CSSVariableSlice {
14
+ cssVariableDefinitions: Record<string, CSSVariableDefinition>
15
+ cssVariableUsages: Record<string, string>
16
+ cssVariableFamilies: CSSVariableFamily[]
17
+ isExplicitTokens: boolean
18
+ detachedProperties: Record<string, boolean>
19
+ tailwindClassMap: Record<string, TailwindClassMapEntry>
20
+ themeScopes: string[]
21
+
22
+ setCSSVariableDefinitions: (
23
+ definitions: Record<string, CSSVariableDefinition>,
24
+ isExplicit?: boolean,
25
+ scopes?: string[],
26
+ ) => void
27
+ setCSSVariableUsages: (usages: Record<string, string>) => void
28
+ clearCSSVariableUsages: () => void
29
+ detachProperty: (selectorPath: string, property: string) => void
30
+ reattachProperty: (selectorPath: string, property: string) => void
31
+ isPropertyDetached: (selectorPath: string, property: string) => boolean
32
+ setTailwindClassMap: (map: Record<string, TailwindClassMapEntry>) => void
33
+ }
34
+
35
+ function detachKey(selectorPath: string, property: string): string {
36
+ return `${selectorPath}::${property}`
37
+ }
38
+
39
+ export const createCSSVariableSlice: StateCreator<
40
+ CSSVariableSlice,
41
+ [],
42
+ [],
43
+ CSSVariableSlice
44
+ > = (set, get) => ({
45
+ cssVariableDefinitions: {},
46
+ cssVariableUsages: {},
47
+ cssVariableFamilies: [],
48
+ isExplicitTokens: false,
49
+ detachedProperties: {},
50
+ tailwindClassMap: {},
51
+ themeScopes: [],
52
+
53
+ setCSSVariableDefinitions: (definitions, isExplicit, scopes) => {
54
+ set({
55
+ cssVariableDefinitions: definitions,
56
+ cssVariableFamilies: groupVariablesIntoFamilies(definitions),
57
+ isExplicitTokens: isExplicit ?? false,
58
+ themeScopes: scopes ?? [],
59
+ })
60
+ },
61
+
62
+ setCSSVariableUsages: (usages) => {
63
+ set({ cssVariableUsages: usages })
64
+ },
65
+
66
+ clearCSSVariableUsages: () => {
67
+ set({ cssVariableUsages: {}, tailwindClassMap: {} })
68
+ },
69
+
70
+ detachProperty: (selectorPath, property) => {
71
+ const key = detachKey(selectorPath, property)
72
+ set((state) => ({
73
+ detachedProperties: { ...state.detachedProperties, [key]: true },
74
+ }))
75
+ },
76
+
77
+ reattachProperty: (selectorPath, property) => {
78
+ const key = detachKey(selectorPath, property)
79
+ set((state) => {
80
+ const next = { ...state.detachedProperties }
81
+ delete next[key]
82
+ return { detachedProperties: next }
83
+ })
84
+ },
85
+
86
+ isPropertyDetached: (selectorPath, property) => {
87
+ const key = detachKey(selectorPath, property)
88
+ return !!get().detachedProperties[key]
89
+ },
90
+
91
+ setTailwindClassMap: (map) => {
92
+ set({ tailwindClassMap: map })
93
+ },
94
+ })
@@ -0,0 +1,78 @@
1
+ import type { StateCreator } from 'zustand'
2
+ import type { BoundingRect } from '@/types/element'
3
+ import type { SourceInfo } from '@/types/claude'
4
+
5
+ export interface ElementSlice {
6
+ selectorPath: string | null
7
+ tagName: string | null
8
+ className: string | null
9
+ elementId: string | null
10
+ attributes: Record<string, string>
11
+ innerText: string | null
12
+ computedStyles: Record<string, string>
13
+ boundingRect: BoundingRect | null
14
+ sourceInfo: SourceInfo | null
15
+
16
+ selectElement: (data: {
17
+ selectorPath: string
18
+ tagName: string
19
+ className: string | null
20
+ id: string | null
21
+ attributes: Record<string, string>
22
+ innerText: string | null
23
+ computedStyles: Record<string, string>
24
+ boundingRect: BoundingRect
25
+ sourceInfo?: SourceInfo | null
26
+ }) => void
27
+ clearSelection: () => void
28
+ updateComputedStyles: (styles: Record<string, string>) => void
29
+ }
30
+
31
+ export const createElementSlice: StateCreator<
32
+ ElementSlice,
33
+ [],
34
+ [],
35
+ ElementSlice
36
+ > = (set) => ({
37
+ selectorPath: null,
38
+ tagName: null,
39
+ className: null,
40
+ elementId: null,
41
+ attributes: {},
42
+ innerText: null,
43
+ computedStyles: {},
44
+ boundingRect: null,
45
+ sourceInfo: null,
46
+
47
+ selectElement: (data) => {
48
+ set({
49
+ selectorPath: data.selectorPath,
50
+ tagName: data.tagName,
51
+ className: data.className,
52
+ elementId: data.id,
53
+ attributes: data.attributes,
54
+ innerText: data.innerText,
55
+ computedStyles: data.computedStyles,
56
+ boundingRect: data.boundingRect,
57
+ sourceInfo: data.sourceInfo ?? null,
58
+ })
59
+ },
60
+
61
+ clearSelection: () => {
62
+ set({
63
+ selectorPath: null,
64
+ tagName: null,
65
+ className: null,
66
+ elementId: null,
67
+ attributes: {},
68
+ innerText: null,
69
+ computedStyles: {},
70
+ boundingRect: null,
71
+ sourceInfo: null,
72
+ })
73
+ },
74
+
75
+ updateComputedStyles: (styles) => {
76
+ set({ computedStyles: styles })
77
+ },
78
+ })
@@ -0,0 +1,35 @@
1
+ import { create } from 'zustand'
2
+ import { createUISlice, type UISlice } from './uiSlice'
3
+ import { createElementSlice, type ElementSlice } from './elementSlice'
4
+ import { createTreeSlice, type TreeSlice } from './treeSlice'
5
+ import { createChangeSlice, type ChangeSlice } from './changeSlice'
6
+ import { createClaudeSlice, type ClaudeSlice } from './claudeSlice'
7
+ import {
8
+ createCSSVariableSlice,
9
+ type CSSVariableSlice,
10
+ } from './cssVariableSlice'
11
+ import { createComponentSlice, type ComponentSlice } from './componentSlice'
12
+ import { createConsoleSlice, type ConsoleSlice } from './consoleSlice'
13
+ import { createTerminalSlice, type TerminalSlice } from './terminalSlice'
14
+
15
+ export type EditorStore = UISlice &
16
+ ElementSlice &
17
+ TreeSlice &
18
+ ChangeSlice &
19
+ ClaudeSlice &
20
+ CSSVariableSlice &
21
+ ComponentSlice &
22
+ ConsoleSlice &
23
+ TerminalSlice
24
+
25
+ export const useEditorStore = create<EditorStore>()((...a) => ({
26
+ ...createUISlice(...a),
27
+ ...createElementSlice(...a),
28
+ ...createTreeSlice(...a),
29
+ ...createChangeSlice(...a),
30
+ ...createClaudeSlice(...a),
31
+ ...createCSSVariableSlice(...a),
32
+ ...createComponentSlice(...a),
33
+ ...createConsoleSlice(...a),
34
+ ...createTerminalSlice(...a),
35
+ }))
@@ -0,0 +1,30 @@
1
+ import type { StateCreator } from 'zustand'
2
+
3
+ export type TerminalStatus =
4
+ | 'disconnected'
5
+ | 'connecting'
6
+ | 'connected'
7
+ | 'error'
8
+
9
+ export interface TerminalSlice {
10
+ terminalStatus: TerminalStatus
11
+ terminalServerPort: number
12
+ writeToTerminal: ((data: string) => void) | null
13
+
14
+ setTerminalStatus: (status: TerminalStatus) => void
15
+ registerTerminalWriter: (writer: ((data: string) => void) | null) => void
16
+ }
17
+
18
+ export const createTerminalSlice: StateCreator<
19
+ TerminalSlice,
20
+ [],
21
+ [],
22
+ TerminalSlice
23
+ > = (set) => ({
24
+ terminalStatus: 'disconnected',
25
+ terminalServerPort: 4001,
26
+ writeToTerminal: null,
27
+
28
+ setTerminalStatus: (status) => set({ terminalStatus: status }),
29
+ registerTerminalWriter: (writer) => set({ writeToTerminal: writer }),
30
+ })
@@ -0,0 +1,69 @@
1
+ import type { StateCreator } from 'zustand'
2
+ import type { TreeNode } from '@/types/tree'
3
+
4
+ // Collect all node IDs that have children for full expansion
5
+ function collectAllIds(node: TreeNode, ids: Set<string>) {
6
+ if (node.children.length === 0) return
7
+ ids.add(node.id)
8
+ for (const child of node.children) {
9
+ collectAllIds(child, ids)
10
+ }
11
+ }
12
+
13
+ export interface TreeSlice {
14
+ rootNode: TreeNode | null
15
+ expandedNodeIds: Set<string>
16
+ searchQuery: string
17
+ highlightedNodeId: string | null
18
+
19
+ setRootNode: (node: TreeNode | null) => void
20
+ setSearchQuery: (query: string) => void
21
+ setHighlightedNodeId: (id: string | null) => void
22
+ toggleNodeExpanded: (nodeId: string) => void
23
+ expandToNode: (nodeId: string) => void
24
+ }
25
+
26
+ export const createTreeSlice: StateCreator<TreeSlice, [], [], TreeSlice> = (
27
+ set,
28
+ get,
29
+ ) => ({
30
+ rootNode: null,
31
+ expandedNodeIds: new Set<string>(),
32
+ searchQuery: '',
33
+ highlightedNodeId: null,
34
+
35
+ setRootNode: (node) => {
36
+ const { expandedNodeIds: prev } = get()
37
+ // On first load (no previous state), start collapsed.
38
+ // On subsequent updates (DOM_UPDATED), preserve user-expanded state.
39
+ if (prev.size === 0) {
40
+ set({ rootNode: node, expandedNodeIds: new Set<string>() })
41
+ } else {
42
+ set({ rootNode: node })
43
+ }
44
+ },
45
+ setSearchQuery: (query) => set({ searchQuery: query }),
46
+ setHighlightedNodeId: (id) => set({ highlightedNodeId: id }),
47
+
48
+ toggleNodeExpanded: (nodeId) => {
49
+ const { expandedNodeIds } = get()
50
+ const next = new Set(expandedNodeIds)
51
+ if (next.has(nodeId)) {
52
+ next.delete(nodeId)
53
+ } else {
54
+ next.add(nodeId)
55
+ }
56
+ set({ expandedNodeIds: next })
57
+ },
58
+
59
+ expandToNode: (nodeId) => {
60
+ // Merge ancestors into existing expanded state (preserve user-toggled branches)
61
+ const { expandedNodeIds: prev } = get()
62
+ const next = new Set(prev)
63
+ const parts = nodeId.split(' > ')
64
+ for (let i = 1; i <= parts.length; i++) {
65
+ next.add(parts.slice(0, i).join(' > '))
66
+ }
67
+ set({ expandedNodeIds: next })
68
+ },
69
+ })