flexible layout ui for web apps
- π https://lettuce.e280.org/ π try it, nerd!
- pane splitting, resizing, vertical, horizontal β you get it
- dude, it's web components β universal compatibility
- you can drag-and-drop tabs between panes
- done efficiently with slots, tab doesn't remount to move
- that's actually legit neato if you have heavy-weight stuff in your tabs
- using
- @e280/sly and lit for ui rendering
- @e280/strata for state management
- @e280/kv for persistence
Important
lettuce is just an early prototype.
more work is yet to be done in terms of features, extensibility, and customizability.
how to setup lettuce in your app
- install
npm install @e280/lettuce lit
- html
<lettuce-desk></lettuce-desk>
- css
lettuce-desk { color: #fff8; background: #111; --scale: 1.5em; --highlight: yellow; --special: aqua; --dropcover: 10%; --warn: red; --warntext: white; --dock: #181818; --taskbar: #181818; --tab: transparent; --gutter: #000; --focal: transparent; --pointerlock: yellow; }
- imports
import {html} from "lit" import * as lettuce from "@e280/lettuce"
- setup your panels β these panels are available for the user to open
const panels = lettuce.asPanels({ alpha: { label: "Alpha", icon: () => html`π¦`, render: () => html`alpha content`, }, bravo: { label: "Bravo", icon: () => html`π§`, render: () => html`bravo content`, }, charlie: { label: "Charlie", icon: () => html`π¨`, render: () => html`charlie content`, }, })
- setup your layout
const layout = new lettuce.Layout({ stock: lettuce.Builder.fn<keyof typeof panels>()(b => ({ default: () => b.cell(b.tabs("alpha", "bravo", "charlie")), empty: () => b.blank(), })), })
- panels are referenced by their string keys.
Layoutis a facility for reading and manipulating.Builder.fnhelps you build a tree of layout nodes with less verbosity (note the spooky-typing double-invocation).stock.emptydefines the fallback state for when a user closes everything.stock.defaultdefines the initial state for a first-time user.
- enable localstorage persistence (optional)
const persistence = new lettuce.Persistence({ layout, key: "lettuceLayoutBlueprint", kv: lettuce.Persistence.localStorageKv(), }) await persistence.load() persistence.setupAutoSave() persistence.setupLoadOnStorageEvent()
- see @e280/kv to learn how to control where the data is saved
- setup a studio for displaying the layout in browser
const studio = new lettuce.Studio({panels, layout})
- register the web components to the dom
studio.ui.registerComponents()
layout engine with serializable state
- import directly to avoid browser concerns (for running under node etc)
import * as lettuce from "@e280/lettuce/layout"
Blueprint- serializable layout data.
- contains a
versionnumber and arootcell.
LayoutNode- any cell, dock, or surface.
- all nodes have a unique string
id. - all nodes have a
kindstring that is "cell", "dock", or "surface".
Cell- a cell is a group that arranges its children either vertically or horizontally.
- this is where splits are expressed.
- a cell's children can be docks or more cells.
Dock- a dock contains the ui with the little tab buttons, splitting buttons, x button, etc.
- a dock's children must be surfaces.
Surface- a surface is the rendering target location of where a panel will be rendered.
- it uses a
<slot>to magically render your panel into the location of this surface.
π₯ layout explorer.ts β read and query immutable state
- read the source code for the real details
- the state that explorer returns is all immutable and readonly, if you try to mutate it, an error will be thrown
layout.explorer.rootlayout.explorer.walk()layout.explorer.allβ is a "scout"layout.explorer.cellsβ is a "scout"layout.explorer.docksβ is a "scout"layout.explorer.surfacesβ is a "scout"- all scouts have:
.getReport(id).requireReport(id).get(id).require(id).parent(id).reports.nodes.count
π₯ layout actions.ts β mutate state
- read the source code for the real details
- these actions are the only way you can mutate or modify the state
layout.actions.mutate()layout.actions.reset(cell?)layout.actions.addSurface(dockId, panel)layout.actions.activateSurface(surfaceId)layout.actions.setDockActiveSurface(dockId, activeSurfaceIndex)layout.actions.resize(id, size)layout.actions.deleteSurface(id)layout.actions.deleteDock(id)layout.actions.splitDock(id, vertical)layout.actions.moveSurface(id, dockId, destinationIndex)
π₯ layout state management, using strata
- get/set the data
const blueprint = layout.getBlueprint()
layout.setBlueprint(blueprint)
- you can manually subscribe to changes like this
layout.on(blueprint => { console.log("layout changed", blueprint) })
- any strata-compatible ui (like sly) will magically auto-rerender
import {view} from "@e280/sly" view(use => () => html` <p>node count: ${layout.explorer.all.count}</p> `)
- you can use strata effects to magically respond to changes
import {effect} from "@e280/strata" effect(() => { console.log("node count changed", layout.explorer.all.count) })
in-browser layout user-experience
π₯ studio ui.ts β control how the ui is deployed
- read the source code for the real details
studio.ui.registerComponents()β shortcut to register the components with their default namesstudio.ui.viewsβ access to ui in the form of sly viewsimport {html} from "lit" html` <div> ${studio.ui.views.Desk()} </div> `
studio.ui.componentsβ access to ui in the form of web componentsimport {dom} from "@e280/sly" // manually registering the web components to the dom dom.register({ // renaming the web component as an example LolDesk: studio.ui.components.LettuceDesk, })
<lol-desk></lol-desk>
pay your respects, gimmie a github star.
