Initial Docs

This commit is contained in:
Sovereignty 2025-06-13 18:13:43 +02:00
commit f24fd7cb96
31 changed files with 2858 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

51
App.tsx Normal file
View file

@ -0,0 +1,51 @@
import React from 'react';
import { MemoryRouter, Routes, Route, Link, NavLink } from 'react-router-dom'; // Changed HashRouter to MemoryRouter
import { Sidebar } from './components/Sidebar';
import { HomePage } from './pages/HomePage';
import { IntroductionPage } from './pages/IntroductionPage';
import { CoreConceptsPage } from './pages/CoreConceptsPage';
import { ValuePage } from './pages/api/ValuePage';
import { TablePage } from './pages/api/TablePage';
import { MapPage } from './pages/api/MapPage';
import { ComputedPage } from './pages/api/ComputedPage';
import { ObserverPage } from './pages/api/ObserverPage';
import { WatchPage } from './pages/api/WatchPage';
import { EffectPage } from './pages/api/EffectPage';
import { ReactionPage } from './pages/api/ReactionPage';
import { ReactorPage } from './pages/api/ReactorPage';
import { ComposePage } from './pages/api/ComposePage';
import { UtilitiesPage } from './pages/api/UtilitiesPage';
import { SymbolsPage } from './pages/api/SymbolsPage';
import { NAV_LINKS } from './constants';
const App: React.FC = () => {
return (
<MemoryRouter> {/* Changed HashRouter to MemoryRouter */}
<div className="flex h-screen bg-gray-900 text-gray-100">
<Sidebar navLinks={NAV_LINKS} />
<main className="flex-1 p-6 sm:p-8 overflow-y-auto">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/introduction" element={<IntroductionPage />} />
<Route path="/core-concepts" element={<CoreConceptsPage />} />
<Route path="/api/value" element={<ValuePage />} />
<Route path="/api/table" element={<TablePage />} />
<Route path="/api/map" element={<MapPage />} />
<Route path="/api/computed" element={<ComputedPage />} />
<Route path="/api/observer" element={<ObserverPage />} />
<Route path="/api/watch" element={<WatchPage />} />
<Route path="/api/effect" element={<EffectPage />} />
<Route path="/api/reaction" element={<ReactionPage />} />
<Route path="/api/reactor" element={<ReactorPage />} />
<Route path="/api/compose" element={<ComposePage />} />
<Route path="/api/utilities" element={<UtilitiesPage />} />
<Route path="/api/symbols" element={<SymbolsPage />} />
</Routes>
</main>
</div>
</MemoryRouter> {/* Changed HashRouter to MemoryRouter */}
);
};
export default App;

14
README.md Normal file
View file

@ -0,0 +1,14 @@
# Run and deploy your AI Studio app
This contains everything you need to run your app locally.
## Run Locally
**Prerequisites:** Node.js
1. Install dependencies:
`npm install`
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
3. Run the app:
`npm run dev`

60
components/CodeBlock.tsx Normal file
View file

@ -0,0 +1,60 @@
import React from 'react';
// Basic syntax highlighting for keywords, strings, numbers, comments
const highlightLua = (code: string): React.ReactNode => {
const keywords = ['local', 'function', 'return', 'if', 'then', 'else', 'end', 'for', 'in', 'do', 'while', 'break', 'true', 'false', 'nil'];
// Regex for matching keywords in a part
const keywordRegex = new RegExp(`^\\b(${keywords.join('|')})\\b$`);
// Construct the main splitting regex pattern as a string
// Ensure backslashes and special characters are correctly escaped for the RegExp constructor
const keywordsPatternForSplit = keywords.join('|');
// Changed \\/ to / in the character class for punctuation
const splitPatternString = `(--\\[\\[.*?\\]\\]--|--.*?$|"[^"]*"|'[^']*'|\\b\\d+(\\.\\d+)?\\b|\\b(?:${keywordsPatternForSplit})\\b|[(){}[\\]\\.,:;=+\\-*\/%^#~<>&|@!])`;
const splitRegex = new RegExp(splitPatternString, 'gm');
const parts = code.split(splitRegex);
return parts.map((part, index) => {
if (!part) return null; // split can produce empty strings or undefined for some cases
if (keywordRegex.test(part)) {
return <span key={index} className="token keyword">{part}</span>;
}
if ((part.startsWith('"') && part.endsWith('"')) || (part.startsWith("'") && part.endsWith("'"))) {
return <span key={index} className="token string">{part}</span>;
}
if (part.match(/^\b\d+(\.\d+)?\b/)) {
return <span key={index} className="token number">{part}</span>;
}
if (part.startsWith('--')) {
return <span key={index} className="token comment">{part}</span>;
}
// Check for punctuation using a set for clarity
const punctuationChars = new Set(['{', '}', '(', ')', '[', ']', '.', ',', ':', ';', '=', '+', '-', '*', '/', '%', '^', '#', '~', '<', '>', '&', '|', '@', '!']);
if (punctuationChars.has(part)) {
return <span key={index} className="token punctuation">{part}</span>;
}
return part;
});
};
interface CodeBlockProps {
code: string;
language: string; // e.g., 'lua', 'typescript'
}
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, language }) => {
const highlightedCode = language === 'lua' ? highlightLua(code.trim()) : code.trim();
return (
<pre className={`language-${language} text-sm rounded-md shadow-md`}>
<code>
{highlightedCode}
</code>
</pre>
);
};

View file

@ -0,0 +1,65 @@
import React from 'react';
import { CodeBlock } from './CodeBlock';
import { ApiDocEntry, ApiSection } from '../types';
interface ContentPageProps {
title: string;
introduction?: string | React.ReactNode;
sections?: ApiSection[];
children?: React.ReactNode;
}
export const ContentPage: React.FC<ContentPageProps> = ({ title, introduction, sections, children }) => {
return (
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-500 mb-6 pb-2 border-b-2 border-gray-700">{title}</h1>
{introduction && (
<div className="prose prose-invert prose-lg max-w-none mb-8 text-gray-300">
{typeof introduction === 'string' ? <p>{introduction}</p> : introduction}
</div>
)}
{children}
{/* Render children (like InfoPanel) before sections if they are direct children of ContentPage */}
{sections && sections.map((section, idx) => (
<section key={idx} className="mb-12">
<h2 className="text-3xl font-semibold text-purple-300 mb-4">{section.title}</h2>
{section.description && <p className="text-gray-400 mb-6 text-lg">{section.description}</p>}
{section.entries.map((entry, entryIdx) => (
<div key={entryIdx} className="mb-10 p-6 bg-gray-800 rounded-xl shadow-lg">
<h3 className="text-xl font-mono font-semibold text-green-400 mb-2 bg-gray-700 p-2 rounded-md inline-block">{entry.signature}</h3>
<div className="text-gray-300 mb-3 prose prose-invert max-w-none">{typeof entry.description === 'string' ? <p>{entry.description}</p> : entry.description}</div>
{entry.parameters && entry.parameters.length > 0 && (
<div className="mb-3">
<h4 className="text-md font-semibold text-pink-400 mb-1">Parameters:</h4>
<ul className="list-disc list-inside space-y-1 pl-4 text-gray-400">
{entry.parameters.map((param, pIdx) => (
<li key={pIdx}>
<code className="text-sm bg-gray-700 px-1 rounded">{param.name}</code>: <code className="text-sm text-cyan-400">{param.type}</code> - {param.description}
</li>
))}
</ul>
</div>
)}
{entry.returns && (
<div className="mb-3">
<h4 className="text-md font-semibold text-pink-400 mb-1">Returns:</h4>
<p className="text-gray-400">
<code className="text-sm text-cyan-400">{entry.returns.type}</code> - {entry.returns.description}
</p>
</div>
)}
{entry.example && (
<div className="mb-3">
<h4 className="text-md font-semibold text-pink-400 mb-1">Example:</h4>
<CodeBlock code={entry.example} language="lua" />
</div>
)}
{/* Old notes rendering removed, as InfoPanel is now used directly within page content */}
</div>
))}
</section>
))}
</div>
);
};

64
components/Icons.tsx Normal file
View file

@ -0,0 +1,64 @@
import React from 'react';
export const HomeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
);
export const BookOpenIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v11.494m0 0A7.5 7.5 0 0012 6.253zM8.25 6.253V3.5A2.25 2.25 0 0110.5 1.25h3A2.25 2.25 0 0115.75 3.5v2.753m-7.5 0V3.5A2.25 2.25 0 005.25 1.25h3A2.25 2.25 0 0010.5 3.5v2.753M12 6.253A7.5 7.5 0 0119.5 13.5V17.25a2.25 2.25 0 01-2.25 2.25h-10.5A2.25 2.25 0 014.5 17.25V13.5A7.5 7.5 0 0112 6.253z" />
</svg>
);
export const CogIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
);
export const PuzzleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z" />
</svg>
);
export const CodeBracketIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
</svg>
);
export const LinkIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
);
export const SparklesIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L1.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.25 12L17 13.75M17 13.75L15.75 12M17 13.75L18.25 15M17 13.75L19 13.75" />
</svg>
);
export const LibraryIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 3v18" />
</svg>
);
export const InformationCircleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
</svg>
);
export const ExclamationTriangleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
);

48
components/InfoPanel.tsx Normal file
View file

@ -0,0 +1,48 @@
import React from 'react';
import { InformationCircleIcon, ExclamationTriangleIcon } from './Icons';
interface InfoPanelProps {
title?: string;
type: 'note' | 'info' | 'warning';
children: React.ReactNode;
}
export const InfoPanel: React.FC<InfoPanelProps> = ({ title, type, children }) => {
let panelClasses = 'p-4 rounded-md my-6 border-l-4 shadow-md';
let titleClasses = 'font-semibold mb-2';
let IconComponent: React.FC<React.SVGProps<SVGSVGElement>> | null = null;
switch (type) {
case 'info':
panelClasses += ' bg-blue-900/30 border-blue-500 text-blue-200';
titleClasses += ' text-blue-300';
IconComponent = InformationCircleIcon;
break;
case 'warning':
panelClasses += ' bg-yellow-900/30 border-yellow-500 text-yellow-200';
titleClasses += ' text-yellow-300';
IconComponent = ExclamationTriangleIcon;
break;
case 'note':
default:
panelClasses += ' bg-gray-800 border-purple-500 text-gray-300';
titleClasses += ' text-purple-300';
IconComponent = InformationCircleIcon; // Default to info icon for notes
break;
}
return (
<div className={panelClasses}>
{title && (
<div className="flex items-center">
{IconComponent && <IconComponent className="h-6 w-6 mr-2 flex-shrink-0" />}
<h4 className={titleClasses}>{title}</h4>
</div>
)}
<div className={`prose prose-sm prose-invert max-w-none ${title ? 'mt-1' : ''} ${IconComponent && !title ? 'ml-8' : ''}`}>
{children}
</div>
</div>
);
};

46
components/Sidebar.tsx Normal file
View file

@ -0,0 +1,46 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { NavSection } from '../types';
import { LibraryIcon } from './Icons';
interface SidebarProps {
navLinks: NavSection[];
}
export const Sidebar: React.FC<SidebarProps> = ({ navLinks }) => {
return (
<aside className="w-64 bg-gray-800 p-4 space-y-6 overflow-y-auto shadow-lg">
<div className="flex items-center space-x-3 mb-6">
<LibraryIcon className="h-10 w-10 text-purple-400" />
<h1 className="text-2xl font-bold text-white">Chemical Docs</h1>
</div>
{navLinks.map((section) => (
<div key={section.title}>
<h2 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2 px-2">{section.title}</h2>
<nav className="space-y-1">
{section.links.map((link) => {
const IconComponent = link.icon;
return (
<NavLink
key={link.path}
to={link.path}
className={({ isActive }) =>
`flex items-center space-x-3 px-3 py-2.5 rounded-md text-sm font-medium transition-colors duration-150 ease-in-out
${isActive
? 'bg-purple-600 text-white shadow-md'
: 'text-gray-300 hover:bg-gray-700 hover:text-white'
}`
}
>
{IconComponent && <IconComponent className="h-5 w-5" />}
<span>{link.label}</span>
</NavLink>
);
})}
</nav>
</div>
))}
</aside>
);
};

