comwit 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +297 -1
  3. package/package.json +22 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 meursyphus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1 +1,297 @@
1
- TODO
1
+ # comwit
2
+
3
+ State management for Next.js.
4
+
5
+ > Give this document to Claude Code. It sets up your project.
6
+
7
+ ## 1. Install
8
+
9
+ ```bash
10
+ npm i comwit
11
+ ```
12
+
13
+ ## 2. Setup Provider
14
+
15
+ Create `app/providers.tsx` and wrap your root layout.
16
+
17
+ ```tsx
18
+ // app/providers.tsx
19
+ 'use client'
20
+
21
+ import { useRouter } from 'next/navigation'
22
+ import { keepPreviousData, MuchaProvider } from 'comwit'
23
+ import { ReactNode } from 'react'
24
+
25
+ type AppContext = {
26
+ router: { push: (href: string) => void }
27
+ }
28
+
29
+ export function Providers({ children }: { children: ReactNode }) {
30
+ const router = useRouter()
31
+ const context: AppContext = { router }
32
+
33
+ return (
34
+ <MuchaProvider
35
+ context={context}
36
+ defaultOptions={{
37
+ query: {
38
+ staleTime: 30_000,
39
+ cacheTime: 120_000,
40
+ gcTime: 180_000,
41
+ placeholderData: keepPreviousData,
42
+ },
43
+ }}
44
+ >
45
+ {children}
46
+ </MuchaProvider>
47
+ )
48
+ }
49
+ ```
50
+
51
+ ```tsx
52
+ // app/layout.tsx
53
+ import { Providers } from './providers'
54
+
55
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
56
+ return (
57
+ <html lang="en">
58
+ <body>
59
+ <Providers>{children}</Providers>
60
+ </body>
61
+ </html>
62
+ )
63
+ }
64
+ ```
65
+
66
+ ## 3. Create `state/.ai.md`
67
+
68
+ Create `src/state/.ai.md` (or `state/.ai.md`) with the full content below.
69
+
70
+ ## 4. Update `CLAUDE.md`
71
+
72
+ Add this to your project's `CLAUDE.md`:
73
+
74
+ ```md
75
+ ## State Management
76
+
77
+ This project uses comwit. When working on files in state/, always read state/.ai.md first.
78
+ ```
79
+
80
+ ---
81
+
82
+ ## state/.ai.md Content
83
+
84
+ Copy everything between the ````fences into your`state/.ai.md`:
85
+
86
+ ````md
87
+ ## Structure
88
+
89
+ ```
90
+ state/{domain}/
91
+ ├── types.ts # State + Actions types
92
+ ├── model.ts # model() with initial state
93
+ ├── actions/
94
+ │ ├── crud.ts # CRUD operations
95
+ │ ├── load.ts # Data fetching via query()
96
+ │ ├── init.ts # SSR hydration via silent()
97
+ │ └── ... # One file per concern
98
+ └── index.ts # create() hook + re-exports
99
+ ```
100
+
101
+ Write order: **types.ts -> model.ts -> actions/\*.ts -> index.ts**
102
+
103
+ ## Rules
104
+
105
+ - Dependencies: pages → state → api (one way)
106
+ - Pass domain objects whole: `<Card post={post} />`
107
+
108
+ ## File Templates
109
+
110
+ ### types.ts
111
+
112
+ **Read this file first.** JSDoc on each field/method guides the implementation.
113
+
114
+ - `Query<Data>` for client-fetched fields — provides `.data` `.isLoading` `.isError` `.error`
115
+ - `Query<Data, Arg>` — second generic = queryFn param type
116
+ - Plain types for SSR-hydrated fields
117
+
118
+ ```ts
119
+ import { Query } from 'comwit'
120
+
121
+ export type Post = { id: string; ... }
122
+
123
+ export type PostState = {
124
+ posts: Query<Post[]> // client fetch
125
+ comments: Query<Comment[], string> // client fetch, second generic = queryFn arg
126
+ current: Post | null // SSR hydrated
127
+ // ...
128
+ }
129
+
130
+ export type PostActions = {
131
+ /** @description Fetch posts via query() */
132
+ loadPosts(): Promise<void>
133
+ /** @description Toggle like. Optimistic on list + current, toast on error */
134
+ like(postId: string): Promise<void>
135
+ // ...
136
+ }
137
+ ```
138
+
139
+ ### model.ts
140
+
141
+ `Query<T>` fields use `query()` with `initialData` + `queryFn`. Plain fields get default values.
142
+
143
+ ```ts
144
+ import { model, query } from 'comwit'
145
+
146
+ export const post = model<PostState>({
147
+ posts: query<Post[]>({ initialData: [], queryFn: () => api.post.findAll() }),
148
+ comments: query<Comment[], string>({
149
+ initialData: [],
150
+ queryFn: (postId) => api.comment.findAll(postId),
151
+ }),
152
+ current: null,
153
+ })
154
+ ```
155
+
156
+ ### actions/\*.ts
157
+
158
+ Actions are self-contained — resolve state via `state()`, side effects via decorators/context. UI only calls.
159
+
160
+ ```ts
161
+ import { action, silent, OnError, OnSuccess } from 'comwit'
162
+ import { user } from '@/state/user/model'
163
+
164
+ export const postActions = action<Pick<PostActions, '...'>, AppContext>(({ state, context }) => {
165
+ class PostActions {
166
+ private model = state(post)
167
+ private user = state(user) // cross-domain — read other model directly
168
+
169
+ // SSR hydrate — silent() suppresses re-render
170
+ init(data: Post) {
171
+ silent(() => {
172
+ this.model.current = data
173
+ })
174
+ }
175
+
176
+ async loadPosts() {
177
+ await this.model.posts.query()
178
+ }
179
+
180
+ // guard: check auth from user domain, not from params
181
+ @OnSuccess(() => context.router.push('/posts'))
182
+ @OnError((e) => toast.error(e instanceof Error ? e.message : 'Failed'))
183
+ async create(title: string) {
184
+ if (!this.user.me) return
185
+ const created = await api.post.create({ userId: this.user.me.id, title })
186
+ this.model.posts.data.push(created)
187
+ }
188
+ }
189
+ return new PostActions()
190
+ })
191
+ ```
192
+
193
+ The pattern above can be automated with `createInterceptor`. See [Decorators](#decorators) below.
194
+
195
+ ```ts
196
+ // reusable guard — state() must be initialized in the factory body
197
+ const LoginRequired = createInterceptor<AppContext>(({ state, context }) => {
198
+ const u = state(user)
199
+ return onAuthorized({
200
+ when: () => Boolean(u.me),
201
+ onDeny: () => context.router.push('/login'),
202
+ })
203
+ })
204
+
205
+ // replaces the manual if (!this.user.me) check above:
206
+ @LoginRequired
207
+ @OnSuccess(() => context.router.push('/posts'))
208
+ async create(title: string) { /* ... */ }
209
+ ```
210
+
211
+ ```tsx
212
+ // call init outside useEffect — silent() makes it safe
213
+ function PostDetail({ initialPost }: { initialPost: Post }) {
214
+ const { actions } = usePost((s) => ({ actions: s.actions }))
215
+ actions.init(initialPost)
216
+ return <Article />
217
+ }
218
+ ```
219
+
220
+ ### index.ts
221
+
222
+ ```ts
223
+ import { create } from 'comwit'
224
+ import type { PostState, PostActions } from './types'
225
+ import { post } from './model'
226
+ import { postActions } from './actions/crud'
227
+
228
+ export const usePost = create<PostState, PostActions>(post, {
229
+ actions: [postActions /* ... */],
230
+ })
231
+ ```
232
+
233
+ ## query() Reference
234
+
235
+ Methods: `.query(arg?)` · `.refetch()` · `.set(data)` · `.nextFetch()` · `.previousFetch()`
236
+ Flags: `isLoading` · `isFetching` · `isSuccess` · `isError` · `error`
237
+ Infinite-only flags: `cursor` · `hasMore` (nextFetch is no-op when `hasMore` is false)
238
+ Options: `staleTime` · `cacheTime` · `gcTime` · `placeholderData` · `force`
239
+
240
+ `queryFn` receives `(arg, context)` where `context.state` is a readonly snapshot of the current query state (includes `data`, `cursor`, etc.).
241
+
242
+ Infinite scroll — use `Query.Infinite<T>` + `query.infinite()`:
243
+
244
+ ```ts
245
+ // types
246
+ trending: Query.Infinite<Post[]>
247
+
248
+ // model — cursor-based pagination
249
+ trending: query.infinite<Post[]>({
250
+ initialData: [],
251
+ queryFn: (_, { state }) => api.post.trending(state.cursor),
252
+ })
253
+
254
+ // action
255
+ async loadMoreTrending() { await this.model.trending.nextFetch() }
256
+ ```
257
+
258
+ ## Usage
259
+
260
+ ```tsx
261
+ const { posts, current, actions } = usePost((s) => ({
262
+ posts: s.posts, // Query<Post[]> — has .data .isLoading .isError
263
+ current: s.current, // Post | null — plain state
264
+ actions: s.actions,
265
+ }))
266
+
267
+ // query fields — check flags, access data via .data
268
+ if (posts.isLoading) return <Skeleton />
269
+ if (posts.isError) return <p>Error: {posts.error}</p>
270
+ return posts.data.map((post) => <Card key={post.id} post={post} />)
271
+ ```
272
+
273
+ Selectors use deep equality — only re-renders when selected values change.
274
+
275
+ ## Decorators
276
+
277
+ All from `'comwit'`. Stack on class methods.
278
+
279
+ | Decorator | Purpose |
280
+ | ------------------------------- | ----------------------------------------------------- |
281
+ | `@OnError(fn)` | Error handler. Re-throw to propagate. |
282
+ | `@OnSuccess(fn)` | Success callback. |
283
+ | `@Debounce(ms)` | Debounce. |
284
+ | `@Throttle(ms)` | Throttle. |
285
+ | `@Authorized({ when, onDeny })` | Auth guard. `when: () => boolean \| Promise<boolean>` |
286
+
287
+ `createInterceptor(({ state, context }) => MethodDecorator)` — reusable decorator with `state`/`context` access, like action factories. See [actions](#actionsts) above for usage.
288
+
289
+ ## Key Concepts
290
+
291
+ - `model(initial)` — global reactive store
292
+ - `action(factory)` — factory with `state`/`context` access
293
+ - `create(model, { actions })` — model + actions -> React hook
294
+ - `silent(fn)` — suppress re-renders (SSR)
295
+ - `query()` / `query.infinite()` — client fetch in model
296
+ - `state(model)` — mutable proxy in actions
297
+ ````
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comwit",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "private": false,
5
5
  "homepage": "https://comwit.io",
6
6
  "repository": {
@@ -14,7 +14,25 @@
14
14
  "access": "public"
15
15
  },
16
16
  "type": "module",
17
- "description": "LLM-friendly React state helper library.",
17
+ "description": "React state management for vibe coding. Less tokens. Built for Claude Code.",
18
+ "keywords": [
19
+ "react",
20
+ "state-management",
21
+ "vibe-coding",
22
+ "claude-code",
23
+ "llm",
24
+ "next.js",
25
+ "valtio",
26
+ "zustand",
27
+ "tanstack-query",
28
+ "oop",
29
+ "proxy"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "license": "MIT",
35
+ "author": "meursyphus",
18
36
  "main": "dist/comwit.cjs",
19
37
  "module": "dist/comwit.js",
20
38
  "types": "dist/index.d.ts",
@@ -27,7 +45,8 @@
27
45
  },
28
46
  "files": [
29
47
  "dist",
30
- "README.md"
48
+ "README.md",
49
+ "LICENSE"
31
50
  ],
32
51
  "scripts": {
33
52
  "dev": "vite build --watch",