Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name 21st.dev Copy Code Helper
- // @namespace local.21st-copy-helper
- // @version 1.0.0
- // @description Copy/download public code files from 21st.dev component pages when the site copy button fails.
- // @match https://21st.dev/@*/components/*
- // @match https://21st.dev/*/components/*
- // @grant GM_xmlhttpRequest
- // @grant GM_setClipboard
- // @grant GM_registerMenuCommand
- // @connect 21st.dev
- // @connect cdn.21st.dev
- // ==/UserScript==
- (function () {
- "use strict";
- const state = {
- files: null,
- loading: null,
- };
- init();
- function init() {
- addPanel();
- if (typeof GM_registerMenuCommand === "function") {
- GM_registerMenuCommand("Copy component code", () => copyKind("component"));
- GM_registerMenuCommand("Copy usage/demo code", () => copyKind("usage"));
- GM_registerMenuCommand("Copy all 21st code", () => copyKind("all"));
- GM_registerMenuCommand("Download all 21st code", downloadAll);
- }
- }
- function addPanel() {
- if (document.getElementById("twentyfirst-copy-helper")) return;
- const panel = document.createElement("div");
- panel.id = "twentyfirst-copy-helper";
- panel.innerHTML = `
- <div class="tfch-title">21st code</div>
- <button type="button" data-action="copy-component">Copy component</button>
- <button type="button" data-action="copy-usage">Copy usage</button>
- <button type="button" data-action="copy-all">Copy all</button>
- <button type="button" data-action="download">Download</button>
- <div class="tfch-status" aria-live="polite"></div>
- `;
- const style = document.createElement("style");
- style.textContent = `
- #twentyfirst-copy-helper {
- position: fixed;
- right: 16px;
- bottom: 16px;
- z-index: 2147483647;
- width: 172px;
- padding: 10px;
- border: 1px solid rgba(120, 120, 120, 0.35);
- border-radius: 8px;
- background: color-mix(in srgb, Canvas 92%, transparent);
- color: CanvasText;
- box-shadow: 0 12px 32px rgba(0, 0, 0, 0.16);
- backdrop-filter: blur(10px);
- font: 12px/1.35 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
- }
- #twentyfirst-copy-helper .tfch-title {
- margin: 0 0 8px;
- font-weight: 700;
- }
- #twentyfirst-copy-helper button {
- display: block;
- width: 100%;
- margin: 6px 0 0;
- padding: 7px 8px;
- border: 1px solid rgba(120, 120, 120, 0.32);
- border-radius: 6px;
- background: ButtonFace;
- color: ButtonText;
- cursor: pointer;
- font: inherit;
- text-align: left;
- }
- #twentyfirst-copy-helper button:hover {
- filter: brightness(0.96);
- }
- #twentyfirst-copy-helper .tfch-status {
- min-height: 16px;
- margin-top: 8px;
- color: color-mix(in srgb, CanvasText 68%, transparent);
- }
- `;
- document.documentElement.append(style, panel);
- panel.addEventListener("click", (event) => {
- const button = event.target.closest("button[data-action]");
- if (!button) return;
- const action = button.dataset.action;
- if (action === "copy-component") copyKind("component");
- if (action === "copy-usage") copyKind("usage");
- if (action === "copy-all") copyKind("all");
- if (action === "download") downloadAll();
- });
- }
- async function copyKind(kind) {
- try {
- setStatus("Loading...");
- const files = await getFiles();
- const selected = files.filter((file) => kind === "all" || file.kind === kind);
- if (!selected.length) {
- throw new Error(`No ${kind} code found`);
- }
- const text = selected
- .map((file) => `// ${file.name}\n${file.content.trimEnd()}\n`)
- .join("\n");
- await writeClipboard(text);
- setStatus(`Copied ${kind}`);
- } catch (error) {
- setStatus(error.message || "Copy failed");
- console.error("[21st copy helper]", error);
- }
- }
- async function downloadAll() {
- try {
- setStatus("Loading...");
- const files = await getFiles();
- if (!files.length) {
- throw new Error("No code files found");
- }
- for (const file of files) {
- downloadText(file.name, file.content);
- }
- setStatus(`Downloaded ${files.length} files`);
- } catch (error) {
- setStatus(error.message || "Download failed");
- console.error("[21st copy helper]", error);
- }
- }
- async function getFiles() {
- if (state.files) return state.files;
- if (state.loading) return state.loading;
- state.loading = loadFiles();
- state.files = await state.loading;
- state.loading = null;
- return state.files;
- }
- async function loadFiles() {
- const html = await requestText(location.href);
- const urls = extractPublicUrls(`${document.documentElement.innerHTML}\n${html}`);
- const registryUrl = urls.find((url) => url.endsWith(".json") && url.includes("/registry."));
- const componentUrl = urls.find((url) => /\/code\.[^/]+\.tsx$/.test(url));
- const usageUrl = urls.find((url) => /\/code\.demo\.[^/]+\.tsx$/.test(url));
- const files = [];
- if (registryUrl) {
- const registry = JSON.parse(await requestText(registryUrl));
- for (const file of registry.files || []) {
- if (!file.path || typeof file.content !== "string") continue;
- files.push({
- kind: "component",
- name: basename(file.path),
- content: file.content,
- });
- }
- }
- if (componentUrl && !files.some((file) => file.kind === "component")) {
- files.push({
- kind: "component",
- name: "component.tsx",
- content: await requestText(componentUrl),
- });
- }
- if (usageUrl) {
- files.push({
- kind: "usage",
- name: "usage.tsx",
- content: await requestText(usageUrl),
- });
- }
- if (!files.length) {
- throw new Error("No public CDN code URLs found");
- }
- return files;
- }
- function extractPublicUrls(text) {
- const normalized = text
- .replaceAll("\\/", "/")
- .replaceAll("\\u002F", "/")
- .replaceAll("&", "&");
- return Array.from(
- new Set(
- [...normalized.matchAll(/https:\/\/cdn\.21st\.dev\/[^"'<>\\\s]+/g)]
- .map(([match]) => match.replace(/\\+$/, ""))
- .filter((url) => /\/(code(\.demo)?|registry)\.[^/]+\.(tsx|json)$/.test(url)),
- ),
- );
- }
- function requestText(url) {
- if (typeof GM_xmlhttpRequest === "function") {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url,
- headers: {
- accept: "text/html,application/json,text/plain,*/*",
- },
- onload: (response) => {
- if (response.status >= 200 && response.status < 300) {
- resolve(response.responseText);
- } else {
- reject(new Error(`HTTP ${response.status}: ${url}`));
- }
- },
- onerror: () => reject(new Error(`Request failed: ${url}`)),
- });
- });
- }
- return fetch(url).then((response) => {
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${url}`);
- return response.text();
- });
- }
- async function writeClipboard(text) {
- if (typeof GM_setClipboard === "function") {
- GM_setClipboard(text, "text");
- return;
- }
- await navigator.clipboard.writeText(text);
- }
- function downloadText(name, content) {
- const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
- const link = document.createElement("a");
- link.href = URL.createObjectURL(blob);
- link.download = name;
- link.style.display = "none";
- document.body.append(link);
- link.click();
- setTimeout(() => {
- URL.revokeObjectURL(link.href);
- link.remove();
- }, 1000);
- }
- function basename(path) {
- return path.split("/").pop() || "component.tsx";
- }
- function setStatus(message) {
- const status = document.querySelector("#twentyfirst-copy-helper .tfch-status");
- if (status) status.textContent = message;
- }
- })();
Advertisement
Add Comment
Please, Sign In to add comment