Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/fix-history-and-react-selectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@codebelt/classy-store': patch
---

Restore mutable nested state after undo/redo while preserving reactive collection methods and keeping recorded snapshots unchanged. Refresh React selections when selectors or equality functions change, even when the store has not mutated. Update the library's development dependencies to their latest stable versions and add regression coverage for both fixes.
9 changes: 9 additions & 0 deletions .changeset/synchronous-react-subscriptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@codebelt/classy-store": minor
---

React subscriptions now always notify synchronously, fixing controlled-input caret jumps without configuration. Remove the `sync` option from `useClassyStore` and hooks returned by `createStoreHook`, and remove imports of `UseClassyStoreOptions`. Selector options still accept `isEqual`. Ordinary `subscribe()` callbacks remain microtask-batched and retain their existing `sync` option.

React selectors may run after each mutation; several writes are not an atomic transaction. React can batch the resulting renders within an event handler. Auto-tracked reads are isolated per render so conditional and abandoned renders do not retain stale dependencies, including for class and null-prototype snapshots. Unchanged tracked references remain stable across parent renders.

Persistence hydration restores mutable nested defaults and custom merged values while preserving prototypes and user accessors. Add real-browser input regressions, lifecycle and SSR coverage, and packed-package checks for React 18 and 19.
27 changes: 27 additions & 0 deletions website-fumadocs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# deps
/node_modules

# generated content
.source

# test & build
/coverage
/.next/
/out/
/build
/public/demos
*.tsbuildinfo

# misc
.DS_Store
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# others
.env*.local
.vercel
next-env.d.ts
32 changes: 32 additions & 0 deletions website-fumadocs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Classy Store Fumadocs Prototype

This is an isolated Fumadocs version of the Classy Store documentation site.
The existing Docusaurus site remains in `../website`; this folder is a prototype
to iterate on before replacing or moving anything.

The app uses Next.js static export and Fumadocs MDX. Its content is copied from
`../website/docs`, so edit these copies while working on the prototype.

## Development

```bash
bun run dev
```

Open http://localhost:3000. The examples bundle is rebuilt from
`../examples/react` when the dev server starts.

## Static output

```bash
bun run build
```

For a GitHub Pages project site at `codebelt.github.io/classy-store`, use:

```bash
bun run build:pages
```

The exported files are written to `out/`. `public/.nojekyll` keeps GitHub Pages
from dropping Next.js's `_next` assets.
6 changes: 6 additions & 0 deletions website-fumadocs/app/(home)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {HomeLayout} from 'fumadocs-ui/layouts/home';
import {baseOptions} from '@/lib/layout.shared';

export default function Layout({children}: LayoutProps<'/'>) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
81 changes: 81 additions & 0 deletions website-fumadocs/app/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Link from 'next/link';

export default function HomePage() {
return (
<main className="flex-1">
<section className="border-b bg-gradient-to-br from-emerald-500 to-teal-500 text-white">
<div className="mx-auto grid max-w-7xl gap-12 px-6 py-20 md:grid-cols-2 md:items-center md:py-28">
<div>
<p className="mb-4 text-sm font-semibold uppercase tracking-[0.18em] text-white/80">
Reactive state, made straightforward
</p>
<h1 className="max-w-xl text-4xl font-bold tracking-tight sm:text-5xl">
State management that reads like TypeScript.
</h1>
<p className="mt-6 max-w-xl text-lg leading-8 text-white/90">
Define state and actions in plain classes. Classy Store handles
reactivity, computed getters, and framework subscriptions for you.
</p>
<div className="mt-8 flex flex-wrap gap-3">
<Link
href="/docs"
className="rounded-lg bg-white px-5 py-3 font-semibold text-emerald-950 shadow-sm transition hover:bg-white/90"
>
Read the docs
</Link>
<Link
href="/demos/index.html"
className="rounded-lg border border-white/50 px-5 py-3 font-semibold text-white transition hover:bg-white/10"
>
Explore examples
</Link>
</div>
</div>

<pre className="overflow-x-auto rounded-2xl border border-white/10 bg-slate-950/90 p-6 text-sm leading-7 text-slate-100 shadow-2xl">
<code>{`import { createClassyStore } from '@codebelt/classy-store';
import { useClassyStore } from '@codebelt/classy-store/react';

class CounterStore {
count = 0;

get doubled() {
return this.count * 2;
}

increment() {
this.count++;
}
}

const counter = createClassyStore(new CounterStore());
const count = useClassyStore(counter, (state) => state.count);`}</code>
</pre>
</div>
</section>

<section className="mx-auto grid max-w-7xl gap-5 px-6 py-16 md:grid-cols-3 md:py-20">
<article className="rounded-2xl border bg-fd-card p-6">
<h2 className="text-lg font-semibold">A class is the store</h2>
<p className="mt-3 leading-7 text-fd-muted-foreground">
Fields hold state, methods update it, and getters are memoized
computed values.
</p>
</article>
<article className="rounded-2xl border bg-fd-card p-6">
<h2 className="text-lg font-semibold">Use your framework</h2>
<p className="mt-3 leading-7 text-fd-muted-foreground">
Bindings are available for React, Vue, Svelte, Solid, and Angular.
</p>
</article>
<article className="rounded-2xl border bg-fd-card p-6">
<h2 className="text-lg font-semibold">Useful tools included</h2>
<p className="mt-3 leading-7 text-fd-muted-foreground">
Persist state, inspect it with Redux DevTools, subscribe to a single
key, or add undo and redo.
</p>
</article>
</section>
</main>
);
}
9 changes: 9 additions & 0 deletions website-fumadocs/app/api/search/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {createFromSource} from 'fumadocs-core/search/server';
import {source} from '@/lib/source';