30
constants.ts Normal file
View file

@ -0,0 +1,30 @@
import { NavSection } from './types';
import { HomeIcon, BookOpenIcon, CogIcon, PuzzleIcon, CodeBracketIcon, LinkIcon, SparklesIcon } from './components/Icons';
export const NAV_LINKS: NavSection[] = [
{
title: 'Overview',
links: [
{ path: '/', label: 'Home', icon: HomeIcon },
{ path: '/introduction', label: 'Introduction', icon: BookOpenIcon },
{ path: '/core-concepts', label: 'Core Concepts', icon: PuzzleIcon },
],
},
{
title: 'API Reference',
links: [
{ path: '/api/value', label: 'Value', icon: CodeBracketIcon },
{ path: '/api/table', label: 'Table', icon: CodeBracketIcon },
{ path: '/api/map', label: 'Map', icon: CodeBracketIcon },
{ path: '/api/computed', label: 'Computed', icon: CodeBracketIcon },
{ path: '/api/observer', label: 'Observer', icon: CodeBracketIcon },
{ path: '/api/watch', label: 'Watch', icon: CodeBracketIcon },
{ path: '/api/effect', label: 'Effect', icon: CodeBracketIcon },
{ path: '/api/reaction', label: 'Reaction', icon: CodeBracketIcon },
{ path: '/api/reactor', label: 'Reactor', icon: CogIcon },
{ path: '/api/compose', label: 'Compose', icon: SparklesIcon },
{ path: '/api/utilities', label: 'Utilities', icon: CodeBracketIcon },
{ path: '/api/symbols', label: 'Symbols', icon: LinkIcon },
],
},]

