@tanstack/devtools 0.5.0 → 0.6.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 (38) hide show
  1. package/dist/esm/components/main-panel.js +8 -2
  2. package/dist/esm/components/main-panel.js.map +1 -1
  3. package/dist/esm/components/tabs.js +10 -0
  4. package/dist/esm/components/tabs.js.map +1 -1
  5. package/dist/esm/context/draw-context.d.ts +13 -0
  6. package/dist/esm/context/draw-context.js +55 -0
  7. package/dist/esm/context/draw-context.js.map +1 -0
  8. package/dist/esm/context/pip-context.js +1 -2
  9. package/dist/esm/context/pip-context.js.map +1 -1
  10. package/dist/esm/context/use-devtools-context.js +10 -1
  11. package/dist/esm/context/use-devtools-context.js.map +1 -1
  12. package/dist/esm/hooks/use-head-changes.d.ts +39 -0
  13. package/dist/esm/hooks/use-head-changes.js +65 -0
  14. package/dist/esm/hooks/use-head-changes.js.map +1 -0
  15. package/dist/esm/styles/tokens.js +4 -1
  16. package/dist/esm/styles/tokens.js.map +1 -1
  17. package/dist/esm/styles/use-styles.d.ts +19 -0
  18. package/dist/esm/styles/use-styles.js +143 -3
  19. package/dist/esm/styles/use-styles.js.map +1 -1
  20. package/dist/esm/tabs/index.d.ts +5 -0
  21. package/dist/esm/tabs/index.js +8 -2
  22. package/dist/esm/tabs/index.js.map +1 -1
  23. package/dist/esm/tabs/plugins-tab.js +31 -13
  24. package/dist/esm/tabs/plugins-tab.js.map +1 -1
  25. package/dist/esm/tabs/seo-tab.d.ts +1 -0
  26. package/dist/esm/tabs/seo-tab.js +291 -0
  27. package/dist/esm/tabs/seo-tab.js.map +1 -0
  28. package/package.json +1 -1
  29. package/src/components/main-panel.tsx +5 -1
  30. package/src/components/tabs.tsx +9 -0
  31. package/src/context/draw-context.tsx +67 -0
  32. package/src/context/pip-context.tsx +1 -3
  33. package/src/context/use-devtools-context.ts +12 -2
  34. package/src/hooks/use-head-changes.ts +110 -0
  35. package/src/styles/use-styles.ts +148 -3
  36. package/src/tabs/index.tsx +25 -0
  37. package/src/tabs/plugins-tab.tsx +51 -23
  38. package/src/tabs/seo-tab.tsx +238 -0
@@ -4,12 +4,133 @@ import { tokens } from './tokens'
4
4
  import type { TanStackDevtoolsConfig } from '../context/devtools-context'
5
5
  import type { Accessor } from 'solid-js'
6
6
 
