prototypey.org - atproto lexicon typescript toolkit - mirror https://github.com/tylersayshi/prototypey

add share link and remove dead typeinfo code (#15)

authored by tyler and committed by GitHub 2bfd1c3a 65e17d36

+218 -131
+165 -54
packages/site/src/components/Header.tsx
··· 1 export function Header() { 2 return ( 3 <header 4 style={{ ··· 31 ATProto lexicon typescript toolkit 32 </p> 33 </div> 34 - <div className="header-links"> 35 - <a 36 - href="https://github.com/tylersayshi/prototypey" 37 - target="_blank" 38 - rel="noopener noreferrer" 39 style={{ 40 color: "var(--color-text-heading)", 41 - textDecoration: "none", 42 - fontSize: "1rem", 43 fontWeight: "600", 44 display: "flex", 45 alignItems: "center", 46 gap: "0.5rem", 47 - transition: "opacity 0.2s", 48 }} 49 - onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.6")} 50 - onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")} 51 - > 52 - <svg 53 - width="20" 54 - height="20" 55 - viewBox="0 0 24 24" 56 - fill="currentColor" 57 - aria-hidden="true" 58 - > 59 - <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" /> 60 - </svg> 61 - GitHub 62 - </a> 63 - <a 64 - href="https://www.npmjs.com/package/prototypey" 65 - target="_blank" 66 - rel="noopener noreferrer" 67 - style={{ 68 - color: "var(--color-text-heading)", 69 - textDecoration: "none", 70 - fontSize: "1rem", 71 - fontWeight: "600", 72 - display: "flex", 73 - alignItems: "center", 74 - gap: "0.5rem", 75 - transition: "opacity 0.2s", 76 }} 77 - onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.6")} 78 - onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")} 79 > 80 - <svg 81 - width="20" 82 - height="20" 83 - viewBox="0 0 24 24" 84 - fill="none" 85 - stroke="currentColor" 86 - strokeWidth="2" 87 - strokeLinecap="round" 88 - strokeLinejoin="round" 89 - aria-hidden="true" 90 - > 91 - <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /> 92 - <polyline points="3.27 6.96 12 12.01 20.73 6.96" /> 93 - <line x1="12" y1="22.08" x2="12" y2="12" /> 94 - </svg> 95 - npm 96 - </a> 97 </div> 98 </div> 99 </div>
··· 1 + import { useState } from "react"; 2 + 3 export function Header() { 4 + const [copied, setCopied] = useState(false); 5 + 6 + const handleShare = () => { 7 + try { 8 + void navigator.clipboard.writeText(window.location.href); 9 + setCopied(true); 10 + setTimeout(() => { 11 + setCopied(false); 12 + }, 1500); 13 + } catch (err) { 14 + console.error("Failed to copy URL:", err); 15 + } 16 + }; 17 + 18 return ( 19 <header 20 style={{ ··· 47 ATProto lexicon typescript toolkit 48 </p> 49 </div> 50 + <div 51 + className="link-container" 52 + style={{ 53 + display: "flex", 54 + flexDirection: "column", 55 + alignItems: "flex-end", 56 + gap: "1rem", 57 + }} 58 + > 59 + <div className="header-links"> 60 + <a 61 + href="https://github.com/tylersayshi/prototypey" 62 + target="_blank" 63 + rel="noopener noreferrer" 64 + style={{ 65 + color: "var(--color-text-heading)", 66 + textDecoration: "none", 67 + fontSize: "1rem", 68 + fontWeight: "600", 69 + display: "flex", 70 + alignItems: "center", 71 + gap: "0.5rem", 72 + transition: "opacity 0.2s", 73 + willChange: "opacity", 74 + }} 75 + onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.6")} 76 + onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")} 77 + > 78 + <svg 79 + width="20" 80 + height="20" 81 + viewBox="0 0 24 24" 82 + fill="currentColor" 83 + aria-hidden="true" 84 + style={{ flexShrink: 0 }} 85 + > 86 + <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" /> 87 + </svg> 88 + github 89 + </a> 90 + <a 91 + href="https://www.npmjs.com/package/prototypey" 92 + target="_blank" 93 + rel="noopener noreferrer" 94 + style={{ 95 + color: "var(--color-text-heading)", 96 + textDecoration: "none", 97 + fontSize: "1rem", 98 + fontWeight: "600", 99 + display: "flex", 100 + alignItems: "center", 101 + gap: "0.5rem", 102 + transition: "opacity 0.2s", 103 + willChange: "opacity", 104 + }} 105 + onMouseEnter={(e) => (e.currentTarget.style.opacity = "0.6")} 106 + onMouseLeave={(e) => (e.currentTarget.style.opacity = "1")} 107 + > 108 + <svg 109 + width="20" 110 + height="20" 111 + viewBox="0 0 24 24" 112 + fill="none" 113 + stroke="currentColor" 114 + strokeWidth="2" 115 + strokeLinecap="round" 116 + strokeLinejoin="round" 117 + aria-hidden="true" 118 + style={{ flexShrink: 0 }} 119 + > 120 + <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /> 121 + <polyline points="3.27 6.96 12 12.01 20.73 6.96" /> 122 + <line x1="12" y1="22.08" x2="12" y2="12" /> 123 + </svg> 124 + npm 125 + </a> 126 + </div> 127 + <button 128 + onClick={handleShare} 129 style={{ 130 color: "var(--color-text-heading)", 131 + fontSize: "0.875rem", 132 fontWeight: "600", 133 display: "flex", 134 alignItems: "center", 135 gap: "0.5rem", 136 + transition: "background-color 0.2s, border-color 0.2s", 137 + backgroundColor: "transparent", 138 + border: "1px solid var(--color-border)", 139 + borderRadius: "0.5rem", 140 + cursor: "pointer", 141 + padding: "0.5rem 1rem", 142 + boxShadow: "none", 143 + outline: "none", 144 + flexShrink: 0, 145 + whiteSpace: "nowrap", 146 }} 147 + onMouseEnter={(e) => { 148 + const bgColor = getComputedStyle(document.documentElement) 149 + .getPropertyValue("--color-bg-secondary") 150 + .trim(); 151 + const borderColor = getComputedStyle(document.documentElement) 152 + .getPropertyValue("--color-text-heading") 153 + .trim(); 154 + e.currentTarget.style.backgroundColor = bgColor; 155 + e.currentTarget.style.borderColor = borderColor; 156 + }} 157 + onMouseLeave={(e) => { 158 + e.currentTarget.style.backgroundColor = "transparent"; 159 + const borderColor = getComputedStyle(document.documentElement) 160 + .getPropertyValue("--color-border") 161 + .trim(); 162 + e.currentTarget.style.borderColor = borderColor; 163 }} 164 + aria-label="share playground" 165 > 166 + {copied ? ( 167 + <> 168 + <svg 169 + width="16" 170 + height="16" 171 + viewBox="0 0 24 24" 172 + fill="none" 173 + stroke="currentColor" 174 + strokeWidth="2" 175 + strokeLinecap="round" 176 + strokeLinejoin="round" 177 + aria-hidden="true" 178 + style={{ flexShrink: 0 }} 179 + > 180 + <polyline points="20 6 9 17 4 12" /> 181 + </svg> 182 + copied url! 183 + </> 184 + ) : ( 185 + <> 186 + <svg 187 + width="16" 188 + height="16" 189 + viewBox="0 0 24 24" 190 + fill="none" 191 + stroke="currentColor" 192 + strokeWidth="2" 193 + strokeLinecap="round" 194 + strokeLinejoin="round" 195 + aria-hidden="true" 196 + style={{ flexShrink: 0 }} 197 + > 198 + <circle cx="18" cy="5" r="3" /> 199 + <circle cx="6" cy="12" r="3" /> 200 + <circle cx="18" cy="19" r="3" /> 201 + <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" /> 202 + <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" /> 203 + </svg> 204 + share 205 + </> 206 + )} 207 + </button> 208 </div> 209 </div> 210 </div>
-1
packages/site/src/components/OutputPanel.tsx
··· 4 interface OutputPanelProps { 5 output: { 6 json: string; 7 - typeInfo: string; 8 error: string; 9 }; 10 }
··· 4 interface OutputPanelProps { 5 output: { 6 json: string; 7 error: string; 8 }; 9 }
+2 -73
packages/site/src/components/Playground.tsx
··· 1 - /* eslint-disable @typescript-eslint/no-unsafe-argument */ 2 - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 3 /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 4 /* eslint-disable @typescript-eslint/no-unsafe-call */ 5 import { useState, useEffect, useRef } from "react"; ··· 20 "code", 21 parseAsCompressed.withDefault(DEFAULT_CODE), 22 ); 23 - const [output, setOutput] = useState({ json: "", typeInfo: "", error: "" }); 24 const [editorReady, setEditorReady] = useState(false); 25 const [theme, setTheme] = useState<"vs-light" | "vs-dark">( 26 window.matchMedia("(prefers-color-scheme: dark)").matches ··· 70 }, [monaco, editorReady]); 71 72 useEffect(() => { 73 - const timeoutId = setTimeout(async () => { 74 try { 75 const nsMatch = /const\s+lex\s*=\s*lx\.lexicon\([^]*?\}\s*\);/.exec( 76 code, ··· 85 const fn = new Function("lx", wrappedCode); 86 87 const result = fn(lx); 88 - let typeInfo = "// Hover over .infer in the editor to see the type"; 89 - 90 - if (monaco && tsWorkerRef.current) { 91 - try { 92 - const uri = monaco.Uri.parse("file:///main.ts"); 93 - const existingModel = monaco.editor.getModel(uri); 94 - 95 - if (existingModel) { 96 - const inferPosition = code.indexOf(`ns.infer`); 97 - if (inferPosition !== -1) { 98 - const offset = inferPosition + `ns.infer`.length - 1; 99 - 100 - const quickInfo = 101 - await tsWorkerRef.current.getQuickInfoAtPosition( 102 - uri.toString(), 103 - offset, 104 - ); 105 - 106 - if (quickInfo?.displayParts) { 107 - const typeText = quickInfo.displayParts 108 - .map((part: { text: string }) => part.text) 109 - .join(""); 110 - 111 - const propertyMatch = typeText.match( 112 - /\(property\)\s+.*?\.infer:\s*([\s\S]+?)$/, 113 - ); 114 - if (propertyMatch) { 115 - typeInfo = formatTypeString(propertyMatch[1]); 116 - } 117 - } 118 - } 119 - } 120 - } catch (err) { 121 - console.error("Type extraction error:", err); 122 - } 123 - } 124 125 if (result && typeof result === "object" && "json" in result) { 126 const jsonOutput = (result as { json: unknown }).json; 127 setOutput({ 128 json: JSON.stringify(jsonOutput, null, 2) + "\n", 129 - typeInfo, 130 error: "", 131 }); 132 } else { 133 setOutput({ 134 json: JSON.stringify(result, null, 2), 135 - typeInfo, 136 error: "", 137 }); 138 } 139 } catch (error) { 140 setOutput({ 141 json: "", 142 - typeInfo: "", 143 error: error instanceof Error ? error.message : "Unknown error", 144 }); 145 } ··· 206 </div> 207 </> 208 ); 209 - } 210 - 211 - function formatTypeString(typeStr: string): string { 212 - let formatted = typeStr.trim(); 213 - 214 - formatted = formatted.replace(/\s+/g, " "); 215 - formatted = formatted.replace(/;\s*/g, "\n"); 216 - formatted = formatted.replace(/{\s*/g, "{\n"); 217 - formatted = formatted.replace(/\s*}/g, "\n}"); 218 - 219 - const lines = formatted.split("\n"); 220 - let indentLevel = 0; 221 - const indentedLines: string[] = []; 222 - 223 - for (const line of lines) { 224 - const trimmed = line.trim(); 225 - if (!trimmed) continue; 226 - 227 - if (trimmed.startsWith("}")) { 228 - indentLevel = Math.max(0, indentLevel - 1); 229 - } 230 - 231 - indentedLines.push(" ".repeat(indentLevel) + trimmed); 232 - 233 - if (trimmed.endsWith("{") && !trimmed.includes("}")) { 234 - indentLevel++; 235 - } 236 - } 237 - 238 - return indentedLines.join("\n"); 239 } 240 241 function MobileStaticDemo({
··· 1 /* eslint-disable @typescript-eslint/no-unsafe-assignment */ 2 /* eslint-disable @typescript-eslint/no-unsafe-call */ 3 import { useState, useEffect, useRef } from "react"; ··· 18 "code", 19 parseAsCompressed.withDefault(DEFAULT_CODE), 20 ); 21 + const [output, setOutput] = useState({ json: "", error: "" }); 22 const [editorReady, setEditorReady] = useState(false); 23 const [theme, setTheme] = useState<"vs-light" | "vs-dark">( 24 window.matchMedia("(prefers-color-scheme: dark)").matches ··· 68 }, [monaco, editorReady]); 69 70 useEffect(() => { 71 + const timeoutId = setTimeout(() => { 72 try { 73 const nsMatch = /const\s+lex\s*=\s*lx\.lexicon\([^]*?\}\s*\);/.exec( 74 code, ··· 83 const fn = new Function("lx", wrappedCode); 84 85 const result = fn(lx); 86 87 if (result && typeof result === "object" && "json" in result) { 88 const jsonOutput = (result as { json: unknown }).json; 89 setOutput({ 90 json: JSON.stringify(jsonOutput, null, 2) + "\n", 91 error: "", 92 }); 93 } else { 94 setOutput({ 95 json: JSON.stringify(result, null, 2), 96 error: "", 97 }); 98 } 99 } catch (error) { 100 setOutput({ 101 json: "", 102 error: error instanceof Error ? error.message : "Unknown error", 103 }); 104 } ··· 165 </div> 166 </> 167 ); 168 } 169 170 function MobileStaticDemo({
+51 -3
packages/site/src/index.css
··· 1 - * { 2 box-sizing: border-box; 3 margin: 0; 4 - padding: 0; 5 } 6 7 :root { ··· 42 } 43 44 body { 45 - margin: 0; 46 display: flex; 47 min-width: 320px; 48 min-height: 100vh; ··· 95 96 .desktop-only { 97 display: none !important; 98 } 99 }
··· 1 + /* 2 + Josh's Custom CSS Reset 3 + https://www.joshwcomeau.com/css/custom-css-reset/ 4 + */ 5 + *, 6 + *::before, 7 + *::after { 8 box-sizing: border-box; 9 + } 10 + 11 + * { 12 margin: 0; 13 + } 14 + 15 + body { 16 + line-height: 1.5; 17 + -webkit-font-smoothing: antialiased; 18 + } 19 + 20 + img, 21 + picture, 22 + video, 23 + canvas, 24 + svg { 25 + display: block; 26 + max-width: 100%; 27 + } 28 + 29 + input, 30 + button, 31 + textarea, 32 + select { 33 + font: inherit; 34 + } 35 + 36 + p, 37 + h1, 38 + h2, 39 + h3, 40 + h4, 41 + h5, 42 + h6 { 43 + overflow-wrap: break-word; 44 + } 45 + 46 + #root { 47 + isolation: isolate; 48 } 49 50 :root { ··· 85 } 86 87 body { 88 display: flex; 89 min-width: 320px; 90 min-height: 100vh; ··· 137 138 .desktop-only { 139 display: none !important; 140 + } 141 + 142 + .link-container { 143 + width: 100%; 144 + flex-direction: row !important; 145 + justify-content: space-between; 146 } 147 }