{"id":40481894,"url":"https://github.com/e280/lettuce","last_synced_at":"2026-01-20T18:33:22.393Z","repository":{"id":286244508,"uuid":"960748636","full_name":"e280/lettuce","owner":"e280","description":"🥬 incredible layout salad","archived":false,"fork":false,"pushed_at":"2025-11-22T21:30:26.000Z","size":504,"stargazers_count":1,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-22T23:20:02.839Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://lettuce.e280.org/","language":"TypeScript","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/e280.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-04-05T01:43:36.000Z","updated_at":"2025-11-17T22:51:06.000Z","dependencies_parsed_at":null,"dependency_job_id":"fdc15c81-659d-47aa-929a-c11a868293a5","html_url":"https://github.com/e280/lettuce","commit_stats":null,"previous_names":["e280/lettuce"],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/e280/lettuce","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Flettuce","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Flettuce/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Flettuce/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Flettuce/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/e280","download_url":"https://codeload.github.com/e280/lettuce/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/e280%2Flettuce/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28609120,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T16:10:39.856Z","status":"ssl_error","status_checked_at":"2026-01-20T16:10:39.493Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2026-01-20T18:33:21.655Z","updated_at":"2026-01-20T18:33:22.384Z","avatar_url":"https://github.com/e280.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n\u003cdiv align=\"center\"\u003e\u003cimg alt=\"\" width=256 src=\"./assets/lettuce.avif\"/\u003e\u003c/div\u003e\n\n\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n\u003e [!IMPORTANT]  \n\u003e *lettuce is just an early prototype.*  \n\u003e *more work is yet to be done in terms of features, extensibility, and customizability.*  \n\n\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n# 🥬 lettuce\n\u003e *flexible layout ui for web apps*\n\n### 🥗 splitty-panelly tabby draggy-droppy leafy layout ui\n- 👉 **https://lettuce.e280.org/** 👈 *try it, nerd!*\n- pane splitting, resizing, vertical, horizontal — you get it\n- dude, it's web components — universal compatibility\n- you can drag-and-drop tabs between panes\n  - done efficiently with *slots,* tab doesn't remount to move\n  - that's actually *legit neato* if you have heavy-weight stuff in your tabs\n- using\n  - [@e280/sly](https://github.com/e280/sly#readme) and [lit](https://lit.dev/) for ui rendering\n  - [@e280/strata](https://github.com/e280/strata#readme) for auto-reactive state management\n  - [@e280/kv](https://github.com/e280/kv#readme) for persistence\n\n### 🥗 what you're about to read\n- [**#quickstart**](#quickstart) — full install for lit apps\n- [**#layout**](#layout) — about the layout engine\n- [**#studio**](#studio) — about the ui systems\n- [**#react**](#react) — react app compatibility\n\n\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n\u003ca id=\"quickstart\"\u003e\u003c/a\u003e\n\n## 🥬 quickstart your layout salad\n\u003e *how to setup lettuce in your lit app*\n\n### 🥗 lettuce installation, html, and css\n1. **install**\n    ```bash\n    npm install @e280/lettuce lit\n    ```\n1. **html**\n    ```html\n    \u003clettuce-desk\u003e\u003c/lettuce-desk\u003e\n    ```\n1. **css**\n    ```css\n    lettuce-desk {\n      color: #fff8;\n      background: #111;\n\n      --scale: 1.5em;\n      --gutter-size: 0.7em;\n      --highlight: yellow;\n      --special: aqua;\n      --dropcover: 10%;\n      --warn: red;\n      --warntext: white;\n      --dock: #181818;\n      --taskbar: #181818;\n      --tab: transparent;\n      --tab-active: var(--dock);\n      --gutter: #000;\n      --focal: transparent;\n      --pointerlock: yellow;\n    }\n    ```\n1. **install shoelace into your html `\u003chead\u003e` (sorry)**\n    ```html\n    \u003clink\n      rel=\"stylesheet\"\n      href=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/themes/dark.css\"\n      onload=\"document.documentElement.classList.add('sl-theme-dark');\"\n    /\u003e\n    \u003cscript type=\"module\" src=\"https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/shoelace.js\" \u003e\u003c/script\u003e\n    ```\n\n### 🥗 lettuce typescript\n1. **imports**\n    ```ts\n    import {html} from \"lit\"\n    import * as lettuce from \"@e280/lettuce\"\n    ```\n1. **setup your panels** — these panels are available for the user to open\n    ```ts\n    const {panels, renderer} = lettuce.litSetup({\n      alpha: {\n        label: \"Alpha\",\n        icon: () =\u003e html`A`,\n        render: () =\u003e html`alpha content`,\n        limit: 1, // optional max open instances\n      },\n      bravo: {\n        label: \"Bravo\",\n        icon: () =\u003e html`B`,\n        render: () =\u003e html`bravo content`,\n      },\n      charlie: {\n        label: \"Charlie\",\n        icon: () =\u003e html`C`,\n        render: () =\u003e html`charlie content`,\n      },\n    })\n    ```\n1. **setup your layout**\n    ```ts\n    const layout = new lettuce.Layout({\n      stock: lettuce.Builder.fn\u003ckeyof typeof panels\u003e()(b =\u003e ({\n        default: () =\u003e b.horizontal(1, b.dock(1, \"alpha\", \"bravo\", \"charlie\")),\n        empty: () =\u003e b.blank(),\n      })),\n      defaultPanel: \"alpha\", // optional default panel for new splits\n    })\n    ```\n    - panels are referenced by their string keys.\n    - optional `limit` restricts how many copies of a panel can exist at the same time (default unlimited). once saturated, the adder buttons disable.\n    - optional `defaultPanel` opens a default panel on new split docks (pick one that can open another instance).\n    - `Layout` is a facility for reading and manipulating.\n    - `Builder.fn` helps you build a tree of layout nodes with less verbosity (note the spooky-typing double-invocation).\n    - `stock.empty` defines the fallback state for when a user closes everything.\n    - `stock.default` defines the initial state for a first-time user.\n1. **enable localstorage persistence (optional)**\n    ```ts\n    const persistence = new lettuce.Persistence({\n      layout,\n      key: \"lettuceLayoutBlueprint\",\n      kv: lettuce.Persistence.localStorageKv(),\n      broadcastChannel: new BroadcastChannel(\"lettuceBroadcast\"),\n    })\n\n    await persistence.load()\n    persistence.setupAutoSave()\n    persistence.setupLoadOnBroadcast()\n    ```\n    - see [@e280/kv](https://github.com/e280/kv#readme) to learn how to control where the data is saved\n1. **setup a studio for displaying the layout in browser**\n    ```ts\n    const studio = new lettuce.Studio({\n      panels,\n      layout,\n      renderer,\n      // controls - optional\n    })\n    ```\n    - `controls` uses `standardControls(ctx)` by default. Override it to render custom taskbar controls (see [customize studio](#studio)).\n1. **register the web components to the dom**\n    ```ts\n    studio.ui.registerComponents()\n    ```\n\n\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n\u003ca id=\"layout\"\u003e\u003c/a\u003e\n\n## 🥬 layout\n\u003e *layout engine with serializable state*\n\n### 🥗 layout package export path\n- **import directly to avoid browser concerns (for running under node etc)**\n    ```ts\n    import * as lettuce from \"@e280/lettuce/layout\"\n    ```\n\n### 🥗 layout concepts explained\n- **`Blueprint`**\n    - serializable layout data.\n    - contains a `version` number and a `root` cell.\n- **`LayoutNode`**\n    - any cell, dock, or surface.\n    - all nodes have a unique string `id`.\n    - all nodes have a `kind` string that is \"cell\", \"dock\", or \"surface\".\n- **`Cell`**\n    - a cell is a group that arranges its children either vertically or horizontally.\n    - this is where splits are expressed.\n    - a cell's children can be docks or more cells.\n- **`Dock`**\n\t- a dock contains the ui with the little tab buttons, splitting buttons, x button, etc.\n\t- a dock's children must be surfaces.\n\t- each dock stores a `taskbarAlignment` (`\"top\" | \"right\" | \"bottom\" | \"left\"`) which dictates where its taskbar renders and how the tabs orient themselves.\n- **`Surface`**\n    - a surface is the rendering target location of where a panel will be rendered.\n    - it uses a `\u003cslot\u003e` to magically render your panel into the location of this surface.\n\n### 🥗 layout [explorer.ts](./s/layout/parts/explorer.ts) — read and query immutable state\n- *read the source code for the real details*\n- the state that explorer returns is all immutable and readonly, if you try to mutate it, an error will be thrown\n- `layout.explorer.root`\n- `layout.explorer.walk()`\n- `layout.explorer.all` — is a \"scout\"\n- `layout.explorer.cells` — is a \"scout\"\n- `layout.explorer.docks` — is a \"scout\"\n- `layout.explorer.surfaces` — is a \"scout\"\n- all scouts have:\n  - `.getReport(id)`\n  - `.requireReport(id)`\n  - `.get(id)`\n  - `.require(id)`\n  - `.parent(id)`\n  - `.reports`\n  - `.nodes`\n  - `.count`\n\n### 🥗 layout [actions.ts](./s/layout/parts/actions.ts) — mutate state\n- *read the source code for the real details*\n- these actions are the only way you can mutate or modify the state\n- `layout.actions.mutate()`\n- `layout.actions.reset(cell?)`\n- `layout.actions.addSurface(dockId, panel)`\n- `layout.actions.activateSurface(surfaceId)`\n- `layout.actions.setDockActiveSurface(dockId, activeSurfaceIndex)`\n- `layout.actions.setDockTaskbarAlignment(dockId, alignment)`\n- `layout.actions.resize(id, size)`\n- `layout.actions.deleteSurface(id)`\n- `layout.actions.deleteDock(id)`\n- `layout.actions.splitDock(id, vertical)`\n- `layout.actions.moveSurface(id, dockId, destinationIndex)`\n\n### 🥗 layout state management, using [strata](https://github.com/e280/strata#readme)\n- **get/set the data**\n    ```ts\n    const blueprint = layout.getBlueprint()\n    ```\n    ```ts\n    layout.setBlueprint(blueprint)\n    ```\n- **you can manually subscribe to changes like this**\n    ```ts\n    layout.on(blueprint =\u003e {\n      console.log(\"layout changed\", blueprint)\n    })\n    ```\n- **any strata-compatible ui (like [sly](https://github.com/e280/sly#readme)) will magically auto-rerender**\n    ```ts\n    import {view} from \"@e280/sly\"\n\n    view(use =\u003e () =\u003e html`\n      \u003cp\u003enode count: ${layout.explorer.all.count}\u003c/p\u003e\n    `)\n    ```\n- **you can use strata effects to magically respond to changes**\n    ```ts\n    import {effect} from \"@e280/strata\"\n\n    effect(() =\u003e {\n      console.log(\"node count changed\", layout.explorer.all.count)\n    })\n    ```\n\n\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n\u003ca id=\"studio\"\u003e\u003c/a\u003e\n\n## 🥬 studio\n\u003e *in-browser layout user-experience*\n\n### 🥗 studio [ui.ts](./s/studio/ui/ui.ts) — control how the ui is deployed\n```ts\nconst studio = new lettuce.Studio({\n  panels,\n  layout,\n  renderer,\n  controls: context =\u003e {\n    const standard = lettuce.standardControlsParts(context)\n    return html`\n      ${standard.spawnPanel()}\n      ${standard.closeDock()}\n      ${standard.splitHorizontal()}\n      ${standard.splitVertical()}\n      // customize non standard taskbar controls as you wish\n      \u003cbutton @click=${() =\u003e context.meta.studio.layout.actions.reset()}\u003eReset\u003c/button\u003e\n      // add your own action button\n      \u003cbutton @click=${() =\u003e someAction()}\u003ewhatever\u003c/button\u003e\n    `\n  },\n})\n```\n- *read the source code for the real details*\n- `standardControls(ctx)` is the default taskbar controls (close, split, alignment, spawn panel, etc.).\n- import `standardControlsParts` instead when you need individual controls.\n- `lettuce.listPanelsChoices(meta, dock)` returns available panels for a dock, including icon, disabled state, and an open() handler.\n- `studio.ui.registerComponents()` — shortcut to register the components with their default names\n- `studio.ui.views` — access to ui in the form of sly views\n    ```ts\n    import {html} from \"lit\"\n\n    html`\n      \u003cdiv\u003e\n        ${studio.ui.views.LettuceDesk()}\n      \u003c/div\u003e\n    `\n    ```\n- `studio.ui.components` — access to ui in the form of web components\n    ```ts\n    import {dom} from \"@e280/sly\"\n\n    // manually registering the web components to the dom\n    dom.register({\n\n      // renaming the web component as an example\n      LolDesk: studio.ui.components.LettuceDesk,\n    })\n    ```\n    ```html\n    \u003clol-desk\u003e\u003c/lol-desk\u003e\n    ```\n\n\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n\u003ca id=\"react\"\u003e\u003c/a\u003e\n\n## 🥬 react\n\u003e *lettuce for your react app*\n\n### 🥗 install `react` and `sly-react`\n- sly-react allows you to turn any sly view into a react component\n    ```sh\n    npm install @e280/sly-react react\n    ```\n\n### 🥗 setup your panels and layout\n- but this time, with jsx render fns\n    ```ts\n    const panels = {\n      alpha: {\n        label: \"Alpha\",\n        icon: () =\u003e html`A`,\n        render: () =\u003e \u003cdiv\u003ealpha content\u003c/div\u003e, // 👈 jsx\n      },\n    }\n    ```\n    - note: your icons still have to be lit-html\n- and an ordinary layout\n    ```ts\n    const layout = new lettuce.Layout({\n      stock: lettuce.Builder.fn\u003ckeyof typeof panels\u003e()(b =\u003e ({\n        default: () =\u003e b.horizontal(1, b.dock(1, \"alpha\")),\n        empty: () =\u003e b.blank(),\n      })),\n    })\n    ```\n\n### 🥗 make your studio and the hook\n- we literally provide sly-react's `reactify` and various react fns to `reactIntegration`\n    ```ts\n    import * as lettuce from \"@e280/lettuce\"\n    import {reactify} from \"@e280/sly-react\"\n    import {useRef, useState, useEffect, createElement} from \"react\"\n\n    const {renderer, makeDeskComponent} = lettuce.reactIntegration({\n      reactify,\n      useRef,\n      useState,\n      useEffect,\n      createElement,\n    })\n\n    const studio = new lettuce.Studio({renderer, panels, layout})\n    const LettuceDesk = makeDeskComponent(studio)\n    ```\n    - lettuce does not depend on react, but accepts react-shaped stuff to perform the integration\n    - studio requires the `renderer` that the react integration gives you\n\n### 🥗 LettuceDesk component usage\n- now you can use the component\n    ```ts\n    const MyReactComponent = () =\u003e {\n      return (\n        \u003cdiv\u003e\n          \u003cLettuceDesk render={surface =\u003e panels[surface.panel].render()} /\u003e\n        \u003c/div\u003e\n      )\n    }\n    ```\n\n\n\n\u003cbr/\u003e\u003cbr/\u003e\n\n## 🥬 i made this open sourcedly just for you\npay your respects, gimmie a github star.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fe280%2Flettuce","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fe280%2Flettuce","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fe280%2Flettuce/lists"}