7
+ const mSecondsToCssSeconds = (mSeconds: number) =>
8
+ `${(mSeconds / 1000).toFixed(2)}s`
9
+
7
10
  const stylesFactory = () => {
8
11
  const { colors, font, size, alpha, border } = tokens
9
12
  const { fontFamily, size: fontSize } = font
10
13
  const css = goober.css
11
14
 
12
15
  return {
16
+ seoTabContainer: css`
17
+ padding: 0;
18
+ margin: 0 auto;
19
+ background: ${colors.darkGray[700]};
20
+ border-radius: 12px;
21
+ box-shadow: 0 2px 16px rgba(0, 0, 0, 0.08);
22
+ overflow-y: auto;
23
+ height: 100%;
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: 0;
27
+ width: 100%;
28
+ overflow-y: auto;
29
+ `,
30
+ seoTabTitle: css`
31
+ font-size: 1.25rem;
32
+ font-weight: 600;
33
+ color: ${colors.purple[400]};
34
+ margin: 0;
35
+ padding: 1rem 1.5rem 0.5rem 1.5rem;
36
+ text-align: left;
37
+ border-bottom: 1px solid ${colors.gray[700]};
38
+ `,
39
+ seoTabSection: css`
40
+ padding: 1.5rem;
41
+ background: ${colors.darkGray[800]};
42
+ border: 1px solid ${colors.gray[700]};
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: 0.5rem;
46
+ margin: 1.5rem;
47
+ margin-bottom: 2rem;
48
+ border-radius: 0.75rem;
49
+ `,
50
+ seoPreviewSection: css`
51
+ display: flex;
52
+ flex-direction: row;
53
+ gap: 16px;
54
+ margin-bottom: 0;
55
+ justify-content: flex-start;
56
+ align-items: flex-start;
57
+ overflow-x: auto;
58
+ flex-wrap: wrap;
59
+ padding-bottom: 0.5rem;
60
+ `,
61
+ seoPreviewCard: css`
62
+ border: 1px solid ${colors.gray[700]};
63
+ border-radius: 8px;
64
+ padding: 12px 10px;
65
+ background: ${colors.darkGray[900]};
66
+ margin-bottom: 0;
67
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
68
+ display: flex;
69
+ flex-direction: column;
70
+ align-items: flex-start;
71
+ min-width: 200px;
72
+ max-width: 240px;
73
+ font-size: 0.95rem;
74
+ gap: 4px;
75
+ `,
76
+ seoPreviewHeader: css`
77
+ font-size: 1rem;
78
+ font-weight: 500;
79
+ margin-bottom: 6px;
80
+ color: ${colors.purple[400]};
81
+ `,
82
+ seoPreviewImage: css`
83
+ max-width: 100%;
84
+ border-radius: 6px;
85
+ margin-bottom: 6px;
86
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
87
+ height: 160px;
88
+ `,
89
+ seoPreviewTitle: css`
90
+ font-size: 1rem;
91
+ font-weight: 600;
92
+ margin-bottom: 2px;
93
+ color: ${colors.gray[100]};
94
+ `,
95
+ seoPreviewDesc: css`
96
+ color: ${colors.gray[300]};
97
+ margin-bottom: 2px;
98
+ font-size: 0.95rem;
99
+ `,
100
+ seoPreviewUrl: css`
101
+ color: ${colors.gray[500]};
102
+ font-size: 0.9rem;
103
+ margin-bottom: 2px;
104
+ word-break: break-all;
105
+ `,
106
+ seoMissingTagsSection: css`
107
+ margin-top: 4px;
108
+ font-size: 0.95rem;
109
+ color: ${colors.red[400]};
110
+ `,
111
+ seoMissingTagsList: css`
112
+ margin: 4px 0 0 0;
113
+ padding: 0;
114
+ list-style: none;
115
+ display: flex;
116
+ flex-wrap: wrap;
117
+ gap: 6px;
118
+ max-width: 240px;
119
+ `,
120
+ seoMissingTag: css`
121
+ background: ${colors.red[500]}22;
122
+ color: ${colors.red[500]};
123
+ border-radius: 4px;
124
+ padding: 1px 6px;
125
+ font-size: 0.9rem;
126
+ font-weight: 500;
127
+ `,
128
+ seoAllTagsFound: css`
129
+ color: ${colors.green[500]};
130
+ font-weight: 500;
131
+ margin-left: 6px;
132
+ font-size: 0.95rem;
133
+ `,
13
134
  devtoolsPanelContainer: (
14
135
  panelLocation: TanStackDevtoolsConfig['panelLocation'],
15
136
  isDetached: boolean,
@@ -231,13 +352,37 @@ const stylesFactory = () => {
231
352
  height: 100%;
232
353
  overflow: hidden;
233
354
  `,
234
- pluginsTabSidebar: css`
235
- width: ${size[48]};
355
+
356
+ pluginsTabDraw: css`
357
+ width: 0px;
358
+ height: 100%;
236
359
  background-color: ${colors.darkGray[800]};
237
- border-right: 1px solid ${colors.gray[700]};
238
360
  box-shadow: 0 1px 0 ${colors.gray[700]};
361
+ `,
362
+ pluginsTabDrawExpanded: css`
363
+ width: ${size[48]};
364
+ border-right: 1px solid ${colors.gray[700]};
365
+ `,
366
+ pluginsTabDrawTransition: (mSeconds: number) => {
367
+ return css`
368
+ transition: width ${mSecondsToCssSeconds(mSeconds)} ease;
369
+ `
370
+ },
371
+
372
+ pluginsTabSidebar: css`
373
+ width: ${size[48]};
239
374
  overflow-y: auto;
375
+ transform: translateX(-100%);
376
+ `,
377
+ pluginsTabSidebarExpanded: css`
378
+ transform: translateX(0);
240
379
  `,
380
+ pluginsTabSidebarTransition: (mSeconds: number) => {
381
+ return css`
382
+ transition: transform ${mSecondsToCssSeconds(mSeconds)} ease;
383
+ `
384
+ },
385
+
241
386
  pluginName: css`
242
387
  font-size: ${fontSize.xs};
243
388
  font-family: ${fontFamily.sans};
@@ -1,5 +1,6 @@
1
1
  import { SettingsTab } from './settings-tab'
2
2
  import { PluginsTab } from './plugins-tab'
3
+ import { SeoTab } from './seo-tab'
3
4
 
4
5
  export const tabs = [
5
6
  {
@@ -24,6 +25,30 @@ export const tabs = [
24
25
  </svg>
25
26
  ),
26
27
  },
28
+ {
29
+ name: 'SEO',
30
+ id: 'seo',
31
+ component: () => <SeoTab />,
32
+ icon: (
33
+ <svg
34
+ xmlns="http://www.w3.org/2000/svg"
35
+ width="24"
36
+ height="24"
37
+ viewBox="0 0 24 24"
38
+ fill="none"
39
+ stroke="currentColor"
40
+ stroke-width="2"
41
+ stroke-linecap="round"
42
+ stroke-linejoin="round"
43
+ class="lucide lucide-file-search2-icon lucide-file-search-2"
44
+ >
45
+ <path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
46
+ <path d="M14 2v4a2 2 0 0 0 2 2h4" />
47
+ <circle cx="11.5" cy="14.5" r="2.5" />
48
+ <path d="M13.3 16.3 15 18" />
49
+ </svg>
50
+ ),
51
+ },
27
52
  {
28
53
  name: 'Settings',
29
54
  id: 'settings',
@@ -1,11 +1,13 @@
1
1
  import { For, createEffect } from 'solid-js'
2
2
  import clsx from 'clsx'
3
+ import { useDrawContext } from '../context/draw-context'
3
4
  import { usePlugins } from '../context/use-devtools-context'
4
5
  import { useStyles } from '../styles/use-styles'
5
6
  import { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from '../constants'
6
7
 
7
8
  export const PluginsTab = () => {
8
9
  const { plugins, activePlugin, setActivePlugin } = usePlugins()
10
+ const { expanded, hoverUtils, animationMs } = useDrawContext()
9
11
  let activePluginRef: HTMLDivElement | undefined
10
12
 
11
13
  createEffect(() => {
@@ -17,32 +19,58 @@ export const PluginsTab = () => {
17
19
  }
18
20
  })
19
21
  const styles = useStyles()
22
+
20
23
  return (
21
24
  <div class={styles().pluginsTabPanel}>
22
- <div class={styles().pluginsTabSidebar}>
23
- <For each={plugins()}>
24
- {(plugin) => {
25
- let pluginHeading: HTMLHeadingElement | undefined
26
- createEffect(() => {
27
- if (pluginHeading) {
28
- typeof plugin.name === 'string'
29
- ? (pluginHeading.textContent = plugin.name)
30
- : plugin.name(pluginHeading)
31
- }
32
- })
33
- return (
34
- <div
35
- onClick={() => setActivePlugin(plugin.id!)}
36
- class={clsx(styles().pluginName, {
37
- active: activePlugin() === plugin.id,
38
- })}
39
- >
40
- <h3 id={PLUGIN_TITLE_CONTAINER_ID} ref={pluginHeading} />
41
- </div>
42
- )
43
- }}
44
- </For>
25
+ <div
26
+ class={clsx(
27
+ styles().pluginsTabDraw,
28
+ {
29
+ [styles().pluginsTabDrawExpanded]: expanded(),
30
+ },
31
+ styles().pluginsTabDrawTransition(animationMs),
32
+ )}
33
+ onMouseEnter={() => {
34
+ hoverUtils.enter()
35
+ }}
36
+ onMouseLeave={() => {
37
+ hoverUtils.leave()
38
+ }}
39
+ >
40
+ <div
41
+ class={clsx(
42
+ styles().pluginsTabSidebar,
43
+ {
44
+ [styles().pluginsTabSidebarExpanded]: expanded(),
45
+ },
46
+ styles().pluginsTabSidebarTransition(animationMs),
47
+ )}
48
+ >
49
+ <For each={plugins()}>
50
+ {(plugin) => {
51
+ let pluginHeading: HTMLHeadingElement | undefined
52
+ createEffect(() => {
53
+ if (pluginHeading) {
54
+ typeof plugin.name === 'string'
55
+ ? (pluginHeading.textContent = plugin.name)
56
+ : plugin.name(pluginHeading)
57
+ }
58
+ })
59
+ return (
60
+ <div
61
+ onClick={() => setActivePlugin(plugin.id!)}
62
+ class={clsx(styles().pluginName, {
63
+ active: activePlugin() === plugin.id,
64
+ })}
65
+ >
66
+ <h3 id={PLUGIN_TITLE_CONTAINER_ID} ref={pluginHeading} />
67
+ </div>
68
+ )
69
+ }}
70
+ </For>
71
+ </div>
45
72
  </div>
73
+
46
74
  <div
47
75
  id={PLUGIN_CONTAINER_ID}
48
76
  ref={activePluginRef}
@@ -0,0 +1,238 @@
1
+ import { For, createSignal } from 'solid-js'
2
+ import { useStyles } from '../styles/use-styles'
3
+ import { useHeadChanges } from '../hooks/use-head-changes'
4
+
5
+ type SocialMeta = {
6
+ title?: string
7
+ description?: string
8
+ image?: string
9
+ url?: string
10
+ }
11
+
12
+ type SocialReport = {
13
+ network: string
14
+ found: Partial<SocialMeta>
15
+ missing: Array<string>
16
+ }
17
+
18
+ const SOCIALS = [
19
+ {
20
+ network: 'Facebook',
21
+ tags: [
22
+ { key: 'og:title', prop: 'title' },
23
+ { key: 'og:description', prop: 'description' },
24
+ { key: 'og:image', prop: 'image' },
25
+ { key: 'og:url', prop: 'url' },
26
+ ],
27
+ color: '#4267B2',
28
+ },
29
+ {
30
+ network: 'X/Twitter',
31
+ tags: [
32
+ { key: 'twitter:title', prop: 'title' },
33
+ { key: 'twitter:description', prop: 'description' },
34
+ { key: 'twitter:image', prop: 'image' },
35
+ { key: 'twitter:url', prop: 'url' },
36
+ ],
37
+ color: '#1DA1F2',
38
+ },
39
+ {
40
+ network: 'LinkedIn',
41
+ tags: [
42
+ { key: 'og:title', prop: 'title' },
43
+ { key: 'og:description', prop: 'description' },
44
+ { key: 'og:image', prop: 'image' },
45
+ { key: 'og:url', prop: 'url' },
46
+ ],
47
+ color: '#0077B5',
48
+ },
49
+ {
50
+ network: 'Discord',
51
+ tags: [
52
+ { key: 'og:title', prop: 'title' },
53
+ { key: 'og:description', prop: 'description' },
54
+ { key: 'og:image', prop: 'image' },
55
+ { key: 'og:url', prop: 'url' },
56
+ ],
57
+ color: '#5865F2',
58
+ },
59
+ {
60
+ network: 'Slack',
61
+ tags: [
62
+ { key: 'og:title', prop: 'title' },
63
+ { key: 'og:description', prop: 'description' },
64
+ { key: 'og:image', prop: 'image' },
65
+ { key: 'og:url', prop: 'url' },
66
+ ],
67
+ color: '#4A154B',
68
+ },
69
+ {
70
+ network: 'Mastodon',
71
+ tags: [
72
+ { key: 'og:title', prop: 'title' },
73
+ { key: 'og:description', prop: 'description' },
74
+ { key: 'og:image', prop: 'image' },
75
+ { key: 'og:url', prop: 'url' },
76
+ ],
77
+ color: '#6364FF',
78
+ },
79
+ {
80
+ network: 'Bluesky',
81
+ tags: [
82
+ { key: 'og:title', prop: 'title' },
83
+ { key: 'og:description', prop: 'description' },
84
+ { key: 'og:image', prop: 'image' },
85
+ { key: 'og:url', prop: 'url' },
86
+ ],
87
+ color: '#1185FE',
88
+ },
89
+ // Add more networks as needed
90
+ ]
91
+ function SocialPreview(props: {
92
+ meta: SocialMeta
93
+ color: string
94
+ network: string
95
+ }) {
96
+ const styles = useStyles()
97
+
98
+ return (
99
+ <div
100
+ class={styles().seoPreviewCard}
101
+ style={{ 'border-color': props.color }}
102
+ >
103
+ <div class={styles().seoPreviewHeader} style={{ color: props.color }}>
104
+ {props.network} Preview
105
+ </div>
106
+ {props.meta.image ? (
107
+ <img
108
+ src={props.meta.image}
109
+ alt="Preview"
110
+ class={styles().seoPreviewImage}
111
+ />
112
+ ) : (
113
+ <div
114
+ class={styles().seoPreviewImage}
115
+ style={{
116
+ background: '#222',
117
+ color: '#888',
118
+ display: 'flex',
119
+ 'align-items': 'center',
120
+ 'justify-content': 'center',
121
+ 'min-height': '80px',
122
+ width: '100%',
123
+ }}
124
+ >
125
+ No Image
126
+ </div>
127
+ )}
128
+ <div class={styles().seoPreviewTitle}>
129
+ {props.meta.title || 'No Title'}
130
+ </div>
131
+ <div class={styles().seoPreviewDesc}>
132
+ {props.meta.description || 'No Description'}
133
+ </div>
134
+ <div class={styles().seoPreviewUrl}>
135
+ {props.meta.url || window.location.href}
136
+ </div>
137
+ </div>
138
+ )
139
+ }
140
+ export const SeoTab = () => {
141
+ const [reports, setReports] = createSignal<Array<SocialReport>>(analyzeHead())
142
+ const styles = useStyles()
143
+
144
+ function analyzeHead(): Array<SocialReport> {
145
+ const metaTags = Array.from(document.head.querySelectorAll('meta'))
146
+ const reports: Array<SocialReport> = []
147
+
148
+ for (const social of SOCIALS) {
149
+ const found: Partial<SocialMeta> = {}
150
+ const missing: Array<string> = []
151
+ for (const tag of social.tags) {
152
+ const meta = metaTags.find(
153
+ (m) =>
154
+ (tag.key.includes('twitter:')
155
+ ? false
156
+ : m.getAttribute('property') === tag.key) ||
157
+ m.getAttribute('name') === tag.key,
158
+ )
159
+
160
+ if (meta && meta.getAttribute('content')) {
161
+ found[tag.prop as keyof SocialMeta] =
162
+ meta.getAttribute('content') || undefined
163
+ } else {
164
+ missing.push(tag.key)
165
+ }
166
+ }
167
+ reports.push({ network: social.network, found, missing })
168
+ }
169
+ return reports
170
+ }
171
+
172
+ useHeadChanges(() => {
173
+ setReports(analyzeHead())
174
+ })
175
+
176
+ return (
177
+ <div class={styles().seoTabContainer}>
178
+ <section class={styles().seoTabSection}>
179
+ <h3 class={styles().sectionTitle}>
180
+ <svg
181
+ class={styles().sectionIcon}
182
+ xmlns="http://www.w3.org/2000/svg"
183
+ width="24"
184
+ height="24"
185
+ viewBox="0 0 24 24"
186
+ fill="none"
187
+ stroke="currentColor"
188
+ stroke-width="2"
189
+ stroke-linecap="round"
190
+ stroke-linejoin="round"
191
+ >
192
+ <path d="m10 9-3 3 3 3" />
193
+ <path d="m14 15 3-3-3-3" />
194
+ <path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" />
195
+ </svg>
196
+ Social previews
197
+ </h3>
198
+ <p class={styles().sectionDescription}>
199
+ See how your current page will look when shared on popular social
200
+ networks. The tool checks for essential meta tags and highlights any
201
+ that are missing.
202
+ </p>
203
+ <div class={styles().seoPreviewSection}>
204
+ <For each={reports()}>
205
+ {(report, i) => {
206
+ const social = SOCIALS[i()]
207
+ return (
208
+ <div>
209
+ <SocialPreview
210
+ meta={report.found}
211
+ color={social!.color}
212
+ network={social!.network}
213
+ />
214
+ {report.missing.length > 0 ? (
215
+ <>
216
+ <div class={styles().seoMissingTagsSection}>
217
+ <strong>Missing tags for {social?.network}:</strong>
218
+
219
+ <ul class={styles().seoMissingTagsList}>
220
+ <For each={report.missing}>
221
+ {(tag) => (
222
+ <li class={styles().seoMissingTag}>{tag}</li>
223
+ )}
224
+ </For>
225
+ </ul>
226
+ </div>
227
+ </>
228
+ ) : null}
229
+ </div>
230
+ )
231
+ }}
232
+ </For>
233
+ </div>
234
+ </section>
235
+ {/* Future sections can be added here as <section class={styles().seoTabSection}>...</section> */}
236
+ </div>
237
+ )
238
+ }