@tanstack/devtools 0.6.19 → 0.6.21

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.
@@ -1,21 +1,33 @@
1
- import { For, createEffect, createMemo, createSignal } from 'solid-js'
1
+ import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
2
2
  import clsx from 'clsx'
3
3
  import { useDrawContext } from '../context/draw-context'
4
4
  import { usePlugins, useTheme } from '../context/use-devtools-context'
5
5
  import { useStyles } from '../styles/use-styles'
6
6
  import { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from '../constants'
7
+ import { PluginMarketplace } from './plugin-marketplace'
7
8
 
8
9
  export const PluginsTab = () => {
9
10
  const { plugins, activePlugins, toggleActivePlugins } = usePlugins()
10
- const { expanded, hoverUtils, animationMs } = useDrawContext()
11
+ const { expanded, hoverUtils, animationMs, setForceExpand } = useDrawContext()
11
12
 
12
13
  const [pluginRefs, setPluginRefs] = createSignal(
13
14
  new Map<string, HTMLDivElement>(),
14
15
  )
16
+ const [showMarketplace, setShowMarketplace] = createSignal(false)
15
17
 
16
18
  const styles = useStyles()
17
19
 
18
20
  const { theme } = useTheme()
21
+
22
+ const hasPlugins = createMemo(
23
+ () => plugins()?.length && plugins()!.length > 0,
24
+ )
25
+
26
+ // Keep sidebar expanded when marketplace is shown
27
+ createEffect(() => {
28
+ setForceExpand(showMarketplace())
29
+ })
30
+
19
31
  createEffect(() => {
20
32
  const currentActivePlugins = plugins()?.filter((plugin) =>
21
33
  activePlugins().includes(plugin.id!),
@@ -30,76 +42,111 @@ export const PluginsTab = () => {
30
42
  })
31
43
  })
32
44
 
45
+ const handleMarketplaceClick = () => setShowMarketplace(!showMarketplace())
46
+
47
+ const handlePluginClick = (pluginId: string) => {
48
+ // Close marketplace when switching to a plugin
49
+ if (showMarketplace()) {
50
+ setShowMarketplace(false)
51
+ }
52
+ toggleActivePlugins(pluginId)
53
+ }
54
+
33
55
  return (
34
- <div class={styles().pluginsTabPanel}>
35
- <div
36
- class={clsx(
37
- styles().pluginsTabDraw(expanded()),
38
- {
39
- [styles().pluginsTabDraw(expanded())]: expanded(),
40
- },
41
- styles().pluginsTabDrawTransition(animationMs),
42
- )}
43
- onMouseEnter={() => hoverUtils.enter()}
44
- onMouseLeave={() => hoverUtils.leave()}
45
- >
56
+ <Show when={hasPlugins()} fallback={<PluginMarketplace />}>
57
+ <div class={styles().pluginsTabPanel}>
46
58
  <div
47
59
  class={clsx(
48
- styles().pluginsTabSidebar(expanded()),
49
- styles().pluginsTabSidebarTransition(animationMs),
60
+ styles().pluginsTabDraw(expanded()),
61
+ {
62
+ [styles().pluginsTabDraw(expanded())]: expanded(),
63
+ },
64
+ styles().pluginsTabDrawTransition(animationMs),
50
65
  )}
66
+ onMouseEnter={() => hoverUtils.enter()}
67
+ onMouseLeave={() => {
68
+ // Don't collapse on mouse leave if marketplace is open
69
+ if (!showMarketplace()) {
70
+ hoverUtils.leave()
71
+ }
72
+ }}
51
73
  >
52
- <For each={plugins()}>
53
- {(plugin) => {
54
- let pluginHeading: HTMLHeadingElement | undefined
55
-
56
- createEffect(() => {
57
- if (pluginHeading) {
58
- typeof plugin.name === 'string'
59
- ? (pluginHeading.textContent = plugin.name)
60
- : plugin.name(pluginHeading, theme())
61
- }
62
- })
63
-
64
- const isActive = createMemo(() =>
65
- activePlugins().includes(plugin.id!),
66
- )
67
-
68
- return (
74
+ <div
75
+ class={clsx(
76
+ styles().pluginsTabSidebar(expanded()),
77
+ styles().pluginsTabSidebarTransition(animationMs),
78
+ )}
79
+ >
80
+ <div class={styles().pluginsList}>
81
+ <For each={plugins()}>
82
+ {(plugin) => {
83
+ let pluginHeading: HTMLHeadingElement | undefined
84
+
85
+ createEffect(() => {
86
+ if (pluginHeading) {
87
+ typeof plugin.name === 'string'
88
+ ? (pluginHeading.textContent = plugin.name)
89
+ : plugin.name(pluginHeading, theme())
90
+ }
91
+ })
92
+
93
+ const isActive = createMemo(() =>
94
+ activePlugins().includes(plugin.id!),
95
+ )
96
+
97
+ return (
98
+ <div
99
+ onClick={() => handlePluginClick(plugin.id!)}
100
+ class={clsx(styles().pluginName, {
101
+ active: isActive(),
102
+ })}
103
+ >
104
+ <h3
105
+ id={`${PLUGIN_TITLE_CONTAINER_ID}-${plugin.id}`}
106
+ ref={pluginHeading}
107
+ />
108
+ </div>
109
+ )
110
+ }}
111
+ </For>
112
+ </div>
113
+
114
+ {/* Add More Tab - visually distinct */}
115
+ <div
116
+ onClick={handleMarketplaceClick}
117
+ class={clsx(styles().pluginNameAddMore, {
118
+ active: showMarketplace(),
119
+ })}
120
+ >
121
+ <h3>Add More</h3>
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ {/* Show marketplace if active, otherwise show plugin panels */}
127
+ <Show
128
+ when={showMarketplace()}
129
+ fallback={
130
+ <For each={activePlugins()}>
131
+ {(pluginId) => (
69
132
  <div
70
- onClick={() => {
71
- toggleActivePlugins(plugin.id!)
133
+ id={`${PLUGIN_CONTAINER_ID}-${pluginId}`}
134
+ ref={(el) => {
135
+ setPluginRefs((prev) => {
136
+ const updated = new Map(prev)
137
+ updated.set(pluginId, el)
138
+ return updated
139
+ })
72
140
  }}
73
- class={clsx(styles().pluginName, {
74
- active: isActive(),
75
- })}
76
- >
77
- <h3
78
- id={`${PLUGIN_TITLE_CONTAINER_ID}-${plugin.id}`}
79
- ref={pluginHeading}
80
- />
81
- </div>
82
- )
83
- }}
84
- </For>
85
- </div>
141
+ class={styles().pluginsTabContent}
142
+ />
143
+ )}
144
+ </For>
145
+ }
146
+ >
147
+ <PluginMarketplace />
148
+ </Show>
86
149
  </div>
87
-
88
- <For each={activePlugins()}>
89
- {(pluginId) => (
90
- <div
91
- id={`${PLUGIN_CONTAINER_ID}-${pluginId}`}
92
- ref={(el) => {
93
- setPluginRefs((prev) => {
94
- const updated = new Map(prev)
95
- updated.set(pluginId, el)
96
- return updated
97
- })
98
- }}
99
- class={styles().pluginsTabContent}
100
- />
101
- )}
102
- </For>
103
- </div>
150
+ </Show>
104
151
  )
105
152
  }
@@ -0,0 +1,218 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ compareVersions,
4
+ parseVersion,
5
+ satisfiesMaxVersion,
6
+ satisfiesMinVersion,
7
+ satisfiesVersionRange,
8
+ } from './semver-utils'
9
+
10
+ describe('parseVersion', () => {
11
+ it('should parse basic semver format', () => {
12
+ const result = parseVersion('1.2.3')
13
+ expect(result).toEqual({
14
+ major: 1,
15
+ minor: 2,
16
+ patch: 3,
17
+ raw: '1.2.3',
18
+ })
19
+ })
20
+
21
+ it('should parse version with v prefix', () => {
22
+ const result = parseVersion('v2.0.1')
23
+ expect(result).toEqual({
24
+ major: 2,
25
+ minor: 0,
26
+ patch: 1,
27
+ raw: 'v2.0.1',
28
+ })
29
+ })
30
+
31
+ it('should parse version with ^ prefix', () => {
32
+ const result = parseVersion('^3.1.4')
33
+ expect(result).toEqual({
34
+ major: 3,
35
+ minor: 1,
36
+ patch: 4,
37
+ raw: '^3.1.4',
38
+ })
39
+ })
40
+
41
+ it('should parse version with ~ prefix', () => {
42
+ const result = parseVersion('~1.5.2')
43
+ expect(result).toEqual({
44
+ major: 1,
45
+ minor: 5,
46
+ patch: 2,
47
+ raw: '~1.5.2',
48
+ })
49
+ })
50
+
51
+ it('should handle prerelease versions', () => {
52
+ const result = parseVersion('1.0.0-alpha.1')
53
+ expect(result).toEqual({
54
+ major: 1,
55
+ minor: 0,
56
+ patch: 0,
57
+ raw: '1.0.0-alpha.1',
58
+ })
59
+ })
60
+
61
+ it('should handle build metadata', () => {
62
+ const result = parseVersion('1.0.0+20130313144700')
63
+ expect(result).toEqual({
64
+ major: 1,
65
+ minor: 0,
66
+ patch: 0,
67
+ raw: '1.0.0+20130313144700',
68
+ })
69
+ })
70
+
71
+ it('should handle version with only major.minor', () => {
72
+ const result = parseVersion('2.1')
73
+ expect(result).toEqual({
74
+ major: 2,
75
+ minor: 1,
76
+ patch: 0,
77
+ raw: '2.1',
78
+ })
79
+ })
80
+
81
+ it('should return null for invalid version', () => {
82
+ expect(parseVersion('')).toBeNull()
83
+ expect(parseVersion('invalid')).toBeNull()
84
+ expect(parseVersion('1')).toBeNull()
85
+ })
86
+
87
+ it('should return null for version with non-numeric parts', () => {
88
+ expect(parseVersion('a.b.c')).toBeNull()
89
+ })
90
+ })
91
+
92
+ describe('compareVersions', () => {
93
+ it('should return 0 for equal versions', () => {
94
+ const v1 = parseVersion('1.2.3')!
95
+ const v2 = parseVersion('1.2.3')!
96
+ expect(compareVersions(v1, v2)).toBe(0)
97
+ })
98
+
99
+ it('should return positive for newer major version', () => {
100
+ const v1 = parseVersion('2.0.0')!
101
+ const v2 = parseVersion('1.0.0')!
102
+ expect(compareVersions(v1, v2)).toBeGreaterThan(0)
103
+ })
104
+
105
+ it('should return negative for older major version', () => {
106
+ const v1 = parseVersion('1.0.0')!
107
+ const v2 = parseVersion('2.0.0')!
108
+ expect(compareVersions(v1, v2)).toBeLessThan(0)
109
+ })
110
+
111
+ it('should compare minor versions when major is equal', () => {
112
+ const v1 = parseVersion('1.5.0')!
113
+ const v2 = parseVersion('1.3.0')!
114
+ expect(compareVersions(v1, v2)).toBeGreaterThan(0)
115
+ })
116
+
117
+ it('should compare patch versions when major and minor are equal', () => {
118
+ const v1 = parseVersion('1.2.5')!
119
+ const v2 = parseVersion('1.2.3')!
120
+ expect(compareVersions(v1, v2)).toBeGreaterThan(0)
121
+ })
122
+ })
123
+
124
+ describe('satisfiesMinVersion', () => {
125
+ it('should return true when current version meets minimum', () => {
126
+ expect(satisfiesMinVersion('2.0.0', '1.0.0')).toBe(true)
127
+ expect(satisfiesMinVersion('1.5.0', '1.0.0')).toBe(true)
128
+ expect(satisfiesMinVersion('1.0.5', '1.0.0')).toBe(true)
129
+ })
130
+
131
+ it('should return true when versions are equal', () => {
132
+ expect(satisfiesMinVersion('1.2.3', '1.2.3')).toBe(true)
133
+ })
134
+
135
+ it('should return false when current version is below minimum', () => {
136
+ expect(satisfiesMinVersion('0.9.0', '1.0.0')).toBe(false)
137
+ expect(satisfiesMinVersion('1.0.0', '1.5.0')).toBe(false)
138
+ expect(satisfiesMinVersion('1.2.3', '1.2.5')).toBe(false)
139
+ })
140
+
141
+ it('should return true if version cannot be parsed', () => {
142
+ expect(satisfiesMinVersion('invalid', '1.0.0')).toBe(true)
143
+ expect(satisfiesMinVersion('1.0.0', 'invalid')).toBe(true)
144
+ })
145
+ })
146
+
147
+ describe('satisfiesMaxVersion', () => {
148
+ it('should return true when current version is below maximum', () => {
149
+ expect(satisfiesMaxVersion('1.0.0', '2.0.0')).toBe(true)
150
+ expect(satisfiesMaxVersion('1.5.0', '2.0.0')).toBe(true)
151
+ })
152
+
153
+ it('should return true when versions are equal', () => {
154
+ expect(satisfiesMaxVersion('1.2.3', '1.2.3')).toBe(true)
155
+ })
156
+
157
+ it('should return false when current version exceeds maximum', () => {
158
+ expect(satisfiesMaxVersion('2.0.0', '1.0.0')).toBe(false)
159
+ expect(satisfiesMaxVersion('1.5.0', '1.0.0')).toBe(false)
160
+ })
161
+
162
+ it('should return true if version cannot be parsed', () => {
163
+ expect(satisfiesMaxVersion('invalid', '1.0.0')).toBe(true)
164
+ expect(satisfiesMaxVersion('1.0.0', 'invalid')).toBe(true)
165
+ })
166
+ })
167
+
168
+ describe('satisfiesVersionRange', () => {
169
+ it('should return satisfied when no range specified', () => {
170
+ expect(satisfiesVersionRange('1.0.0')).toEqual({ satisfied: true })
171
+ })
172
+
173
+ it('should validate minimum version only', () => {
174
+ expect(satisfiesVersionRange('2.0.0', '1.0.0')).toEqual({
175
+ satisfied: true,
176
+ })
177
+ expect(satisfiesVersionRange('0.9.0', '1.0.0')).toEqual({
178
+ satisfied: false,
179
+ reason: 'Requires v1.0.0 or higher (current: v0.9.0)',
180
+ })
181
+ })
182
+
183
+ it('should validate maximum version only', () => {
184
+ expect(satisfiesVersionRange('1.0.0', undefined, '2.0.0')).toEqual({
185
+ satisfied: true,
186
+ })
187
+ expect(satisfiesVersionRange('3.0.0', undefined, '2.0.0')).toEqual({
188
+ satisfied: false,
189
+ reason: 'Requires v2.0.0 or lower (current: v3.0.0)',
190
+ })
191
+ })
192
+
193
+ it('should validate both min and max versions', () => {
194
+ expect(satisfiesVersionRange('1.5.0', '1.0.0', '2.0.0')).toEqual({
195
+ satisfied: true,
196
+ })
197
+ expect(satisfiesVersionRange('0.9.0', '1.0.0', '2.0.0')).toEqual({
198
+ satisfied: false,
199
+ reason: 'Requires v1.0.0 or higher (current: v0.9.0)',
200
+ })
201
+ expect(satisfiesVersionRange('2.5.0', '1.0.0', '2.0.0')).toEqual({
202
+ satisfied: false,
203
+ reason: 'Requires v2.0.0 or lower (current: v2.5.0)',
204
+ })
205
+ })
206
+
207
+ it('should handle version prefixes', () => {
208
+ expect(satisfiesVersionRange('^1.5.0', '1.0.0', '2.0.0')).toEqual({
209
+ satisfied: true,
210
+ })
211
+ expect(satisfiesVersionRange('~1.5.0', '1.0.0', '2.0.0')).toEqual({
212
+ satisfied: true,
213
+ })
214
+ expect(satisfiesVersionRange('v1.5.0', '1.0.0', '2.0.0')).toEqual({
215
+ satisfied: true,
216
+ })
217
+ })
218
+ })
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Simple semver utilities for version comparison
3
+ * Handles basic semver format: major.minor.patch
4
+ */
5
+
6
+ interface ParsedVersion {
7
+ major: number
8
+ minor: number
9
+ patch: number
10
+ raw: string
11
+ }
12
+
13
+ /**
14
+ * Parse a semver string into components
15
+ */
16
+ export function parseVersion(version: string): ParsedVersion | null {
17
+ if (!version) return null
18
+
19
+ // Remove leading 'v', '^', '~', and any prerelease/build metadata
20
+ const cleanVersion = version
21
+ .replace(/^[v^~]/, '')
22
+ .split('-')[0]
23
+ ?.split('+')[0]
24
+
25
+ if (!cleanVersion) return null
26
+
27
+ const parts = cleanVersion.split('.')
28
+
29
+ if (parts.length < 2) return null
30
+
31
+ const major = parseInt(parts[0] ?? '0', 10)
32
+ const minor = parseInt(parts[1] ?? '0', 10)
33
+ const patch = parseInt(parts[2] ?? '0', 10)
34
+
35
+ if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
36
+ return null
37
+ }
38
+
39
+ return {
40
+ major,
41
+ minor,
42
+ patch,
43
+ raw: version,
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Compare two versions
49
+ * Returns: -1 if v1 < v2, 0 if equal, 1 if v1 > v2
50
+ */
51
+ export function compareVersions(v1: ParsedVersion, v2: ParsedVersion): number {
52
+ if (v1.major !== v2.major) return v1.major - v2.major
53
+ if (v1.minor !== v2.minor) return v1.minor - v2.minor
54
+ return v1.patch - v2.patch
55
+ }
56
+
57
+ /**
58
+ * Check if a version satisfies a minimum requirement
59
+ */
60
+ export function satisfiesMinVersion(
61
+ currentVersion: string,
62
+ minVersion: string,
63
+ ): boolean {
64
+ const current = parseVersion(currentVersion)
65
+ const min = parseVersion(minVersion)
66
+
67
+ if (!current || !min) return true // If we can't parse, assume it's OK
68
+
69
+ return compareVersions(current, min) >= 0
70
+ }
71
+
72
+ /**
73
+ * Check if a version is below a maximum requirement
74
+ */
75
+ export function satisfiesMaxVersion(
76
+ currentVersion: string,
77
+ maxVersion: string,
78
+ ): boolean {
79
+ const current = parseVersion(currentVersion)
80
+ const max = parseVersion(maxVersion)
81
+
82
+ if (!current || !max) return true
83
+
84
+ return compareVersions(current, max) <= 0
85
+ }
86
+
87
+ /**
88
+ * Check if a version satisfies both min and max requirements
89
+ */
90
+ export function satisfiesVersionRange(
91
+ currentVersion: string,
92
+ minVersion?: string,
93
+ maxVersion?: string,
94
+ ): { satisfied: boolean; reason?: string } {
95
+ if (!minVersion && !maxVersion) {
96
+ return { satisfied: true }
97
+ }
98
+
99
+ if (minVersion && !satisfiesMinVersion(currentVersion, minVersion)) {
100
+ return {
101
+ satisfied: false,
102
+ reason: `Requires v${minVersion} or higher (current: v${currentVersion})`,
103
+ }
104
+ }
105
+
106
+ if (maxVersion && !satisfiesMaxVersion(currentVersion, maxVersion)) {
107
+ return {
108
+ satisfied: false,
109
+ reason: `Requires v${maxVersion} or lower (current: v${currentVersion})`,
110
+ }
111
+ }
112
+
113
+ return { satisfied: true }
114
+ }