LemonadeJS Studio
Studio is the first-party block catalog: 43 components, 786 contract checks, every one passing before the catalog is allowed to build. A block is not a snippet. It is a publishable npm package that ships with a machine-readable contract, a conformance proof, generated documentation, a playground demo, and three deployment forms:
import Modal from '@lemonadejs/modal';
import '@lemonadejs/modal/style.css';
html`<${Modal} title="Hello" closable draggable>...</${Modal}>`;
// by value, no registration
setComponents({ Modal }); // then <Modal /> by name anywhere
createWebComponent(Modal); // <lm-modal> in plain HTML, React, Vue
Component library considerations
A component library is a bundle of decisions. Adopting a block means inheriting those decisions: coherent choices for the library’s goals that become conditions for the consumer:
- A host framework. Most block libraries target one framework; the host application needs that framework’s runtime to use them.
- A styling system. Components are styled through the library’s own engine, tokens or utility classes; customization requires learning that system.
- Documented, not proven, interfaces. The documentation says the
component accepts
disabled; the mechanism that keeps that claim true across versions is maintainer discipline and the changelog, nothing a consumer can run. - Floating-layer machinery. Selects, menus, tooltips, dialogs and drawers all need positioning, stacking and dismissal. Libraries solve this with shared internal layers or a positioning dependency: machinery with its own surface to learn when behavior needs overriding.
- Source-priced discovery. For an agent, learning a component means reading its source or prose docs: thousands of tokens per block, re-paid every session.
The Studio’s design brief chose a different set of conditions, described below, with its own costs, listed at the end.
Architecture
One primitive, eight composers
The catalog’s architecture decision is visible in its dependency graph:
every floating surface is the Modal. Eight blocks declare
@lemonadejs/modal as their only dependency and build on its behaviors
(open/close origins, anchoring, auto-adjust to the viewport, backdrop,
element-scoped Escape handling):
- Contextmenu: every menu level is a headerless, auto-adjusting Modal; submenus are a stack of them, flipping direction when out of space.
- Dropdown: select, autocomplete and picker anchor their panel on the Modal, with the datagrid’s fixed-row-height virtualization inside.
- Calendar: date, datetime and range picker on the same anchored panel.
- Color: the picker panel, same anchor mechanics.
- Drawer: the Modal’s
left/rightpositions are already full-height side panels; the drawer adds anchor mapping and the slide-in. The bottom sheet is the same primitive. - Dialog: confirm / alert / prompt as centered Modal sheets.
- Actionsheet is an iOS-style sheet: headerless Modal, position bottom, backdrop.
- Schedule: its built-in event editor opens on the Modal.
And it goes a level deeper: Quickmenu, Toolbar and Topmenu each build on the Contextmenu, which is itself a stack of Modals, so eleven of the forty-three blocks ultimately stand on the one primitive.
Composition is not code reuse for its own sake. It is where fixes flow. When the Modal stopped snapshotting its anchor coordinates at construction and began re-reading them per open (the bug documented in computed()), panel placement was repaired in every block that anchors on it, in the same commit. One primitive fixed, every composer repaired. The difference from the shared internal layers above is not the sharing. It is that the shared layer is itself a published, contract-verified block, with the same public surface its composers use.
The Modal earns the load it carries: 49 contract checks, 8-edge resize, viewport-clamped dragging, a minimize dock, and the destroy receipt: 100,000 create/open/drag/destroy cycles with zero detached DOM nodes (see Destroy).
The catalog
| Block | Package | Checks | Notes |
|---|---|---|---|
| Accordion | @lemonadejs/accordion | 9 | |
| Actionsheet | @lemonadejs/actionsheet | 13 | sheet on the Modal primitive |
| Alert | @lemonadejs/alert | 15 | |
| Backdrop | @lemonadejs/backdrop | 13 | |
| Button | @lemonadejs/button | 22 | |
| Buttongroup | @lemonadejs/buttongroup | 18 | |
| Calendar | @lemonadejs/calendar | 39 | date/datetime/range picker on the Modal primitive |
| Card | @lemonadejs/card | 20 | |
| Carousel | @lemonadejs/carousel | 14 | single-file: styles ship inside the component |
| Color | @lemonadejs/color | 14 | picker panel on the Modal primitive |
| Contextmenu | @lemonadejs/contextmenu | 6 | levels are a stack of Modals |
| Cropper | @lemonadejs/cropper | 21 | |
| Datagrid | @lemonadejs/datagrid | 23 | virtualized big-data grid (100k rows, 5ms mount) |
| Dialog | @lemonadejs/dialog | 19 | confirm/alert/prompt on the Modal primitive |
| Drawer | @lemonadejs/drawer | 15 | side panels + bottom sheet on the Modal primitive |
| Dropdown | @lemonadejs/dropdown | 39 | select/autocomplete/picker on the Modal primitive |
| Formify | @lemonadejs/formify | 8 | your markup in, one data object out |
| Gantt | @lemonadejs/gantt | 24 | %-positioned bars that align across instances |
| Imagelist | @lemonadejs/imagelist | 14 | |
| Kanban | @lemonadejs/kanban | 7 | card DOM identity survives cross-column moves (one flat keyed list) |
| List | @lemonadejs/list | 26 | search, pagination, remote mode |
| Login | @lemonadejs/login | 36 | multi-screen authentication flows |
| Modal | @lemonadejs/modal | 49 | the platform primitive |
| Navbar | @lemonadejs/navbar | 13 | |
| Progress | @lemonadejs/progress | 15 | |
| Quickmenu | @lemonadejs/quickmenu | 12 | |
| Rating | @lemonadejs/rating | 20 | |
| Router | @lemonadejs/router | 11 | |
| Schedule | @lemonadejs/schedule | 35 | event editor on the Modal primitive |
| Signature | @lemonadejs/signature | 21 | |
| Slider | @lemonadejs/slider | 20 | |
| Speeddial | @lemonadejs/speeddial | 18 | |
| Switch | @lemonadejs/switch | 22 | native checkbox core |
| Tabs | @lemonadejs/tabs | 18 | |
| Timeline | @lemonadejs/timeline | 32 | |
| Toast | @lemonadejs/toast | 11 | |
| Toggle | @lemonadejs/toggle | 14 | |
| Toolbar | @lemonadejs/toolbar | 10 | |
| Tooltip | @lemonadejs/tooltip | 13 | |
| Topmenu | @lemonadejs/topmenu | 4 | |
| Transferlist | @lemonadejs/transferlist | 12 | |
| Treeview | @lemonadejs/treeview | 7 | one recursive keyed view function |
| Wheel | @lemonadejs/wheel | 14 |
What every block guarantees
Each row in that table is backed by the same set of shipped artifacts, none of them hand-maintained:
contract.json: the machine-readable interface (props with types and defaults, bind, events, api), exported by the package itself. An agent decides “is this the block I need?” without reading source. See Contracts.verify.json, the conformance proof:verify(Block)exercises every declared prop (as plain value and as live state), every event, bind, and the api actually exposed, and fails on any engine warning during any check. The “Checks” column above counts these.- The registry gate.
npm run registryregenerates every contract and proof; one failing block fails the build. A block whose proof fails does not enter the catalog: enforced, not reviewed. - A generated README, synthesized from the source header, the contract’s inline comments and the proof. Documentation cannot drift from the interface because it is a projection of it.
- A playground demo:
npm run devserves every block’s demo page. - Three deployments, one component: by value, by name, and as a custom element; the React adapter derives from the same contract. See Deployments and React.
- Destroy-gated. Drag systems hold one persistent cleanup, suites assert listener balance, and the engine-level heap/WeakRef gates run on every change (Destroy).
Distribution models
Component distribution today follows three broad models, each serving its purpose well:
| Framework libraries | Design-system libraries | Copy-source kits | Studio | |
|---|---|---|---|---|
| Framework reach | one framework | one framework | one framework + its styling stack | any: native, custom element, React adapter |
| Styling | the library’s styling engine and theme system | design language, tokens | utility classes you own | plain CSS, lm-<name>-* classes, data-* variants |
| Distribution | npm dependency | npm dependency | code copied into your repo | npm package, a few KB, zero deps beyond composed primitives |
| Machine-readable schema | TS types (compile-time) | TS types (compile-time) | the source itself | contract.json ships in the package |
| Conformance proof | the test suite, maintainer-side | the test suite, maintainer-side | your responsibility | verify.json, regenerated and gated |
| Docs currency | maintained by hand | maintained by hand | your responsibility | generated from the contract |
Differences that favor the established models:
- Theming depth. Mature design-token systems, palettes, dark-mode
infrastructure and styling escape hatches are far more than
lm-*classes plus CSS variables. An organization living in a themed design system is better served by machinery built for that. - Accessibility maturity. Years of hardening in focus management and screen-reader behavior; the Studio Modal’s focus trap is still on the roadmap (a named TODO, not a hidden gap).
- Breadth and battle-testing. Hundreds of components, millions of installs, every edge case already filed by someone. Studio is 43 blocks and a beta.
- The copy-source ownership model. Copying readable source into
your repo so you can edit it gives full control. Studio blocks are
one readable file because the same property matters. A Studio fork
additionally keeps something mechanical to check edits against:
verify()still runs.
What the Studio adds is the right side of the table: a contract an agent reads in one request, and a proof that the implementation honors it, regenerated, and refused at the gate when it fails.
In practice
A settings panel: one block composed inside another, two-way bound, then deployed to a non-lemonade host without changes.
import { html, mount, ref, createWebComponent } from 'lemonadejs';
import Modal from '@lemonadejs/modal';
import Switch from '@lemonadejs/switch';
const Settings = (props, { state }) => {
const open = state(false);
const dark = state(false);
return html`<div>
<button onclick="${() => (open.value = true)}">Settings</button>
<${Modal} bind="${open}" title="Settings" closable draggable>
<${Switch} bind="${dark}" label="Dark mode"
onchange="${(v) => document.body.classList.toggle('dark', v)}" />
</${Modal}>
</div>`;
};
mount(Settings, document.getElementById('root'));
// The same blocks in a non-lemonade page, no rewrite:
createWebComponent(Modal); // <lm-modal title="Settings" closable>
createWebComponent(Switch); // <lm-switch label="Dark mode">
bind is the Modal’s open state (two-way: closing from the X writes
false back) and the Switch’s checked state: the same
binding model on every block, because every
block is built from the same recipe. That recipe, including how to add
a block of your own, is the next chapter:
Building blocks.
Reference
@lemonadejs/<name> one package per block
import Block from '@lemonadejs/<name>';
import '@lemonadejs/<name>/style.css';
import contract from '@lemonadejs/<name>/contract.json';
components/<name>/ (in the repo)
src/index.ts, src/style.css the implementation
contract.json, verify.json generated interface + proof
README.md generated documentation
demo.ts / demo.html playground page
components/registry.json all 43 contracts, one request
npm run dev playground on :3000
npm run registry regenerate contracts/proofs: THE GATE
npm run docs regenerate READMEs + catalog index
Blocks compose like any component (Components), verify like any contract (Tests), and die cleanly or do not ship (Destroy).