Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
website/build
website/.docusaurus
website/.cache-loader
website/static/demos
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

251 changes: 237 additions & 14 deletions examples/rendering/bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/rendering/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"@codebelt/classy-store": "file:../..",
"bun-plugin-tailwind": "0.1.2",
"prism-react-renderer": "^2.4.1",
"proxy-compare": "3.0.1",
"react": "19.2.4",
"react-dom": "19.2.4",
Expand Down
120 changes: 30 additions & 90 deletions examples/rendering/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,41 @@
// This component must be the top-most import in this file!
import {ReactScan} from './ReactScan';
import {ReactScan} from './components/ReactScan';
import './index.css';
import {useEffect, useState} from 'react';
import {AsyncDemo} from './AsyncDemo';
import {CollectionsDemo} from './CollectionsDemo';
import {PersistPage} from './PersistPage';
import {ReactiveFundamentalsDemo} from './ReactiveFundamentalsDemo';
import {StructuralSharingDemo} from './StructuralSharingDemo';

// ── Hash Router ─────────────────────────────────────────────────────────────

function getRoute(): string {
const hash = globalThis.location?.hash ?? '';
return hash.replace(/^#/, '') || '/';
}

function useHashRoute() {
const [route, setRoute] = useState(getRoute);

useEffect(() => {
const handler = () => setRoute(getRoute());
globalThis.addEventListener('hashchange', handler);
return () => globalThis.removeEventListener('hashchange', handler);
}, []);

return route;
}

// ── Navigation ──────────────────────────────────────────────────────────────

function NavBar({route}: {route: string}) {
const links = [
{href: '#/', label: 'Reactivity', active: route === '/'},
{href: '#/persist', label: 'Persist', active: route === '/persist'},
];

return (
<nav className="flex items-center justify-center gap-1 mb-8">
{links.map((link) => (
<a
key={link.href}
href={link.href}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
link.active
? 'bg-zinc-800 text-zinc-100'
: 'text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/50'
}`}
>
{link.label}
</a>
))}
</nav>
);
}

// ── Reactivity Demos Page ───────────────────────────────────────────────────

function ReactivityPage() {
return (
<>
<header className="text-center mb-12">
<h1 className="text-4xl sm:text-5xl font-bold mb-3 bg-linear-to-r from-indigo-400 to-violet-400 bg-clip-text text-transparent">
@codebelt/classy-store
</h1>
<p className="text-zinc-400 max-w-xl mx-auto">
Class-based reactive state for React. Watch the render badges to see
exactly when each component re-renders.
</p>
</header>

<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<ReactiveFundamentalsDemo />
<AsyncDemo />
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<CollectionsDemo />
</div>

<div className="grid grid-cols-1 gap-6">
<StructuralSharingDemo />
</div>
</>
);
}

// ── App ─────────────────────────────────────────────────────────────────────
import {Layout} from './components/Layout';
import {useHashRoute} from './hooks/useHashRoute';
import {CollectionsPage} from './pages/CollectionsPage';
import {DevtoolsPage} from './pages/DevtoolsPage';
import {HistoryPage} from './pages/HistoryPage';
import {OverviewPage} from './pages/OverviewPage';
import {PersistPage} from './pages/PersistPage';
import {ReactivityPage} from './pages/ReactivityPage';
import {ShallowEqualPage} from './pages/ShallowEqualPage';
import {SnapshotsPage} from './pages/SnapshotsPage';
import {SubscribeKeyPage} from './pages/SubscribeKeyPage';
import {UseLocalStorePage} from './pages/UseLocalStorePage';

const routes: Record<string, React.ComponentType> = {
'/': OverviewPage,
'/reactivity': ReactivityPage,
'/collections': CollectionsPage,
'/snapshots': SnapshotsPage,
'/use-local-store': UseLocalStorePage,
'/persist': PersistPage,
'/history': HistoryPage,
'/devtools': DevtoolsPage,
'/subscribe-key': SubscribeKeyPage,
'/shallow-equal': ShallowEqualPage,
};

export function App() {
const route = useHashRoute();
const Page = routes[route] ?? OverviewPage;

return (
<div className="max-w-6xl mx-auto px-6 py-12">
<Layout route={route}>
<ReactScan />
<NavBar route={route} />
{route === '/persist' ? <PersistPage /> : <ReactivityPage />}
</div>
<Page />
</Layout>
);
}

Expand Down
23 changes: 0 additions & 23 deletions examples/rendering/src/PersistPage.tsx

This file was deleted.

18 changes: 18 additions & 0 deletions examples/rendering/src/components/ApiSignature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function ApiSignature({
importPath,
signature,
}: {
importPath: string;
signature: string;
}) {
return (
<div className="rounded-lg bg-zinc-900 border border-zinc-800 px-4 py-3 mb-6 font-mono text-xs">
<div className="text-zinc-500">
import {'{'}{' '}
<span className="text-indigo-400">{signature.split('(')[0]}</span> {'}'}{' '}
from <span className="text-emerald-400">'{importPath}'</span>
</div>
<div className="text-zinc-300 mt-1">{signature}</div>
</div>
);
}
40 changes: 40 additions & 0 deletions examples/rendering/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type {ButtonHTMLAttributes, ReactNode} from 'react';

type Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
type Size = 'sm' | 'md';

const variantStyles: Record<Variant, string> = {
primary: 'bg-indigo-500 text-white hover:bg-indigo-400',
secondary: 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600',
danger: 'bg-rose-600 text-white hover:bg-rose-500',
ghost:
'bg-transparent text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50',
};

const sizeStyles: Record<Size, string> = {
sm: 'px-2.5 py-1 text-xs',
md: 'px-3 py-1.5 text-sm',
};

export function Button({
variant = 'primary',
size = 'md',
children,
className = '',
...props
}: {
variant?: Variant;
size?: Size;
children: ReactNode;
className?: string;
} & ButtonHTMLAttributes<HTMLButtonElement>) {
return (
<button
type="button"
className={`rounded-lg font-medium transition-colors cursor-pointer ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
{...props}
>
{children}
</button>
);
}
41 changes: 41 additions & 0 deletions examples/rendering/src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {Highlight, themes} from 'prism-react-renderer';

export function CodeBlock({
code,
language = 'tsx',
title,
compact = false,
}: {
code: string;
language?: string;
title?: string;
compact?: boolean;
}) {
const trimmed = code.trim();

return (
<div className="rounded-lg overflow-hidden border border-zinc-800 bg-zinc-950">
{title && (
<div className="px-3 py-1.5 bg-zinc-900 border-b border-zinc-800 text-xs text-zinc-400 font-mono">
{title}
</div>
)}
<Highlight theme={themes.nightOwl} code={trimmed} language={language}>
{({tokens, getLineProps, getTokenProps}) => (
<pre
className={`overflow-x-auto font-mono text-xs leading-relaxed ${compact ? 'p-3' : 'p-4'}`}
style={{background: 'transparent'}}
>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({line})}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({token})} />
))}
</div>
))}
</pre>
)}
</Highlight>
</div>
);
}
76 changes: 76 additions & 0 deletions examples/rendering/src/components/DemoContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {type ReactNode, useState} from 'react';
import {CodeBlock} from './CodeBlock';

