@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
@@ -0,0 +1,291 @@
1
+ import { template, insert, createComponent, memo, effect, className, setAttribute } from "solid-js/web";
2
+ import { createSignal, For } from "solid-js";
3
+ import { useStyles } from "../styles/use-styles.js";
4
+ import { useHeadChanges } from "../hooks/use-head-changes.js";
5
+ var _tmpl$ = /* @__PURE__ */ template(`<div><div> Preview</div><div></div><div></div><div>`), _tmpl$2 = /* @__PURE__ */ template(`<img alt=Preview>`), _tmpl$3 = /* @__PURE__ */ template(`<div>No Image`), _tmpl$4 = /* @__PURE__ */ template(`<div><section><h3><svg xmlns=http://www.w3.org/2000/svg width=24 height=24 viewBox="0 0 24 24"fill=none stroke=currentColor stroke-width=2 stroke-linecap=round stroke-linejoin=round><path d="m10 9-3 3 3 3"></path><path d="m14 15 3-3-3-3"></path><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"></path></svg>Social previews</h3><p>See how your current page will look when shared on popular social networks. The tool checks for essential meta tags and highlights any that are missing.</p><div>`), _tmpl$5 = /* @__PURE__ */ template(`<div>`), _tmpl$6 = /* @__PURE__ */ template(`<div><strong>Missing tags for <!>:</strong><ul>`), _tmpl$7 = /* @__PURE__ */ template(`<li>`);
6
+ const SOCIALS = [
7
+ {
8
+ network: "Facebook",
9
+ tags: [{
10
+ key: "og:title",
11
+ prop: "title"
12
+ }, {
13
+ key: "og:description",
14
+ prop: "description"
15
+ }, {
16
+ key: "og:image",
17
+ prop: "image"
18
+ }, {
19
+ key: "og:url",
20
+ prop: "url"
21
+ }],
22
+ color: "#4267B2"
23
+ },
24
+ {
25
+ network: "X/Twitter",
26
+ tags: [{
27
+ key: "twitter:title",
28
+ prop: "title"
29
+ }, {
30
+ key: "twitter:description",
31
+ prop: "description"
32
+ }, {
33
+ key: "twitter:image",
34
+ prop: "image"
35
+ }, {
36
+ key: "twitter:url",
37
+ prop: "url"
38
+ }],
39
+ color: "#1DA1F2"
40
+ },
41
+ {
42
+ network: "LinkedIn",
43
+ tags: [{
44
+ key: "og:title",
45
+ prop: "title"
46
+ }, {
47
+ key: "og:description",
48
+ prop: "description"
49
+ }, {
50
+ key: "og:image",
51
+ prop: "image"
52
+ }, {
53
+ key: "og:url",
54
+ prop: "url"
55
+ }],
56
+ color: "#0077B5"
57
+ },
58
+ {
59
+ network: "Discord",
60
+ tags: [{
61
+ key: "og:title",
62
+ prop: "title"
63
+ }, {
64
+ key: "og:description",
65
+ prop: "description"
66
+ }, {
67
+ key: "og:image",
68
+ prop: "image"
69
+ }, {
70
+ key: "og:url",
71
+ prop: "url"
72
+ }],
73
+ color: "#5865F2"
74
+ },
75
+ {
76
+ network: "Slack",
77
+ tags: [{
78
+ key: "og:title",
79
+ prop: "title"
80
+ }, {
81
+ key: "og:description",
82
+ prop: "description"
83
+ }, {
84
+ key: "og:image",
85
+ prop: "image"
86
+ }, {
87
+ key: "og:url",
88
+ prop: "url"
89
+ }],
90
+ color: "#4A154B"
91
+ },
92
+ {
93
+ network: "Mastodon",
94
+ tags: [{
95
+ key: "og:title",
96
+ prop: "title"
97
+ }, {
98
+ key: "og:description",
99
+ prop: "description"
100
+ }, {
101
+ key: "og:image",
102
+ prop: "image"
103
+ }, {
104
+ key: "og:url",
105
+ prop: "url"
106
+ }],
107
+ color: "#6364FF"
108
+ },
109
+ {
110
+ network: "Bluesky",
111
+ tags: [{
112
+ key: "og:title",
113
+ prop: "title"
114
+ }, {
115
+ key: "og:description",
116
+ prop: "description"
117
+ }, {
118
+ key: "og:image",
119
+ prop: "image"
120
+ }, {
121
+ key: "og:url",
122
+ prop: "url"
123
+ }],
124
+ color: "#1185FE"
125
+ }
126
+ // Add more networks as needed
127
+ ];
128
+ function SocialPreview(props) {
129
+ const styles = useStyles();
130
+ return (() => {
131
+ var _el$ = _tmpl$(), _el$2 = _el$.firstChild, _el$3 = _el$2.firstChild, _el$4 = _el$2.nextSibling, _el$5 = _el$4.nextSibling, _el$6 = _el$5.nextSibling;
132
+ insert(_el$2, () => props.network, _el$3);
133
+ insert(_el$, (() => {
134
+ var _c$ = memo(() => !!props.meta.image);
135
+ return () => _c$() ? (() => {
136
+ var _el$7 = _tmpl$2();
137
+ effect((_p$) => {
138
+ var _v$8 = props.meta.image, _v$9 = styles().seoPreviewImage;
139
+ _v$8 !== _p$.e && setAttribute(_el$7, "src", _p$.e = _v$8);
140
+ _v$9 !== _p$.t && className(_el$7, _p$.t = _v$9);
141
+ return _p$;
142
+ }, {
143
+ e: void 0,
144
+ t: void 0
145
+ });
146
+ return _el$7;
147
+ })() : (() => {
148
+ var _el$8 = _tmpl$3();
149
+ _el$8.style.setProperty("background", "#222");
150
+ _el$8.style.setProperty("color", "#888");
151
+ _el$8.style.setProperty("display", "flex");
152
+ _el$8.style.setProperty("align-items", "center");
153
+ _el$8.style.setProperty("justify-content", "center");
154
+ _el$8.style.setProperty("min-height", "80px");
155
+ _el$8.style.setProperty("width", "100%");
156
+ effect(() => className(_el$8, styles().seoPreviewImage));
157
+ return _el$8;
158
+ })();
159
+ })(), _el$4);
160
+ insert(_el$4, () => props.meta.title || "No Title");
161
+ insert(_el$5, () => props.meta.description || "No Description");
162
+ insert(_el$6, () => props.meta.url || window.location.href);
163
+ effect((_p$) => {
164
+ var _v$ = styles().seoPreviewCard, _v$2 = props.color, _v$3 = styles().seoPreviewHeader, _v$4 = props.color, _v$5 = styles().seoPreviewTitle, _v$6 = styles().seoPreviewDesc, _v$7 = styles().seoPreviewUrl;
165
+ _v$ !== _p$.e && className(_el$, _p$.e = _v$);
166
+ _v$2 !== _p$.t && ((_p$.t = _v$2) != null ? _el$.style.setProperty("border-color", _v$2) : _el$.style.removeProperty("border-color"));
167
+ _v$3 !== _p$.a && className(_el$2, _p$.a = _v$3);
168
+ _v$4 !== _p$.o && ((_p$.o = _v$4) != null ? _el$2.style.setProperty("color", _v$4) : _el$2.style.removeProperty("color"));
169
+ _v$5 !== _p$.i && className(_el$4, _p$.i = _v$5);
170
+ _v$6 !== _p$.n && className(_el$5, _p$.n = _v$6);
171
+ _v$7 !== _p$.s && className(_el$6, _p$.s = _v$7);
172
+ return _p$;
173
+ }, {
174
+ e: void 0,
175
+ t: void 0,
176
+ a: void 0,
177
+ o: void 0,
178
+ i: void 0,
179
+ n: void 0,
180
+ s: void 0
181
+ });
182
+ return _el$;
183
+ })();
184
+ }
185
+ const SeoTab = () => {
186
+ const [reports, setReports] = createSignal(analyzeHead());
187
+ const styles = useStyles();
188
+ function analyzeHead() {
189
+ const metaTags = Array.from(document.head.querySelectorAll("meta"));
190
+ const reports2 = [];
191
+ for (const social of SOCIALS) {
192
+ const found = {};
193
+ const missing = [];
194
+ for (const tag of social.tags) {
195
+ const meta = metaTags.find((m) => (tag.key.includes("twitter:") ? false : m.getAttribute("property") === tag.key) || m.getAttribute("name") === tag.key);
196
+ if (meta && meta.getAttribute("content")) {
197
+ found[tag.prop] = meta.getAttribute("content") || void 0;
198
+ } else {
199
+ missing.push(tag.key);
200
+ }
201
+ }
202
+ reports2.push({
203
+ network: social.network,
204
+ found,
205
+ missing
206
+ });
207
+ }
208
+ return reports2;
209
+ }
210
+ useHeadChanges(() => {
211
+ setReports(analyzeHead());
212
+ });
213
+ return (() => {
214
+ var _el$9 = _tmpl$4(), _el$0 = _el$9.firstChild, _el$1 = _el$0.firstChild, _el$10 = _el$1.firstChild, _el$11 = _el$1.nextSibling, _el$12 = _el$11.nextSibling;
215
+ insert(_el$12, createComponent(For, {
216
+ get each() {
217
+ return reports();
218
+ },
219
+ children: (report, i) => {
220
+ const social = SOCIALS[i()];
221
+ return (() => {
222
+ var _el$13 = _tmpl$5();
223
+ insert(_el$13, createComponent(SocialPreview, {
224
+ get meta() {
225
+ return report.found;
226
+ },
227
+ get color() {
228
+ return social.color;
229
+ },
230
+ get network() {
231
+ return social.network;
232
+ }
233
+ }), null);
234
+ insert(_el$13, (() => {
235
+ var _c$2 = memo(() => report.missing.length > 0);
236
+ return () => _c$2() ? (() => {
237
+ var _el$14 = _tmpl$6(), _el$15 = _el$14.firstChild, _el$16 = _el$15.firstChild, _el$18 = _el$16.nextSibling;
238
+ _el$18.nextSibling;
239
+ var _el$19 = _el$15.nextSibling;
240
+ insert(_el$15, () => social?.network, _el$18);
241
+ insert(_el$19, createComponent(For, {
242
+ get each() {
243
+ return report.missing;
244
+ },
245
+ children: (tag) => (() => {
246
+ var _el$20 = _tmpl$7();
247
+ insert(_el$20, tag);
248
+ effect(() => className(_el$20, styles().seoMissingTag));
249
+ return _el$20;
250
+ })()
251
+ }));
252
+ effect((_p$) => {
253
+ var _v$14 = styles().seoMissingTagsSection, _v$15 = styles().seoMissingTagsList;
254
+ _v$14 !== _p$.e && className(_el$14, _p$.e = _v$14);
255
+ _v$15 !== _p$.t && className(_el$19, _p$.t = _v$15);
256
+ return _p$;
257
+ }, {
258
+ e: void 0,
259
+ t: void 0
260
+ });
261
+ return _el$14;
262
+ })() : null;
263
+ })(), null);
264
+ return _el$13;
265
+ })();
266
+ }
267
+ }));
268
+ effect((_p$) => {
269
+ var _v$0 = styles().seoTabContainer, _v$1 = styles().seoTabSection, _v$10 = styles().sectionTitle, _v$11 = styles().sectionIcon, _v$12 = styles().sectionDescription, _v$13 = styles().seoPreviewSection;
270
+ _v$0 !== _p$.e && className(_el$9, _p$.e = _v$0);
271
+ _v$1 !== _p$.t && className(_el$0, _p$.t = _v$1);
272
+ _v$10 !== _p$.a && className(_el$1, _p$.a = _v$10);
273
+ _v$11 !== _p$.o && setAttribute(_el$10, "class", _p$.o = _v$11);
274
+ _v$12 !== _p$.i && className(_el$11, _p$.i = _v$12);
275
+ _v$13 !== _p$.n && className(_el$12, _p$.n = _v$13);
276
+ return _p$;
277
+ }, {
278
+ e: void 0,
279
+ t: void 0,
280
+ a: void 0,
281
+ o: void 0,
282
+ i: void 0,
283
+ n: void 0
284
+ });
285
+ return _el$9;
286
+ })();
287
+ };
288
+ export {
289
+ SeoTab
290
+ };
291
+ //# sourceMappingURL=seo-tab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-tab.js","sources":["../../../src/tabs/seo-tab.tsx"],"sourcesContent":["import { For, createSignal } from 'solid-js'\nimport { useStyles } from '../styles/use-styles'\nimport { useHeadChanges } from '../hooks/use-head-changes'\n\ntype SocialMeta = {\n title?: string\n description?: string\n image?: string\n url?: string\n}\n\ntype SocialReport = {\n network: string\n found: Partial<SocialMeta>\n missing: Array<string>\n}\n\nconst SOCIALS = [\n {\n network: 'Facebook',\n tags: [\n { key: 'og:title', prop: 'title' },\n { key: 'og:description', prop: 'description' },\n { key: 'og:image', prop: 'image' },\n { key: 'og:url', prop: 'url' },\n ],\n color: '#4267B2',\n },\n {\n network: 'X/Twitter',\n tags: [\n { key: 'twitter:title', prop: 'title' },\n { key: 'twitter:description', prop: 'description' },\n { key: 'twitter:image', prop: 'image' },\n { key: 'twitter:url', prop: 'url' },\n ],\n color: '#1DA1F2',\n },\n {\n network: 'LinkedIn',\n tags: [\n { key: 'og:title', prop: 'title' },\n { key: 'og:description', prop: 'description' },\n { key: 'og:image', prop: 'image' },\n { key: 'og:url', prop: 'url' },\n ],\n color: '#0077B5',\n },\n {\n network: 'Discord',\n tags: [\n { key: 'og:title', prop: 'title' },\n { key: 'og:description', prop: 'description' },\n { key: 'og:image', prop: 'image' },\n { key: 'og:url', prop: 'url' },\n ],\n color: '#5865F2',\n },\n {\n network: 'Slack',\n tags: [\n { key: 'og:title', prop: 'title' },\n { key: 'og:description', prop: 'description' },\n { key: 'og:image', prop: 'image' },\n { key: 'og:url', prop: 'url' },\n ],\n color: '#4A154B',\n },\n {\n network: 'Mastodon',\n tags: [\n { key: 'og:title', prop: 'title' },\n { key: 'og:description', prop: 'description' },\n { key: 'og:image', prop: 'image' },\n { key: 'og:url', prop: 'url' },\n ],\n color: '#6364FF',\n },\n {\n network: 'Bluesky',\n tags: [\n { key: 'og:title', prop: 'title' },\n { key: 'og:description', prop: 'description' },\n { key: 'og:image', prop: 'image' },\n { key: 'og:url', prop: 'url' },\n ],\n color: '#1185FE',\n },\n // Add more networks as needed\n]\nfunction SocialPreview(props: {\n meta: SocialMeta\n color: string\n network: string\n}) {\n const styles = useStyles()\n\n return (\n <div\n class={styles().seoPreviewCard}\n style={{ 'border-color': props.color }}\n >\n <div class={styles().seoPreviewHeader} style={{ color: props.color }}>\n {props.network} Preview\n </div>\n {props.meta.image ? (\n <img\n src={props.meta.image}\n alt=\"Preview\"\n class={styles().seoPreviewImage}\n />\n ) : (\n <div\n class={styles().seoPreviewImage}\n style={{\n background: '#222',\n color: '#888',\n display: 'flex',\n 'align-items': 'center',\n 'justify-content': 'center',\n 'min-height': '80px',\n width: '100%',\n }}\n >\n No Image\n </div>\n )}\n <div class={styles().seoPreviewTitle}>\n {props.meta.title || 'No Title'}\n </div>\n <div class={styles().seoPreviewDesc}>\n {props.meta.description || 'No Description'}\n </div>\n <div class={styles().seoPreviewUrl}>\n {props.meta.url || window.location.href}\n </div>\n </div>\n )\n}\nexport const SeoTab = () => {\n const [reports, setReports] = createSignal<Array<SocialReport>>(analyzeHead())\n const styles = useStyles()\n\n function analyzeHead(): Array<SocialReport> {\n const metaTags = Array.from(document.head.querySelectorAll('meta'))\n const reports: Array<SocialReport> = []\n\n for (const social of SOCIALS) {\n const found: Partial<SocialMeta> = {}\n const missing: Array<string> = []\n for (const tag of social.tags) {\n const meta = metaTags.find(\n (m) =>\n (tag.key.includes('twitter:')\n ? false\n : m.getAttribute('property') === tag.key) ||\n m.getAttribute('name') === tag.key,\n )\n\n if (meta && meta.getAttribute('content')) {\n found[tag.prop as keyof SocialMeta] =\n meta.getAttribute('content') || undefined\n } else {\n missing.push(tag.key)\n }\n }\n reports.push({ network: social.network, found, missing })\n }\n return reports\n }\n\n useHeadChanges(() => {\n setReports(analyzeHead())\n })\n\n return (\n <div class={styles().seoTabContainer}>\n <section class={styles().seoTabSection}>\n <h3 class={styles().sectionTitle}>\n <svg\n class={styles().sectionIcon}\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <path d=\"m10 9-3 3 3 3\" />\n <path d=\"m14 15 3-3-3-3\" />\n <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\" />\n </svg>\n Social previews\n </h3>\n <p class={styles().sectionDescription}>\n See how your current page will look when shared on popular social\n networks. The tool checks for essential meta tags and highlights any\n that are missing.\n </p>\n <div class={styles().seoPreviewSection}>\n <For each={reports()}>\n {(report, i) => {\n const social = SOCIALS[i()]\n return (\n <div>\n <SocialPreview\n meta={report.found}\n color={social!.color}\n network={social!.network}\n />\n {report.missing.length > 0 ? (\n <>\n <div class={styles().seoMissingTagsSection}>\n <strong>Missing tags for {social?.network}:</strong>\n\n <ul class={styles().seoMissingTagsList}>\n <For each={report.missing}>\n {(tag) => (\n <li class={styles().seoMissingTag}>{tag}</li>\n )}\n </For>\n </ul>\n </div>\n </>\n ) : null}\n </div>\n )\n }}\n </For>\n </div>\n </section>\n {/* Future sections can be added here as <section class={styles().seoTabSection}>...</section> */}\n </div>\n )\n}\n"],"names":["SOCIALS","network","tags","key","prop","color","SocialPreview","props","styles","useStyles","_el$","_tmpl$","_el$2","firstChild","_el$3","_el$4","nextSibling","_el$5","_el$6","_$insert","_c$","_$memo","meta","image","_el$7","_tmpl$2","_$effect","_p$","_v$8","_v$9","seoPreviewImage","e","_$setAttribute","t","_$className","undefined","_el$8","_tmpl$3","style","setProperty","title","description","url","window","location","href","_v$","seoPreviewCard","_v$2","_v$3","seoPreviewHeader","_v$4","_v$5","seoPreviewTitle","_v$6","seoPreviewDesc","_v$7","seoPreviewUrl","removeProperty","a","o","i","n","s","SeoTab","reports","setReports","createSignal","analyzeHead","metaTags","Array","from","document","head","querySelectorAll","social","found","missing","tag","find","m","includes","getAttribute","push","useHeadChanges","_el$9","_tmpl$4","_el$0","_el$1","_el$10","_el$11","_el$12","_$createComponent","For","each","children","report","_el$13","_tmpl$5","_c$2","length","_el$14","_tmpl$6","_el$15","_el$16","_el$18","_el$19","_el$20","_tmpl$7","seoMissingTag","_v$14","seoMissingTagsSection","_v$15","seoMissingTagsList","_v$0","seoTabContainer","_v$1","seoTabSection","_v$10","sectionTitle","_v$11","sectionIcon","_v$12","sectionDescription","_v$13","seoPreviewSection"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/devtools",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "TanStack Devtools is a set of tools for building advanced devtools for your application.",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -1,8 +1,10 @@
1
1
  import clsx from 'clsx'
