npm package: @lofi-ui/lofi
Description: Simple, unstyled, web elements for common UI components
Author: Caleb Porzio
Latest version: 0.1.4

# Lofi

The web's missing UI primitives.

## Install it

Lofi was written in vanilla JavaScript modules with no build process whatsoever. Simply include `index.js` on your page and you're off:

```html
<head>
    <!-- ... -->

    <script src="./index.js" type="module"></script>
</head>
```

## Use it

To get a glimpse of how lofi works, paste the following HTML into a page and watch it sing...

```html
<ui-invoke anchor>
    <button type="button">Open</button>

    <ui-menu popover>
        <ui-item>Foo</ui-item>
        <ui-item>Bar</ui-item>
        <ui-item>Baz</ui-item>
    </ui-menu>
</ui-invoke>
```

## Components

Lofi ships with the following components:

* [Radio Group](#radio-group)
* [Disclosure]() Call it Accordion? Chop it?
* [Dropdown]()
* [Select]()
* [Combobox]()
* [Tooltip]()
* [Menu]()
* [Switch]()
* [Radio]()
* [Checkbox]()
* [Modal]()
* [Field]()
* [Tabs]()

### Radio Group

```html
<ui-radio-group>
    <ui-radio value="foo" checked>Foo</ui-radio>
    <ui-radio value="bar">Bar</ui-radio>
    <ui-radio value="baz">Baz</ui-radio>
</ui-radio-group>
```

### Disclosure

```html
<ui-disclosure>
    <button>...</button>

    <div class="hidden data-[open]:block">
        <!-- ... -->
    </div>
</ui-disclosure>
```

### Combobox

```html
<ui-combobox>
    <input type="text">

    <ui-listbox>
        <ui-option value="foo"> <button>Foo</button> </ui-option>
        <ui-option value="bar"> <button>Bar</button> </ui-option>
        <ui-option value="baz"> <button>Baz</button> </ui-option>
    </ui-listbox>
</ui-combobox>
```

### Tooltip

```html
<ui-tooltip>
    <button>Tooltip</button>

    <div popover>
        <!-- ... -->
    </div>
</ui-tooltip>
```

### Listbox

```html
<ui-listbox>
    <ui-option value="foo">Foo</ui-option>
    <ui-option value="bar">Bar</ui-option>
    <ui-option value="baz">Baz</ui-option>
</ui-listbox>
```

### Switch

```html
<ui-switch></ui-switch>
```

### Dropdown

```html
<ui-dropdown>
    <button>Open</button>

    <div popover>
        <!-- ... -->
    </div>
</ui-dropdown>
```

### Modal

```html
<ui-modal>
    <button>Open</button>

    <dialog>
        <!-- ... -->
    </dialog>
</ui-modal>
```

### Field

```html
<ui-field>
    <ui-label>Something</ui-label>
    <ui-description>Some description</ui-description>

    <input type="text">
</ui-field>
```

### Tabs

```html
<ui-tabs>
    <ui-tab-group>
        <ui-tab panel="foo">
            <button>Foo</button>
        </ui-tab>

        <ui-tab panel="bar">
            <button>Bar</button>
        </ui-tab>

        <ui-tab panel="baz">
            <button>Baz</button>
        </ui-tab>
    </ui-tab-group>

    <ui-tab-panel name="foo">
        <div>...</div>
    </ui-tab-panel>

    <ui-tab-panel name="bar">
        <div>...</div>
    </ui-tab-panel>

    <ui-tab-panel name="baz">
        <div>...</div>
    </ui-tab-panel>
</ui-tabs>
```

### Menu

```html
<ui-menu>
    <ui-item>
        <button>Foo</button>
    </ui-item>

    <ui-item>
        <button>Bar</button>
    </ui-item>

    <ui-item>
        <button>Baz</button>
    </ui-item>
</ui-menu>
```

```html
<ui-combobox>
    <input />

    <ui-listbox>
        <ui-option value="foo">
            <button>Foo</button>
        </ui-option>

        <ui-option value="bar">
            <button>Bar</button>
        </ui-option>

        <ui-option value="baz">
            <button>Baz</button>
        </ui-option>
    </ui-listbox>
</ui-combobox>
```

---

## Most basic example

Let's start with the most basic Lofi element. A simple switch that can be pressed to toggle it on and off:

```html
<ui-switch>
    <!-- ... -->
</ui-switch>
```

Lofi will handle all the accessibility features like labels, aria attributes, focusability, & keyboard interactivity.

You can style it in Tailwind using its data attributes:

```html
<ui-switch class="group bg-white data-[checked]:bg-black">
    <!-- ... -->
</ui-switch>
```

You can bind to its state from any framework as if it were a native input element:

```html
<ui-switch x-model="notifications"> <!-- Alpine -->
<ui-switch v-model="notifications"> <!-- Vue -->
<ui-switch :value="notifications" onChange={setNotifications}> <!-- React, etc... -->
```

Here are some other elements of a similar nature.

**A custom radio group component**

```html
<ui-radio-group>
  <ui-radio value="table">Table</ui-radio>
  <ui-radio value="grid">Grid</ui-radio>
  <ui-radio value="list">List</ui-radio>
</ui-radio-group>
```

**A tabs component**

```html
<ui-tabs>
  <ui-tab-group>
    <ui-tab panel="foo">Foo</ui-tab>
    <ui-tab panel="bar">Bar</ui-tab>
    <ui-tab panel="baz">Baz</ui-tab>
  </ui-tab-group>

  <ui-tab-panel name="foo">...</ui-tab-panel>
  <ui-tab-panel name="bar">...</ui-tab-panel>
  <ui-tab-panel name="baz">...</ui-tab-panel>
</ui-tabs>
```

## Dialogs and popovers

When perusing lofi, you might notice an obvious lack of `<ui-dialog>` or `<ui-popover>` elements. This is because browsers now ship with native popovers and dialogs.

A key principle behind Lofi is: __prefer native solutions; you get more for free.__

However, there are still some gaps that Lofi can fill to make using them a more pleasant experience.

Let's first look at a truly native HTML popover:

```html
<button popovertarget="dropdown">Open...</button>

<div popover id="dropdown">
    <!-- Dropdown contents... -->
</div>
```

As you can see, the `popover` attribute is simple. Attach it to an element, invoke it with a button using `popovertarget`, and everything works like magic.

Briefly, here's what you get out of the box with a native popover:
* Light dismissal (press escape to close and focus invoke element)
* Top-layer rendering (no need for portals or z-index hacks)
* No need for extra aria attributes

However, there are two key hangups with going full native:
* You need to provide non-conflicting hardcoded IDs (this can be a problem in re-usable server-side templates)
* You have to "anchor" the popover's positioning to the button with JavaScript (Using something like [Floating-UI](https://floating-ui.com/))

Now let's look at a native dialog element:

```html
<button onclick="this.nextElementSibling.showModal()">Open...</button>

<dialog>
    <!-- Modal contents -->
</dialog>
```

Pros of the `<dialog>` element:
* Automatically makes page "inert" while open
* Traps focus inside dialog
* Renders in the top-layer (no need for z-index hacks and what not)

This is great, but you can only invoke a modal dialog using `element.showModal()` in JavaScript. There is no "dialogtarget" attribute like there is for popovers. (There is a proposal for a more universal API like `invoketarget` to solve this)

Now that the stage is set, let me introduce you to Lofi's most powerful element: `<ui-invoke>`

The `<ui-invoke>` element provides a way to declaratively link a invoke (often a button) with an invokeable target (often a popover or dialog).

Here's a native button/dialog combo declaratively linked by `<ui-invoke>`:

```html
<ui-invoke>
  <button>Open...</button>

  <dialog>
    <!-- ... -->
  </dialog>
</ui-invoke>
```

This API is an elegant alternative to a more traditional—albeit more agressive and less-composable—API like the following:

```html
<!-- A dystopian vingette into what Lofi almost become... -->
<ui-dialog>
  <ui-dialog-invoke>
    <button>Open...</button>
  </ui-dialog-invoke>

  <ui-dialog-panel>
    <dialog>
      <!-- ... -->
    </dialog>
  </ui-dialog-panel>
</ui-dialog>
```

Let's tour more examples of `<ui-invoke>`:

Here's a simple navigation menu using the semantic `<nav>` element provided by browsers, in combination with the `popover` attribute:

```html
<ui-invoke>
  <button>Open...</button>

  <nav popover>
    <a href="..."></a>
    <a href="..."></a>
    <a href="..."></a>
  </nav>
</ui-invoke>
```

`<ui-invoke>` even works with non-overlay elements. For example, you can use it in combination with Lofi's `<ui-disclosure>` element to combose an accordion widget:

```html
<ui-invoke>
  <button>Do you accept refunds?</button>

  <ui-disclosure class="hidden data-[open]:block">
    Sure, if you ask nicely...
  </ui-disclosure>
</ui-invoke>
```

Here's an example of using `popover` in combination with Lofi's `<ui-menu>` element to construct a robust action menu:

```html
<ui-invoke>
  <button>Open...</button>

  <ui-menu popover>
    <ui-item>Edit</ui-item>
    <ui-item>Archive</ui-item>
    <ui-item>Delete</ui-item>
  </ui-menu>
</ui-invoke>
```

The beauty of the `<ui-invoke>` and `popover` attribute combo, is that you can "opt-out" of using components as overlays by placeing them outside `<ui-invoke>`.

Here is the action menu from above rendered as a static action list on the page:

```html
<ui-menu>
  <ui-item onclick="...">Edit</ui-item>
  <ui-item onclick="...">Archive</ui-item>
  <ui-item onclick="...">Delete</ui-item>
</ui-menu>
```

Another component you might use this pattern with is: `<ui-listbox>` (a stylable select menu of options). Here is a listbox statically displayed on a page:

```html
<ui-listbox x-model="role" multiple>
  <ui-option value="read">Read-only</ui-option>
  <ui-option value="write">Edit</ui-option>
  <ui-option value="admin">Admin</ui-option>
</ui-listbox>
```

Now we can make that a common select menu dropdown like so:

```html
<ui-invoke>
  <button>Open...</button>

  <ui-listbox popover x-model="role" multiple>
    <ui-option value="read">Read-only</ui-option>
    <ui-option value="write">Edit</ui-option>
    <ui-option value="admin">Admin</ui-option>
  </ui-listbox>
</ui-invoke>
```

And of course, in addition to controlling it's value both externally and internally, you can style it with tailwind:

```html
<ui-invoke>
  <button>Open...</button>

  <ui-listbox popover x-model="role" multiple>
    <ui-option value="read" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Read-only</ui-option>
    <ui-option value="write" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Edit</ui-option>
    <ui-option value="admin" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Admin</ui-option>
  </ui-listbox>
</ui-invoke>
```

To take it even further, we can compose a full autocomplete component by combining the listbox with a combobox text input like so:

```html
<ui-combobox>
  <input type="text" placeholder="Type to search...">

  <ui-listbox popover x-model="role" multiple>
    <ui-option value="read" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Read-only</ui-option>
    <ui-option value="write" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Edit</ui-option>
    <ui-option value="admin" class="data-[selected]:bg-blue-300 data-[active]:bg-gray-50">Admin</ui-option>
  </ui-listbox>
</ui-combobox>
```

Or maybe you want to compose a command pallet in a dialog instead, you can do that too:

```html
<ui-invoke>
  <button>Open command...</button>

  <dialog>
    <ui-combobox>
      <input type="text" placeholder="Type to search..." autofocus>
    </ui-combobox>

    <ui-listbox>
      <ui-option>Dashboard</ui-option>
      <ui-option>New Post</ui-option>
      <ui-option>Log out</ui-option>
    </ui-listbox>
  </dialog>
</ui-invoke>
```

### APIs:

```html
```


### For Sebastian

* `ui-invoke` -> limit it? (only popovers/dialogs) or expand (breaks down with combobox)
* `ui-trigger` vs `ui-invoke`?
* Abandon `ui-invoke`? in favor of `ui-dialog` and `ui-popover`
    * What about non-modal dialogs? are those in `ui-popover?` maybe it should just be `ui-disclosure`? idk...
    * Now what about <ui-invoke> nested inside ui-dialog? to close the dialog?
* What about `x-combobox in combination with popover?`
* Use `x-model` on non-form-controls like `ui-tabs`
* `ui-tabs/tablist` using independantly with a tags
  * Other things like menu items and options etc...
* `ui-dialog/popover` be used with plain divs and no popover attribute?
    * redundant aria attributes with something like popover?


points:
* lint for "dialog" and "popover" native stuff at runtime
* If I'm relying on dedicated children, I should error out when it's missing


## Fundamental things:
* Should ui-checkbox still be x-modealable when inside x-checkbox-group?
  * If so, should it still dispatch bubbling change events?
    * If so, should they be intercepted by ui-checkbox-group to prevent double handling by x-model?
* Which attributes/getter/setters should exist on wrappers, and which on child buttons, and which duplicated?
* How should "disabled" behavior be handled? What get's "disabled"
  * Potentially disabled things: aria, interaction handlers, x-model/selectability?
* When to use data-active and when to use data-focus (and is data-focus the same as data-focus-visible)
* How is "morphing" handled (particularly IDs)
* How does it react when things like data-checked and checked are changed in the DOM?