UNPKG

seitu

Version:
231 lines (179 loc) • 5.71 kB
# Seitu [![npm version](https://badge.fury.io/js/seitu.svg)](https://npmjs.com/package/seitu) ![You need Seitu](https://img.shields.io/badge/You_need-Seitu-purple) Type-safe reactive primitives with one contract: `get()`, `subscribe()`, `set()`. Works with in-memory state, validated `localStorage` / IndexedDB, media queries, scroll position, element size. Bindings for React, Vue, Solid, Svelte. SSR-safe. [Documentation](https://seitu.letstri.dev) · [Playground](https://github.com/letstri/seitu/tree/main/playground) ```bash pnpm add seitu ``` Import from an entry point (`seitu`, `seitu/web`, `seitu/react`, `seitu/vue`, `seitu/solid`, `seitu/svelte`, `seitu/utils`) or from a single feature to pull in nothing else: ```ts import { createStore } from 'seitu/core/store' import { createWebStorage } from 'seitu/web/web-storage' ``` ## Examples ### State and derived values ```ts import { createComputed, createStore } from 'seitu' const cart = createStore({ items: [{ price: 10, qty: 2 }], coupon: 0.1 }) const total = createComputed( cart, (c) => c.items.reduce((s, i) => s + i.price * i.qty, 0) * (1 - c.coupon) ) total.get() // 18 cart.set((c) => ({ ...c, coupon: 0 })) total.subscribe((t) => console.log(t)) // 20 ``` ### Debounce and throttle ```ts import { createDebounced, createDebouncedFn, createStore } from 'seitu' const query = createStore('') createDebounced(query, 300).subscribe((q) => fetch(`/search?q=${q}`)) const save = createDebouncedFn((draft: string) => api.save(draft), 500) save('hello') // runs after 500 ms of silence save.flush() // or now ``` ### Validated `localStorage` ```ts import { createWebStorage, createWebStorageValue } from 'seitu/web' import * as z from 'zod' const settings = createWebStorage({ type: 'localStorage', schemas: { theme: z.enum(['light', 'dark']), sidebar: z.object({ open: z.boolean() }), }, defaultValues: { theme: 'light', sidebar: { open: true } }, }) settings.set({ theme: 'dark' }) // partial update settings.get() // { theme: 'dark', sidebar: { open: true } }, invalid data falls back to defaults const theme = createWebStorageValue({ type: 'localStorage', key: 'theme', schema: z.enum(['light', 'dark']), defaultValue: 'light', }) // single key theme.set('light') ``` ### Cookie the server can render ```ts import { createCookieValue } from 'seitu/web' import * as z from 'zod' const lang = createCookieValue({ key: 'lang', schema: z.enum(['en', 'fr']), defaultValue: 'en', getServerCookies: () => requestCookieHeader, // the framework's `Cookie` header reader }) // SSR renders the cookie value, hydration matches it lang.set('fr') ``` ### IndexedDB ```ts import { createIndexedDb, createIndexedDbStorage, createIndexedDbTable, } from 'seitu/web' import * as z from 'zod' const db = createIndexedDb({ name: 'app', stores: { session: createIndexedDbStorage({ schemas: { token: z.string().nullable() }, defaultValues: { token: null }, }), // key/value, sync get() via cache todos: createIndexedDbTable({ keyPath: 'id', indexes: { status: 'status' }, schema: z.object({ id: z.string(), status: z.enum(['open', 'done']) }), }), // rows, validated on write and read }, }) // one connection, stores created on demand const { session, todos } = db.stores await db.ready // open + session hydrated await session.set({ token: 'abc' }) await todos.put({ id: '1', status: 'open' }) const open = todos.query((t) => t.index('status').getAll('open'), { initial: [], }) // reactive ``` ### Browser state ```ts import { createIsOnline, createMediaQuery, createScrollState } from 'seitu/web' const isDark = createMediaQuery({ query: '(prefers-color-scheme: dark)' }) // type-checked query const online = createIsOnline() const feed = createScrollState({ element: () => document.querySelector('#feed'), threshold: 200, }) feed.subscribe(({ bottom }) => bottom.reached && loadMore()) ``` ## Frameworks ```tsx // React import { useSubscription } from 'seitu/react' function Theme() { const theme = useSubscription(settings, { selector: (s) => s.theme }) return ( <button onClick={() => settings.set({ theme: theme === 'light' ? 'dark' : 'light' }) } > {theme} </button> ) } ``` ```vue <!-- Vue --> <script setup lang="ts"> import { useSubscription } from 'seitu/vue' const theme = useSubscription(settings, { selector: (s) => s.theme }) </script> <template> <button @click="settings.set({ theme: theme === 'light' ? 'dark' : 'light' })" > {{ theme }} </button> </template> ``` ```tsx // Solid import { useSubscription } from 'seitu/solid' function Theme() { const theme = useSubscription(settings, { selector: (s) => s.theme }) return ( <button onClick={() => settings.set({ theme: theme() === 'light' ? 'dark' : 'light' }) } > {theme()} </button> ) } ``` ```svelte <!-- Svelte --> <script lang="ts"> import { useSubscription } from 'seitu/svelte' const theme = useSubscription(settings, { selector: (s) => s.theme }) </script> <button onclick={() => settings.set({ theme: $theme === 'light' ? 'dark' : 'light' })}>{$theme}</button> ``` Inline creation (elements, props). Use a callback ref so the subscription is rebuilt when the element mounts, unmounts, or remounts: ```tsx const [el, setEl] = useState<HTMLDivElement | null>(null) const scroll = useSubscription(() => createScrollState({ element: el }), { deps: [el], }) return <div ref={setEl} /> ``` ## Agent skills Skills ship inside the npm package and describe the installed version's API. ```bash npx skills add letstri/seitu ``` ## License MIT