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/right positions 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

BlockPackageChecksNotes
Accordion@lemonadejs/accordion9
Actionsheet@lemonadejs/actionsheet13sheet on the Modal primitive
Alert@lemonadejs/alert15
Backdrop@lemonadejs/backdrop13
Button@lemonadejs/button22
Buttongroup@lemonadejs/buttongroup18
Calendar@lemonadejs/calendar39date/datetime/range picker on the Modal primitive
Card@lemonadejs/card20
Carousel@lemonadejs/carousel14single-file: styles ship inside the component
Color@lemonadejs/color14picker panel on the Modal primitive
Contextmenu@lemonadejs/contextmenu6levels are a stack of Modals
Cropper@lemonadejs/cropper21
Datagrid@lemonadejs/datagrid23virtualized big-data grid (100k rows, 5ms mount)
Dialog@lemonadejs/dialog19confirm/alert/prompt on the Modal primitive
Drawer@lemonadejs/drawer15side panels + bottom sheet on the Modal primitive
Dropdown@lemonadejs/dropdown39select/autocomplete/picker on the Modal primitive
Formify@lemonadejs/formify8your markup in, one data object out
Gantt@lemonadejs/gantt24%-positioned bars that align across instances
Imagelist@lemonadejs/imagelist14
Kanban@lemonadejs/kanban7card DOM identity survives cross-column moves (one flat keyed list)
List@lemonadejs/list26search, pagination, remote mode
Login@lemonadejs/login36multi-screen authentication flows
Modal@lemonadejs/modal49the platform primitive
Navbar@lemonadejs/navbar13
Progress@lemonadejs/progress15
Quickmenu@lemonadejs/quickmenu12
Rating@lemonadejs/rating20
Router@lemonadejs/router11
Schedule@lemonadejs/schedule35event editor on the Modal primitive
Signature@lemonadejs/signature21
Slider@lemonadejs/slider20
Speeddial@lemonadejs/speeddial18
Switch@lemonadejs/switch22native checkbox core
Tabs@lemonadejs/tabs18
Timeline@lemonadejs/timeline32
Toast@lemonadejs/toast11
Toggle@lemonadejs/toggle14
Toolbar@lemonadejs/toolbar10
Tooltip@lemonadejs/tooltip13
Topmenu@lemonadejs/topmenu4
Transferlist@lemonadejs/transferlist12
Treeview@lemonadejs/treeview7one recursive keyed view function
Wheel@lemonadejs/wheel14

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 registry regenerates 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 dev serves 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 librariesDesign-system librariesCopy-source kitsStudio
Framework reachone frameworkone frameworkone framework + its styling stackany: native, custom element, React adapter
Stylingthe library’s styling engine and theme systemdesign language, tokensutility classes you ownplain CSS, lm-<name>-* classes, data-* variants
Distributionnpm dependencynpm dependencycode copied into your reponpm package, a few KB, zero deps beyond composed primitives
Machine-readable schemaTS types (compile-time)TS types (compile-time)the source itselfcontract.json ships in the package
Conformance proofthe test suite, maintainer-sidethe test suite, maintainer-sideyour responsibilityverify.json, regenerated and gated
Docs currencymaintained by handmaintained by handyour responsibilitygenerated 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).