Signed-off-by: Hadi <hadi@example.fr>
This commit is contained in:
Hadi
2026-08-18 16:15:03 +02:00
parent 5f99fa7521
commit aa6496901f
48 changed files with 2001 additions and 3626 deletions
+140
View File
@@ -0,0 +1,140 @@
# drawer
A full-height panel flush against the left or right edge of an already-rendered background, on top
of it dimmed - the sidebar/drawer equivalent of [`modal`](../modal/README.md), which this package
otherwise mirrors closely: same stack of panels triggered from anywhere via an exported `tea.Msg`
(`ShowMsg`/`Show`) rather than a direct reference to the `Model` that ends up rendering it, same
composite-over-an-already-rendered-string `Render`, no assumption about how the host builds that
string.
## Quick start
```go
import (
"github.com/anotherhadi/ilovetui/drawer"
)
type model struct {
d drawer.Model
width, height int
}
func newModel() model {
return model{d: drawer.New()}
}
func (m model) Init() tea.Cmd { return m.d.Init() }
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyPressMsg:
if msg.String() == "l" {
return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"))
}
if msg.String() == "esc" && m.d.Open() {
return m, drawer.Close()
}
}
var cmd tea.Cmd
m.d, cmd = m.d.Update(msg)
return m, cmd
}
func (m model) View() tea.View {
background := renderYourUI(m.width, m.height)
view := tea.NewView(m.d.Render(background))
view.AltScreen = true
return view
}
```
Any component in the same bubbletea program can trigger a drawer via `drawer.Show`, without
holding a reference to the `drawer.Model` that will actually render it - that `Model` just needs
to see every `tea.Msg` the program produces (i.e. get its `Update` called from the top-level
`Update`), same as any other child model.
## Content is a model
A drawer's body is a `tea.Model`, not a string. While a drawer is on top of the stack it gets
every message the `drawer.Model` receives, its `Init` runs when it opens, and its commands come
back out - so it can hold a file list, a filter form, or a picker that reports its choice with a
`tea.Msg` of its own, which the component that opened it listens for:
```go
type pickedMsg struct{ file string }
// somewhere else
return m, drawer.Show("Files", newFileList(dir), drawer.WithSide(drawer.Right))
```
Only the topmost drawer is updated: everything beneath it is dimmed and frozen until the drawers
above it close.
For a drawer with nothing to interact with, `drawer.Text` wraps a plain string:
```go
return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"))
```
The box shrinks to fit whatever the content draws (unless `WithWidth` fixes it), so a content
that wants a specific size sets it on itself - the drawer only ever sees the rendered result.
## Side and width
```go
return m, drawer.Show("Nav", drawer.Text("Home\nProjects\nSettings"),
drawer.WithSide(drawer.Right), drawer.WithWidth(24))
```
`WithSide` anchors the drawer to `drawer.Left` (the default) or `drawer.Right`. `WithWidth` fixes
the drawer's total width instead of shrinking to fit its content, still capped by whatever
actually fits the background - same unit as the `Model`-level `WithMaxWidth`. Either way, the
drawer always spans the background's full height, flush top to bottom.
## Showing and closing
```go
return m, drawer.Show("Files", newFileList(dir), drawer.WithSide(drawer.Right))
return m, drawer.Close() // close the topmost drawer
```
The stack is a plain LIFO, with no identity: a drawer is closed by being on top, never by being
named. There is nothing to tag a drawer with, and nothing that can target one in the middle of the
stack - the topmost is both the only one that receives messages and the only one `Close` can
reach, so the two rules never disagree.
- `drawer.Open()` reports whether at least one drawer is currently shown - handy for a host that
wants to route key presses to the drawer instead of its normal UI while one is open. Note that a
host doing this also makes it impossible for a key to open a second drawer, which is what keeps
the stack shallow without any bookkeeping.
- A content model closes its own drawer by returning `drawer.Close()`, since it only ever runs
while it is the topmost one.
## Stacking
Drawers stack: showing a second one while the first is still open pushes it on top, dimming both
the background and the first drawer to the same flat color, same as `modal`. Opening a left drawer
and a right drawer at once still stacks - if you want both visible at full color simultaneously,
render them as two ordinary panels via `layout` instead; this package is for transient
sidebars, not permanent chrome.
## Styling
```go
d := drawer.New(drawer.WithMaxWidth(30), drawer.WithStyles(myStyles))
return m, drawer.Show("Nav", drawer.Text("content"), drawer.WithDrawerStyle(oneOffStyles))
```
`WithMaxWidth` caps how wide a drawer can grow (border and padding included) before wrapping; a
drawer narrower than the cap shrinks to fit its content instead of padding out to it, unless shown
with `WithWidth`. A drawer can also never overflow past the edge of whatever background it's
rendered on. `WithStyles` sets the default look for every drawer shown by this `Model`;
`WithDrawerStyle` (a `Show` option) overrides it for one drawer alone. `DefaultStyles()` builds
from `style.S`, same palette roles as `modal.DefaultStyles()`.
## Examples
- `examples/drawer` - a left nav drawer and a right inspector drawer, either one at a time.