diff --git a/README.md b/README.md index b1fc940..d2e4df2 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,11 @@ # zulip -A modern, no-build web application using Lit web components with Zustand state management. +A minimal, no-build web app kit using only Lit-based web components via importmaps. -## Architecture Overview -* **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 +## Quick Start Example -## 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) +### 1. Define a Component ```javascript // components/counter.js @@ -98,15 +22,16 @@ class Counter extends LitElement { } static styles = css` - button { padding: 1rem; font-size: 1.2rem; margin: 0.5rem; } + button { padding: 1rem; font-size: 1.2rem; } ` render() { return html`
-

Count: ${this.#count.value}

- - +

Count: ${this.count}

+
` } @@ -115,24 +40,53 @@ class Counter extends LitElement { customElements.define('my-counter', Counter) ``` -### 4. Instantiate in parent (such as app-root) +### 2. Use in App root (or other composition node) ```js -import counter from "./counter.js": +import "./my-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 @@ -141,6 +95,4 @@ class AppRoot extends LitElement { 1. **Future-proof** - Built on web standards -1. **Familiar patterns** - Zustand works like Redux/Context - -1. **Type-safe** - Can add JSDoc or TypeScript without build step +1. **Easy to learn** - Simple mental model for state, with natural evolution into Zustand/IndexedDB or similar extensions diff --git a/components/app-root.js b/components/app-root.js index c5226f9..88f59a6 100644 --- a/components/app-root.js +++ b/components/app-root.js @@ -4,8 +4,18 @@ import "./page-home.js"; import "./page-items.js"; class AppRoot extends LitElement { - #user = new StoreController(this, store, s => s.user); - #route = new StoreController(this, store, s => s.route); + static properties = { + user: { type: Object, state: true }, + route: { type: String, state: true }, + items: { type: Array, state: true } + }; + + constructor() { + super(); + this.user = null; + this.route = 'home'; + this.items = []; + } static styles = css` :host { diff --git a/index.html b/index.html index 91255c2..879175b 100644 --- a/index.html +++ b/index.html @@ -8,8 +8,7 @@ { "imports": { "lit": "https://esm.sh/lit@3", - "lit/decorators.js": "https://esm.sh/lit@3/decorators.js", - "zustand/vanilla": "https://esm.sh/zustand@5/vanilla" + "lit/decorators.js": "https://esm.sh/lit@3/decorators.js" } } diff --git a/store/index.js b/store/index.js deleted file mode 100644 index 3328f31..0000000 --- a/store/index.js +++ /dev/null @@ -1,13 +0,0 @@ -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 }), -}))