65
index.html Normal file
View file

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chemical Library Docs</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Custom scrollbar for better aesthetics */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #2d3748; /* dark-gray-800 */
}
::-webkit-scrollbar-thumb {
background: #4a5568; /* dark-gray-600 */
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #718096; /* dark-gray-500 */
}
/* PrismJS okaidia theme adaptations */
pre[class*="language-"] {
background: #2d2d2d;
color: #f8f8f2;
border-radius: 0.375rem; /* rounded-md */
padding: 1rem;
overflow: auto;
font-family: 'Courier New', Courier, monospace;
font-size: 0.875rem; /* text-sm */
}
.token.comment, .token.prolog, .token.doctype, .token.cdata { color: #6272a4; }
.token.punctuation { color: #f8f8f2; }
.token.namespace { opacity: .7; }
.token.property, .token.tag, .token.constant, .token.symbol, .token.deleted { color: #ff79c6; }
.token.boolean, .token.number { color: #bd93f9; }
.token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { color: #50fa7b; }
.token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string, .token.variable { color: #f8f8f2; }
.token.atrule, .token.attr-value, .token.function, .token.class-name { color: #8be9fd; }
.token.keyword { color: #ff79c6; }
.token.regex, .token.important { color: #ffb86c; }
.token.important, .token.bold { font-weight: bold; }
.token.italic { font-style: italic; }
.token.entity { cursor: help; }
</style>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@^19.0.0",
"react-dom/": "https://esm.sh/react-dom@^19.0.0/",
"react/": "https://esm.sh/react@^19.0.0/",
"react-router-dom": "https://esm.sh/react-router-dom@6.22.3"
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body class="bg-gray-900 text-gray-100">
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>

16
index.tsx Normal file
View file

@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

5
metadata.json Normal file
View file

@ -0,0 +1,5 @@
{
"name": "Chemical Library Docs",
"description": "A documentation website for the Chemical reactive state and UI library.",
"requestFramePermissions": []
}

21
package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "chemical-library-docs",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "6.22.3"
},
"devDependencies": {
"@types/node": "^22.14.0",
"typescript": "~5.7.2",
"vite": "^6.2.0"
}
}

254
pages/CoreConceptsPage.tsx Normal file
View file

@ -0,0 +1,254 @@
import React from 'react';
import { ContentPage } from '../components/ContentPage';
import { Link } from 'react-router-dom';
import { ApiSection } from '../types'; // Import ApiSection
export const CoreConceptsPage: React.FC = () => {
const intro = (
<p>
Chemical is built upon a few core concepts that work together to create reactive and manageable applications. Understanding these concepts is key to effectively using the library.
</p>
);
const sections: ApiSection[] = [
{
title: "Reactivity",
entries: [
{
signature: "Stateful Objects (Values, Tables, Maps)",
description: (
<>
At the heart of Chemical are stateful objects. These are containers for your application's data.
The primary stateful object is a <Link to="/api/value" className="text-purple-400 hover:underline">Value</Link>.
For collections, Chemical provides <Link to="/api/table" className="text-purple-400 hover:underline">Table</Link> (for arrays)
and <Link to="/api/map" className="text-purple-400 hover:underline">Map</Link> (for dictionaries).
When the data within these objects changes, Chemical's reactive system is notified.
</>
),
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Table = Chemical.Table
local Map = Chemical.Map
local name = Value("Guest")
local scores = Table({10, 20, 30})
local settings = Map({ theme = "dark", volume = 0.5 })
name:set("PlayerOne") -- Triggers reactivity
scores:insert(40) -- Triggers reactivity
settings:key("volume", 0.7) -- Triggers reactivity
`,
},
{
signature: "Computed Values",
description: (
<>
<Link to="/api/computed" className="text-purple-400 hover:underline">Computed</Link> values
are derived from one or more stateful objects. They automatically update whenever their underlying dependencies change.
This allows you to create complex data relationships without manual synchronization.
</>
),
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local firstName = Value("John")
local lastName = Value("Doe")
local fullName = Computed(function()
return firstName:get() .. " " .. lastName:get()
end)
print(fullName:get()) -- Output: John Doe
firstName:set("Jane")
-- fullName:get() will be "Jane Doe" after scheduler runs
print("Full name after set:", fullName:get()) -- Might print old value here due to async update
`,
},
{
signature: "Effects",
description: (
<>
<Link to="/api/effect" className="text-purple-400 hover:underline">Effects</Link> are used to perform side effects in response to state changes.
This could include updating the UI, making network requests, or logging. Effects automatically re-run when their dependencies change.
They can also return a cleanup function that runs before the next execution or when the effect is destroyed.
</>
),
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Effect = Chemical.Effect
local count = Value(0)
Effect(function()
print("Current count is:", count:get())
-- Optional cleanup function
return function()
print("Cleaning up effect for count (previous value was):", count:get())
end
end)
count:set(1) -- Effect runs
count:set(2) -- Cleanup for count 1 runs, then effect for count 2 runs
`,
},
{
signature: "Observers and Watch",
description: (
<>
<Link to="/api/observer" className="text-purple-400 hover:underline">Observers</Link> provide a way to listen to changes in a specific stateful object and run callbacks.
<Link to="/api/watch" className="text-purple-400 hover:underline">Watch</Link> is a higher-level utility built on Observers for simpler change tracking.
</>
),
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Observer = Chemical.Observer
local Watch = Chemical.Watch
local name = Value("Alex")
-- Using Observer
local obs = Observer(name)
obs:onChange(function(newValue, oldValue)
print("Observer: Name changed from", oldValue, "to", newValue)
end)
-- Using Watch
local watchHandle = Watch(name, function(newValue, oldValue)
print("Watch: Name changed from", oldValue, "to", newValue)
end)
name:set("Jordan")
-- Both Observer and Watch callbacks will fire.
obs:destroy()
watchHandle:destroy()
`,
},
],
},
{
title: "UI Composition with `Compose`",
entries: [
{
signature: "Declarative UI",
description: (
<>
Chemical's <Link to="/api/compose" className="text-purple-400 hover:underline">Compose</Link> API allows you to define UI structures declaratively.
Properties of UI elements can be bound to stateful objects (Values or Computeds), and the UI will automatically update when the state changes.
</>
),
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Compose = Chemical.Compose
local OnEvent = Chemical.OnEvent
local Children = Chemical.Symbols.Children
-- Assuming playerGui is defined: local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local isVisible = Value(true)
local buttonText = Value("Click Me")
local MyScreen = Compose("ScreenGui")({
Parent = playerGui,
[Children] = {
Compose("TextButton")({
Text = buttonText, -- Bound to the buttonText Value
Visible = isVisible, -- Bound to the isVisible Value
Size = UDim2.new(0, 100, 0, 50),
-- Parent is implicitly MyScreen due to [Children] structure
[OnEvent("Activated")] = function()
print("Button clicked!")
isVisible:set(false) -- Change state, UI updates
end
})
}
})
buttonText:set("Submit") -- Button's text updates automatically
`,
},
],
},
{
title: "Networking with Reactors",
entries: [
{
signature: "Reactions and Reactors",
description: (
<>
For networked applications, Chemical provides <Link to="/api/reactor" className="text-purple-400 hover:underline">Reactors</Link> and <Link to="/api/reaction" className="text-purple-400 hover:underline">Reactions</Link>.
A Reactor is a server-side factory for creating Reactions. Reactions are stateful objects whose state can be replicated from the server to clients.
Changes made to a Reaction on the server are automatically propagated to subscribed clients. Reaction properties are accessed directly.
</>
),
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Reactor = Chemical.Reactor
local Effect = Chemical.Effect
-- Server-side: Define a Reactor for player scores
local ScoreReactor = Reactor({ Name = "PlayerScore" }, function(playerIdString)
-- 'playerIdString' (the key) is available but typically not stored in the returned table.
return {
currentScore = Value(0)
}
end)
-- Create a Reaction for a specific player
local player1ScoreReaction = ScoreReactor:create("player123")
-- Access Reaction properties directly:
player1ScoreReaction.currentScore:set(100) -- This change will be sent to clients
-- Client-side: Await and use the Reaction
local player1ScoreClientReaction = ScoreReactor:await("player123") -- Yields
if player1ScoreClientReaction then
Effect(function()
-- Access Reaction properties directly:
print("Player 123 score on client:", player1ScoreClientReaction.currentScore:get())
end)
end
`,
},
],
},
{
title: "Underlying ECS",
entries: [
{
signature: "Entity-Component-System",
description: (
<>
Chemical internally uses an Entity-Component-System (ECS) architecture (specifically `JECS`) to manage its reactive objects and their relationships.
While direct interaction with the ECS is usually not required for typical use cases, understanding this can be helpful for advanced scenarios or debugging.
Each reactive object created by Chemical (Value, Computed, Effect, etc.) is an "entity" with associated "components" (like its current value, previous value, or callback functions) and "tags" (like `IsStateful`, `IsDirty`).
</>
),
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
-- This is a conceptual example, direct ECS manipulation is advanced.
local myValue = Value(10)
-- myValue.entity is the ECS entity ID.
-- Chemical.World:get(myValue.entity, Chemical.ECS.Components.Value) would return 10.
-- When myValue:set(20) is called, Chemical.World:set(...) updates the component
-- and adds the Chemical.ECS.Tags.IsDirty tag.
-- The Scheduler then processes entities with IsDirty.
`,
},
],
}
];
return <ContentPage title="Core Concepts" introduction={intro} sections={sections} />;
};

85
pages/HomePage.tsx Normal file
View file

@ -0,0 +1,85 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { BookOpenIcon, PuzzleIcon, SparklesIcon, CogIcon } from '../components/Icons';
export const HomePage: React.FC = () => {
return (
<div className="text-center">
<h1 className="text-5xl font-extrabold mb-6 text-transparent bg-clip-text bg-gradient-to-r from-purple-400 via-pink-500 to-red-500">
Chemical Docs v0.2.5
</h1>
<p className="text-xl text-gray-300 mb-12 max-w-2xl mx-auto">
Your comprehensive guide to the Chemical library a powerful Luau solution for reactive state management and declarative UI composition.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-12">
<FeatureCard
icon={<BookOpenIcon className="h-12 w-12 text-purple-400 mb-4" />}
title="Reactive Primitives"
description="Understand Values, Computeds, and Effects for building dynamic applications."
linkTo="/api/value"
linkText="Explore Values"
/>
<FeatureCard
icon={<PuzzleIcon className="h-12 w-12 text-green-400 mb-4" />}
title="Stateful Collections"
description="Manage complex state with reactive Tables and Maps."
linkTo="/api/table"
linkText="Learn about Tables"
/>
<FeatureCard
icon={<SparklesIcon className="h-12 w-12 text-yellow-400 mb-4" />}
title="Declarative UI"
description="Compose user interfaces with ease using the `Compose` API."
linkTo="/api/compose"
linkText="Discover Compose"
/>
<FeatureCard
icon={<CogIcon className="h-12 w-12 text-blue-400 mb-4" />}
title="Networked State"
description="Synchronize state between server and clients with Reactions and Reactors."
linkTo="/api/reactor"
linkText="Understand Reactors"
/>
</div>
<div className="space-x-4">
<Link
to="/introduction"
className="bg-purple-600 hover:bg-purple-700 text-white font-semibold py-3 px-6 rounded-lg text-lg transition duration-150 ease-in-out shadow-md hover:shadow-lg"
>
Get Started
</Link>
<Link
to="/core-concepts"
className="bg-gray-700 hover:bg-gray-600 text-white font-semibold py-3 px-6 rounded-lg text-lg transition duration-150 ease-in-out shadow-md hover:shadow-lg"
>
Core Concepts
</Link>
</div>
</div>
);
};
interface FeatureCardProps {
icon: React.ReactNode;
title: string;
description: string;
linkTo: string;
linkText: string;
}
const FeatureCard: React.FC<FeatureCardProps> = ({ icon, title, description, linkTo, linkText }) => (
<div className="bg-gray-800 p-8 rounded-xl shadow-xl hover:shadow-2xl transition-shadow duration-300 flex flex-col items-center">
{icon}
<h3 className="text-2xl font-semibold text-white mb-3">{title}</h3>
<p className="text-gray-400 mb-6 text-center">{description}</p>
<Link
to={linkTo}
className="mt-auto text-purple-400 hover:text-purple-300 font-medium transition-colors"
>
{linkText} &rarr;
</Link>
</div>
);

220
pages/IntroductionPage.tsx Normal file
View file

@ -0,0 +1,220 @@
import React from 'react';
import { ContentPage } from '../components/ContentPage';
import { CodeBlock } from '../components/CodeBlock';
export const IntroductionPage: React.FC = () => {
const intro = (
<>
<p className="mb-4">
Chemical is a Luau library designed for building applications with a reactive programming paradigm, primarily within environments like Roblox. It provides tools for managing state, creating derived data, handling side effects, and composing user interfaces in a declarative way.
</p>
<p className="mb-4">
Inspired by modern frontend frameworks, Chemical aims to simplify complex state interactions and UI logic by providing a structured and predictable way to handle data flow.
</p>
</>
);
const sections = [
{
title: "Key Features",
entries: [
{
signature: "Reactive Primitives",
description: "Includes `Value`, `Computed`, and `Effect` for fine-grained reactive control.",
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local Effect = Chemical.Effect
-- Create a reactive value
local count = Value(0)
-- Create a computed value derived from count
local doubled = Computed(function()
return count:get() * 2
end)
-- Create an effect that runs when count changes
Effect(function()
print("Count changed to:", count:get())
print("Doubled is:", doubled:get())
end)
count:set(5) -- This will trigger the effect
-- Output:
-- Count changed to: 5
-- Doubled is: 10
`,
},
{
signature: "Stateful Collections",
description: "Manage lists and dictionaries with `Table` and `Map`, which propagate changes reactively.",
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local Effect = Chemical.Effect
local items = Table({"apple", "banana"})
Effect(function()
print("Items:", table.concat(items:get(), ", "))
end)
items:insert("cherry")
-- Output:
-- Items: apple, banana
-- (After scheduler runs)
-- Items: apple, banana, cherry
`,
},
{
signature: "Declarative UI Composition",
description: "Use the `Compose` API to build and update UI elements based on reactive state.",
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local Compose = Chemical.Compose
local Children = Chemical.Symbols.Children -- Alias for Children key
-- Assuming playerGui is defined: local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local name = Value("Player")
local score = Value(100)
local MyScreenGui = Compose("ScreenGui")({
Parent = playerGui,
[Children] = { -- Use the aliased Children symbol
Compose("TextLabel")({
Name = "PlayerName",
Position = UDim2.new(0.5, 0, 0.1, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Text = Computed(function()
return "Name: " .. name:get()
end),
TextColor3 = Color3.new(1,1,1),
BackgroundTransparency = 1,
Size = UDim2.new(0, 200, 0, 30)
}),
Compose("TextLabel")({
Name = "PlayerScore",
Position = UDim2.new(0.5, 0, 0.2, 0),
AnchorPoint = Vector2.new(0.5, 0.5),
Text = Computed(function()
return "Score: " .. score:get()
end),
TextColor3 = Color3.new(1,1,1),
BackgroundTransparency = 1,
Size = UDim2.new(0, 200, 0, 30)
})
}
})
-- Later, when state changes, the UI updates automatically
name:set("Hero")
score:set(150)
`,
},
{
signature: "Networked State with Reactors",
description: "Synchronize state across server and clients using `Reactor` and `Reaction` objects for multiplayer experiences.",
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Reactor = Chemical.Reactor
local Effect = Chemical.Effect
-- Server-side
local PlayerDataReactor = Reactor({ Name = "PlayerData" }, function(key_playerIdString, initialData)
-- 'key_playerIdString' is the key, typically not stored in this returned table.
initialData = initialData or {}
return {
Health = Value(initialData.Health or 100),
Mana = Value(initialData.Mana or 50)
}
end)
local player1DataReaction = PlayerDataReactor:create("player1_id", { Health = 90 })
-- Access properties directly on the reaction:
player1DataReaction.Health:set(80) -- Change will replicate
-- Client-side
local player1DataClient = PlayerDataReactor:await("player1_id")
-- player1DataClient is now the Reaction object
if player1DataClient then
Effect(function()
print("Player 1 Health:", player1DataClient.Health:get())
end)
end
`,
},
],
},
{
title: "Philosophy",
entries: [
{
signature: "Data Flow",
description: "Chemical promotes a unidirectional data flow. State changes trigger computations, which in turn can trigger effects (like UI updates or network calls). This makes applications easier to reason about.",
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local Effect = Chemical.Effect
-- State (Value) -> Derived State (Computed) -> Side Effect (Effect)
local function UpdateDisplay(text) print("Display: " .. text) end
local price = Value(10)
local quantity = Value(2)
local totalCost = Computed(function()
return price:get() * quantity:get()
end)
Effect(function()
UpdateDisplay("Total Cost: $" .. totalCost:get())
end)
price:set(12) -- Triggers totalCost recomputation, then updates display
quantity:set(3) -- Triggers totalCost recomputation, then updates display
`
},
{
signature: "Modularity",
description: "The library is structured with clear separation of concerns: core reactivity, state management, UI composition, and networking are distinct but interoperable parts.",
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
-- Assuming modules are set up in ReplicatedStorage.Chemical.Modules
-- local PlayerProfileReactor = require(game.ReplicatedStorage.Chemical.Modules.PlayerProfileReactor) -- Uses Reactor
-- local HUD = require(game.ReplicatedStorage.Chemical.Modules.HUDComponent) -- Uses Compose
-- Mock for example purposes
local PlayerProfileReactor = {
await = function(playerId)
print("Mock Reactor: Awaiting profile for", playerId)
-- Simulate awaiting a profile by returning it directly for example simplicity
-- In a real scenario, this would yield until the data is available.
return {
UserId = playerId,
Name = Chemical.Value("MockPlayer-" .. playerId:sub(1,4))
}
end
}
local HUD = { Render = function(profile) print("Rendering HUD for", profile.Name:get()) end }
-- End Mock
local player = game.Players.LocalPlayer
local profileReaction = PlayerProfileReactor:await(tostring(player.UserId))
if profileReaction then
HUD.Render(profileReaction) -- HUD component consumes reactive profile data
end
`
}
]
}
];
return <ContentPage title="Introduction to Chemical" introduction={intro} sections={sections} />;
};

222
pages/api/ComposePage.tsx Normal file
View file

@ -0,0 +1,222 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { ApiSection } from '../../types';
const composeApi: ApiSection[] = [
{
title: 'Chemical.Compose',
description: 'The `Compose` API is used for declaratively creating and managing Roblox UI Instances. It allows you to bind UI properties to reactive state (Values or Computeds) and handle UI events.',
entries: [
{
signature: 'Chemical.Compose(target: string | Instance): (properties: GuiProperties) => Instance & CompositionHandle',
description: 'Starts a UI composition. It takes a target, which can be a string (ClassName of the Instance to create, e.g., "TextLabel") or an existing Instance to adopt and manage.',
parameters: [
{ name: 'target', type: 'string | Instance', description: 'The ClassName of the UI element to create, or an existing Instance to manage.' },
],
returns: { type: '(properties: GuiProperties) => Instance & CompositionHandle', description: 'A function that takes a properties table and returns the created/managed Instance. The instance is augmented with a `.Destroy()` method specific to the composition.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Compose = Chemical.Compose
-- Assuming playerGui is defined, e.g., local playerGui = game.Players.LocalPlayer:WaitForChild("PlayerGui")
local MyScreen = Compose("ScreenGui")({ Parent = playerGui })
-- Create a new TextLabel
local MyLabelFactory = Compose("TextLabel")
local labelInstance = MyLabelFactory({
Name = "ScoreLabel",
Text = "Score: 0",
Parent = MyScreen -- Parent to the ScreenGui
})
-- Adopt an existing Frame
local existingFrame = Instance.new("Frame")
existingFrame.Parent = MyScreen -- Parent to the ScreenGui
local MyFrameManager = Compose(existingFrame)
MyFrameManager({
BackgroundColor3 = Color3.new(1, 0, 0)
})
`,
},
],
},
{
title: 'Properties Table (GuiProperties)',
description: 'The properties table passed to the composer function defines the attributes and event handlers for the UI Instance.',
entries: [
{
signature: 'Standard Properties (e.g., Name, Size, Position, Text, BackgroundColor3)',
description: 'You can set any standard Roblox Instance property. If the value provided is a Chemical `Value` or `Computed`, the Instance property will reactively update whenever the stateful object changes.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Compose = Chemical.Compose
local Children = Chemical.Symbols.Children -- Alias for Children key
-- Assuming playerGui is defined
local labelText = Value("Loading...")
local labelColor = Value(Color3.new(1,1,1))
local MyScreen = Compose("ScreenGui")({
Parent = playerGui,
[Children] = {
Compose("TextLabel")({
Text = labelText, -- Reactive binding
TextColor3 = labelColor, -- Reactive binding
Size = UDim2.new(0, 200, 0, 50)
})
}
})
labelText:set("Ready!") -- The TextLabel's Text property updates
labelColor:set(Color3.new(0,1,0)) -- The TextLabel's TextColor3 updates
`,
},
{
signature: 'Parent: Instance',
description: 'Sets the `Parent` of the created/managed Instance.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Compose = Chemical.Compose
local Children = Chemical.Symbols.Children -- Alias for Children key
-- Assuming playerGui is defined
local MyScreen = Compose("ScreenGui")({ Parent = playerGui })
local MyFrame = Compose("Frame")({ Parent = MyScreen })
Compose("TextLabel")({
Parent = MyFrame, -- Sets the parent to MyFrame
[Children] = {} -- Empty children if none, but key must be present if Children symbol is used.
})
`,
},
{
signature: '[Chemical.Symbols.Children]: { Instance | CompositionHandle }',
description: 'An array of child Instances or other Compositions to parent under this element. The key for this property MUST be the `Chemical.Symbols.Children` symbol (often aliased to a local variable `Children` and used as `[Children]`).',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Compose = Chemical.Compose
local Children = Chemical.Symbols.Children -- Alias the symbol
-- Assuming playerGui exists
Compose("ScreenGui")({
Parent = playerGui,
[Children] = { -- Use the aliased Children symbol
Compose("Frame")({
Name = "Container",
Size = UDim2.fromScale(0.8, 0.8),
Position = UDim2.fromScale(0.1, 0.1),
[Children] = { -- Use the aliased Children symbol
Compose("TextButton")({ Name = "Button1", Text = "B1", Size = UDim2.fromScale(0.2, 0.1) }),
Compose("ImageLabel")({ Name = "Icon", Size = UDim2.fromScale(0.2,0.2), Position = UDim2.fromOffset(100,0) })
}
})
}
})
`,
},
{
signature: '[Chemical.OnEvent(eventName: string)]: (Instance, ...args) -> ()',
description: 'Attaches an event handler to a Roblox Instance event (e.g., "Activated", "MouseButton1Click", "MouseEnter"). The `eventName` should match the Roblox event name.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Compose = Chemical.Compose
local OnEvent = Chemical.OnEvent
local Children = Chemical.Symbols.Children -- Alias for Children key
-- Assuming playerGui is defined
local MyScreen = Compose("ScreenGui")({
Parent = playerGui,
[Children] = {
Compose("TextButton")({
Text = "Click Me",
[OnEvent("Activated")] = function(buttonInstance)
print(buttonInstance.Name .. " was activated!")
end,
[OnEvent("MouseEnter")] = function(buttonInstance)
buttonInstance.BackgroundColor3 = Color3.new(0.5, 0.5, 0.5)
end,
[OnEvent("MouseLeave")] = function(buttonInstance)
buttonInstance.BackgroundColor3 = Color3.new(0.2, 0.2, 0.2)
end
})
}
})
`,
},
{
signature: '[Chemical.OnChange(propertyName: string)]: ((newValue: any) -> ()) | Value<any>',
description: 'Listens for changes to a specific property of the Instance (using `Instance:GetPropertyChangedSignal(propertyName)`). The provided callback function will be executed, or if a Chemical `Value` is provided, its `:set()` method will be called with the new property value.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Compose = Chemical.Compose
local OnChange = Chemical.OnChange
local Children = Chemical.Symbols.Children -- Alias for Children key
local Effect = Chemical.Effect
-- Assuming playerGui is defined
local currentText = Value("")
local MyScreen = Compose("ScreenGui")({
Parent = playerGui,
[Children] = {
Compose("TextBox")({
Name = "MyInput",
PlaceholderText = "Enter text here...",
[OnChange("Text")] = function(newText) -- Callback function
print("TextBox text changed to:", newText)
end,
-- OR bind to a Value:
-- [OnChange("Text")] = currentText
})
}
})
-- If bound to currentText:
-- Effect(function() print("currentText Value is:", currentText:get()) end)
`,
},
],
},
{
title: 'Composition Handle',
description: 'The function returned by `Chemical.Compose(target)` returns the Instance itself, but this instance is also augmented with a specific `Destroy` method related to the composition.',
entries: [
{
signature: 'compositionHandle:Destroy()',
description: 'Destroys the UI composition created by `Chemical.Compose`. This includes: destroying the root Instance, disconnecting all event listeners and property change signals established by the composition, and destroying any Effects that were created to bind reactive properties. If the composition managed child compositions, their `Destroy` methods are also called.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Compose = Chemical.Compose
local Children = Chemical.Symbols.Children -- Alias for Children key
-- Assuming playerGui is defined
local MyScreen = Compose("ScreenGui")({
Parent = playerGui,
[Children] = {
Compose("TextLabel")({
Text = "Hello"
})
}
})
local labelComposition = MyScreen.Children[1] -- Accessing the composed TextLabel
-- ... later
-- To destroy the label specifically (if it were assigned to a variable directly from Compose)
-- labelComposition:Destroy()
-- To destroy the entire screen and all its composed children:
MyScreen:Destroy() -- The ScreenGui instance and its TextLabel are destroyed, and all Chemical bindings are cleaned up.
`,
notes: "This is different from `instance:Destroy()`. While `instance:Destroy()` destroys the Roblox Instance, `compositionHandle:Destroy()` also cleans up Chemical-specific resources like effects and event connections associated with that composition."
}
]
}
];
export const ComposePage: React.FC = () => {
return <ContentPage title="Chemical.Compose" sections={composeApi} />;
};

150
pages/api/ComputedPage.tsx Normal file
View file

@ -0,0 +1,150 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { InfoPanel } from '../../components/InfoPanel';
import { ApiSection } from '../../types';
const computedApi: ApiSection[] = [
{
title: 'Chemical.Computed',
description: 'Creates a reactive value that is derived from other reactive Values or Computeds. It automatically recalculates its value whenever any of its dependencies change.',
entries: [
{
signature: 'Chemical.Computed<T>(computeFn: () => T, cleanupFn?: (oldValue: T) => ()): Computed<T>',
description: 'Constructs a new Computed value.',
parameters: [
{ name: 'computeFn', type: '() => T', description: 'A function that calculates the value. Any reactive objects accessed via `:get()` inside this function become dependencies.' },
{ name: 'cleanupFn', type: '((oldValue: T) => ())?', description: "An optional function. When a dependency of the `Computed` changes, this `cleanupFn` is called with the `Computed`'s *previous* value right *before* the main `computeFn` is re-executed to calculate the new value. The `cleanupFn` is also called with the last computed value when the `Computed` object itself is destroyed." },
],
returns: { type: 'Computed<T>', description: 'A new Computed object.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local Effect = Chemical.Effect
local price = Value(100)
local discount = Value(0.1) -- 10% discount
local finalPrice = Computed(function()
print("Debug: Recalculating final price...")
return price:get() * (1 - discount:get())
end, function(oldFinalPrice)
-- This cleanupFn receives the value 'finalPrice' held *before* this re-computation.
print("Debug: Cleanup - Old final price was:", oldFinalPrice)
end)
-- The 'finalPrice' Computed has its 'computeFn' run once upon creation (or lazily on first :get()).
print("Initial final price (after first computation):", finalPrice:get())
-- Expected:
-- Debug: Recalculating final price...
-- Initial final price (after first computation): 90
price:set(200) -- Dependency changed
-- IMPORTANT: 'finalPrice:get()' here will return the *old* value (90)
-- because the 'finalPrice' Computed has not re-evaluated yet.
-- Re-evaluation is batched and occurs in the Chemical scheduler's next tick.
print("Final price immediately after price:set(200):", finalPrice:get())
-- Expected Output: Final price immediately after price:set(200): 90
-- An Effect depending on 'finalPrice' will see the updated value in a subsequent scheduler tick.
-- The scheduler will typically:
-- 1. Detect 'price' changed.
-- 2. Mark 'finalPrice' (Computed) as dirty.
-- 3. In the next update cycle, process 'finalPrice':
-- a. Call cleanupFn for 'finalPrice' with its old value (90).
-- b. Call computeFn for 'finalPrice' to get the new value (180).
-- 4. Mark 'priceEffect' (Effect) as dirty because 'finalPrice' changed.
-- 5. Process 'priceEffect', which will then :get() the new value of 'finalPrice' (180).
local priceEffect = Effect(function()
print("Effect: finalPrice is now:", finalPrice:get())
end)
-- Expected output sequence from scheduler after price:set(200):
-- Debug: Cleanup - Old final price was: 90
-- Debug: Recalculating final price...
-- Effect: finalPrice is now: 180
`,
},
],
},
{
title: 'Computed Methods',
entries: [
{
signature: 'computed:get(): T',
description: 'Retrieves the current (potentially cached) value of the Computed. If called within another Computed or Effect, it establishes a dependency. Does not trigger re-computation itself; re-computation is handled by the scheduler when dependencies change.',
returns: { type: 'T', description: 'The current computed value.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local x = Value(5)
local y = Value(10)
local sum = Computed(function() return x:get() + y:get() end)
local currentSum = sum:get() -- currentSum is 15
`,
},
{
signature: 'computed:destroy()',
description: 'Destroys the Computed object. If a `cleanupFn` was provided during construction, it will be called with the last computed value. This also removes its dependencies on other reactive objects.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local name = Value("Test")
local upperName = Computed(function() return name:get():upper() end, function(oldUpper) print("Cleaning up:", oldUpper) end)
-- ...
upperName:destroy() -- "Cleaning up: TEST" will be printed if "TEST" was the last computed value.
`,
},
{
signature: 'computed:clean()',
description: 'Explicitly runs the `cleanupFn` if one was provided, using the current value of the Computed. This is typically managed internally but can be called manually if needed.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local data = Value({ value = 1 })
local processed = Computed(function()
return data:get().value * 2
end, function(oldProcessedValue)
print("Cleaning processed value:", oldProcessedValue)
end)
processed:get() -- Ensure it's computed once, current value is 2
processed:clean() -- "Cleaning processed value: 2"
`,
notes: "This method is less commonly used directly. Destruction or dependency changes usually handle cleanup.",
},
],
},
];
export const ComputedPage: React.FC = () => {
return (
<ContentPage title="Chemical.Computed" sections={computedApi}>
<InfoPanel type="note" title="Execution Timing & Batching">
<p>
Computed value updates are batched and processed asynchronously by the Chemical scheduler,
typically at the end of the current frame or scheduler tick.
</p>
<p className="mt-2">
If a dependency changes multiple times within a single frame, the Computed will only
re-calculate <strong>once</strong> using the <strong>most recent (final) values</strong> of its
dependencies when the scheduler processes the batch.
</p>
<p className="mt-2">
Consequently, reading a Computed value (e.g., <code>myComputed:get()</code>) immediately
after one of its dependencies is set will yield its <strong>old value</strong>. Effects and
other Computeds depending on it will receive the updated value in the subsequent
scheduler cycle.
</p>
</InfoPanel>
</ContentPage>
);
};

120
pages/api/EffectPage.tsx Normal file
View file

@ -0,0 +1,120 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { InfoPanel } from '../../components/InfoPanel';
import { ApiSection } from '../../types';
const effectApi: ApiSection[] = [
{
title: 'Chemical.Effect',
description: 'Creates a side effect that runs after a batch of state changes has been processed and all dependent Computeds have been updated. Effects are used for interacting with the outside world, like updating UI, making network calls, or logging. They automatically re-run when their dependencies change.',
entries: [
{
signature: 'Chemical.Effect(effectFn: () => CleanUpFunction | ()): Effect',
description: 'Constructs a new Effect.',
parameters: [
{ name: 'effectFn', type: '() => CleanUpFunction | ()', description: 'A function that performs the side effect. Any reactive objects accessed via `:get()` inside this function become dependencies. This function can optionally return a `CleanUpFunction`.' },
],
returns: { type: 'Effect', description: 'A new Effect object.' },
notes: '`CleanUpFunction`: A function of type `() -> ()` that is called before the `effectFn` re-runs due to dependency changes, or when the Effect is destroyed.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Effect = Chemical.Effect
local name = Value("User")
local counter = Value(0)
local loggingEffect = Effect(function()
-- This effect depends on 'name' and 'counter'
local currentName = name:get()
local currentCount = counter:get()
print("Effect triggered: Name is " .. currentName .. ", Count is " .. currentCount)
-- Optional cleanup function
return function()
print("Cleaning up effect for Name: " .. currentName .. ", Count: " .. currentCount)
end
end)
name:set("Admin") -- Effect re-runs (cleanup for "User", 0 runs first)
counter:set(5) -- Effect re-runs (cleanup for "Admin", 0 runs first)
-- Note: If both set calls happen in the same frame, the effect might only run once
-- with ("Admin", 5), after the cleanup for ("User", 0) or ("Admin",0) depending on intermediate states.
`,
},
],
},
{
title: 'Effect Methods',
entries: [
{
signature: 'effect:destroy()',
description: 'Destroys the Effect object. If the `effectFn` returned a cleanup function, it will be called. This also removes its dependencies on other reactive objects.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Effect = Chemical.Effect
local isActive = Value(true)
local myEffect = Effect(function()
if isActive:get() then
print("Effect is active")
return function() print("Effect cleaning up: was active") end
else
print("Effect is inactive")
return function() print("Effect cleaning up: was inactive") end
end
end)
isActive:set(false) -- Effect re-runs
myEffect:destroy() -- The last cleanup function ("Effect cleaning up: was inactive") runs.
`,
},
{
signature: 'effect:clean()',
description: 'Explicitly runs the cleanup function returned by the last execution of `effectFn`, if one exists. This is typically managed internally by the scheduler or when the effect is destroyed, but can be called manually.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Effect = Chemical.Effect
local data = Value("initial")
local effectWithCleanup = Effect(function()
local currentData = data:get()
print("Processing:", currentData)
return function() print("Cleaning up for:", currentData) end
end)
data:set("updated") -- Effect re-runs, cleanup for "initial" runs.
effectWithCleanup:clean() -- Cleanup for "updated" runs.
`,
notes: "This method is less commonly used directly. Destruction usually handles cleanup.",
},
],
},
];
export const EffectPage: React.FC = () => {
return (
<ContentPage title="Chemical.Effect" sections={effectApi}>
<InfoPanel type="note" title="Execution Timing & Batching">
<p>
Effects are batched and run by the Chemical scheduler. This typically happens at the
end of the current frame or scheduler tick, <strong>after</strong> all direct state changes
(e.g., from <code>:set()</code> on a Value) and <code>Computed</code> value updates for that tick
have been processed.
</p>
<p className="mt-2">
If an Effect's dependencies change multiple times within a single frame, the Effect
will only run <strong>once</strong>, reacting to the <strong>most recent (final) state</strong> of its
dependencies for that frame.
</p>
<p className="mt-2">
The <code>CleanUpFunction</code> (if returned by the <code>effectFn</code>) is called before the
<code>effectFn</code> re-runs due to dependency changes, or when the Effect is destroyed.
</p>
</InfoPanel>
</ContentPage>
);
};

117
pages/api/MapPage.tsx Normal file
View file

@ -0,0 +1,117 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { ApiSection } from '../../types';
const mapApi: ApiSection[] = [
{
title: 'Chemical.Map',
description: 'Creates a reactive dictionary (table with string or mixed keys). Changes to the map structure or its values can trigger updates in dependent Computeds and Effects. For reactivity of individual values within the map, those values would typically be Chemical.Value objects if fine-grained updates are needed, though this pattern is more common within Reactions.',
entries: [
{
signature: 'Chemical.Map<{[K]: V}>(initialObject: {[K]: V}): Map<{[K]: V}>',
description: 'Constructs a new reactive Map object.',
parameters: [
{ name: 'initialObject', type: '{[K]: V}', description: 'The initial dictionary/map (plain Lua table) to store.' },
],
returns: { type: 'Map<{[K]: V}>', description: 'A new Map object.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Map = Chemical.Map
local Table = Chemical.Table -- if needed for values
local playerConfig = Map({
name = "Hero",
level = 10, -- Store primitives directly
inventory = {"sword", "shield"} -- Store plain tables directly
})
`,
notes: "Chemical.Map is best used with primitive values or plain tables as its direct values. If you need a map of reactive objects, consider if a Reaction is more appropriate, especially for networked state."
},
],
},
{
title: 'Map Methods',
entries: [
{
signature: 'map:get(): {[K]: V}',
description: 'Retrieves the current dictionary/map. If called within a Computed or Effect, it establishes a dependency on this Map.',
returns: { type: '{[K]: V}', description: 'The current dictionary/map.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Map = Chemical.Map
local settings = Map({ volume = 0.8, difficulty = "hard" })
local currentSettings = settings:get()
-- currentSettings is { volume = 0.8, difficulty = "hard" }
`,
},
{
signature: 'map:set(newObject: {[K]: V})',
description: 'Replaces the entire dictionary/map with a new one. This is a significant change and will notify all dependents.',
parameters: [
{ name: 'newObject', type: '{[K]: V}', description: 'The new dictionary/map (plain Lua table) to set.' },
],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Map = Chemical.Map
local config = Map({ enabled = true })
config:set({ enabled = false, mode = "advanced" })
-- config:get() is now { enabled = false, mode = "advanced" }
`,
},
{
signature: 'map:key(key: K, value?: V)',
description: 'Sets or removes a key-value pair in the map. If `value` is provided, the key is set to this value. If `value` is `nil`, the key is removed from the map.',
parameters: [
{ name: 'key', type: 'K', description: 'The key to set or remove.' },
{ name: 'value', type: 'V?', description: 'The value to set for the key. If nil, the key is removed.' },
],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Map = Chemical.Map
local user = Map({ name = "Alice" })
user:key("age", 30) -- user:get() is now { name = "Alice", age = 30 }
user:key("name", nil) -- user:get() is now { age = 30 }
user:key("status", "active") -- user:get() is { age = 30, status = "active" }
`,
},
{
signature: 'map:clear(cleanup?: (value: V) -> ())',
description: 'Removes all key-value pairs from the map. An optional cleanup function can be provided, which will be called for each value (recursively for nested plain tables).',
parameters: [
{ name: 'cleanup', type: '((value: V) -> ())?', description: 'Optional function to call for each value being cleared.'}
],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Map = Chemical.Map
local data = Map({ a = 1, b = { c = 2 } })
data:clear(function(value)
print("Cleaning up:", value) -- For 'a', value is 1. For 'b', value is {c=2}. For 'c', value is 2.
end)
-- data:get() is now {}
`,
notes: "If values within the map are Chemical stateful objects (e.g., if the Map is part of a Reaction), their own :destroy() methods should be managed by the Reaction's destruction or explicit calls, not typically by Map:clear's cleanup."
},
{
signature: 'map:destroy()',
description: 'Destroys the Map object and its resources. If the Map is part of a Reaction, this destruction is typically handled by the Reaction itself.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Map = Chemical.Map
local sessionData = Map({ id = "xyz", expires = os.time() + 3600 })
-- ...
sessionData:destroy()
`,
},
],
},
];
export const MapPage: React.FC = () => {
return <ContentPage title="Chemical.Map" sections={mapApi} />;
};

131
pages/api/ObserverPage.tsx Normal file
View file

@ -0,0 +1,131 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { InfoPanel } from '../../components/InfoPanel';
import { ApiSection } from '../../types';
const observerApi: ApiSection[] = [
{
title: 'Chemical.Observer',
description: 'Creates an observer that reacts to changes in a stateful source (Value, Table, Map, or Computed). It allows you to register callbacks that fire when the source changes.',
entries: [
{
signature: 'Chemical.Observer<T>(sourceObject: Stateful<T>): Observer<T>',
description: 'Constructs a new Observer for the given stateful source.',
parameters: [
{ name: 'sourceObject', type: 'Stateful<T>', description: 'The Value, Table, Map, or Computed object to observe.' },
],
returns: { type: 'Observer<T>', description: 'A new Observer object.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Map = Chemical.Map
local Observer = Chemical.Observer
local name = Value("Alice")
local nameObserver = Observer(name)
local settings = Map({ theme = "dark" })
local settingsObserver = Observer(settings)
`,
},
],
},
{
title: 'Observer Methods',
entries: [
{
signature: 'observer:onChange(callback: (newValue: T, oldValue: T) -> ()): Connection',
description: 'Registers a callback function that will be invoked when the observed source value changes. The callback receives the new and old values.',
parameters: [
{ name: 'callback', type: '(newValue: T, oldValue: T) -> ()', description: 'The function to call on change.' },
],
returns: { type: 'Connection', description: 'A connection object with a `:disconnect()` method to stop listening.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Observer = Chemical.Observer
local age = Value(25)
local ageObs = Observer(age)
local connection = ageObs:onChange(function(newAge, oldAge)
print("Age changed from " .. oldAge .. " to " .. newAge)
end)
age:set(26) -- Output: Age changed from 25 to 26 (callback fires immediately)
connection:disconnect() -- Stop observing
age:set(27) -- Callback will not fire
`,
},
{
signature: 'observer:onKVChange(callback: (path: (string|number)[], newValue: any, oldValue: any) -> ()): Connection',
description: 'If the observed source is a Table or Map, this registers a callback for fine-grained changes to its keys/values. The callback receives the path (array of keys/indices) to the changed element, the new value at that path, and the old value. This is useful for deep comparison of table structures.',
parameters: [
{ name: 'callback', type: '(path, newValue, oldValue) -> ()', description: 'The function to call on key-value change.' },
],
returns: { type: 'Connection', description: 'A connection object with a `:disconnect()` method.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Map = Chemical.Map
local Observer = Chemical.Observer
local userProfile = Map({
name = "Bob",
details = { "email", "phone" } -- Plain table for details
})
local profileObs = Observer(userProfile)
profileObs:onKVChange(function(path, newValue, oldValue)
print("Profile changed at path:", table.concat(path, "."), "from", oldValue, "to", newValue)
end)
userProfile:key("name", "Robert")
-- Output: Profile changed at path: name from Bob to Robert (callback fires immediately)
-- To observe changes within the 'details' plain table, you'd typically
-- replace the 'details' table itself or use a Chemical.Table if 'details' needed its own reactivity.
local newDetails = table.clone(userProfile:get().details)
newDetails[1] = "new_email@example.com"
userProfile:key("details", newDetails)
-- Output: Profile changed at path: details from table: XXX to table: YYY (actual addresses will vary)
-- And then another event for the specific change if the new table is compared deeply:
-- Output: Profile changed at path: details.1 from email to new_email@example.com
`,
notes: "This method is only effective if the source object's value is a table/map and it's marked for deep comparison (Chemical handles this for Table/Map types)."
},
{
signature: 'observer:destroy()',
description: 'Destroys the Observer object, disconnecting all its registered callbacks and cleaning up resources.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Observer = Chemical.Observer
local status = Value("active")
local statusObs = Observer(status)
statusObs:onChange(function() print("Status updated") end)
-- ... later
statusObs:destroy()
status:set("inactive") -- The onChange callback will no longer fire
`,
},
],
},
];
export const ObserverPage: React.FC = () => {
return (
<ContentPage title="Chemical.Observer" sections={observerApi}>
<InfoPanel type="note" title="Execution Timing">
<p>
Callbacks registered with an Observer (e.g., <code>observer:onChange()</code>, <code>observer:onKVChange()</code>)
are executed <strong>immediately and synchronously</strong> when the observed source's state is
updated (e.g., via <code>:set()</code> for a <code>Value</code>, or through relevant methods for <code>Table</code>/<code>Map</code>).
</p>
</InfoPanel>
</ContentPage>
);
};

165
pages/api/ReactionPage.tsx Normal file
View file

@ -0,0 +1,165 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { ApiSection } from '../../types';
const reactionApi: ApiSection[] = [
{
title: 'Chemical.Reaction',
description: 'A Reaction is a stateful container, specifically designed for network replication when created by a `Reactor`. It holds reactive state (Values, Tables, Maps, or even nested Reactions) that can be synchronized between server and client. Properties of the container defined in the Reactor constructor are directly accessible on the Reaction instance itself due to proxying.',
entries: [
{
signature: 'Reaction<T>(name: string, key: string, container: T): Reaction<T>',
description: 'Constructs a new Reaction. Typically, Reactions are created via `Chemical.Reactor:create()` on the server and `Chemical.Reactor:await()` on the client, not directly using this constructor by the end-user for networked state.',
parameters: [
{ name: 'name', type: 'string', description: 'The name of the Reactor this Reaction belongs to.' },
{ name: 'key', type: 'string', description: 'A unique key identifying this Reaction instance within its Reactor.' },
{ name: 'container', type: 'T', description: 'The initial data structure (often a table containing Chemical Values, Tables, etc.) for the Reaction.' },
],
returns: { type: 'Reaction<T>', description: 'A new Reaction object.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Map = Chemical.Map
local Reaction = Chemical.Reaction -- For conceptual understanding
-- This is how a Reactor might internally create a Reaction.
-- User code typically uses Reactor:create() or Reactor:await().
-- Hypothetical internal creation:
local myPlayerData = Reaction("PlayerData", "player123", {
health = Value(100),
position = Map({ x = 0, y = 0, z = 0 })
})
-- Direct access (due to proxying):
myPlayerData.health:set(90)
print(myPlayerData.position:get().x) -- Accessing nested Map's value
`,
notes: "End-users primarily interact with Reactions through the `Reactor` API for creation and synchronization. Properties of the Reaction's container are directly accessible on the Reaction instance."
},
],
},
{
title: 'Reaction Properties (Conceptual)',
description: 'Reactions, when created by a Reactor, have these conceptual properties managed by the system for networking.',
entries: [
{
signature: 'reaction.Name: string',
description: 'The name of the Reactor that this Reaction instance belongs to.',
example: ` -- (Conceptual access)
-- local myReaction = PlayerDataReactor:create("player1")
-- print(myReaction.Name) -- "PlayerData"
`
},
{
signature: 'reaction.Key: string',
description: 'The unique key identifying this Reaction instance.',
example: ` -- (Conceptual access)
-- local myReaction = PlayerDataReactor:create("player1")
-- print(myReaction.Key) -- "player1"
`
},
{
signature: 'reaction.To: {Player} | Symbol',
description: 'Specifies which players this Reaction is replicated to. Set by the Reactor configuration.',
example: ` -- (Conceptual access, set during Reactor config)
-- reaction.To might be Chemical.Symbols.All("Players") or an array of Player objects.
`
}
]
},
{
title: 'Reaction Methods',
description: 'Properties of the Reaction\'s container are directly accessible. Reaction instances also provide the following methods:',
entries: [
{
signature: 'reaction:get(): T',
description: 'Retrieves the raw, underlying Lua table/container object that was returned by the Reactor\'s constructor. This is useful if you need the plain data structure itself, without Chemical\'s proxying or methods. If called within a Computed or Effect, it establishes a dependency on the entire Reaction container.',
returns: { type: 'T', description: 'The raw internal container of the Reaction.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
-- Assuming 'playerDataReaction' is a Reaction created by a Reactor,
-- and its constructor returned a table like:
-- { stats = Chemical.Map({ strength = Value(10) }) }
-- Direct access to proxied properties:
playerDataReaction.stats:get().strength:set(12) -- Works directly on the Chemical.Value
print(playerDataReaction.stats:get().strength:get()) -- Output: 12
-- Using :get() to retrieve the raw container:
local rawPlayerDataContainer = playerDataReaction:get()
-- rawPlayerDataContainer is the actual table: { stats = <Chemical.Map object> }
local rawStatsMap = rawPlayerDataContainer.stats -- This is the Chemical.Map object
local rawStrengthValue = rawStatsMap:get().strength -- This is the Chemical.Value object
print(rawStrengthValue:get()) -- Output: 12
`,
},
{
signature: 'reaction:destroy()',
description: 'Destroys the Reaction object. On the server, this also triggers a destroy message to clients. Cleans up all nested Chemical objects within its scope.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
-- Assuming 'playerDataReaction' is an existing Reaction
-- Server-side
playerDataReaction:destroy() -- Notifies clients, cleans up server resources.
-- Client-side
-- The Reaction will be destroyed automatically when server sends the message.
`,
},
{
signature: 'reaction:serialize(): any',
description: 'Returns a version of the Reaction\'s state suitable for serialization (e.g., for saving to a DataStore). It typically includes only primitive values from nested stateful objects.',
returns: { type: 'any', description: 'A serializable representation of the Reaction\'s data.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Table = Chemical.Table
local Reaction = Chemical.Reaction -- For example context
local reaction = Reaction("MyData", "item1", {
name = Value("Test Item"),
count = Value(5),
tags = Table({"tag1", "tag2"})
})
local serializedData = reaction:serialize()
-- serializedData might be: { name = "Test Item", count = 5, tags = {"tag1", "tag2"} }
`,
},
{
signature: 'reaction:snapshot(): any',
description: 'Returns a deep copy of the current state of the Reaction, with nested stateful objects represented by their current values.',
returns: { type: 'any', description: 'A snapshot of the Reaction\'s data.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value -- Assuming Value is used inside Reaction
-- Assuming 'myReaction' exists with a structure like { name = Value("Snap") }
local snap = myReaction:snapshot()
-- snap would be { name = "Snap" } (the raw value)
`,
},
{
signature: 'reaction:blueprint(): Blueprint | { Blueprint }',
description: 'Generates a blueprint of the Reaction\'s structure and initial values. This is used internally for replication to describe how to reconstruct the Reaction on the client.',
returns: { type: 'Blueprint | { Blueprint }', description: 'A blueprint object or table of blueprints.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
-- Assuming 'myReaction' exists
-- Used internally by the Reactor system.
local bp = myReaction:blueprint()
-- 'bp' would contain type information (T) and values (V) for reconstruction.
`,
notes: "This is primarily for internal use by the Chemical library."
},
],
},
];
export const ReactionPage: React.FC = () => {
return <ContentPage title="Chemical.Reaction" sections={reactionApi} />;
};

130
pages/api/ReactorPage.tsx Normal file
View file

@ -0,0 +1,130 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { ApiSection } from '../../types';
const reactorApi: ApiSection[] = [
{
title: 'Chemical.Reactor',
description: 'A Reactor is a server-side factory for creating and managing `Reaction` objects. Reactions are stateful objects whose state is automatically replicated from the server to specified clients.',
entries: [
{
signature: 'Chemical.Reactor<T, U...>(config: ReactorConfig, constructor: (key: string, ...args: U) => T): ReactorInstance<T, U>',
description: 'Creates a new Reactor factory.',
parameters: [
{ name: 'config', type: 'ReactorConfig', description: 'Configuration for the Reactor. Must include a unique `Name`. Optionally, `Subjects` can specify which players receive updates (defaults to all players).' },
{ name: 'constructor', type: '(key: string, ...args: U) => T', description: "A function that returns the initial state structure for a new Reaction. This function receives the `key` of the Reaction (e.g., a player's ID) and any additional arguments passed to `reactor:create()`. The provided `key` is primarily for the Reactor system's internal identification and typically does not need to be stored as a field in the state object returned by this constructor, as the Reaction instance will have `.Name` and `.Key` properties managed by Chemical." },
],
returns: { type: 'ReactorInstance<T, U>', description: 'A new Reactor instance.' },
notes: 'ReactorConfig: `{ Name: string, Subjects?: Player[] | Symbol }`. The `Symbol` can be `Chemical.Symbols.All("Players")`. `T` is the type of the state structure returned by the constructor. `U...` are the types of additional arguments for the constructor.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Table = Chemical.Table
local Reactor = Chemical.Reactor
local Symbols = Chemical.Symbols
-- Define a Reactor for player game data
local PlayerGameDataReactor = Reactor({
Name = "PlayerGameData",
-- Subjects = {player1, player2} -- Optional: replicate only to specific players
-- Subjects = Symbols.All("Players") -- Default: replicate to all
}, function(playerIdString, initialLives, initialScore)
-- This constructor defines the structure of each Reaction.
-- Note: 'playerIdString' (the key) is passed here but typically
-- not stored directly in this returned table, as the Reaction object
-- will have .Key and .Name properties managed by Chemical.
return {
lives = Value(initialLives or 3),
score = Value(initialScore or 0),
inventory = Table({})
}
end)
`,
},
],
},
{
title: 'ReactorInstance Methods',
entries: [
{
signature: 'reactor:create(key: string, ...args: U): Reaction<T> -- Server-Only',
description: 'Creates a new `Reaction` instance managed by this Reactor. The Reaction\'s state will be initialized using the constructor function provided when the Reactor was defined. This method can only be called on the server.',
parameters: [
{ name: 'key', type: 'string', description: 'A unique key to identify this Reaction instance (e.g., a player\'s UserId).' },
{ name: '...args', type: 'U', description: 'Additional arguments to pass to the Reaction\'s constructor function.' },
],
returns: { type: 'Reaction<T>', description: 'The newly created (and replicating) Reaction object.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value -- Assuming Value is used in constructor
local Table = Chemical.Table -- Assuming Table is used in constructor
-- Assuming PlayerGameDataReactor is defined as in the previous example
-- Server-side:
local player1Data = PlayerGameDataReactor:create("player1_id", 5, 1000)
-- player1Data.lives:get() would be 5
-- player1Data.score:get() would be 1000
player1Data.score:set(1500) -- Access directly; this change will be replicated.
player1Data.inventory:insert("Sword") -- Access directly
`,
},
{
signature: 'reactor:await(key: string): Reaction<T> -- Client-Only',
description: 'Yields (waits) until a `Reaction` instance, created on the server and managed by this Reactor, becomes available on the client. It then returns the Reaction object directly.',
parameters: [
{ name: 'key', type: 'string', description: 'The unique key of the Reaction instance to await.' },
],
returns: { type: 'Reaction<T>', description: 'The Reaction object, once available.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Effect = Chemical.Effect
-- Assuming PlayerGameDataReactor is defined and available on the client
-- and its constructor creates { lives = Value(), score = Value(), inventory = Table() }
-- Client-side:
local player1Data = PlayerGameDataReactor:await("player1_id") -- Yields until data is ready
-- Now player1Data is the actual Reaction object
if player1Data then
print("Player 1 score:", player1Data.score:get())
Effect(function()
print("Player 1 lives updated to:", player1Data.lives:get())
print("Player 1 inventory count:", #player1Data.inventory:get())
end)
else
warn("Failed to get player data for player1_id")
end
`,
},
{
signature: 'reactor:onCreate(callback: (key: string, reaction: Reaction<T>) -> ()): Connection -- Client-Only',
description: 'Registers a callback function that will be invoked whenever a new `Reaction` instance managed by this Reactor is created and replicated to the client. This is useful for reacting to dynamically created Reactions.',
parameters: [
{ name: 'callback', type: '(key: string, reaction: Reaction<T>) -> ()', description: 'The function to call. It receives the `key` of the new Reaction and the Reaction object itself.' },
],
returns: { type: 'Connection', description: 'A connection object with a `:Disconnect()` method to stop listening.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
-- Assuming PlayerGameDataReactor is defined and available on the client
-- Client-side:
local connection = PlayerGameDataReactor:onCreate(function(playerId, playerData)
print("New player data received for:", playerId)
-- Access properties directly:
-- print("Initial score for new player:", playerData.score:get())
end)
-- To stop listening:
-- connection:Disconnect()
`,
},
],
},
];
export const ReactorPage: React.FC = () => {
return <ContentPage title="Chemical.Reactor" sections={reactorApi} />;
};

127
pages/api/SymbolsPage.tsx Normal file
View file

@ -0,0 +1,127 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { ApiSection } from '../../types';
const symbolsApi: ApiSection[] = [
{
title: 'Chemical.Symbols',
description: 'Chemical uses special "Symbol" objects for certain keys in the `Compose` API, particularly for event handling and child management. These ensure uniqueness and provide type information.',
entries: [
{
signature: 'Chemical.OnEvent(eventName: GuiTypes.EventNames): Symbol',
description: 'Creates a symbol representing a Roblox UI event. Used as a key in the `Compose` properties table to attach an event listener.',
parameters: [
{ name: 'eventName', type: 'string (GuiTypes.EventNames)', description: 'The name of the Roblox GUI event (e.g., "Activated", "MouseButton1Click", "MouseEnter").' },
],
returns: { type: 'Symbol', description: 'An event symbol.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Compose = Chemical.Compose
local OnEvent = Chemical.OnEvent
local Children = Chemical.Symbols.Children -- Alias for Children key
-- Assuming playerGui is defined
local MyScreen = Compose("ScreenGui")({
Parent = playerGui,
[Children] = { -- Use the aliased Children symbol
Compose("TextButton")({
Text = "Click",
[OnEvent("Activated")] = function()
print("Button Activated!")
end
})
}
})
`,
},
{
signature: 'Chemical.OnChange(propertyName: GuiTypes.PropertyNames): Symbol',
description: 'Creates a symbol representing a property change event for a Roblox Instance. Used as a key in the `Compose` properties table to listen to `GetPropertyChangedSignal`.',
parameters: [
{ name: 'propertyName', type: 'string (GuiTypes.PropertyNames)', description: 'The name of the Instance property to observe (e.g., "Text", "Visible", "Size").' },
],
returns: { type: 'Symbol', description: 'A property change symbol.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Compose = Chemical.Compose
local OnChange = Chemical.OnChange
local Children = Chemical.Symbols.Children -- Alias for Children key
local Effect = Chemical.Effect
-- Assuming playerGui is defined
local currentText = Value("")
local MyScreen = Compose("ScreenGui")({
Parent = playerGui,
[Children] = { -- Use the aliased Children symbol
Compose("TextBox")({
PlaceholderText = "Type here",
[OnChange("Text")] = currentText -- Bind TextBox's Text property to currentText Value
})
}
})
Effect(function()
print("TextBox content:", currentText:get())
end)
`,
},
{
signature: 'Chemical.Symbols.Children: Symbol',
description: 'A pre-defined symbol used as a key in the `Compose` properties table to specify an array of child Instances or Compositions.',
returns: { type: 'Symbol', description: 'The children symbol.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Compose = Chemical.Compose
local Children = Chemical.Symbols.Children -- Alias the symbol
-- Assuming playerGui exists
local MyScreen = Compose("ScreenGui")({
Parent = playerGui,
[Children] = { -- Use the aliased Children symbol
Compose("Frame")({
Name = "ParentFrame",
Size = UDim2.new(0.5, 0, 0.5, 0),
Position = UDim2.fromScale(0.25, 0.25),
BackgroundColor3 = Color3.fromRGB(50,50,50),
[Children] = { -- Use the aliased Children symbol
Compose("TextLabel")({ Text = "Child 1", Size = UDim2.new(1,0,0.5,0) }),
Compose("TextLabel")({ Text = "Child 2", Position = UDim2.fromScale(0, 0.5), Size = UDim2.new(1,0,0.5,0) })
}
})
}
})
`,
},
{
signature: 'Chemical.Symbols.All(subjectType: string): Symbol',
description: 'Creates a generic symbol typically used to represent "all instances of a certain type" or a global subject, for example, in Reactor configurations to target all players.',
parameters: [
{ name: 'subjectType', type: 'string', description: 'A string describing the subject (e.g., "Players").' }
],
returns: { type: 'Symbol', description: 'A generic "All" symbol.'},
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Reactor = Chemical.Reactor
local Value = Chemical.Value
local Symbols = Chemical.Symbols -- Keep this for Symbols.All access
local AllPlayers = Symbols.All("Players")
-- Used in Reactor configuration
local GlobalReactor = Reactor({
Name = "GlobalState",
Subjects = AllPlayers -- Replicates to all players
}, function()
return { message = Value("Hello All!") }
end)
`
}
],
},
];
export const SymbolsPage: React.FC = () => {
return <ContentPage title="Chemical Symbols" sections={symbolsApi} />;
};

174
pages/api/TablePage.tsx Normal file
View file

@ -0,0 +1,174 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { ApiSection } from '../../types';
const tableApi: ApiSection[] = [
{
title: 'Chemical.Table',
description: 'Creates a reactive array (table with numerical indices). Changes to the array structure can trigger updates in dependent Computeds and Effects. For reactivity of individual elements, those elements would typically be Chemical.Value objects if fine-grained updates on element changes are needed, though this pattern is more common within Reactions.',
entries: [
{
signature: 'Chemical.Table<T[]>(initialArray: T[]): Table<T[]>',
description: 'Constructs a new reactive Table object.',
parameters: [
{ name: 'initialArray', type: 'T[]', description: 'The initial array (plain Lua table) to store.' },
],
returns: { type: 'Table<T[]>', description: 'A new Table object.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local userScores = Table({100, 150, 90})
local userNames = Table({"Alice", "Bob"})
`,
notes: "Chemical.Table is best used with arrays of primitive values or plain tables. If you need an array of reactive objects, consider if a Reaction is more appropriate for managing that collection, especially for networked state."
},
],
},
{
title: 'Table Methods',
entries: [
{
signature: 'table:get(): T[]',
description: 'Retrieves the current array. If called within a Computed or Effect, it establishes a dependency on this Table.',
returns: { type: 'T[]', description: 'The current array.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local colors = Table({"red", "green"})
local currentColors = colors:get() -- currentColors is {"red", "green"}
`,
},
{
signature: 'table:set(newArray: T[])',
description: 'Replaces the entire array with a new one. This is a significant change and will notify all dependents.',
parameters: [
{ name: 'newArray', type: 'T[]', description: 'The new array (plain Lua table) to set.' },
],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local items = Table({1, 2})
items:set({3, 4, 5}) -- items:get() is now {3, 4, 5}
`,
},
{
signature: 'table:insert(value: V)',
description: 'Inserts a value at the end of the array.',
parameters: [
{ name: 'value', type: 'V', description: 'The value to insert.' },
],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local fruits = Table({"apple"})
fruits:insert("banana") -- fruits:get() is now {"apple", "banana"}
`,
},
{
signature: 'table:remove(value: V): V?',
description: 'Removes the first occurrence of the specified value from the array. Returns the removed value, or nil if not found.',
parameters: [
{ name: 'value', type: 'V', description: 'The value to remove.' },
],
returns: { type: 'V?', description: 'The removed value or nil.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local numbers = Table({10, 20, 30, 20})
numbers:remove(20) -- numbers:get() is now {10, 30, 20}
`,
},
{
signature: 'table:find(value: V): number?',
description: 'Finds the index of the first occurrence of a value in the array.',
parameters: [
{ name: 'value', type: 'V', description: 'The value to find.' },
],
returns: { type: 'number?', description: 'The index of the value, or nil if not found.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local letters = Table({'a', 'b', 'c'})
local indexB = letters:find('b') -- indexB is 2
local indexD = letters:find('d') -- indexD is nil
`,
},
{
signature: 'table:setAt(index: number, value: V)',
description: 'Sets the value at a specific index in the array.',
parameters: [
{ name: 'index', type: 'number', description: 'The 1-based index to set.' },
{ name: 'value', type: 'V', description: 'The value to set at the index.' },
],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local data = Table({1, 2, 3})
data:setAt(2, 99) -- data:get() is now {1, 99, 3}
`,
},
{
signature: 'table:getAt(index: number): V?',
description: 'Retrieves the value at a specific index in the array.',
parameters: [
{ name: 'index', type: 'number', description: 'The 1-based index to get.' },
],
returns: { type: 'V?', description: 'The value at the index, or nil if out of bounds.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local names = Table({"Eve", "Adam"})
local firstName = names:getAt(1) -- firstName is "Eve"
`,
},
{
signature: 'table:clear()',
description: 'Removes all elements from the array.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local tasks = Table({"task1", "task2"})
tasks:clear() -- tasks:get() is now {}
`,
},
{
signature: 'table:destroy()',
description: 'Destroys the Table object and its resources.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local activeUsers = Table({"userA"})
-- ...
activeUsers:destroy()
`,
},
{
signature: '#table: number (via __len metamethod)',
description: 'Returns the number of elements in the array. Can be accessed using the `#` operator.',
returns: { type: 'number', description: 'The length of the array.'},
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Table = Chemical.Table
local list = Table({10, 20, 30})
print(#list) -- Output: 3
`
}
],
},
];
export const TablePage: React.FC = () => {
return <ContentPage title="Chemical.Table" sections={tableApi} />;
};

168
pages/api/UtilitiesPage.tsx Normal file
View file

@ -0,0 +1,168 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { ApiSection } from '../../types';
const utilitiesApi: ApiSection[] = [
{
title: 'Chemical.Is',
description: 'A collection of functions to check the type or state of Chemical objects or other values.',
entries: [
{ signature: 'Is.Stateful(obj: any): boolean', description: 'Checks if `obj` is a Chemical stateful object (Value, Table, Map, Computed, Reaction).', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Value = Chemical.Value\nlocal Is = Chemical.Is\n\nlocal v = Value(1) \nprint(Is.Stateful(v)) -- true` },
{ signature: 'Is.Settable(obj: any): boolean', description: 'Checks if `obj` is a Chemical object that has a `:set()` method (Value, Table, Map).', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Value = Chemical.Value\nlocal Is = Chemical.Is\n\nlocal v = Value(1) \nprint(Is.Settable(v)) -- true` },
{ signature: 'Is.Primitive(obj: any): boolean', description: 'Checks if `obj` is a Lua primitive type (string, number, boolean, nil).', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Is = Chemical.Is\n\nprint(Is.Primitive(123)) -- true \nprint(Is.Primitive({})) -- false` },
{ signature: 'Is.Literal(obj: any): boolean', description: 'Checks if `obj` is a Lua literal (string, number, boolean, nil - excludes functions, tables, userdata, threads).', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Is = Chemical.Is\n\nprint(Is.Literal(true)) -- true \nprint(Is.Literal(function() end)) -- false` },
{ signature: 'Is.Symbol(obj: any, typeOf?: string): boolean', description: 'Checks if `obj` is a Chemical Symbol, optionally checking for a specific symbol type (e.g., "Event", "Change").', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal OnEvent = Chemical.OnEvent\nlocal Is = Chemical.Is\n\nlocal onAct = OnEvent("Activated") \nprint(Is.Symbol(onAct)) -- true \nprint(Is.Symbol(onAct, "Event")) -- true` },
{ signature: 'Is.Array(obj: any): boolean', description: 'Checks if `obj` is a plain Lua table intended to be used as an array (not a Chemical Table/Map).', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Is = Chemical.Is\n\nprint(Is.Array({1,2,3})) -- true` },
{ signature: 'Is.StatefulTable(obj: any): boolean', description: 'Checks if `obj` is a Chemical.Table instance.', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Table = Chemical.Table\nlocal Is = Chemical.Is\n\nlocal t = Table({}) \nprint(Is.StatefulTable(t)) -- true` },
{ signature: 'Is.StatefulDictionary(obj: any): boolean', description: 'Checks if `obj` is a Chemical.Map instance.', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Map = Chemical.Map\nlocal Is = Chemical.Is\n\nlocal m = Map({}) \nprint(Is.StatefulDictionary(m)) -- true` },
{ signature: 'Is.Blueprint(obj: any): boolean', description: 'Checks if `obj` is a Chemical blueprint object (used internally for replication).', example: `-- Internal use mostly` },
{ signature: 'Is.Dead(obj: any): boolean', description: 'Checks if a Chemical object has been destroyed (i.e., its `__destroyed` flag is true).', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Value = Chemical.Value\nlocal Is = Chemical.Is\n\nlocal v = Value(1) \nv:destroy() \nprint(Is.Dead(v)) -- true` },
{ signature: 'Is.Destroyed(obj: Instance): boolean', description: 'Checks if a Roblox Instance has been destroyed (its Parent is nil and cannot be reparented).', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Is = Chemical.Is\n\nlocal part = Instance.new("Part") \npart:Destroy() \nprint(Is.Destroyed(part)) -- true` },
],
},
{
title: 'Chemical.Peek',
description: 'Retrieves the current value of a stateful object without establishing a reactive dependency. Useful for inspecting state within functions where you do not want to trigger re-computation or re-execution if that state changes later.',
entries: [
{
signature: 'Chemical.Peek<T>(statefulObject: Stateful<T>): T',
description: 'Gets the raw value of a Chemical stateful object (Value, Computed, etc.).',
parameters: [ { name: 'statefulObject', type: 'Stateful<T>', description: 'The Chemical object to peek into.' }],
returns: { type: 'T', description: 'The raw underlying value.'},
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local Peek = Chemical.Peek
local count = Value(10)
local doubled = Computed(function()
-- If we used count:get() here, this Computed would depend on it.
-- Using Peek avoids that dependency for this specific logic,
-- though it's unusual to use Peek inside a Computed's main function.
local currentCount = Peek(count)
print("Peeking at count:", currentCount) -- This line won't cause re-computation if count changes later just because of this peek.
return currentCount * 2
end)
local val = Peek(doubled) -- Gets the current computed value without depending on 'doubled'.
print(val) -- 20
`,
},
],
},
{
title: 'Chemical.Array',
description: 'Utility functions for working with Lua tables (arrays/dictionaries), often in conjunction with Chemical stateful objects.',
entries: [
{ signature: 'Array.Transform<K, V, R>(tbl: {[K]: V}, doFn: (k: K, v: V) => R): {[K]: R}', description: 'Recursively transforms values in a table. If a value is a table, it\'s transformed recursively. Otherwise, `doFn` is called.', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Array = Chemical.Array\n\nlocal data = { a = 1, b = { c = 2 } } \nlocal transformed = Array.Transform(data, function(k, v) return v * 10 end) \n-- transformed is { a = 10, b = { c = 20 } }` },
{ signature: 'Array.ShallowTransform<K, V, R>(tbl: {[K]: V}, doFn: (k: K, v: V) => R): {[K]: R}', description: 'Shallowly transforms values in a table using `doFn`. Does not recurse.', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Array = Chemical.Array\n\nlocal data = { a = 1, b = { c = 2 } } \nlocal transformed = Array.ShallowTransform(data, function(k, v) return typeof(v) == "number" and v * 10 or v end) \n-- transformed is { a = 10, b = { c = 2 } }` },
{ signature: 'Array.Traverse(tbl: {}, doFn: (k: any, v: any) -> ())', description: 'Recursively traverses a table, calling `doFn` for non-table values.', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Array = Chemical.Array\n\nlocal data = { a = 1, b = { c = 2 } } \nArray.Traverse(data, function(k, v) print(k, v) end)` },
{ signature: 'Array.Walk(target: {any}, visitor: (path: {any}, value: any) -> ())', description: 'Recursively walks a table, calling `visitor` with the path (array of keys) and value for every node.', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Array = Chemical.Array\n\nlocal data = { a = 1, b = { c = 2 } } \nArray.Walk(data, function(path, value) print(table.concat(path, "."), value) end)` },
{ signature: 'Array.FindOnPath<K, V>(tbl: {[K]: V}, path: (string|number)[]): V', description: 'Retrieves a value from a nested table using an array of keys/indices representing the path.', example: `local Chemical = require(game.ReplicatedStorage.Chemical)\nlocal Array = Chemical.Array\n\nlocal data = { user = { profile = { name = "Alice" } } } \nlocal name = Array.FindOnPath(data, {"user", "profile", "name"}) \n-- name is "Alice"` },
],
},
{
title: 'Chemical.Alive',
description: 'Checks if a Chemical object (which has an `.entity` property) is still "alive" in the underlying ECS world (i.e., its entity has not been deleted).',
entries: [
{
signature: 'Chemical.Alive(obj: HasEntity): boolean',
description: 'Returns true if the Chemical object\'s entity still exists in the ECS world, false otherwise.',
parameters: [ { name: 'obj', type: 'HasEntity', description: 'A Chemical object with an `.entity` property.' }],
returns: {type: 'boolean', description: 'True if alive, false otherwise.'},
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Alive = Chemical.Alive
local myValue = Value(10)
print(Alive(myValue)) -- true
myValue:destroy()
print(Alive(myValue)) -- false (or will error if myValue itself is nilled out)
`,
},
],
},
{
title: 'Chemical.Destroy',
description: 'A versatile function to destroy various types of objects, including Chemical objects, Roblox Instances, RBXScriptConnections, or even call cleanup functions.',
entries: [
{
signature: 'Chemical.Destroy(subject: any)',
description: 'Attempts to destroy the given subject. It intelligently handles different types: \n- Chemical objects: Calls their `:destroy()` method. \n- Instances: Calls `:Destroy()`. \n- Connections: Calls `:Disconnect()`. \n- Tables: Clears and nils out metatable, or recursively destroys elements if it\'s an array of destroyables. \n- Functions: Calls the function. \n- Threads: Calls `task.cancel()` on the thread.',
parameters: [ { name: 'subject', type: 'any', description: 'The item to destroy.' }],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Destroy = Chemical.Destroy
local val = Value(10)
local part = Instance.new("Part")
local conn = part.Touched:Connect(function() end)
local tbl = { Value(1), Instance.new("Frame") }
local cleanupFn = function() print("cleaned") end
Destroy(val)
Destroy(part)
Destroy(conn)
Destroy(tbl) -- Destroys Value(1) and Frame
Destroy(cleanupFn) -- Prints "cleaned"
`,
},
],
},
{
title: 'Chemical.Blueprint',
description: 'Functions related to creating and reading "blueprints" of Chemical state. This is primarily used internally for state replication (e.g., by `Reactor`).',
entries: [
{
signature: 'Blueprint.From(value: any): BlueprintData',
description: 'Creates a blueprint from a given value or Chemical stateful object. The blueprint describes the structure and initial data, including types of Chemical objects.',
parameters: [ { name: 'value', type: 'any', description: 'The value or Chemical object to blueprint.'} ],
returns: { type: 'BlueprintData', description: 'A data structure representing the blueprint.'},
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Map = Chemical.Map
local Blueprint = Chemical.Blueprint
local Tags = Chemical.ECS.Tags -- Assuming ECS is exposed this way for example
local myMap = Map({
name = Value("Test"),
count = 10
})
local bp = Blueprint.From(myMap)
-- bp would be a table like:
-- { T = Tags.IsStatefulDictionary, V = {
-- name = { T = Tags.IsStateful, V = "Test" },
-- count = { T = Tags.IsStatic, V = 10 }
-- }
-- }
`,
notes: '`BlueprintData` typically looks like `{ T = EntityTag, V = valueOrNestedBlueprints }`.'
},
{
signature: 'Blueprint.Read(blueprintData: BlueprintData): any',
description: 'Reconstructs a Chemical stateful object or plain Lua value from a blueprint.',
parameters: [ { name: 'blueprintData', type: 'BlueprintData', description: 'The blueprint data to read.'} ],
returns: { type: 'any', description: 'The reconstructed Chemical object or value.'},
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Blueprint = Chemical.Blueprint
-- (Assuming 'bp' from the Blueprint.From example and relevant Tags are accessible)
local reconstructedMap = Blueprint.Read(bp)
-- reconstructedMap would be a new Chemical.Map equivalent to myMap
print(reconstructedMap:get().name:get()) -- Output: Test
`,
},
],
},
];
export const UtilitiesPage: React.FC = () => {
return <ContentPage title="Utility Functions" sections={utilitiesApi} />;
};

112
pages/api/ValuePage.tsx Normal file
View file

@ -0,0 +1,112 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { ApiSection } from '../../types';
const valueApi: ApiSection[] = [
{
title: 'Chemical.Value',
description: 'Creates a reactive value container. Changes to this value can be observed, and other reactive objects (like Computeds and Effects) can depend on it.',
entries: [
{
signature: 'Chemical.Value<T>(initialValue: T): Value<T>',
description: 'Constructs a new reactive Value object.',
parameters: [
{ name: 'initialValue', type: 'T', description: 'The initial value to store.' },
],
returns: { type: 'Value<T>', description: 'A new Value object.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local myNumber = Value(10)
local myString = Value("hello")
local myTable = Value({ message = "world" })
`,
},
],
},
{
title: 'Value Methods',
entries: [
{
signature: 'value:get(): T',
description: 'Retrieves the current value. If called within a Computed or Effect, it establishes a dependency on this Value.',
returns: { type: 'T', description: 'The current value.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local count = Value(5)
local currentValue = count:get() -- currentValue is 5
`,
},
{
signature: 'value:set(newValue: T)',
description: 'Updates the value. This will notify any dependents (Computeds, Effects, Observers) that the value has changed.',
parameters: [
{ name: 'newValue', type: 'T', description: 'The new value to set.' },
],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local name = Value("Guest")
name:set("User123") -- name:get() will now return "User123"
`,
},
{
signature: 'value:destroy()',
description: 'Destroys the Value object, cleaning up its resources and notifying dependents that it no longer exists. Any Effects or Computeds that depend solely on this Value might also be destroyed or cleaned up.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local temporaryValue = Value(100)
-- ... use temporaryValue ...
temporaryValue:destroy()
`,
},
],
},
{
title: 'Numerical Value Methods',
description: 'These methods are available if the Value was initialized with a number.',
entries: [
{
signature: 'value:increment(n: number)',
description: 'Increments the numerical value by `n`. If `n` is not provided, it defaults to 1.',
parameters: [
{ name: 'n', type: 'number', description: 'The amount to increment by (defaults to 1).' },
],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local counter = Value(0)
counter:increment(5) -- counter:get() is now 5
counter:increment() -- counter:get() is now 6
`,
},
{
signature: 'value:decrement(n: number)',
description: 'Decrements the numerical value by `n`. If `n` is not provided, it defaults to 1.',
parameters: [
{ name: 'n', type: 'number', description: 'The amount to decrement by (defaults to 1).' },
],
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local stock = Value(10)
stock:decrement(2) -- stock:get() is now 8
stock:decrement() -- stock:get() is now 7
`,
},
],
},
];
export const ValuePage: React.FC = () => {
return <ContentPage title="Chemical.Value" sections={valueApi} />;
};

99
pages/api/WatchPage.tsx Normal file
View file

@ -0,0 +1,99 @@
import React from 'react';
import { ContentPage } from '../../components/ContentPage';
import { InfoPanel } from '../../components/InfoPanel';
import { ApiSection } from '../../types';
const watchApi: ApiSection[] = [
{
title: 'Chemical.Watch',
description: 'A utility function that creates an observer for a reactive source (Value or Computed) and immediately runs a callback when the source changes. It provides a simpler API for common observation tasks compared to manually using `Chemical.Observer`.',
entries: [
{
signature: 'Chemical.Watch<T>(source: Value<T> | Computed<T>, watchCallback: (newValue: T, oldValue: T) -> ()): WatchHandle',
description: 'Creates a watcher that runs the `watchCallback` whenever the `source` reactive object changes.',
parameters: [
{ name: 'source', type: 'Value<T> | Computed<T>', description: 'The reactive Value or Computed object to watch.' },
{ name: 'watchCallback', type: '(newValue: T, oldValue: T) -> ()', description: 'The function to execute when the source changes. It receives the new and old values.' },
],
returns: { type: 'WatchHandle', description: 'A handle object with a `:destroy()` method to stop watching.' },
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Computed = Chemical.Computed
local Watch = Chemical.Watch
local playerName = Value("Player1")
local health = Value(100)
local watchHandleName = Watch(playerName, function(newName, oldName)
print("Watch (Name): Name changed from '" .. oldName .. "' to '" .. newName .. "'")
end)
local derivedStatus = Computed(function()
return playerName:get() .. " - Health: " .. health:get()
end)
local watchHandleStatus = Watch(derivedStatus, function(newStatus, oldStatus)
print("Watch (Status): Status changed from '" .. oldStatus .. "' to '" .. newStatus .. "'")
end)
playerName:set("Champion")
-- Output (Watch Name): Player name changed from 'Player1' to 'Champion' (immediately)
-- Output (Watch Status): (after scheduler runs for Computed) Status changed from 'Player1 - Health: 100' to 'Champion - Health: 100'
health:set(80)
-- Output (Watch Status): (after scheduler runs for Computed) Status changed from 'Champion - Health: 100' to 'Champion - Health: 80'
-- To stop watching:
watchHandleName:destroy()
watchHandleStatus:destroy()
`,
},
],
},
{
title: 'WatchHandle Methods',
entries: [
{
signature: 'watchHandle:destroy()',
description: 'Stops the watcher, disconnecting the underlying observer and preventing the `watchCallback` from being called on future changes to the source.',
example: `
local Chemical = require(game.ReplicatedStorage.Chemical)
local Value = Chemical.Value
local Watch = Chemical.Watch
local score = Value(100)
local handle = Watch(score, function(newScore) print("Score:", newScore) end)
score:set(110) -- Prints "Score: 110"
handle:destroy()
score:set(120) -- Does not print
`,
},
],
}
];
export const WatchPage: React.FC = () => {
return (
<ContentPage title="Chemical.Watch" sections={watchApi}>
<InfoPanel type="note" title="Execution Timing">
<ul>
<li className="mb-1">
When watching a <code>Chemical.Value</code>, the <code>watchCallback</code> is executed
<strong> immediately and synchronously</strong> when the Value is updated via <code>:set()</code>.
</li>
<li>
When watching a <code>Chemical.Computed</code>, the <code>watchCallback</code> is executed
<strong> immediately and synchronously</strong> when the <code>Computed</code> value itself
<strong> settles</strong> on its new value. Remember that <code>Computed</code> values update
asynchronously as part of the Chemical scheduler's batch processing.
</li>
</ul>
</InfoPanel>
</ContentPage>
);
};

30
tsconfig.json Normal file
View file

@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"allowJs": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"paths": {
"@/*" : ["./*"]
}
}
}

28
types.ts Normal file
View file

@ -0,0 +1,28 @@
import React from 'react';
export interface NavLinkItem {
path: string;
label: string;
icon?: React.FC<React.SVGProps<SVGSVGElement>>; // Changed from JSX.Element
}
export interface NavSection {
title: string;
links: NavLinkItem[];
}
export interface ApiDocEntry {
signature: string;
description: string | React.ReactNode;
parameters?: { name: string; type: string; description: string }[];
returns?: { type: string; description: string };
example: string;
notes?: string;
}
export interface ApiSection {
title: string;
description?: string;
entries: ApiDocEntry[];
}

17
vite.config.ts Normal file
View file

@ -0,0 +1,17 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});