Skip to content

Commit 54ee4f5

Browse files
authored
Update Examples and update docusaurus (#10)
* feat: add `useLocalStore` hook for component-scoped reactive stores with tests and documentation * feat: add `useLocalStore` hook for component-scoped reactive stores with tests and documentation * feat: add `withHistory`, `devtools`, and `subscribeKey` utilities for enhanced state management - `withHistory`: Adds undo/redo functionality to stores with a snapshot stack. - `devtools`: Integrates stores with Redux DevTools for debugging and time travel. - `subscribeKey`: Enables subscribing to changes on specific properties in stores. * docs: update architecture and documentation for `useLocalStore` and utilities - Added `useLocalStore` details to architecture and docs. - Expanded file structures to include `devtools`, `subscribeKey`, and `withHistory` utilities. - Refined examples and tutorials for consistency and clarity. * refactor(utils): improve state handling with try-finally and enhance safety checks - Wrapped state application logic in `try-finally` blocks to ensure cleanup (`isTimeTraveling`, `paused`, `hydrating`) regardless of errors. - Improved safety by skipping operations during invalid states (e.g., `hydration`/`disposed` in `persist`). - Added `shallowEqual` utility export to utils index. * test(utils): add extensive test coverage for edge cases in utilities - Added additional tests for `devtools` to cover time travel, error handling, and message payloads. - Extended test cases for `persist` to handle null states, invalid envelopes, and unsubscribe behavior. - Introduced more cases for `subscribeKey` with various data types and subscriber behavior. - Enhanced `shallowEqual` tests to include edge cases such as array vs object comparison and handling undefined values. * refactor(examples): restructure and modernize examples for clarity and modularity * feat(demos): integrate interactive examples and static demos into documentation site - Added `demos:build` script to generate static demos for the documentation site. - Updated `DemoContainer` component for improved tab structure and usability. - Introduced a new `Examples` page in the docs with an iframe for demo previews. - Enhanced fallback in `PostStore` with static data for cases when the API is unavailable.
1 parent 64489ea commit 54ee4f5

52 files changed

Lines changed: 2794 additions & 459 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
3737
website/build
3838
website/.docusaurus
3939
website/.cache-loader
40+
website/static/demos

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/rendering/bun.lock

Lines changed: 237 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/rendering/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@codebelt/classy-store": "file:../..",
1313
"bun-plugin-tailwind": "0.1.2",
14+
"prism-react-renderer": "^2.4.1",
1415
"proxy-compare": "3.0.1",
1516
"react": "19.2.4",
1617
"react-dom": "19.2.4",

examples/rendering/src/App.tsx

Lines changed: 30 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,41 @@
11
// This component must be the top-most import in this file!
2-
import {ReactScan} from './ReactScan';
2+
import {ReactScan} from './components/ReactScan';
33
import './index.css';
4-
import {useEffect, useState} from 'react';
5-
import {AsyncDemo} from './AsyncDemo';
6-
import {CollectionsDemo} from './CollectionsDemo';
7-
import {PersistPage} from './PersistPage';
8-
import {ReactiveFundamentalsDemo} from './ReactiveFundamentalsDemo';
9-
import {StructuralSharingDemo} from './StructuralSharingDemo';
10-
11-
// ── Hash Router ─────────────────────────────────────────────────────────────
12-
13-
function getRoute(): string {
14-
const hash = globalThis.location?.hash ?? '';
15-
return hash.replace(/^#/, '') || '/';
16-
}
17-
18-
function useHashRoute() {
19-
const [route, setRoute] = useState(getRoute);
20-
21-
useEffect(() => {
22-
const handler = () => setRoute(getRoute());
23-
globalThis.addEventListener('hashchange', handler);
24-
return () => globalThis.removeEventListener('hashchange', handler);
25-
}, []);
26-
27-
return route;
28-
}
29-
30-
// ── Navigation ──────────────────────────────────────────────────────────────
31-
32-
function NavBar({route}: {route: string}) {
33-
const links = [
34-
{href: '#/', label: 'Reactivity', active: route === '/'},
35-
{href: '#/persist', label: 'Persist', active: route === '/persist'},
36-
];
37-
38-
return (
39-
<nav className="flex items-center justify-center gap-1 mb-8">
40-
{links.map((link) => (
41-
<a
42-
key={link.href}
43-
href={link.href}
44-
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
45-
link.active
46-
? 'bg-zinc-800 text-zinc-100'
47-
: 'text-zinc-500 hover:text-zinc-300 hover:bg-zinc-800/50'
48-
}`}
49-
>
50-
{link.label}
51-
</a>
52-
))}
53-
</nav>
54-
);
55-
}
56-
57-
// ── Reactivity Demos Page ───────────────────────────────────────────────────
58-
59-
function ReactivityPage() {
60-
return (
61-
<>
62-
<header className="text-center mb-12">
63-
<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">
64-
@codebelt/classy-store
65-
</h1>
66-
<p className="text-zinc-400 max-w-xl mx-auto">
67-
Class-based reactive state for React. Watch the render badges to see
68-
exactly when each component re-renders.
69-
</p>
70-
</header>
71-
72-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
73-
<ReactiveFundamentalsDemo />
74-
<AsyncDemo />
75-
</div>
76-
77-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
78-
<CollectionsDemo />
79-
</div>
80-
81-
<div className="grid grid-cols-1 gap-6">
82-
<StructuralSharingDemo />
83-
</div>
84-
</>
85-
);
86-
}
87-
88-
// ── App ─────────────────────────────────────────────────────────────────────
4+
import {Layout} from './components/Layout';
5+
import {useHashRoute} from './hooks/useHashRoute';
6+
import {CollectionsPage} from './pages/CollectionsPage';
7+
import {DevtoolsPage} from './pages/DevtoolsPage';
8+
import {HistoryPage} from './pages/HistoryPage';
9+
import {OverviewPage} from './pages/OverviewPage';
10+
import {PersistPage} from './pages/PersistPage';
11+
import {ReactivityPage} from './pages/ReactivityPage';
12+
import {ShallowEqualPage} from './pages/ShallowEqualPage';
13+
import {SnapshotsPage} from './pages/SnapshotsPage';
14+
import {SubscribeKeyPage} from './pages/SubscribeKeyPage';
15+
import {UseLocalStorePage} from './pages/UseLocalStorePage';
16+
17+
const routes: Record<string, React.ComponentType> = {
18+
'/': OverviewPage,
19+
'/reactivity': ReactivityPage,
20+
'/collections': CollectionsPage,
21+
'/snapshots': SnapshotsPage,
22+
'/use-local-store': UseLocalStorePage,
23+
'/persist': PersistPage,
24+
'/history': HistoryPage,
25+
'/devtools': DevtoolsPage,
26+
'/subscribe-key': SubscribeKeyPage,
27+
'/shallow-equal': ShallowEqualPage,
28+
};
8929

9030
export function App() {
9131
const route = useHashRoute();
32+
const Page = routes[route] ?? OverviewPage;
9233

9334
return (
94-
<div className="max-w-6xl mx-auto px-6 py-12">
35+
<Layout route={route}>
9536
<ReactScan />
96-
<NavBar route={route} />
97-
{route === '/persist' ? <PersistPage /> : <ReactivityPage />}
98-
</div>
37+
<Page />
38+
</Layout>
9939
);
10040
}
10141

examples/rendering/src/PersistPage.tsx

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function ApiSignature({
2+
importPath,
3+
signature,
4+
}: {
5+
importPath: string;
6+
signature: string;
7+
}) {
8+
return (
9+
<div className="rounded-lg bg-zinc-900 border border-zinc-800 px-4 py-3 mb-6 font-mono text-xs">
10+
<div className="text-zinc-500">
11+
import {'{'}{' '}
12+
<span className="text-indigo-400">{signature.split('(')[0]}</span> {'}'}{' '}
13+
from <span className="text-emerald-400">'{importPath}'</span>
14+
</div>
15+
<div className="text-zinc-300 mt-1">{signature}</div>
16+
</div>
17+
);
18+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type {ButtonHTMLAttributes, ReactNode} from 'react';
2+
3+
type Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
4+
type Size = 'sm' | 'md';
5+
6+
const variantStyles: Record<Variant, string> = {
7+
primary: 'bg-indigo-500 text-white hover:bg-indigo-400',
8+
secondary: 'bg-zinc-700 text-zinc-300 hover:bg-zinc-600',
9+
danger: 'bg-rose-600 text-white hover:bg-rose-500',
10+
ghost:
11+
'bg-transparent text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/50',
12+
};
13+
14+
const sizeStyles: Record<Size, string> = {
15+
sm: 'px-2.5 py-1 text-xs',
16+
md: 'px-3 py-1.5 text-sm',
17+
};
18+
19+
export function Button({
20+
variant = 'primary',
21+
size = 'md',
22+
children,
23+
className = '',
24+
...props
25+
}: {
26+
variant?: Variant;
27+
size?: Size;
28+
children: ReactNode;
29+
className?: string;
30+
} & ButtonHTMLAttributes<HTMLButtonElement>) {
31+
return (
32+
<button
33+
type="button"
34+
className={`rounded-lg font-medium transition-colors cursor-pointer ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
35+
{...props}
36+
>
37+
{children}
38+
</button>
39+
);
40+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {Highlight, themes} from 'prism-react-renderer';
2+
3+
export function CodeBlock({
4+
code,
5+
language = 'tsx',
6+
title,
7+
compact = false,
8+
}: {
9+
code: string;
10+
language?: string;
11+
title?: string;
12+
compact?: boolean;
13+
}) {
14+
const trimmed = code.trim();
15+
16+
return (
17+
<div className="rounded-lg overflow-hidden border border-zinc-800 bg-zinc-950">
18+
{title && (
19+
<div className="px-3 py-1.5 bg-zinc-900 border-b border-zinc-800 text-xs text-zinc-400 font-mono">
20+
{title}
21+
</div>
22+
)}
23+
<Highlight theme={themes.nightOwl} code={trimmed} language={language}>
24+
{({tokens, getLineProps, getTokenProps}) => (
25+
<pre
26+
className={`overflow-x-auto font-mono text-xs leading-relaxed ${compact ? 'p-3' : 'p-4'}`}
27+
style={{background: 'transparent'}}
28+
>
29+
{tokens.map((line, i) => (
30+
<div key={i} {...getLineProps({line})}>
31+
{line.map((token, key) => (
32+
<span key={key} {...getTokenProps({token})} />
33+
))}
34+
</div>
35+
))}
36+
</pre>
37+
)}
38+
</Highlight>
39+
</div>
40+
);
41+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {type ReactNode, useState} from 'react';
2+
import {CodeBlock} from './CodeBlock';
3+
4+
interface CodeTab {
5+
label: string;
6+
code: string;
7+
language?: string;
8+
}
9+
10+
export function DemoContainer({
11+
title,
12+
description,
13+
children,
14+
codeTabs,
15+
}: {
16+
title: string;
17+
description?: string;
18+
children: ReactNode;
19+
codeTabs?: CodeTab[];
20+
}) {
21+
const [activeTab, setActiveTab] = useState(0);
22+
23+
const allTabs = [
24+
{label: 'Demo', type: 'demo' as const},
25+
...(codeTabs ?? []).map((t) => ({...t, type: 'code' as const})),
26+
];
27+
28+
return (
29+
<div className="bg-zinc-900 border border-zinc-800 rounded-xl overflow-hidden">
30+
{/* Header */}
31+
<div className="px-6 pt-5 pb-0">
32+
<h3 className="text-lg font-semibold text-zinc-100 mb-1">{title}</h3>
33+
{description && (
34+
<p className="text-sm text-zinc-400 mb-0">{description}</p>
35+
)}
36+
</div>
37+
38+
{/* Tabs */}
39+
<div className="flex border-b border-zinc-800 px-6 mt-4">
40+
{allTabs.map((tab, i) => (
41+
<button
42+
key={tab.label}
43+
type="button"
44+
onClick={() => setActiveTab(i)}
45+
className={`px-3 py-2 text-xs font-medium transition-colors cursor-pointer -mb-px ${
46+
activeTab === i
47+
? 'text-zinc-100 border-b-2 border-indigo-400'
48+
: 'text-zinc-500 hover:text-zinc-300'
49+
}`}
50+
>
51+
{tab.label}
52+
</button>
53+
))}
54+
</div>
55+
56+
{/* Content */}
57+
{activeTab === 0 ? (
58+
<div className="p-6">{children}</div>
59+
) : (
60+
(() => {
61+
const codeTab = codeTabs?.[activeTab - 1];
62+
if (!codeTab) return null;
63+
return (
64+
<div className="max-h-[500px] overflow-y-auto bg-zinc-950">
65+
<CodeBlock
66+
code={codeTab.code}
67+
language={codeTab.language ?? 'tsx'}
68+
compact={true}
69+
/>
70+
</div>
71+
);
72+
})()
73+
)}
74+
</div>
75+
);
76+
}

0 commit comments

Comments
 (0)