interface CodeTab {
label: string;
code: string;
language?: string;
}

export function DemoContainer({
title,
description,
children,
codeTabs,
}: {
title: string;
description?: string;
children: ReactNode;
codeTabs?: CodeTab[];
}) {
const [activeTab, setActiveTab] = useState(0);

const allTabs = [
{label: 'Demo', type: 'demo' as const},
...(codeTabs ?? []).map((t) => ({...t, type: 'code' as const})),
];

return (
<div className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden">
{/* Header */}
<div className="px-6 pt-5 pb-0">
<h3 className="text-lg font-semibold text-zinc-100 mb-1">{title}</h3>
{description && (
<p className="text-sm text-zinc-400 mb-0">{description}</p>
)}
</div>

{/* Tabs */}
<div className="flex border-b border-zinc-800 px-6 mt-4">
{allTabs.map((tab, i) => (
<button
key={tab.label}
type="button"
onClick={() => setActiveTab(i)}
className={`px-3 py-2 text-xs font-medium transition-colors cursor-pointer -mb-px ${
activeTab === i
? 'text-zinc-100 border-b-2 border-indigo-400'
: 'text-zinc-500 hover:text-zinc-300'
}`}
>
{tab.label}
</button>
))}
</div>

{/* Content */}
{activeTab === 0 ? (
<div className="p-6">{children}</div>
) : (
(() => {
const codeTab = codeTabs?.[activeTab - 1];
if (!codeTab) return null;
return (
<div className="max-h-[500px] overflow-y-auto bg-zinc-950">
<CodeBlock
code={codeTab.code}
language={codeTab.language ?? 'tsx'}
compact={true}
/>
</div>
);
})()
)}
</div>
);
}
Loading