Skip to content

Commit ac0dbf0

Browse files
committed
feat: Add Collapsible component and integrate it into EmptyState; enhance CodeBlock for TypeScript support and update icon handling
1 parent da4510d commit ac0dbf0

6 files changed

Lines changed: 166 additions & 15 deletions

File tree

src/components/common/CodeBlock.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ interface CodeBlockProps {
44
}
55

66
interface SyntaxToken {
7-
type: 'keyword' | 'property' | 'operator' | 'variable' | 'punctuation' | 'text';
7+
type: 'keyword' | 'property' | 'operator' | 'variable' | 'punctuation' | 'text' | 'type';
88
value: string;
99
}
1010

@@ -40,6 +40,42 @@ export function CodeBlock({ code, language = 'javascript' }: CodeBlockProps) {
4040
return tokens;
4141
};
4242

43+
const tokenizeTypeScript = (code: string): SyntaxToken[] => {
44+
const tokens: SyntaxToken[] = [];
45+
46+
// Split by lines first to preserve line breaks
47+
const lines = code.split('\n');
48+
49+
lines.forEach((line, lineIndex) => {
50+
if (lineIndex > 0) {
51+
tokens.push({ type: 'text', value: '\n' });
52+
}
53+
54+
// Simple keyword-based highlighting for TypeScript
55+
const words = line.split(/(\s+|[{}();:"])/);
56+
57+
words.forEach(word => {
58+
if (!word) return;
59+
60+
if (word === 'interface' || word === 'import') {
61+
tokens.push({ type: 'keyword', value: word });
62+
} else if (word === 'Window') {
63+
tokens.push({ type: 'type', value: word });
64+
} else if (word === '__TANSTACK_QUERY_CLIENT__') {
65+
tokens.push({ type: 'property', value: word });
66+
} else if (word.startsWith('"') && word.endsWith('"')) {
67+
tokens.push({ type: 'text', value: word });
68+
} else if (['{', '}', ';', ':', '(', ')'].includes(word)) {
69+
tokens.push({ type: 'punctuation', value: word });
70+
} else {
71+
tokens.push({ type: 'text', value: word });
72+
}
73+
});
74+
});
75+
76+
return tokens;
77+
};
78+
4379
const getTokenClassName = (type: SyntaxToken['type']): string => {
4480
switch (type) {
4581
case 'keyword':
@@ -52,16 +88,18 @@ export function CodeBlock({ code, language = 'javascript' }: CodeBlockProps) {
5288
return 'text-blue-800 dark:text-blue-300'; // VSCode variable dark blue
5389
case 'punctuation':
5490
return 'text-gray-600 dark:text-gray-400'; // VSCode punctuation gray
91+
case 'type':
92+
return 'text-green-600 dark:text-green-400'; // VSCode type green
5593
case 'text':
5694
default:
5795
return 'text-gray-800 dark:text-gray-200'; // Default text color
5896
}
5997
};
6098

61-
const tokens = language === 'javascript' ? tokenizeJavaScript(code) : [{ type: 'text' as const, value: code }];
99+
const tokens = language === 'javascript' ? tokenizeJavaScript(code) : language === 'typescript' ? tokenizeTypeScript(code) : [{ type: 'text' as const, value: code }];
62100

63101
return (
64-
<code className="text-sm font-mono">
102+
<code className="text-sm font-mono whitespace-pre-wrap">
65103
{tokens.map((token, index) => (
66104
<span key={index} className={getTokenClassName(token.type)}>
67105
{token.value}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import React, { useState } from 'react';
2+
import { IconRenderer } from './IconRenderer';
3+
4+
interface CollapsibleProps {
5+
title: string;
6+
icon?: React.ReactNode;
7+
defaultExpanded?: boolean;
8+
className?: string;
9+
children: React.ReactNode;
10+
}
11+
12+
export function Collapsible({
13+
title,
14+
icon,
15+
defaultExpanded = false,
16+
className = '',
17+
children
18+
}: CollapsibleProps) {
19+
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
20+
21+
const handleToggle = () => {
22+
setIsExpanded(!isExpanded);
23+
};
24+
25+
return (
26+
<div className={`collapsible-container ${className}`}>
27+
<button
28+
onClick={handleToggle}
29+
className="collapsible-trigger cursor-pointer flex items-center gap-2 text-sm font-medium text-blue-800 dark:text-blue-200 hover:text-blue-900 dark:hover:text-blue-100 w-full text-left"
30+
aria-expanded={isExpanded}
31+
type="button"
32+
>
33+
<IconRenderer
34+
iconName="ChevronDown"
35+
className={`collapsible-icon w-4 h-4 transition-transform ${isExpanded ? 'expanded' : ''}`}
36+
/>
37+
{icon && <span className="collapsible-title-icon">{icon}</span>}
38+
<span>{title}</span>
39+
</button>
40+
<div className={`collapsible-content ${isExpanded ? 'expanded' : ''}`}>
41+
<div className="collapsible-inner">
42+
{children}
43+
</div>
44+
</div>
45+
</div>
46+
);
47+
}

src/components/common/IconRenderer.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CheckCircle, Clock, HelpCircle, Moon, Pause, RotateCw, XCircle, type LucideProps } from "lucide-react";
1+
import { CheckCircle, ChevronDown, Clock, HelpCircle, Moon, Pause, RotateCw, XCircle, type LucideProps } from "lucide-react";
22
import type { IconName } from "../../types/query";
33

44
const iconMap: Record<IconName, React.ForwardRefExoticComponent<Omit<LucideProps, "ref"> & React.RefAttributes<SVGSVGElement>>> = {
@@ -9,6 +9,7 @@ const iconMap: Record<IconName, React.ForwardRefExoticComponent<Omit<LucideProps
99
HelpCircle,
1010
Pause,
1111
Moon,
12+
ChevronDown,
1213
} as const;
1314

1415
interface IconRendererProps {

src/components/layout/EmptyState.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,38 @@
1-
import { CodeBlock } from '../common/CodeBlock';
1+
import { CodeBlock } from "../common/CodeBlock";
2+
import { Collapsible } from "../common/Collapsible";
23

34
export function EmptyState() {
45
return (
5-
<div className="flex-1 flex items-center justify-center">
6-
<div className="max-w-lg mx-auto p-6 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg text-center enter-animation">
6+
<div className="flex-1 flex items-center justify-center overflow-hidden">
7+
<div className="overflow-auto max-h-full max-w-lg mx-auto p-6 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg text-center enter-animation">
78
<div className="mb-4">
8-
<img
9-
src="/icon-48.png"
10-
alt="TanStack Query DevTools"
11-
className="w-12 h-12 mx-auto"
12-
/>
9+
<img src="/icon-48.png" alt="TanStack Query DevTools" className="w-12 h-12 mx-auto" />
1310
</div>
1411
<h3 className="text-2xl font-semibold mb-3 text-gray-900 dark:text-gray-100">Connect Your App</h3>
1512
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">To use TanStack Query DevTools, add this line to your application:</p>
1613
<div className="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded p-3 mb-4">
17-
<CodeBlock code="window.__TANSTACK_QUERY_CLIENT__ = queryClient" language="javascript" />
14+
<CodeBlock code="window.__TANSTACK_QUERY_CLIENT__ = queryClient;" language="javascript" />
1815
</div>
19-
<p className="text-xs text-gray-500 dark:text-gray-400">Place this code where you create your QueryClient instance, typically in your app setup or main component.</p>
16+
<p className="text-xs text-gray-500 dark:text-gray-400 mb-4">Place this code where you create your QueryClient instance, typically in your app setup or main component.</p>
17+
18+
<Collapsible title="TypeScript Users" className="text-left bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded p-3">
19+
<div className="mt-3 space-y-3">
20+
<p className="text-sm text-blue-700 dark:text-blue-300">
21+
If you're using TypeScript, you'll also need to create a <code className="bg-blue-100 dark:bg-blue-800 px-1 py-0.5 rounded text-xs font-mono">global.d.ts</code> file in your project root with the following declaration:
22+
</p>
23+
<div className="bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded p-3">
24+
<CodeBlock
25+
code={`interface Window {
26+
__TANSTACK_QUERY_CLIENT__:
27+
import("@tanstack/query-core")
28+
.QueryClient;
29+
}`}
30+
language="typescript"
31+
/>
32+
</div>
33+
<p className="text-xs text-blue-600 dark:text-blue-400">This type declaration allows TypeScript to recognize the global property and prevents compilation errors.</p>
34+
</div>
35+
</Collapsible>
2036
</div>
2137
</div>
2238
);

src/styles/animations.css

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,53 @@
11
@layer animations {
2+
/* Reusable Collapsible Component Animation */
3+
.collapsible-container {
4+
/* Container base styles are handled by className prop */
5+
}
6+
7+
.collapsible-trigger {
8+
/* Button base styles are handled by Tailwind classes */
9+
}
10+
11+
.collapsible-content {
12+
display: grid;
13+
grid-template-rows: 0fr;
14+
transition: grid-template-rows 0.3s ease-out;
15+
overflow: hidden;
16+
}
17+
18+
.collapsible-content.expanded {
19+
grid-template-rows: 1fr;
20+
}
21+
22+
.collapsible-inner {
23+
min-height: 0; /* Critical for CSS Grid animation */
24+
overflow: hidden;
25+
}
26+
27+
.collapsible-icon {
28+
transition: transform 0.3s ease;
29+
}
30+
31+
.collapsible-icon.expanded {
32+
transform: rotate(180deg);
33+
}
34+
35+
/* Reduced motion support */
36+
@media (prefers-reduced-motion: reduce) {
37+
.collapsible-content {
38+
transition: none;
39+
grid-template-rows: 1fr;
40+
}
41+
42+
.collapsible-icon {
43+
transition: none;
44+
}
45+
46+
.collapsible-icon.expanded {
47+
transform: none;
48+
}
49+
}
50+
251
/* Animations layer - Micro-interactions and animations */
352

453
/* Enhanced Card Interactions */

src/types/query.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export interface MutationData {
2323

2424
export type ViewType = "queries" | "mutations";
2525

26-
export type IconName = "CheckCircle" | "XCircle" | "Clock" | "RotateCw" | "HelpCircle" | "Pause" | "Moon";
26+
export type IconName = "CheckCircle" | "XCircle" | "Clock" | "RotateCw" | "HelpCircle" | "Pause" | "Moon" | "ChevronDown";
2727

2828
export interface StatusDisplay {
2929
icon: IconName;

0 commit comments

Comments
 (0)