Skip to content

Add roblox-creator-docs extension #20024

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

Merged
merged 5 commits into from
Jun 30, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions extensions/roblox-creator-docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules

# Raycast specific files
raycast-env.d.ts
.raycast-swift-build
.swiftpm
compiled_raycast_swift

# misc
.DS_Store
4 changes: 4 additions & 0 deletions extensions/roblox-creator-docs/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"printWidth": 120,
"singleQuote": false
}
3 changes: 3 additions & 0 deletions extensions/roblox-creator-docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Roblox Creator Docs Changelog

## [Initial Version] - 2025-06-30
3 changes: 3 additions & 0 deletions extensions/roblox-creator-docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Roblox Creator Docs

Fast look-up for Roblox Creator Docs
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions extensions/roblox-creator-docs/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const { defineConfig } = require("eslint/config");
const raycastConfig = require("@raycast/eslint-config");

module.exports = defineConfig([
...raycastConfig,
]);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3,674 changes: 3,674 additions & 0 deletions extensions/roblox-creator-docs/package-lock.json

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions extensions/roblox-creator-docs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "roblox-creator-docs",
"title": "Roblox Creator Docs",
"description": "Fast look-up for Roblox Creator Docs",
"icon": "extension-icon.png",
"author": "ben_rowlands",
"license": "MIT",
"commands": [
{
"name": "creator-docs",
"title": "Roblox Creator Docs",
"subtitle": "Search Creator Docs",
"description": "Browse and open Roblox Creator Docs in default browser",
"mode": "view"
}
],
"dependencies": {
"@raycast/api": "^1.100.3",
"@raycast/utils": "^1.17.0",
"react": "^18.2.0",
"node-fetch": "^2.6.7",
"jszip": "^3.10.1",
"js-yaml": "^4.1.0"
},
"devDependencies": {
"@raycast/eslint-config": "^2.0.4",
"@types/node": "22.13.10",
"@types/node-fetch": "^2.6.2",
"@types/react": "19.0.10",
"@types/js-yaml": "^4.0.5",
"eslint": "^9.22.0",
"prettier": "^3.5.3",
"typescript": "^5.8.2"
},
"scripts": {
"build": "ray build",
"dev": "node --max-old-space-size=4096 $(which ray) develop",
"fix-lint": "ray lint --fix",
"lint": "ray lint",
"prepublishOnly": "echo \"\\n\\nIt seems like you are trying to publish the Raycast extension to npm.\\n\\nIf you did intend to publish it to npm, remove the \\`prepublishOnly\\` script and rerun \\`npm publish\\` again.\\nIf you wanted to publish it to the Raycast Store instead, use \\`npm run publish\\` instead.\\n\\n\" && exit 1",
"publish": "npx @raycast/api@latest publish"
}
}
348 changes: 348 additions & 0 deletions extensions/roblox-creator-docs/src/creator-docs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
import { Action, ActionPanel, List, open, showToast, Toast } from "@raycast/api";
import { useState, useEffect } from "react";
import RobloxDocsDataFetcher, { DocItem } from "./data-fetcher";

