diff --git a/README.md b/README.md index d2e4df2..b1fc940 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,87 @@ # zulip -A minimal, no-build web app kit using only Lit-based web components via importmaps. +A modern, no-build web application using Lit web components with Zustand state management. +## Architecture Overview -## Quick Start Example +* **Lit-based web components** - Standards-based, framework-agnostic UI +* **No-build tooling** - Import maps only, no bundler required +* **Zustand state management** - Lightweight, vanilla JS store +* **StoreController** - Reactive controller that bridges Zustand and Lit -### 1. Define a Component +## Project Structure + +``` +components/ -> standard lit components, including top-level "app-root" +controllers/ -> lit reactive controllers (such as Zustand<->Lit bridge) +store/ -> defines specific accessor sets +index.html -> entry point with inline importmap and app-root +README.md +``` + +## Data Flow Diagram + +```mermaid +sequenceDiagram + participant User + participant LitComponent + participant StoreController + participant ZustandStore + + User->>LitComponent: Click button + LitComponent->>ZustandStore: Call action (e.g., addItem) + ZustandStore->>ZustandStore: Update state + ZustandStore->>StoreController: Notify subscribers + StoreController->>LitComponent: requestUpdate() + LitComponent->>LitComponent: Re-render + LitComponent->>User: Show updated UI +``` + +## Example + +### 1. Define Store (Zustand Vanilla) + +```javascript +// store/index.js +import { createStore } from 'zustand/vanilla' + +export const store = createStore((set, get) => ({ + count: 0, + increment: () => set(s => ({ count: s.count + 1 })), + decrement: () => set(s => ({ count: s.count - 1 })), +})) +``` + +### 2. Create StoreController (Bridge) + +```javascript +// controllers/store.js +export class StoreController { + constructor(host, store, selector) { + this.host = host + this.store = store + this.selector = selector + host.addController(this) + } + + hostConnected() { + this._unsub = this.store.subscribe((state) => { + const next = this.selector(state) + if (next !== this.value) { + this.value = next + this.host.requestUpdate() + } + }) + this.value = this.selector(this.store.getState()) + } + + hostDisconnected() { + this._unsub?.() + } +} +``` + +### 3. Create Component (Lit + StoreController) ```javascript // components/counter.js @@ -22,16 +98,15 @@ class Counter extends LitElement { } static styles = css` - button { padding: 1rem; font-size: 1.2rem; } + button { padding: 1rem; font-size: 1.2rem; margin: 0.5rem; } ` render() { return html`
-

Count: ${this.count}

- +

Count: ${this.#count.value}

+ +
` } @@ -40,53 +115,24 @@ class Counter extends LitElement { customElements.define('my-counter', Counter) ``` -### 2. Use in App root (or other composition node) +### 4. Instantiate in parent (such as app-root) ```js -import "./my-counter.js"; +import counter from "./counter.js": class AppRoot extends LitElement { - ... render() { - return html``; + return html` + + `; } } ``` -### 3. Define Data Flows - -#### Parent-to-Child (Props) - -```javascript -// Parent passes data down -html`` - -// Child receives via properties -static properties = { - items: { type: Array } -} -``` - -#### Child-to-Parent (Events) - -```javascript -// Child emits event -this.dispatchEvent(new CustomEvent('add-item', { - detail: { item: newItem }, - bubbles: true, - composed: true -})) - -// Parent listens -html` this.handleAdd(e.detail.item)}>` -``` - ## Benefits Over TSX 1. **No build step** - Edit and refresh, instant feedback -1. **Minimal dependencies** - Just Lit, nothing else - 1. **Smaller bundle** - No framework runtime, just standards 1. **Better encapsulation** - Shadow DOM, scoped styles @@ -95,4 +141,6 @@ html` this.handleAdd(e.detail.item)}> s.user); + #route = new StoreController(this, store, s => s.route); static styles = css` :host { diff --git a/index.html b/index.html index 879175b..91255c2 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,8 @@ { "imports": { "lit": "https://esm.sh/lit@3", - "lit/decorators.js": "https://esm.sh/lit@3/decorators.js" + "lit/decorators.js": "https://esm.sh/lit@3/decorators.js", + "zustand/vanilla": "https://esm.sh/zustand@5/vanilla" } } diff --git a/store/index.js b/store/index.js new file mode 100644 index 0000000..3328f31 --- /dev/null +++ b/store/index.js @@ -0,0 +1,13 @@ +import { createStore } from 'zustand/vanilla' + +export const store = createStore((set, get) => ({ + user: null, + items: [], + route: 'home', + + // Actions + setUser: (user) => set({ user }), + addItem: (item) => set(s => ({ items: [...s.items, item] })), + removeItem: (id) => set(s => ({ items: s.items.filter(i => i.id !== id) })), + navigate: (route) => set({ route }), +}))