Guest User

Untitled

a guest
Jul 2nd, 2026
468
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.29 KB | None | 0 0
  1. // ==UserScript==
  2. // @name 21st.dev Copy Code Helper
  3. // @namespace local.21st-copy-helper
  4. // @version 1.0.0
  5. // @description Copy/download public code files from 21st.dev component pages when the site copy button fails.
  6. // @match https://21st.dev/@*/components/*
  7. // @match https://21st.dev/*/components/*
  8. // @grant GM_xmlhttpRequest
  9. // @grant GM_setClipboard
  10. // @grant GM_registerMenuCommand
  11. // @connect 21st.dev
  12. // @connect cdn.21st.dev
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. "use strict";
  17.  
  18. const state = {
  19. files: null,
  20. loading: null,
  21. };
  22.  
  23. init();
  24.  
  25. function init() {
  26. addPanel();
  27.  
  28. if (typeof GM_registerMenuCommand === "function") {
  29. GM_registerMenuCommand("Copy component code", () => copyKind("component"));
  30. GM_registerMenuCommand("Copy usage/demo code", () => copyKind("usage"));
  31. GM_registerMenuCommand("Copy all 21st code", () => copyKind("all"));
  32. GM_registerMenuCommand("Download all 21st code", downloadAll);
  33. }
  34. }
  35.  
  36. function addPanel() {
  37. if (document.getElementById("twentyfirst-copy-helper")) return;
  38.  
  39. const panel = document.createElement("div");
  40. panel.id = "twentyfirst-copy-helper";
  41. panel.innerHTML = `
  42. <div class="tfch-title">21st code</div>
  43. <button type="button" data-action="copy-component">Copy component</button>
  44. <button type="button" data-action="copy-usage">Copy usage</button>
  45. <button type="button" data-action="copy-all">Copy all</button>
  46. <button type="button" data-action="download">Download</button>
  47. <div class="tfch-status" aria-live="polite"></div>
  48. `;
  49.  
  50. const style = document.createElement("style");
  51. style.textContent = `
  52. #twentyfirst-copy-helper {
  53. position: fixed;
  54. right: 16px;
  55. bottom: 16px;
  56. z-index: 2147483647;
  57. width: 172px;
  58. padding: 10px;
  59. border: 1px solid rgba(120, 120, 120, 0.35);
  60. border-radius: 8px;
  61. background: color-mix(in srgb, Canvas 92%, transparent);
  62. color: CanvasText;
  63. box-shadow: 0 12px 32px rgba(0, 0, 0, 0.16);
  64. backdrop-filter: blur(10px);
  65. font: 12px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  66. }
  67.  
  68. #twentyfirst-copy-helper .tfch-title {
  69. margin: 0 0 8px;
  70. font-weight: 700;
  71. }
  72.  
  73. #twentyfirst-copy-helper button {
  74. display: block;
  75. width: 100%;
  76. margin: 6px 0 0;
  77. padding: 7px 8px;
  78. border: 1px solid rgba(120, 120, 120, 0.32);
  79. border-radius: 6px;
  80. background: ButtonFace;
  81. color: ButtonText;
  82. cursor: pointer;
  83. font: inherit;
  84. text-align: left;
  85. }
  86.  
  87. #twentyfirst-copy-helper button:hover {
  88. filter: brightness(0.96);
  89. }
  90.  
  91. #twentyfirst-copy-helper .tfch-status {
  92. min-height: 16px;
  93. margin-top: 8px;
  94. color: color-mix(in srgb, CanvasText 68%, transparent);
  95. }
  96. `;
  97.  
  98. document.documentElement.append(style, panel);
  99.  
  100. panel.addEventListener("click", (event) => {
  101. const button = event.target.closest("button[data-action]");
  102. if (!button) return;
  103.  
  104. const action = button.dataset.action;
  105. if (action === "copy-component") copyKind("component");
  106. if (action === "copy-usage") copyKind("usage");
  107. if (action === "copy-all") copyKind("all");
  108. if (action === "download") downloadAll();
  109. });
  110. }
  111.  
  112. async function copyKind(kind) {
  113. try {
  114. setStatus("Loading...");
  115. const files = await getFiles();
  116. const selected = files.filter((file) => kind === "all" || file.kind === kind);
  117.  
  118. if (!selected.length) {
  119. throw new Error(`No ${kind} code found`);
  120. }
  121.  
  122. const text = selected
  123. .map((file) => `// ${file.name}\n${file.content.trimEnd()}\n`)
  124. .join("\n");
  125.  
  126. await writeClipboard(text);
  127. setStatus(`Copied ${kind}`);
  128. } catch (error) {
  129. setStatus(error.message || "Copy failed");
  130. console.error("[21st copy helper]", error);
  131. }
  132. }
  133.  
  134. async function downloadAll() {
  135. try {
  136. setStatus("Loading...");
  137. const files = await getFiles();
  138.  
  139. if (!files.length) {
  140. throw new Error("No code files found");
  141. }
  142.  
  143. for (const file of files) {
  144. downloadText(file.name, file.content);
  145. }
  146.  
  147. setStatus(`Downloaded ${files.length} files`);
  148. } catch (error) {
  149. setStatus(error.message || "Download failed");
  150. console.error("[21st copy helper]", error);
  151. }
  152. }
  153.  
  154. async function getFiles() {
  155. if (state.files) return state.files;
  156. if (state.loading) return state.loading;
  157.  
  158. state.loading = loadFiles();
  159. state.files = await state.loading;
  160. state.loading = null;
  161. return state.files;
  162. }
  163.  
  164. async function loadFiles() {
  165. const html = await requestText(location.href);
  166. const urls = extractPublicUrls(`${document.documentElement.innerHTML}\n${html}`);
  167. const registryUrl = urls.find((url) => url.endsWith(".json") && url.includes("/registry."));
  168. const componentUrl = urls.find((url) => /\/code\.[^/]+\.tsx$/.test(url));
  169. const usageUrl = urls.find((url) => /\/code\.demo\.[^/]+\.tsx$/.test(url));
  170. const files = [];
  171.  
  172. if (registryUrl) {
  173. const registry = JSON.parse(await requestText(registryUrl));
  174. for (const file of registry.files || []) {
  175. if (!file.path || typeof file.content !== "string") continue;
  176. files.push({
  177. kind: "component",
  178. name: basename(file.path),
  179. content: file.content,
  180. });
  181. }
  182. }
  183.  
  184. if (componentUrl && !files.some((file) => file.kind === "component")) {
  185. files.push({
  186. kind: "component",
  187. name: "component.tsx",
  188. content: await requestText(componentUrl),
  189. });
  190. }
  191.  
  192. if (usageUrl) {
  193. files.push({
  194. kind: "usage",
  195. name: "usage.tsx",
  196. content: await requestText(usageUrl),
  197. });
  198. }
  199.  
  200. if (!files.length) {
  201. throw new Error("No public CDN code URLs found");
  202. }
  203.  
  204. return files;
  205. }
  206.  
  207. function extractPublicUrls(text) {
  208. const normalized = text
  209. .replaceAll("\\/", "/")
  210. .replaceAll("\\u002F", "/")
  211. .replaceAll("&amp;", "&");
  212.  
  213. return Array.from(
  214. new Set(
  215. [...normalized.matchAll(/https:\/\/cdn\.21st\.dev\/[^"'<>\\\s]+/g)]
  216. .map(([match]) => match.replace(/\\+$/, ""))
  217. .filter((url) => /\/(code(\.demo)?|registry)\.[^/]+\.(tsx|json)$/.test(url)),
  218. ),
  219. );
  220. }
  221.  
  222. function requestText(url) {
  223. if (typeof GM_xmlhttpRequest === "function") {
  224. return new Promise((resolve, reject) => {
  225. GM_xmlhttpRequest({
  226. method: "GET",
  227. url,
  228. headers: {
  229. accept: "text/html,application/json,text/plain,*/*",
  230. },
  231. onload: (response) => {
  232. if (response.status >= 200 && response.status < 300) {
  233. resolve(response.responseText);
  234. } else {
  235. reject(new Error(`HTTP ${response.status}: ${url}`));
  236. }
  237. },
  238. onerror: () => reject(new Error(`Request failed: ${url}`)),
  239. });
  240. });
  241. }
  242.  
  243. return fetch(url).then((response) => {
  244. if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
  245. return response.text();
  246. });
  247. }
  248.  
  249. async function writeClipboard(text) {
  250. if (typeof GM_setClipboard === "function") {
  251. GM_setClipboard(text, "text");
  252. return;
  253. }
  254.  
  255. await navigator.clipboard.writeText(text);
  256. }
  257.  
  258. function downloadText(name, content) {
  259. const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
  260. const link = document.createElement("a");
  261. link.href = URL.createObjectURL(blob);
  262. link.download = name;
  263. link.style.display = "none";
  264. document.body.append(link);
  265. link.click();
  266. setTimeout(() => {
  267. URL.revokeObjectURL(link.href);
  268. link.remove();
  269. }, 1000);
  270. }
  271.  
  272. function basename(path) {
  273. return path.split("/").pop() || "component.tsx";
  274. }
  275.  
  276. function setStatus(message) {
  277. const status = document.querySelector("#twentyfirst-copy-helper .tfch-status");
  278. if (status) status.textContent = message;
  279. }
  280. })();
  281.  
Advertisement
Add Comment
Please, Sign In to add comment