2
+ import { DrawClientProvider } from '../context/draw-context'
2
3
  import { useDevtoolsSettings, useHeight } from '../context/use-devtools-context'
3
4
  import { useStyles } from '../styles/use-styles'
4
5
  import { TANSTACK_DEVTOOLS } from '../utils/storage'
5
6
  import { usePiPWindow } from '../context/pip-context'
7
+
6
8
  import type { Accessor, JSX } from 'solid-js'
7
9
 
8
10
  export const MainPanel = (props: {
@@ -30,7 +32,9 @@ export const MainPanel = (props: {
30
32
  styles().devtoolsPanelContainerResizing(props.isResizing),
31
33
  )}
32
34
  >
33
- {props.children}
35
+ <DrawClientProvider animationMs={400}>
36
+ {props.children}
37
+ </DrawClientProvider>
34
38
  </div>
35
39
  )
36
40
  }
@@ -2,6 +2,7 @@ import clsx from 'clsx'
2
2
  import { For } from 'solid-js'
3
3
  import { useStyles } from '../styles/use-styles'
4
4
  import { useDevtoolsState } from '../context/use-devtools-context'
5
+ import { useDrawContext } from '../context/draw-context'
5
6
  import { tabs } from '../tabs'
6
7
  import { usePiPWindow } from '../context/pip-context'