export default function Command() {
const [searchText, setSearchText] = useState("");
const [filteredDocs, setFilteredDocs] = useState<DocItem[]>([]);
const [allDocs, setAllDocs] = useState<DocItem[]>([]);
const [isLoading, setIsLoading] = useState(true);

// Performance optimization: limit displayed results
const MAX_DISPLAYED_RESULTS = 50;

// Initialize data fetcher
const dataFetcher = new RobloxDocsDataFetcher();

useEffect(() => {
loadDocsData();
}, []);

useEffect(() => {
if (searchText.trim() === "") {
// When no search term, show only first items to avoid memory issues
setFilteredDocs(allDocs.slice(0, MAX_DISPLAYED_RESULTS));
} else {
const searchLower = searchText.toLowerCase();

// Filter and score results for relevance
const scoredResults = allDocs
.map((doc) => {
let score = 0;
const titleLower = doc.title.toLowerCase();
const descLower = doc.description.toLowerCase();

// Exact title match gets highest score
if (titleLower === searchLower) score += 100;
// Title starts with search term
else if (titleLower.startsWith(searchLower)) score += 50;
// Title contains search term
else if (titleLower.includes(searchLower)) score += 25;

// Description matches
if (descLower.includes(searchLower)) score += 10;

// Keyword matches
if (doc.keywords.some((keyword) => keyword.toLowerCase().includes(searchLower))) score += 15;

// Category/type matches
if (doc.category.toLowerCase().includes(searchLower)) score += 5;
if (doc.type.toLowerCase().includes(searchLower)) score += 5;

return { doc, score };
})
.filter((item) => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, MAX_DISPLAYED_RESULTS)
.map((item) => item.doc);

setFilteredDocs(scoredResults);
}
}, [searchText, allDocs, MAX_DISPLAYED_RESULTS]);

const loadDocsData = async () => {
try {
setIsLoading(true);
showToast({
style: Toast.Style.Animated,
title: "Loading Roblox Creator Docs...",
message: "Checking for updates...",
});

const docs = await dataFetcher.fetchDocsData();
setAllDocs(docs);
setFilteredDocs(docs.slice(0, MAX_DISPLAYED_RESULTS));

showToast({
style: Toast.Style.Success,
title: "Docs Loaded Successfully",
message: `Found ${docs.length} documentation pages`,
});
} catch (error) {
console.error("Error loading docs data:", error);
showToast({
style: Toast.Style.Failure,
title: "Failed to Load Docs",
message: "Using fallback data. Check your internet connection.",
});
} finally {
setIsLoading(false);
}
};

const clearCacheAndRefresh = async () => {
try {
showToast({
style: Toast.Style.Animated,
title: "Clearing Cache...",
message: "Forcing fresh data fetch",
});

// Clear the cache
dataFetcher.clearCache();

// Reload data
await loadDocsData();
} catch (error) {
console.error("Error clearing cache:", error);
showToast({
style: Toast.Style.Failure,
title: "Failed to Clear Cache",
message: "Please try again",
});
}
};

const getIconForType = (type: DocItem["type"], title?: string) => {
// Apply smart icon detection for classes, references, and guides
// since Roblox class docs can be categorized under any of these types
if (title && (type === "class" || type === "reference" || type === "guide")) {
const classIcon = getClassIcon(title);
// Only use class icon if it's not the default, otherwise fall back to type icon
if (classIcon !== "⚙️") {
return classIcon;
}
}

switch (type) {
case "class":
return "⚙️";
case "service":
return "🔧";
case "tutorial":
return "📚";
case "reference":
return "📖";
case "guide":
return "📝";
case "enum":
return "🔢";
case "global":
return "🌐";
case "property":
return "🔶";
case "method":
return "🔵";
case "event":
return "⚡";
case "callback":
return "🔄";
case "function":
return "🟢";
default:
return "📄";
}
};

const getClassIcon = (className: string) => {
const name = className.toLowerCase();

// Audio/Sound classes
if (name.includes("audio") || name.includes("sound") || name.includes("music")) {
return "🔊";
}

// UI/GUI classes
if (
name.includes("gui") ||
name.includes("frame") ||
name.includes("button") ||
name.includes("label") ||
name.includes("textbox") ||
name.includes("screen")
) {
return "🖥️";
}

// Parts and physical objects
if (
name.includes("part") ||
name.includes("mesh") ||
name.includes("union") ||
name.includes("wedge") ||
name.includes("cylinder") ||
name.includes("sphere")
) {
return "🧊";
}

// Lighting and visual effects
if (
name.includes("light") ||
name.includes("effect") ||
name.includes("beam") ||
name.includes("fire") ||
name.includes("smoke") ||
name.includes("sparkles")
) {
return "💡";
}

// Animation classes
if (
name.includes("animation") ||
name.includes("keyframe") ||
name.includes("pose") ||
name.includes("motor") ||
name.includes("joint")
) {
return "🎭";
}

// Camera and rendering
if (name.includes("camera") || name.includes("viewport")) {
return "📷";
}

// Player and character related
if (
name.includes("player") ||
name.includes("humanoid") ||
name.includes("character") ||
name.includes("backpack") ||
name.includes("starter")
) {
return "👤";
}

// Workspace and game structure
if (
name.includes("workspace") ||
name.includes("folder") ||
name.includes("model") ||
name.includes("configuration")
) {
return "📁";
}

// Physics and forces
if (
name.includes("body") ||
name.includes("force") ||
name.includes("velocity") ||
name.includes("position") ||
name.includes("attachment")
) {
return "⚡";
}

// Input and controls
if (
name.includes("input") ||
name.includes("mouse") ||
name.includes("keyboard") ||
name.includes("touch") ||
name.includes("gamepad")
) {
return "🎮";
}

// Network and communication
if (
name.includes("remote") ||
name.includes("event") ||
name.includes("function") ||
name.includes("bindable") ||
name.includes("http")
) {
return "📡";
}

// Script and programming
if (name.includes("script") || name.includes("module") || name.includes("local")) {
return "📜";
}

// Default class icon
return "⚙️";
};

return (
<List
searchText={searchText}
onSearchTextChange={setSearchText}
searchBarPlaceholder="Search Roblox Creator Docs..."
isLoading={isLoading}
throttle
>
<List.Section
title="Results"
subtitle={
filteredDocs.length >= MAX_DISPLAYED_RESULTS
? `${filteredDocs.length}+ docs (showing top ${MAX_DISPLAYED_RESULTS})`
: `${filteredDocs.length} docs from ${allDocs.length} total`
}
>
{filteredDocs.map((doc) => (
<List.Item
key={doc.id}
icon={getIconForType(doc.type, doc.title)}
title={doc.title}
subtitle={doc.category}
accessories={[
{ text: doc.type },
{ text: doc.description.length > 50 ? doc.description.substring(0, 47) + "..." : doc.description },
]}
actions={
<ActionPanel>
<Action title="Open in Browser" onAction={() => open(doc.url)} icon="🌐" />
<Action.CopyToClipboard
title="Copy URL"
content={doc.url}
shortcut={{ modifiers: ["cmd"], key: "c" }}
icon="📋"
/>
<Action.CopyToClipboard
title="Copy Title"
content={doc.title}
shortcut={{ modifiers: ["cmd", "shift"], key: "c" }}
icon="📝"
/>
<Action
title="Refresh Data"
onAction={loadDocsData}
shortcut={{ modifiers: ["cmd"], key: "r" }}
icon="🔄"
/>
<Action
title="Clear Cache & Refresh"
onAction={clearCacheAndRefresh}
shortcut={{ modifiers: ["cmd", "shift"], key: "r" }}
icon="🗑️"
/>
</ActionPanel>
}
/>
))}
</List.Section>

{filteredDocs.length === 0 && !isLoading && (
<List.EmptyView
icon="../assets/no-results.png"
title="No Results Found"
description={`No documentation found for "${searchText}". Try a different search term.`}
/>
)}
</List>
);
}
488 changes: 488 additions & 0 deletions extensions/roblox-creator-docs/src/data-fetcher.ts

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions extensions/roblox-creator-docs/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"include": ["src/**/*", "raycast-env.d.ts"],
"compilerOptions": {
"lib": ["ES2023"],
"module": "commonjs",
"target": "ES2023",
"strict": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"jsx": "react-jsx",
"resolveJsonModule": true
}
}