{"id":20931255,"url":"https://github.com/efflore/capsula","last_synced_at":"2026-04-21T23:02:18.455Z","repository":{"id":261848922,"uuid":"885434352","full_name":"efflore/capsula","owner":"efflore","description":"Capsula - base class for Web Components with reactive states and UI effects","archived":false,"fork":false,"pushed_at":"2024-12-14T15:32:04.000Z","size":266,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-19T19:08:35.937Z","etag":null,"topics":["capsula","custom-elements","effects","reactivity","signals","web-components"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/efflore.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-11-08T15:21:18.000Z","updated_at":"2024-12-14T15:32:08.000Z","dependencies_parsed_at":"2024-12-14T16:22:28.408Z","dependency_job_id":"c30db1fd-3009-47ba-a916-62b4c2f9c94b","html_url":"https://github.com/efflore/capsula","commit_stats":null,"previous_names":["efflore/capsula"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/efflore%2Fcapsula","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/efflore%2Fcapsula/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/efflore%2Fcapsula/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/efflore%2Fcapsula/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/efflore","download_url":"https://codeload.github.com/efflore/capsula/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243324507,"owners_count":20273113,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["capsula","custom-elements","effects","reactivity","signals","web-components"],"created_at":"2024-11-18T21:40:13.656Z","updated_at":"2025-12-28T23:30:15.166Z","avatar_url":"https://github.com/efflore.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Capsula\n\nVersion 0.9.3\n\n**Capsula** - transform reusable markup, styles and behavior into powerful, reactive, and maintainable Web Components.\n\n`Capsula` is a base class for Web Components with reactive states and UI effects. Capsula is tiny, around 3kB gzipped JS code, of which unused functions can be tree-shaken by build tools. It uses [Cause \u0026 Effect](https://github.com/efflore/cause-effect) internally for state management with signals and [Pulse](https://github.com/efflore/pulse) for scheduled DOM updates.\n\n## Key Features\n\n* **Reusable Components**: Create highly modular and reusable components to encapsulate styles and behavior.\n* **Declarative States**: Bring static, server-rendered content to life with dynamic interactivity and state management.\n* **Signal-Based Reactivity**: Employ signals for efficient state propagation, ensuring your components react instantly to changes.\n* **Declarative Effects**: Use granular effects to automatically synchronize UI states with minimal code.\n* **Context Support**: Share global states across your component tree without tightly coupling logic.\n\n## Installation\n\n```bash\n# with npm\nnpm install @efflore/capsula\n\n# or with bun\nbun add @efflore/capsula\n```\n\nFor the functional core of your application we recommend [FlowSure](https://github.com/efflore/flow-sure) to create a robust and expressive data flow, supporting error handling and async processing with Result monads.\n\n## Basic Usage\n\n### Show Appreciation\n\nServer-rendered markup:\n\n```html\n\u003cshow-appreciation aria-label=\"Show appreciation\"\u003e\n    \u003cbutton type=\"button\"\u003e\n        \u003cspan class=\"emoji\"\u003e💐\u003c/span\u003e\n        \u003cspan class=\"count\"\u003e5\u003c/span\u003e\n    \u003c/button\u003e\n\u003c/show-appreciation\u003e\n```\n\nCapsula component:\n\n```js\nimport { Capsula, asInteger, setText } from '@efflore/capsula'\n\nclass ShowAppreciation extends Capsula {\n    #count = Symbol() // Use a private Symbol as state key\n\n    connectedCallback() {\n        // Initialize count state\n        this.set(this.#count, asInteger(this.querySelector('.count').textContent) ?? 0)\n\n        // Bind click event to increment count\n        this.first('button').on('click', () =\u003e this.set(this.#count, v =\u003e ++v))\n\n        // Update .count text when count changes\n        this.first('.count').sync(setText(this.#count))\n    }\n\n    // Expose read-only property for count\n    get count() {\n        return this.get(this.#count)\n    }\n}\nShowAppreciation.define('show-appreciation')\n```\n\nExample styles:\n\n```css\nshow-appreciation {\n    display: inline-block;\n\n    \u0026 button {\n        display: flex;\n        flex-direction: row;\n        gap: var(--space-s);\n        border: 1px solid var(--color-border);\n        border-radius: var(--space-xs);\n        background-color: var(--color-secondary);\n        color: var(--color-text);\n        padding: var(--space-xs) var(--space-s);\n        cursor: pointer;\n        font-size: var(--font-size-m);\n        line-height: var(--line-height-xs);\n        transition: transform var(--transition-short) var(--easing-inout);\n\n        \u0026:hover {\n            background-color: var(--color-secondary-hover);\n        }\n\n        \u0026:active {\n            background-color: var(--color-secondary-active);\n\n            .emoji {\n                transform: scale(1.1);\n            }\n        }\n    }\n}\n```\n\n### Tab List and Panels\n\nAn example demonstrating how to pass states from one component to another. Server-rendered markup:\n\n```html\n\u003ctab-list\u003e\n    \u003cmenu\u003e\n        \u003cli\u003e\u003cbutton type=\"button\"\u003eTab 1\u003c/button\u003e\u003c/li\u003e\n        \u003cli\u003e\u003cbutton type=\"button\"\u003eTab 2\u003c/button\u003e\u003c/li\u003e\n        \u003cli\u003e\u003cbutton type=\"button\"\u003eTab 3\u003c/button\u003e\u003c/li\u003e\n    \u003c/menu\u003e\n    \u003ctab-panel open\u003e\n        \u003ch2\u003eTab 1\u003c/h2\u003e\n        \u003cp\u003eContent of tab panel 1\u003c/p\u003e\n    \u003c/tab-panel\u003e\n    \u003ctab-panel\u003e\n        \u003ch2\u003eTab 2\u003c/h2\u003e\n        \u003cp\u003eContent of tab panel 2\u003c/p\u003e\n    \u003c/tab-panel\u003e\n    \u003ctab-panel\u003e\n        \u003ch2\u003eTab 3\u003c/h2\u003e\n        \u003cp\u003eContent of tab panel 3\u003c/p\u003e\n    \u003c/tab-panel\u003e\n\u003c/tab-list\u003e\n```\n\nCapsula components:\n\n```js\nimport { Capsula, setAttribute, toggleAttribute } from '@efflore/capsula'\n\nclass TabList extends Capsula {\n    connectedCallback() {\n\n        // Set inital active tab by querying tab-panel[open]\n        let openPanelIndex = 0;\n        this.querySelectorAll('tab-panel').forEach((el, index) =\u003e {\n            if (el.hasAttribute('open')) openPanelIndex = index\n        })\n        this.set('active', openPanelIndex)\n\n        // Handle click events on menu buttons and update active tab index\n        this.all('menu button')\n            .on('click', (_el, index) =\u003e () =\u003e this.set('active', index))\n            .sync((host, target, index) =\u003e setAttribute(\n                'aria-pressed',\n                () =\u003e host.get('active') === index ? 'true' : 'false')(host, target)\n            )\n\n        // Pass open attribute to tab-panel elements based on active tab index\n        this.all('tab-panel').pass({\n            open: (_el, index) =\u003e () =\u003e index === this.get('active')\n        })\n    }\n}\nTabList.define('tab-list')\n\nclass TabPanel extends Capsula {\n    connectedCallback() {\n        this.self.sync(toggleAttribute('open'))\n    }\n}\nTabPanel.define('tab-panel')\n```\n\nExample styles:\n\n```css\ntab-list menu {\n    list-style: none;\n    display: flex;\n    gap: 0.2rem;\n    padding: 0;\n\n    \u0026 button[aria-pressed=\"true\"] {\n        color: red;\n    }\n}\n\ntab-panel {\n    display: none;\n\n    \u0026[open] {\n        display: block;\n    }\n}\n```\n\n### Lazy Load\n\nA more complex component demonstrating async fetch from the server:\n\n```html\n\u003clazy-load src=\"/lazy-load/snippet.html\"\u003e\n    \u003cdiv class=\"loading\"\u003eLoading...\u003c/div\u003e\n    \u003cdiv class=\"error\"\u003e\u003c/div\u003e\n\u003c/lazy-load\u003e\n```\n\n```js\nimport { Capsula, setText, setProperty, effect, enqueue } from '@efflore/capsula'\n\nclass LazyLoad extends Capsula {\n    static observedAttributes = ['src']\n    static states = {\n        src: v =\u003e {\n                let url = ''\n                try {\n                    url = new URL(v, location.href) // ensure 'src' attribute is a valid URL\n                    if (url.origin !== location.origin) // sanity check for cross-origin URLs\n                        throw new TypeError('Invalid URL origin')\n                } catch (error) {\n                    console.error(error, url)\n                    url = ''\n                }\n                return url.toString()\n            },\n        error: ''\n    }\n\n    connectedCallback() {\n\n        // Show / hide loading message\n        this.first('.loading')\n            .sync(setProperty('ariaHidden', () =\u003e !!this.get('error')))\n\n        // Set and show / hide error message\n        this.first('.error')\n            .sync(setText('error'))\n            .sync(setProperty('ariaHidden', () =\u003e !this.get('error')))\n\n        // Load content from provided URL\n        effect(async () =\u003e {\n            const src = this.get('src')\n            if (!src) return // silently fail if no valid URL is provided\n            try {\n                const response = await fetch(src)\n                if (response.ok) {\n                    const content = await response.text()\n                    enqueue(() =\u003e {\n                        // UNSAFE!, use only trusted sources in 'src' attribute\n                        this.root.innerHTML = content\n                        this.root.querySelectorAll('script').forEach(script =\u003e {\n                            const newScript = document.createElement('script')\n                            newScript.appendChild(document.createTextNode(script.textContent))\n                            this.root.appendChild(newScript)\n                            script.remove()\n                        })\n                    }, [this.root, 'h'])\n                    this.set('error', '')\n                } else {\n                    this.set('error', response.status + ':'+ response.statusText)\n                }\n            } catch (error) {\n                this.set('error', error)\n            }\n        })\n    }\n}\nLazyLoad.define('lazy-load')\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fefflore%2Fcapsula","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fefflore%2Fcapsula","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fefflore%2Fcapsula/lists"}