7
8
 
@@ -18,6 +19,8 @@ export const Tabs = (props: TabsProps) => {
18
19
  `width=${window.innerWidth},height=${state().height},top=${window.screen.height},left=${window.screenLeft}}`,
19
20
  )
20
21
  }
22
+ const { hoverUtils } = useDrawContext()
23
+
21
24
  return (
22
25
  <div class={styles().tabContainer}>
23
26
  <For each={tabs}>
@@ -26,6 +29,12 @@ export const Tabs = (props: TabsProps) => {
26
29
  type="button"
27
30
  onClick={() => setState({ activeTab: tab.id })}
28
31
  class={clsx(styles().tab, { active: state().activeTab === tab.id })}
32
+ onMouseEnter={() => {
33
+ if (tab.id === 'plugins') hoverUtils.enter()
34
+ }}
35
+ onMouseLeave={() => {
36
+ if (tab.id === 'plugins') hoverUtils.leave()
37
+ }}
29
38
  >
30
39
  {tab.icon}
31
40
  </button>
@@ -0,0 +1,67 @@
1
+ import {
2
+ createContext,
3
+ createMemo,
4
+ createSignal,
5
+ onCleanup,
6
+ useContext,
7
+ } from 'solid-js'
8
+ import type { ParentComponent } from 'solid-js'
9
+
10
+ const useDraw = (props: { animationMs: number }) => {
11
+ const [activeHover, setActiveHover] = createSignal<boolean>(false)
12
+ const [forceExpand, setForceExpand] = createSignal<boolean>(false)
13
+
14
+ const expanded = createMemo(() => activeHover() || forceExpand())
15
+
16
+ let hoverTimeout: ReturnType<typeof setTimeout> | null = null
17
+ onCleanup(() => {
18
+ if (hoverTimeout) clearTimeout(hoverTimeout)
19
+ })
20
+
21
+ const hoverUtils = {
22
+ enter: () => {
23
+ if (hoverTimeout) {
24
+ clearTimeout(hoverTimeout)
25
+ hoverTimeout = null
26
+ }
27
+ setActiveHover(true)
28
+ },
29
+
30
+ leave: () => {
31
+ hoverTimeout = setTimeout(() => {
32
+ setActiveHover(false)
33
+ }, props.animationMs)
34
+ },
35
+ }
36
+
37
+ return {
38
+ expanded,
39
+ setForceExpand,
40
+ hoverUtils,
41
+ animationMs: props.animationMs,
42
+ }
43
+ }
44
+
45
+ type ContextType = ReturnType<typeof useDraw>
46
+
47
+ const DrawContext = createContext<ContextType | undefined>(undefined)
48
+
49
+ export const DrawClientProvider: ParentComponent<{
50
+ animationMs: number
51
+ }> = (props) => {
52
+ const value = useDraw({ animationMs: props.animationMs })
53
+
54
+ return (
55
+ <DrawContext.Provider value={value}>{props.children}</DrawContext.Provider>
56
+ )
57
+ }
58
+
59
+ export function useDrawContext() {
60
+ const context = useContext(DrawContext)
61
+
62
+ if (context === undefined) {
63
+ throw new Error(`useDrawContext must be used within a DrawClientProvider`)
64
+ }
65
+
66
+ return context
67
+ }
@@ -6,7 +6,7 @@ import {
6
6
  onCleanup,
7
7
  useContext,
8
8
  } from 'solid-js'
9
- import { clearDelegatedEvents, delegateEvents } from 'solid-js/web'
9
+ import { delegateEvents } from 'solid-js/web'
10
10
  import type { Accessor, JSX } from 'solid-js'
11
11
 
12
12
  interface PiPProviderProps {
@@ -69,8 +69,6 @@ export const PiPProvider = (props: PiPProviderProps) => {
69
69
  pip.document.head.innerHTML = ''
70
70
  // Remove existing body
71
71
  pip.document.body.innerHTML = ''
72
- // Clear Delegated Events
73
- clearDelegatedEvents(pip.document)
74
72
 
75
73
  pip.document.title = 'TanStack Devtools'
76
74
  pip.document.body.style.margin = '0'
@@ -1,6 +1,7 @@
1
- import { createMemo, useContext } from 'solid-js'
1
+ import { createEffect, createMemo, useContext } from 'solid-js'
2
2
  import { DevtoolsContext } from './devtools-context.jsx'
3
- /* import type { DevtoolsPlugin } from './devtools-context' */
3
+ import { useDrawContext } from './draw-context.jsx'
4
+
4
5
  import type { DevtoolsStore } from './devtools-store.js'
5
6
 
6
7
  /**
@@ -19,10 +20,19 @@ const useDevtoolsContext = () => {
19
20
 
20
21
  export const usePlugins = () => {
21
22
  const { store, setStore } = useDevtoolsContext()
23
+ const { setForceExpand } = useDrawContext()
22
24
 
23
25
  const plugins = createMemo(() => store.plugins)
24
26
  const activePlugin = createMemo(() => store.state.activePlugin)
25
27
 
28
+ createEffect(() => {
29
+ if (activePlugin() == null) {
30
+ setForceExpand(false)
31
+ } else {
32
+ setForceExpand(true)
33
+ }
34
+ })
35
+
26
36
  const setActivePlugin = (pluginId: string) => {
27
37
  setStore((prev) => ({
28
38
  ...prev,
@@ -0,0 +1,110 @@
1
+ import { onCleanup, onMount } from 'solid-js'
2
+
3
+ type HeadChange =
4
+ | { kind: 'added'; node: Node }
5
+ | { kind: 'removed'; node: Node }
6
+ | {
7
+ kind: 'attr'
8
+ target: Element
9
+ name: string | null
10
+ oldValue: string | null
11
+ }
12
+ | { kind: 'title'; title: string }
13
+
14
+ type UseHeadChangesOptions = {
15
+ /**
16
+ * Observe attribute changes on elements inside <head>
17
+ * Default: true
18
+ */
19
+ attributes?: boolean
20
+ /**
21
+ * Observe added/removed nodes in <head>
22
+ * Default: true
23
+ */
24
+ childList?: boolean
25
+ /**
26
+ * Observe descendants of <head>
27
+ * Default: true
28
+ */
29
+ subtree?: boolean
30
+ /**
31
+ * Also observe <title> changes explicitly
32
+ * Default: true
33
+ */
34
+ observeTitle?: boolean
35
+ }
36
+
37
+ export function useHeadChanges(
38
+ onChange: (change: HeadChange, raw?: MutationRecord) => void,
39
+ opts: UseHeadChangesOptions = {},
40
+ ) {
41
+ const {
42
+ attributes = true,
43
+ childList = true,
44
+ subtree = true,
45
+ observeTitle = true,
46
+ } = opts
47
+
48
+ onMount(() => {
49
+ const headObserver = new MutationObserver((mutations) => {
50
+ for (const m of mutations) {
51
+ if (m.type === 'childList') {
52
+ m.addedNodes.forEach((node) => onChange({ kind: 'added', node }, m))
53
+ m.removedNodes.forEach((node) =>
54
+ onChange({ kind: 'removed', node }, m),
55
+ )
56
+ } else if (m.type === 'attributes') {
57
+ const el = m.target as Element
58
+ onChange(
59
+ {
60
+ kind: 'attr',
61
+ target: el,
62
+ name: m.attributeName,
63
+ oldValue: m.oldValue ?? null,
64
+ },
65
+ m,
66
+ )
67
+ } else {
68
+ // If someone mutates a Text node inside <title>, surface it as a title change.
69
+ const isInTitle =
70
+ m.target.parentNode &&
71
+ (m.target.parentNode as Element).tagName.toLowerCase() === 'title'
72
+ if (isInTitle) onChange({ kind: 'title', title: document.title }, m)
73
+ }
74
+ }
75
+ })
76
+
77
+ headObserver.observe(document.head, {
78
+ childList,
79
+ attributes,
80
+ subtree,
81
+ attributeOldValue: attributes,
82
+ characterData: true, // helps catch <title> text node edits
83
+ characterDataOldValue: false,
84
+ })
85
+
86
+ // Extra explicit observer for <title>, since `document.title = "..."`
87
+ // may not always bubble as a head mutation in all setups.
88
+ let titleObserver: MutationObserver | undefined
89
+ if (observeTitle) {
90
+ const titleEl =
91
+ document.head.querySelector('title') ||
92
+ // create a <title> if missing so future changes are observable
93
+ document.head.appendChild(document.createElement('title'))
94
+
95
+ titleObserver = new MutationObserver(() => {
96
+ onChange({ kind: 'title', title: document.title })
97
+ })
98
+ titleObserver.observe(titleEl, {
99
+ childList: true,
100
+ characterData: true,
101
+ subtree: true,
102
+ })
103
+ }
104
+
105
+ onCleanup(() => {
106
+ headObserver.disconnect()
107
+ titleObserver?.disconnect()
108
+ })
109
+ })
110
+ }