-
Notifications
You must be signed in to change notification settings - Fork 5.9k
Add designlang extension #27449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add designlang extension #27449
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| # Designlang Changelog | ||
|
|
||
| ## [Initial Release] - {PR_MERGE_DATE} | ||
|
|
||
| - **Extract Design From URL** — runs `npx designlang <url>` and opens the output folder when ready. Optional "full" toggle pulls screenshots, responsive captures, and interaction states. | ||
| - **Score Website Design** — rates a site's design-system quality across 7 categories (color discipline, typography, spacing, shadows, radii, accessibility, tokenization) and reports an overall A–F grade. | ||
| - **Copy CLI Command For URL** — accepts a URL argument and copies `npx designlang <url>` to the clipboard. | ||
| - One preference: `outputDir` (default `~/designlang-output`). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # designlang for Raycast | ||
|
|
||
| Three commands: | ||
|
|
||
| - **Extract Design From URL** — runs `designlang <url>` and opens the output folder. | ||
| - **Score Website Design** — shows the 7-category design-system score in a Raycast detail view. | ||
| - **Copy CLI Command For URL** — `npx designlang <url>` → clipboard. | ||
|
|
||
| ## Requirements | ||
|
|
||
| - Node.js 20+ | ||
| - `npx` on PATH | ||
|
|
||
| ## Publish | ||
|
|
||
| ```bash | ||
| cd raycast-extension | ||
| npm install | ||
| npm run build | ||
| npm run publish | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| { | ||
| "$schema": "https://www.raycast.com/schemas/extension.json", | ||
| "name": "designlang", | ||
| "title": "Designlang", | ||
| "description": "Extract design tokens from any website — colors, typography, spacing, shadows, shadcn/Tailwind/Figma outputs.", | ||
| "icon": "icon.png", | ||
| "author": "masyv", | ||
| "license": "MIT", | ||
| "categories": ["Design Tools", "Developer Tools"], | ||
| "keywords": ["design", "tokens", "css", "tailwind", "figma", "shadcn"], | ||
| "commands": [ | ||
| { | ||
| "name": "extract", | ||
| "title": "Extract Design From URL", | ||
| "description": "Run designlang on a URL and open the output folder.", | ||
| "mode": "view" | ||
| }, | ||
| { | ||
| "name": "score", | ||
| "title": "Score Website Design", | ||
| "description": "Rate a site's design-system quality (7 categories, A–F).", | ||
| "mode": "view" | ||
| }, | ||
| { | ||
| "name": "copy-cli", | ||
| "title": "Copy CLI Command For URL", | ||
| "description": "Copy `npx designlang <url>` to your clipboard.", | ||
| "mode": "no-view", | ||
| "arguments": [ | ||
| { | ||
| "name": "url", | ||
| "placeholder": "https://stripe.com", | ||
| "type": "text", | ||
| "required": true | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "preferences": [ | ||
| { | ||
| "name": "outputDir", | ||
| "type": "textfield", | ||
| "title": "Output directory", | ||
| "description": "Where extracted files are written.", | ||
| "default": "~/designlang-output", | ||
| "required": false | ||
| } | ||
| ], | ||
| "dependencies": { | ||
| "@raycast/api": "^1.80.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@raycast/eslint-config": "^1.0.0", | ||
| "@types/node": "^20.0.0", | ||
| "@types/react": "^18.3.0", | ||
| "eslint": "^8.57.0", | ||
| "prettier": "^3.3.0", | ||
| "typescript": "^5.5.0" | ||
| }, | ||
| "scripts": { | ||
| "build": "ray build -e dist", | ||
| "dev": "ray develop", | ||
| "lint": "ray lint", | ||
| "publish": "ray publish" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { Clipboard, LaunchProps, showHUD, showToast, Toast } from "@raycast/api"; | ||
|
|
||
| export default async function Command(props: LaunchProps<{ arguments: { url?: string } }>) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Add an {
"name": "copy-cli",
"title": "Copy CLI Command For URL",
"description": "Copy `npx designlang <url>` to your clipboard.",
"mode": "no-view",
"arguments": [
{
"name": "url",
"placeholder": "https://stripe.com",
"type": "text",
"required": true
}
]
}Prompt To Fix With AIThis is a comment left during a code review.
Path: extensions/designlang/src/copy-cli.tsx
Line: 3
Comment:
**URL argument not declared — command always fails**
`props.arguments?.url` is accessed here, but the `copy-cli` command has no `arguments` entry in `package.json`. Raycast only populates `props.arguments` for arguments explicitly declared in the manifest, so `props.arguments?.url` will always be `undefined`, the guard on line 5 will always fire, and the command will always toast "No URL provided" — it can never actually copy anything.
Add an `arguments` block to the `copy-cli` command in `package.json`:
```json
{
"name": "copy-cli",
"title": "Copy CLI Command For URL",
"description": "Copy `npx designlang <url>` to your clipboard.",
"mode": "no-view",
"arguments": [
{
"name": "url",
"placeholder": "https://stripe.com",
"type": "text",
"required": true
}
]
}
```
How can I resolve this? If you propose a fix, please make it concise. |
||
| const rawUrl = (props.arguments?.url || "").trim(); | ||
| if (!rawUrl) { | ||
| await showToast({ style: Toast.Style.Failure, title: "No URL provided" }); | ||
| return; | ||
| } | ||
| const url = rawUrl.startsWith("http") ? rawUrl : `https://${rawUrl}`; | ||
| await Clipboard.copy(`npx designlang ${url}`); | ||
| await showHUD("Copied `npx designlang …` to clipboard"); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import { | ||
| Action, | ||
| ActionPanel, | ||
| Form, | ||
| Toast, | ||
| getPreferenceValues, | ||
| open, | ||
| showToast, | ||
| } from "@raycast/api"; | ||
| import { execFile } from "child_process"; | ||
| import { homedir } from "os"; | ||
| import { useState } from "react"; | ||
|
|
||
| function resolveOutputDir(raw?: string) { | ||
| const v = (raw || "~/designlang-output").trim(); | ||
| return v.startsWith("~") ? v.replace(/^~/, homedir()) : v; | ||
| } | ||
|
|
||
| function normalizeUrl(u: string) { | ||
| const t = u.trim(); | ||
| return t.startsWith("http") ? t : `https://${t}`; | ||
| } | ||
|
|
||
| export default function Command() { | ||
| const prefs = getPreferenceValues<Preferences.Extract>(); | ||
| const [url, setUrl] = useState(""); | ||
| const [full, setFull] = useState(false); | ||
|
|
||
| async function run(values: { url: string; full: boolean }) { | ||
| const outDir = resolveOutputDir(prefs.outputDir); | ||
| const args = [ | ||
| "-y", | ||
| "designlang", | ||
| normalizeUrl(values.url), | ||
| "--out", | ||
| outDir, | ||
| ]; | ||
| if (values.full) args.push("--full"); | ||
|
|
||
| const toast = await showToast({ | ||
| style: Toast.Style.Animated, | ||
| title: "designlang: extracting...", | ||
| }); | ||
|
|
||
| execFile("npx", args, { maxBuffer: 50 * 1024 * 1024 }, async (err, stdout, stderr) => { | ||
| if (err) { | ||
| toast.style = Toast.Style.Failure; | ||
| toast.title = "Extraction failed"; | ||
| toast.message = (stderr || err.message).slice(0, 200); | ||
| return; | ||
| } | ||
| toast.style = Toast.Style.Success; | ||
| toast.title = "Done"; | ||
| toast.message = outDir; | ||
| toast.primaryAction = { title: "Open folder", onAction: () => open(outDir) }; | ||
| await open(outDir); | ||
| }); | ||
| } | ||
|
|
||
| return ( | ||
| <Form | ||
| actions={ | ||
| <ActionPanel> | ||
| <Action.SubmitForm title="Extract Design" onSubmit={run} /> | ||
| </ActionPanel> | ||
| } | ||
| > | ||
| <Form.TextField | ||
| id="url" | ||
| title="URL" | ||
| placeholder="https://stripe.com" | ||
| value={url} | ||
| onChange={setUrl} | ||
| /> | ||
| <Form.Checkbox id="full" label="Full extraction (screenshots + responsive + interactions)" value={full} onChange={setFull} /> | ||
| <Form.Description text="Runs `npx designlang <url>` and opens the output folder when it finishes." /> | ||
| </Form> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import { Action, ActionPanel, Detail, Form, Toast, showToast } from "@raycast/api"; | ||
| import { execFile } from "child_process"; | ||
| import { useState } from "react"; | ||
|
|
||
| function normalizeUrl(u: string) { | ||
| const t = u.trim(); | ||
| return t.startsWith("http") ? t : `https://${t}`; | ||
| } | ||
|
|
||
| export default function Command() { | ||
| const [url, setUrl] = useState(""); | ||
| const [result, setResult] = useState<string | null>(null); | ||
|
|
||
| async function run(values: { url: string }) { | ||
| const toast = await showToast({ style: Toast.Style.Animated, title: "designlang: scoring..." }); | ||
| execFile( | ||
| "npx", | ||
| ["-y", "designlang", "score", normalizeUrl(values.url)], | ||
| { maxBuffer: 20 * 1024 * 1024 }, | ||
| (err, stdout, stderr) => { | ||
| if (err) { | ||
| toast.style = Toast.Style.Failure; | ||
| toast.title = "Scoring failed"; | ||
| toast.message = (stderr || err.message).slice(0, 200); | ||
| return; | ||
| } | ||
| toast.style = Toast.Style.Success; | ||
| toast.title = "Scored"; | ||
| // Strip ANSI control codes | ||
| setResult("```\n" + stdout.replace(/\u001b\[[0-9;]*m/g, "") + "\n```"); | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| if (result) { | ||
| return <Detail markdown={result} />; | ||
| } | ||
|
|
||
| return ( | ||
| <Form | ||
| actions={ | ||
| <ActionPanel> | ||
| <Action.SubmitForm title="Score Design" onSubmit={run} /> | ||
| </ActionPanel> | ||
| } | ||
| > | ||
| <Form.TextField id="url" title="URL" placeholder="https://linear.app" value={url} onChange={setUrl} /> | ||
| </Form> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "$schema": "https://json.schemastore.org/tsconfig", | ||
| "include": ["src/**/*"], | ||
| "compilerOptions": { | ||
| "lib": ["ES2023"], | ||
| "module": "commonjs", | ||
| "target": "ES2022", | ||
| "strict": true, | ||
| "isolatedModules": true, | ||
| "esModuleInterop": true, | ||
| "jsx": "react-jsx", | ||
| "resolveJsonModule": true, | ||
| "noEmit": true | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
metadata/folder with screenshotsBoth
extractandscorearemode: "view"commands. Per the Raycast store guidelines, every extension with view-type commands must ship ametadata/folder containing Raycast-styled screenshots (1600×1000 PNG). Without this folder the extension will be rejected during store review.Rule Used: What: Extensions with view-type commands must incl... (source)
Prompt To Fix With AI