export const revalidate = false;

export const {staticGET: GET} = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
});
67 changes: 67 additions & 0 deletions website-fumadocs/app/docs/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
MarkdownCopyButton,
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/docs/page';
import {createRelativeLink} from 'fumadocs-ui/mdx';
import type {Metadata} from 'next';
import {notFound} from 'next/navigation';
import {getMDXComponents} from '@/components/mdx';
import {getPageImageUrl, getPageMarkdownUrl, gitConfig} from '@/lib/shared';
import {source} from '@/lib/source';

export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();

const MDX = page.data.body;
const markdownUrl = getPageMarkdownUrl(page).url;

return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">
{page.data.description}
</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pb-6">
<MarkdownCopyButton markdownUrl={markdownUrl} />
<ViewOptionsPopover
markdownUrl={markdownUrl}
githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/website-fumadocs/content/docs/${page.path}`}
/>
</div>
<DocsBody>
<MDX
components={getMDXComponents({
// this allows you to link to other pages with relative file paths
a: createRelativeLink(source, page),
})}
/>
</DocsBody>
</DocsPage>
);
}

export async function generateStaticParams() {
return source.generateParams();
}

export async function generateMetadata(
props: PageProps<'/docs/[[...slug]]'>,
): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();

return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImageUrl(page).url,
},
};
}
11 changes: 11 additions & 0 deletions website-fumadocs/app/docs/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {DocsLayout} from 'fumadocs-ui/layouts/docs';
import {baseOptions} from '@/lib/layout.shared';
import {source} from '@/lib/source';

export default function Layout({children}: LayoutProps<'/docs'>) {
return (
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
{children}
</DocsLayout>
);
}
17 changes: 17 additions & 0 deletions website-fumadocs/app/global.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";

html {
scrollbar-gutter: stable;
font-family:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
}

html > body[data-scroll-locked] {
/* biome-ignore lint/complexity/noImportantStyles: Reset scroll-lock compensation while scrollbar-gutter reserves the space. */
margin-right: 0px !important;
/* biome-ignore lint/complexity/noImportantStyles: Reset the matching scroll-lock compensation value. */
--removed-body-scroll-bar-size: 0px !important;
}
Binary file added website-fumadocs/app/icon.ico
Binary file not shown.
23 changes: 23 additions & 0 deletions website-fumadocs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type {Metadata} from 'next';
import {Provider} from '@/components/provider';
import './global.css';

export const metadata: Metadata = {
metadataBase: new URL('https://codebelt.github.io/classy-store/'),
title: {
default: 'Classy Store',
template: '%s | Classy Store',
},
description:
'Class-based reactive state management for React, Vue, Svelte, Solid, and Angular.',
};

export default function Layout({children}: LayoutProps<'/'>) {
return (
<html lang="en" suppressHydrationWarning={true}>
<body className="flex min-h-screen flex-col font-sans">
<Provider>{children}</Provider>
</body>
</html>
);
}
8 changes: 8 additions & 0 deletions website-fumadocs/app/llms-full.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {withDocsBasePath} from '@/lib/shared';
import {docsLlms} from '@/lib/source';

export const revalidate = false;

export async function GET() {
return new Response(withDocsBasePath(await docsLlms.full()));
}
27 changes: 27 additions & 0 deletions website-fumadocs/app/llms.mdx/docs/[[...slug]]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {notFound} from 'next/navigation';
import {getPageMarkdownUrl} from '@/lib/shared';
import {docsLlms, source} from '@/lib/source';

export const revalidate = false;

export async function GET(
_req: Request,
{params}: RouteContext<'/llms.mdx/docs/[[...slug]]'>,
) {
const {slug} = await params;
// remove the appended "content.md"
const page = source.getPage(slug?.slice(0, -1));
if (!page) notFound();

return new Response(await docsLlms.page(page), {
headers: {
'Content-Type': 'text/markdown',
},
});
}

export function generateStaticParams() {
return source.getPages().map((page) => ({
slug: getPageMarkdownUrl(page).segments,
}));
}
8 changes: 8 additions & 0 deletions website-fumadocs/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {withDocsBasePath} from '@/lib/shared';
import {docsLlms} from '@/lib/source';

export const revalidate = false;

export async function GET() {
return new Response(withDocsBasePath(await docsLlms.index()));
}
28 changes: 28 additions & 0 deletions website-fumadocs/app/og/docs/[...slug]/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {generateOGImage} from 'fumadocs-ui/og';
import {notFound} from 'next/navigation';
import {appName, getPageImageUrl} from '@/lib/shared';
import {source} from '@/lib/source';

export const revalidate = false;

export async function GET(
_req: Request,
{params}: RouteContext<'/og/docs/[...slug]'>,
) {
const {slug} = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();

return generateOGImage({
title: page.data.title,
description: page.data.description,
site: appName,
});
}

export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImageUrl(page).segments,
}));
}
Loading
Loading