# ember-primitives

With `ember-primitives` you only ship as much JS to your users as you need. Import whatever you want.

## Install

```hbs live
<SetupInstructions />
```

While installation is recommended, you are welcome to copy any implementation to your codebase, and most docs pages have a one-off command to help automate this, enabling you to own the code you ship to your users.

## Compatibility

- ember-source 4.8+
- Glint 1.0+
- TypeScript 4.8+
- ember-auto-import 2.6+
- embroider 3.0+

### CSS Compatibility

- [Open Props](https://open-props.style/)
- [Tailwind](https://tailwindcss.com/)
- [Tachyons](https://tachyons.io/)
- [Bootstrap](https://getbootstrap.com/)
- [Bulma](https://bulma.io/)
- [water.css](https://watercss.kognise.dev/)
- and more!
  (everything is supported)

### Design System Compatibility

- [Carbon, by IBM](https://github.com/carbon-design-system/carbon)
- [Comet, by Discovery Education](https://comet.discoveryeducation.com/)
- [Fundamental, by SAP](https://sap.github.io/fundamental-styles)
- [GOV.UK, by the UK Government](https://design-system.service.gov.uk/)
- [Liquid Oxygen](https://liquid.emd.design/liquid/)
- [PatternFly, by Red Hat](https://www.patternfly.org)
- [Primer, by GitHub](https://primer.style/)
- [Protocol, by Mozilla](https://protocol.mozilla.org/)
- [Toucan, by CrowdStrike](https://github.com/CrowdStrike/tailwind-toucan-base/)
- [Spectrum, by Adobe](https://opensource.adobe.com/spectrum-css/)
- [Vanilla, by Canonical](https://vanillaframework.io/)
- and more!
  (if your design system has standalone CSS, then it is compatible!)


---

# Testing: a11y

Utilities for helping test the accessibility behaviors.

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/a11y" 
    @name="setupTabster" />
</template>
```


---

# Testing: dom

Utilities for interacting with the (shadow) dom in testing

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/dom" 
    @name="findInFirstShadow" />

  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/dom" 
    @name="findInShadow" />

  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/dom" 
    @name="findShadow" />

  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/dom" 
    @name="hasShadowRoot" />
</template>
```


---

# Testing: OTP

Utilities for working with the one-time-password component.


```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/otp" 
    @name="fillOTP" />
</template>
```


---

# Testing: Rating

Utilities for working with the Rating component

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/rating" 
    @name="rating" />
</template>
```


---

# Testing: Routing

Additional utilities for working with routing in ember.

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/routing" 
    @name="setupRouting" />

  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/routing" 
    @name="getRouter" />
</template>
```


---

# Testing: Zoetrope

Utilities for working with the Zoetrope component

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs 
    @package="ember-primitives"  
    @module="declarations/test-support/zoetrope" 
    @name="ZoetropeHelper" />
</template>
```


---

# Accessibility

All components strive for compliance with the [WAI-ARIA](https://www.w3.org/TR/wai-aria/) specification, which is a set of guidelines for accessibility, following [the recommended patterns](https://www.w3.org/WAI/ARIA/apg/patterns/).
The ARIA design patterns can be easily searched on [their site index](https://www.w3.org/WAI/ARIA/apg/example-index/).


## Automatic Accountability

Violations that can be caught via CSS are highlighted in the UI so that the developer knows exactly what to fix.

For example, an `<ExternalLink />` missing an href

![image of a link without the href, being highlighted in text that can't be ignored](/images/link-missing-href.png)
<!--
```gjs live no-shadow
import { ExternalLink } from 'ember-primitives';

<template>
  <ExternalLink>
    link to no where
  </ExternalLink>
</template>
```
-->


Another example, a `<Switch />` without a label:

![image of a switch without a label, being highlighted in text that can't be ignored](/images/checkbox-missing-label.png)

<!--
```gjs live no-shadow
import { Switch } from 'ember-primitives';

<template>
  <Switch style="display: inline-block" as |s|>
    <s.Control />
  </Switch>
</template>
```
-->

This only happens during development, and in production, the CSS that applies these warnings is not included.

## Keyboard Support

ember-primitives uses _The Platform_ where possible and implements W3C recommendations for patterns where _The Platform_ does not provide solutions. To help lift the burden of maintenance for keyboard support implementation, ember-primitives uses [tabster](https://tabster.io/) for adding that additional keyboard support. Using tabster is optional, and is not included in your build if you don't use the below setup instructions (for example, if you had a different keyboard manager in your project and wanted to use that)

This keyboard support is enabled by default but does require initialization. You can initialize keyboard support in your application router by calling the `setupTabster()` function:
```ts
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import{ setupTabster } from 'ember-primitives/tabster';

export default class Application extends Route {
  async beforeModel() {
    // the 'this' is passed so that tabster is cleaned up
    // when the route (or in this case: application)
    // is destroyed  or unmounted
    await setupTabster(this);
  }
}
```

This is customizable, in case your application already uses tabster -- you may pass options to the `setup()` method:
```ts
// To use your own tabster
await setupTabster(this, { tabster: myTabsterCoreInstance });
// To specify your own "tabster root" 
await setupTabster(this, { setTabsterRoot: false });
```

The tabster root is an element which which tells tabster to pay attention for tabster-using features.
It can be set this way:
```html
<div data-tabster='{ "root": {} }'></div>
```

By default, this attribute-value pair is set on the `body` element.


---

# Accordion

An accordion component is an element that organizes content into collapsible sections, enabling users to expand or collapse them for efficient information presentation and navigation.

<Callout>

Before reaching for this component, consider if the [native `<details>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details) is sufficient for your use case.


<details><summary>example of <code>details</code></summary>

Like with the component, `<details>` and `<summary>` can be styled with CSS.

```gjs live preview
// No imports needed!
// If you need only one open a time, use the "name" attribute
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details#name

<template>
  <div class="demo">
    <details>
      <summary>the header</summary>

      <span>
        Ember.js is a productive, battle-tested JavaScript framework for building modern web
        applications. It includes everything you need to build rich UIs that work on any device.
      </span>
    </details>

    <details>
      <summary>another header</summary>

      <span>
        Ember.js is a productive, battle-tested JavaScript framework for building modern web
        applications. It includes everything you need to build rich UIs that work on any device.
      </span>
    </details>

    <details>
      <summary>a third header</summary>

      <span>
        Ember.js is a productive, battle-tested JavaScript framework for building modern web
        applications. It includes everything you need to build rich UIs that work on any device.
      </span>
    </details>
  </div>

  <style>
    @scope {
      details {
        position: relative;
        padding-bottom: 1rem;
      }
      summary {
        cursor: pointer;
        margin-bottom: -1rem;
        transition-property: all;
        transition-duration: 150ms;
        transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);

        background: white;
        padding: 0.5rem;
        color: black;
        border: 1px solid;
        border-radius: 0.25rem;
        position: relative;
        z-index: 2;
      }
      details[open] > summary {
        background: #dcdcff; 
        font-weight: bold;
        margin-bottom: 1rem;
      }

      details > span {
        position: relative;
        z-index: 1;
      }

      .demo {
        margin: 1rem; 
        padding: 1rem;
        display: grid;
        gap: 1rem;
      }
    }
  </style>
</template>
```

</details>

</Callout>

## Examples

<details open>
<summary><h3>Bootstrap - Single - Uncontrolled</h3></summary>

```gjs live preview
import { Accordion, Shadowed } from 'ember-primitives';

<template>
  <Shadowed>
    <Accordion class='accordion' @type='single' as |A|>
      <A.Item class='accordion-item' @value='what' as |I|>
        <I.Header class='accordion-header' as |H|>
          <H.Trigger
            aria-expanded='{{I.isExpanded}}'
            class='accordion-button {{unless I.isExpanded "collapsed"}}'
          >What is Ember?</H.Trigger>
        </I.Header>
        <I.Content class='accordion-collapse {{if I.isExpanded "show"}}'>
          <div class='accordion-body'>
            Ember.js is a productive, battle-tested JavaScript framework for building modern web
            applications. It includes everything you need to build rich UIs that work on any device.
          </div>
        </I.Content>
      </A.Item>
      <A.Item class='accordion-item' @value='why' as |I|>
        <I.Header class='accordion-header' as |H|>
          <H.Trigger
            aria-expanded='{{I.isExpanded}}'
            class='accordion-button {{unless I.isExpanded "collapsed"}}'
          >Why should I use Ember?</H.Trigger>
        </I.Header>
        <I.Content class='accordion-collapse {{if I.isExpanded "show"}}'>
          <div class='accordion-body'>
            Use Ember.js for its opinionated structure and extensive ecosystem, which simplify
            development and ensure long-term stability for web applications.
          </div>
        </I.Content>
      </A.Item>
    </Accordion>

    <link
      href='https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css'
      rel='stylesheet'
      crossorigin='anonymous'
    />

    <style>
      @scope {
        .accordion-body { color: black; }
      }
    </style>
  </Shadowed>
</template>
```

</details>

<details>
<summary><h3>Multiple - Uncontrolled</h3></summary>

```gjs live preview
import { Accordion } from 'ember-primitives';

<template>
  <Accordion @type='multiple' as |A|>
    <A.Item @value='what' as |I|>
      <I.Header as |H|>
        <H.Trigger>What is Ember?</H.Trigger>
      </I.Header>
      <I.Content>Ember.js is a productive, battle-tested JavaScript framework for building modern
        web applications. It includes everything you need to build rich UIs that work on any device.</I.Content>
    </A.Item>
    <A.Item @value='why' as |I|>
      <I.Header as |H|>
        <H.Trigger>Why should I use Ember?</H.Trigger>
      </I.Header>
      <I.Content>Use Ember.js for its opinionated structure and extensive ecosystem, which simplify
        development and ensure long-term stability for web applications.</I.Content>
    </A.Item>
  </Accordion>
</template>
```

</details>

<details>
<summary><h3>Single - Controlled - Collapsible</h3></summary>

```gjs live preview
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Accordion } from 'ember-primitives';

export default class ControlledAccordion extends Component {
  <template>
    <Accordion
      @type='single'
      @collapsible={{true}}
      @value={{this.value}}
      @onValueChange={{this.updateValue}}
      as |A|
    >
      <A.Item @value='what' as |I|>
        <I.Header as |H|>
          <H.Trigger>What is Ember?</H.Trigger>
        </I.Header>
        <I.Content>Ember.js is a productive, battle-tested JavaScript framework for building modern
          web applications. It includes everything you need to build rich UIs that work on any
          device.</I.Content>
      </A.Item>
      <A.Item @value='why' as |I|>
        <I.Header as |H|>
          <H.Trigger>Why should I use Ember?</H.Trigger>
        </I.Header>
        <I.Content>Use Ember.js for its opinionated structure and extensive ecosystem, which
          simplify development and ensure long-term stability for web applications.</I.Content>
      </A.Item>
    </Accordion>
  </template>

  @tracked value = 'what';

  updateValue = (value) => {
    this.value = value;
  };
}
```

</details>

<details>
<summary><h3>Multiple - Controlled</h3></summary>

```gjs live preview
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import { Accordion } from 'ember-primitives';

export default class ControlledAccordion extends Component {
  <template>
    <Accordion @type='multiple' @value={{this.values}} @onValueChange={{this.updateValues}} as |A|>
      <A.Item @value='what' as |I|>
        <I.Header as |H|>
          <H.Trigger>What is Ember?</H.Trigger>
        </I.Header>
        <I.Content>Ember.js is a productive, battle-tested JavaScript framework for building modern
          web applications. It includes everything you need to build rich UIs that work on any
          device.</I.Content>
      </A.Item>
      <A.Item @value='why' as |I|>
        <I.Header as |H|>
          <H.Trigger>Why should I use Ember?</H.Trigger>
        </I.Header>
        <I.Content>Use Ember.js for its opinionated structure and extensive ecosystem, which
          simplify development and ensure long-term stability for web applications.</I.Content>
      </A.Item>
    </Accordion>
  </template>

  @tracked values = ['what', 'why'];

  updateValues = (values) => {
    this.values = values;
  };
}
```

</details>

## Install

```hbs live
<SetupInstructions @src="components/accordion.gts" />
```

## Features

- Full keyboard navigation
- Can be controlled or uncontrolled
- Can expand one or multiple items
- Can be animated


## Anatomy

```js
import { Accordion } from 'ember-primitives';
```

or for non tree-shaking environments:

```js
import { Accordion } from 'ember-primitives/components/accordion';
```

```gjs
import { Accordion } from 'ember-primitives';

<template>
  <Accordion as |A|>
    <A.Item as |I|>
      <I.Header as |H|>
        <H.Trigger>Trigger</H.Trigger>
      </I.Header>
      <I.Content>Content</I.Content>
  </Accordion>
</template>
```

## API Reference

<details>
<summary><h3>Accordion</h3></summary>

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module='declarations/components/accordion' 
    @name='Accordion' 
  />
</template>
```

### State Attributes

|       key       | description                                  |
| :-------------: | :------------------------------------------- |
| `data-disabled` | Indicates whether the accordion is disabled. |

</details>

<details>
<summary><h3>AccordionItem</h3></summary>

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module='declarations/components/accordion' 
    @name='AccordionItemExternalSignature' 
  />
</template>
```

### State Attributes

|       key       | description                                                                           |
| :-------------: | :------------------------------------------------------------------------------------ |
|  `data-state`   | "open" or "closed", depending on whether the accordion item is expanded or collapsed. |
| `data-disabled` | Indicates whether the accordion item is disabled.                                     |

</details>

<details>
<summary><h3>AccordionHeader</h3></summary>

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module='declarations/components/accordion' 
    @name='AccordionHeaderExternalSignature' 
  />
</template>
```

### State Attributes

|       key       | description                                                                           |
| :-------------: | :------------------------------------------------------------------------------------ |
|  `data-state`   | "open" or "closed", depending on whether the accordion item is expanded or collapsed. |
| `data-disabled` | Indicates whether the accordion item is disabled.                                     |

</details>

<details>
<summary><h3>AccordionTrigger</h3></summary>

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module='declarations/components/accordion' 
    @name='AccordionTriggerExternalSignature' 
  />
</template>
```

### State Attributes

|       key       | description                                                                           |
| :-------------: | :------------------------------------------------------------------------------------ |
|  `data-state`   | "open" or "closed", depending on whether the accordion item is expanded or collapsed. |
| `data-disabled` | Indicates whether the accordion item is disabled.                                     |

</details>

<details>
<summary><h3>AccordionContent</h3></summary>

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module='declarations/components/accordion' 
    @name='AccordionContentExternalSignature' />
</template>
```

</details>

## Accessibility

- Sets `aria-expanded` on the accordion trigger to indicate whether the accordion item is expanded or collapsed.
- Uses `aria-controls` and `id` to associate the accordion trigger with the accordion content.
- Sets `hidden` on the accordion content when it is collapsed.

## Keyboard Interactions

|                key                | description                                    |
| :-------------------------------: | :--------------------------------------------- |
|          <kbd>Tab</kbd>           | Moves focus to the next focusable element.     |
| <kbd>Shift</kbd> + <kbd>Tab</kbd> | Moves focus to the previous focusable element. |
|         <kbd>Space</kbd>          | Toggles the accordion item.                    |
|         <kbd>Enter</kbd>          | Toggles the accordion item.                    |


---

# Avatar

An image element with a fallback for representing the user.


<div class="featured-demo">

```gjs live preview no-shadow
import { Avatar } from 'ember-primitives';

<template>
  <div class="demo">
    <Avatar class="container" @src="https://avatars.githubusercontent.com/u/199018?v=4" as |a|>
      <a.Image alt="GitHub profile picture of NullVoxPopuli" />
      <a.Fallback>NVP</a.Fallback>
    </Avatar>

    <Avatar class="container" @src="broken URL" as |a|>
      <a.Image alt="GitHub profile picture of NullVoxPopuli" />
      <a.Fallback @delayMs={{600}}>NVP</a.Fallback>
    </Avatar>

    <Avatar class="container" @src="https://static.wikia.nocookie.net/starcraft/images/2/21/CarbotZerglingLevel_SC2_Portrait1.jpg" as |a|>
      <a.Image alt="Zergling" />
      <a.Fallback>Z</a.Fallback>
    </Avatar>

    <Avatar class="container" @src="https://static.wikia.nocookie.net/starcraft/images/b/bc/Vorazun_SC2_Portrait1.jpg" as |a|>
      <a.Image alt="Vorazun's profile picture" />
      <a.Fallback>V</a.Fallback>
    </Avatar>

    <Avatar class="container" @src="https://static.wikia.nocookie.net/starcraft/images/3/34/GhostKerrigan_SC2_Portrait1.jpg" as |a|>
      <a.Image alt="Sarah Kerrigan's profile picture" />
      <a.Fallback>SK</a.Fallback>
    </Avatar>
  </div>

  <style>
    .demo {
      display: flex;
      gap: 1rem;
    }
    .container {
      display: flex;
      height: 4rem;
      width: 4rem;
      border-radius: 1rem;
      overflow: hidden;
      align-items: center;
      place-content: center;
      border: 2px solid #A300DE;

      > img {
        width: 100%;
        height: 100%;
        object-fit: contain;
      }
    }
  </style>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/avatar.gts" />
```

## Features

* Automatic and manual control over when the image renders.
* Fallback accepts any content.
* Optionally delay fallback rendering to avoid content flashing.

## Anatomy

```js 
import { Avatar } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { Avatar } from 'ember-primitives/components/avatar';
```


```gjs 
import { Avatar } from 'ember-primitives';

<template>
  <Avatar @src="..." as |a|>
    <a.Image />
    <a.Fallback>
      any content here
    </a.Fallback>
  </Avatar>
</template>
```

## Accessibility

An `alt` attribute is required, and in development, the UI will show an indication of a missing `alt` value if one is not provided.

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/avatar" 
    @name="Avatar" />
</template>
```

### State Attributes

There are state attributes available on the the root element of this component.
These may allow for stateful CSS-only stylings of the Avatar component.

| key | description |  
| :---: | :----------- |  
| `data-loading` | the loading state of the image | 
| `data-error` | will be "true" if the image failed to load | 



---

# Breadcrumb

A breadcrumb navigation component that displays the current page's location within a navigational hierarchy.

Breadcrumbs help users understand their current location and provide a way to navigate back through the hierarchy.

<div class="featured-demo">

```gjs live preview 
import { Breadcrumb, Menu, PortalTargets } from 'ember-primitives';

<template>
  <Breadcrumb class="not-prose" as |b|>
    <li>
      <a href="/">Home</a>
    </li>
    <b.Separator>/</b.Separator>
    <li>
      <Menu @offsetOptions={{8}} as |m|>
        <m.Trigger class="menu-trigger">
          Docs
          <ChevronDown />
        </m.Trigger>
        <m.Content class="menu-content" as |c|>
          <c.LinkItem @href="/docs">Overview</c.LinkItem>
          <c.LinkItem @href="/docs/getting-started">Getting Started</c.LinkItem>
          <c.LinkItem @href="/docs/components">Components</c.LinkItem>
        </m.Content>
      </Menu>
    </li>
    <b.Separator>/</b.Separator>
    <li>
      <a href="/docs/components">Components</a>
    </li>
    <b.Separator>/</b.Separator>
    <li aria-current="page">
      Breadcrumb
    </li>
  </Breadcrumb>

  <PortalTargets />
  <style>
    @scope { 
      nav {
        user-select: none;
        background: var(--color-page-background);
        border-radius: 0.25rem;
        filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.2));
        padding: 0.25rem 1rem;
        width: min-content;
      }
      
      nav ol {
        list-style: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding: 0;
        margin: 0;
      }

      nav a {
        color: #0066cc;
        text-decoration: none;
      }

      nav a:hover {
        text-decoration: underline;
      }

      nav li[aria-current="page"] {
        color: #666;
        font-weight: 600;
      }

      nav span[aria-hidden] {
        color: #999;
        user-select: none;
      }

      .menu-trigger {
        all: unset;
        color: #0066cc;
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        gap: 0.25rem;
      }

      .menu-trigger:hover {
        text-decoration: underline;
      }

      .menu-trigger svg {
        width: 12px;
        height: 12px;
      }

      .menu-content {
        min-width: 180px;
        background: #fff;
        color: #111827;
        padding: 8px 0;
        border-radius: 6px;
        border: none;
        font-size: 14px;
        z-index: 10;
        border: 1px solid gray;
        box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
        display: flex;
        flex-direction: column;
      }

      .menu-content [role="menuitem"] {
        all: unset;
        display: block;
        padding: 4px 12px;
        cursor: pointer;
        color: #111827;
      }

      .menu-content [role="menuitem"]:focus,
      .menu-content [role="menuitem"]:hover {
        background-color: #f9fafb;
      }
    }
  </style>
</template>

const ChevronDown = <template>
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
    <polyline points="6 9 12 15 18 9"></polyline>
  </svg>
</template>;
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/breadcrumb.gts" />
```

Introduced in [0.51.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.51.0-ember-primitives)

## Features

* Semantic HTML structure using `<nav>`, `<ol>`, and `<li>` elements
* Proper ARIA attributes for accessibility
* Flexible separator component
* Full control over styling
* No unnecessary abstractions - use any link component or element directly

## Anatomy

```gjs
import { Breadcrumb } from 'ember-primitives/components/breadcrumb';

<template>
  <Breadcrumb as |b|>
    <li>
      <a href="/">Home</a>
    </li>
    <b.Separator>/</b.Separator>
    <li>
      <a href="/docs">Docs</a>
    </li>
    <b.Separator>/</b.Separator>
    <li aria-current="page">
      Current Page
    </li>
  </Breadcrumb>
</template>
```

## Examples

### Using the Link Component

You can use any link component, `<a>`, `<LinkTo>`, `<Link>`, etc:

```gjs live preview
import { Breadcrumb, Link } from 'ember-primitives';

<template>
  <Breadcrumb @label="example-links" class="not-prose" as |b|>
    <li>
      <Link @href="/">Home</Link>
    </li>
    <b.Separator>/</b.Separator>
    <li>
      <Link @href="/docs">Docs</Link>
    </li>
    <b.Separator>/</b.Separator>
    <li aria-current="page">
      Breadcrumb
    </li>
  </Breadcrumb>

  <style>
    @scope { 
      nav {
        background: var(--color-page-background);
        padding: 0.25rem 1rem;
        width: min-content;
      }
      
      nav ol {
        list-style: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding: 0;
        margin: 0;
      }

      nav a {
        color: #0066cc;
        text-decoration: none;
      }

      nav a:hover {
        text-decoration: underline;
      }

      nav li[aria-current="page"] {
        color: #666;
        font-weight: 600;
      }

      nav span[aria-hidden] {
        color: #999;
        user-select: none;
      }
    }
  </style>
</template>
```

### Custom Separator

You can use any content as a separator, including icons or symbols:

```gjs live preview
import { Breadcrumb } from 'ember-primitives';

<template>
  <Breadcrumb @label="example-separator" class="not-prose" as |b|>
    <li>
      <a href="/">Home</a>
    </li>
    <b.Separator>&gt;</b.Separator>
    <li>
      <a href="/products">Products</a>
    </li>
    <b.Separator>&gt;</b.Separator>
    <li aria-current="page">
      Details
    </li>
  </Breadcrumb>

  <style>
    @scope {
      nav {
        user-select: none;
        background: var(--color-page-background);
        border-radius: 0.25rem;
        filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.2));
        padding: 0.25rem 1rem;
        width: min-content;
      }
      
      nav ol {
        list-style: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding: 0;
        margin: 0;
      }

      nav a {
        color: #0066cc;
        text-decoration: none;
      }

      nav a:hover {
        text-decoration: underline;
      }

      nav li[aria-current="page"] {
        color: #666;
      }

      nav span[aria-hidden] {
        color: #999;
      }
    }
  </style>
</template>
```

### Using Buttons

Since breadcrumbs can contain any component, you can even use buttons for non-navigation actions:

```gjs live preview
import { Breadcrumb } from 'ember-primitives';

<template>
  <Breadcrumb @label="button-example" class="not-prose" as |b|>
    <li>
      <a href="/">Home</a>
    </li>
    <b.Separator>/</b.Separator>
    <li>
      <button type="button" class="breadcrumb-button">
        Actions
      </button>
    </li>
    <b.Separator>/</b.Separator>
    <li aria-current="page">
      Current
    </li>
  </Breadcrumb>

  <style>
    @scope {
      nav {
        user-select: none;
        background: var(--color-page-background);
        border-radius: 0.25rem;
        filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.2));
        padding: 0.25rem 1rem;
        width: min-content;
      }
      
      nav ol {
        list-style: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding: 0;
        margin: 0;
      }

      nav a,
      nav .breadcrumb-button {
        color: #0066cc;
        text-decoration: none;
        background: none;
        border: none;
        padding: 0;
        font: inherit;
        cursor: pointer;
      }

      nav a:hover,
      nav .breadcrumb-button:hover {
        text-decoration: underline;
      }

      nav li[aria-current="page"] {
        color: #666;
      }

      nav span[aria-hidden] {
        color: #999;
      }
    }
  </style>
</template>
```

### Custom Label

You can provide a custom accessible label for the breadcrumb navigation:

```gjs live preview
import { Breadcrumb } from 'ember-primitives';

<template>
  <Breadcrumb @label="Page Navigation" class="not-prose" as |b|>
    <li>
      <a href="/">Home</a>
    </li>
    <b.Separator>/</b.Separator>
    <li aria-current="page">
      About
    </li>
  </Breadcrumb>

  <style>
    @scope {
      nav[aria-label] {
        user-select: none;
        background: var(--color-page-background);
        border-radius: 0.25rem;
        filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.2));
        padding: 0.25rem 1rem;
        width: min-content;
      }
      
      nav[aria-label] ol {
        list-style: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding: 0;
        margin: 0;
      }

      nav[aria-label] a {
        color: #0066cc;
        text-decoration: none;
      }

      nav[aria-label] a:hover {
        text-decoration: underline;
      }

      nav[aria-label] li[aria-current="page"] {
        color: #666;
      }

      nav[aria-label] span[aria-hidden] {
        color: #999;
      }
    }
  </style>
</template>
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/breadcrumb" 
    @name="Signature" 
  />
</template>
```

## Accessibility

### ARIA Attributes

The breadcrumb component uses proper ARIA attributes to ensure accessibility:

* The root `<nav>` element has `aria-label="Breadcrumb"` (or a custom label if provided)
* Separators have `aria-hidden="true"` to hide them from screen readers
* The current page item should have `aria-current="page"` to indicate the current location

### Screen Reader Support

Screen readers will announce the breadcrumb navigation as a landmark with the label "Breadcrumb" (or custom label). Each link will be announced individually, and separators are hidden from screen readers to avoid clutter.

### Best Practices

* Always mark the current page with `aria-current="page"` on the last item
* The current page item should not be a link
* Keep breadcrumb labels concise and descriptive
* Ensure sufficient color contrast for links and text


---

# Forms

This type of form is a thin, almost invisible, wrapper around the [native `<form>`][mdn-form]. 

It requires an `@onChange` argument, which receives the results of [`FormData`][mdn-FormData]'s `entries()` method, in the form of an key-[`FormDataEntryValue`][mdn-FormDataEntryValue] object. 

You'll notice in this demo, there is no `on`, `fn`, or any event binding to the inputs -- only the initial value is set.

<Callout>

You don't need this component, as the boilerplate here is quite small. However, the abstractions in ember-primitives use enough forms that abstracting away the boilerplate is still useful.

  <br />
  <details><summary>For interacting with servers</summary>

  If you need to submit data to a server, this `<Form />` component is not needed. You can use the same (de)serialization of data <-> FormData techniques to directly POST to your server without the need to use JavaScript. This `<Form />` component is specifically for single-page-app forms that don't directly submit data to the server and require additional processing before a `fetch`-based (or similar) POST/PUT/PATCH/etc

  </details>

</Callout>
<br>

<details><summary>Two philosophies around forms</summary>

These topics are mostly out of scope for this documentation, but here is a quick overview.

There are two ways to create forms: **Controlled** and **Uncontrolled**. 

This `<Form />` component follows the _uncontrolled_ pattern, and is a light wrapper that has automatic two-way binding without wiring anything up. 

There are also _controlled_ forms which focuses on explicitly managing data, events, etc, and is generally good for _constraining_ what developers can do as they consume your abstraction -- which tend to be good for building design systems ([see here](https://github.com/universal-ember/dev/issues/2)).

It's totally feasible to build a _Controlled_ API from an _Uncontrolled_ implementation.

</details>
<br />

<div class="featured-demo">

```gjs live preview
import { Form } from 'ember-primitives';
import { TrackedObject } from 'tracked-built-ins';

const data = new TrackedObject({ firstName: 'Gwen' });

function update(newValues) {
  for (let [key, value] of Object.entries(newValues)) {
    if (data[key] !== value) {
      data[key] = value; // only update changed values
    }
  }
}

<template>
  <div class="layout">
    <Form @onChange={{update}}>
      <label>
        First Name
        <input name="firstName" value={{data.firstName}}>
      </label>

      <fieldset>
        <legend>Travel to a universe</legend>
        <label>65   <input name="universe" type="radio" value="65"></label>
        <label>616  <input name="universe" type="radio" value="616"></label>
      </fieldset>
    </Form>

    <pre>{{JSON.stringify data null 3}}</pre>
  </div>

  <style>
    @scope {
      .layout { 
        display: grid; 
        gap: 1rem;
        grid-auto-flow: column;
      }
      form, fieldset {
        display: flex;
        gap: 1rem;
        flex-wrap: wrap;
        flex-direction: column;
      }
      input { max-width: 100%; color: black; }
      pre { 
        overflow: hidden; 
        white-space: pre-wrap;
      } 
    }
  </style>
</template>
```

</div>

Something to note when using native `<form>` is that _all values_ are strings (per [`FormDataEntryValue`][mdn-FormDataEntryValue]). 
If you wish to use booleans, numbers, or complex objects, you'll need to manage the conversion to and from those values to strings (for the form) yourself. 

The light abstraction is this pattern, and almost exactly the implementation used:
```gjs
const handleInput = (onChange, event) => {
  let formData = new FormData(event.currentTarget);
  let data = Object.fromEntries(formData.entries());

  onChange(data);
}

const handleSubmit = (onChange, event) => {
  event.preventDefault();
  handleInput(onChange, event);
};

<template>
  <form
    {{on 'input' (fn handleInput @onChange)}}
    {{on 'submit' (fn handleSubmit @onChange)}}
    ...attributes
  >
    {{yield}}
  </form>
</template>
```

[mdn-form]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form
[mdn-FormData]:  https://developer.mozilla.org/en-US/docs/Web/API/FormData
[mdn-FormDataEntryValue]: https://udn.realityripple.com/docs/Web/API/FormDataEntryValue

## Install

```hbs live
<SetupInstructions @src="components/form.gts" />
```

## Features 

* All types of inputs and controls are supported
* Best accessibility
* No need to add boilerplate to inputs
* Works with any shape of data

## Anatomy

```js 
import { Form } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { Form } from 'ember-primitives/components/form';
```


```gjs 
import { Form } from 'ember-primitives';

<template>
  <Form>
      Controls here
  </Form>
</template>
```


## Accessibility

Because this is a light wrapper around [`<form>`][mdn-form], accessibility is the same as native behavior. Nothing custom was needed.

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/form" 
    @name="Signature" 
  />
</template>
```

### State Attributes

None.


---

# Heading

The `<Heading>` component correlates to the `<h1>` through `<h6>` [Section Heading][mdn-h] elements, where the **level** is determined _automatically_ based on how the DOM has rendered.

This enables distributed teams to correctly produce appropriate section heading levels without knowledge of where their work will be rendered in the overall document -- and extra helpful for design systems teams where is _is not possible_ to know the appropriate heading level ahead of time.

[mdn-h]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/Heading_Elements

<section>

## Usage

In your app, you can use any of `<section>`, `<article>`, and `<aside>` elements to denote when the [_Section Heading_][mdn-h] element should change its level.
Note that this demo starts with `h3`, because this docs page already has an `h1`, and _this_ section (Usage) uses an `h2`.

<div class="featured-demo auto-height">

```gjs live preview 
import { Heading } from 'ember-primitives/components/heading';
import { InViewport } from 'ember-primitives/viewport';
  
<template>
  <InViewport style="min-height:300px;">
  <main aria-label="heading-demo" class="not-prose">
    <Heading>a heading</Heading>

    <nav>
      <Heading>a heading</Heading>
    </nav>

    <article>
      <Heading>a heading</Heading>
      <a href="#">
        <Heading>a heading</Heading>
      </a>
      <section>
        <Heading>a heading</Heading>
        <article>
          <Heading>a heading</Heading>
        </article>
      </section>
      <footer>
        <Heading>a heading</Heading>

      </footer>
    </article>
  </main>
  </InViewport>

  <style>
    @scope {
      h1, h2, h3, h4, h5, h6 { 
        margin-top: 0; margin-bottom: 0;
        color: white;
      }

      h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
        position: absolute;
        margin-left: -1.2em;
        font-size: 0.7em;
        text-align: right;
        opacity: 0.8;
      }

      h1 { font-size: 2.5rem; }
      h2 { font-size: 2.25rem; }
      h3 { font-size: 2rem; }
      h4 { font-size: 1.5rem; }
      h5 { font-size: 1.25rem; }
      h6 { font-size: 1rem; }
      a { color: white; }

      h1::before { content: 'h1'; }
      h2::before { content: 'h2'; }
      h3::before { content: 'h3'; }
      h4::before { content: 'h4'; }
      h5::before { content: 'h5'; }
      h6::before { content: 'h6'; }

      article, section, aside, nav, main, footer {
        border: 1px dotted;
        padding: 0.25rem 1.5rem;
        padding-left: 2rem;
        padding-right: 0.5rem;
        position: relative;

        &::before {
          position: absolute;
          right: 0.5rem;
          top: -1rem;
          font-size: 0.7rem;
          text-align: right;
          opacity: 0.8;
        }
      }

      section, article {
        display: flex;
        flex-direction: column;
        gap: 0.75rem;
      }

      article::before { content: '<article>'; }
      section::before { content: '<section>'; }
      aside::before { content: '<aside>'; }
      nav::before { content: '<nav>'; }
      main::before { content: '<main>'; }
      footer::before { content: '<footer>'; }

      main {
        display: grid;
        gap: 2.5rem;
        grid-template-columns: max-content 1fr;
        grid-template-areas:
          "heading heading"
          "nav content"
          "nav content"
          "nav content";

      }

      >:first-child { grid-area: heading; }
      nav { grid-area: nav; }
      article { grid-area: content; }
    }
  </style>
</template>
```

</div>
</section>

## Install

```hbs live
<SetupInstructions @src="components/heading.gts" />
```

Introduced in [0.44.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.44.0-ember-primitives)

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/heading" 
    @name="Heading" />
</template>
```


---

# Key and KeyCombo

Provides the markup necessary to render keyboard shortcuts and hotkeys and other keyboard interactions. The primary behavior on top of the native [`<kbd>`][mdn-kbd] element is automatic adding of the `+` symbol for multiple keys, as well as handling of macOS vs non-macOS shortcut variances.

[mdn-kbd]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd

<div class="featured-demo">

```gjs live preview
import { Key, KeyCombo } from 'ember-primitives';

<template>
  A single key:
  <Key>a</Key>
  <br><br>
  A combination of keys: 
  <KeyCombo @keys="ctrl+a" @mac="cmd+a" />

  <style>
    kbd {
      background-color: #eee;
      border-radius: 3px;
      border: 1px solid #b4b4b4;
      box-shadow:
        0 1px 1px rgba(0, 0, 0, 0.2),
        0 2px 0 0 rgba(255, 255, 255, 0.7) inset;
      color: #333;
      display: inline-block;
      font-size: 0.85em;
      font-weight: 700;
      line-height: 1;
      padding: 2px 4px;
      white-space: nowrap;
      /* CSS from https://developer.mozilla.org/en-US/docs/Web/HTML/Element/kbd */
    } 
  </style>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/keys.gts" />
```

## Features

* Handling of auto-switching the display combination based on viewing operating system (macOS vs non-macOS)
* Accepts array of keys, or `+`-separated string

## Anatomy

```js 
import { Key, KeyCombo } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { Key, KeyCombo } from 'ember-primitives/components/keys';
```


```gjs 
import { Key, KeyCombo } from 'ember-primitives';

<template>
    <Key>ctrl</Key>
    <Key>anything here</Key>

    <KeyCombo @key="ctrl+x" />
    <KeyCombo @key="ctrl+x" @mac="command+x" />
    <KeyCombo @key={{array "ctrl" "x"}} @mac={{array "command" "x"}} />
</template>
```

## Accessibilty

This is an extremely thin wrapper around [`kbd`][mdn-kbd], so accessibility is the same as native.

## API Reference

There are two components in this module

### `<KeyCombo>`

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/keys" 
    @name="KeyComboSignature" />
</template>
```

#### KeyCombo Classes

For styling with a stylesheet

- `ember-primitives__key-combination`
- `ember-primitives__key-combination__separator`

<section>

### `<Key>`

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/keys" 
    @name="KeySignature" />
</template>
```

#### Key Classes

For styling with a stylesheet

- `ember-primitives__key`


</section>


---

# Progress

Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.

<Callout>

Before reaching for this component, consider if the [native `<progress>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/progress) is sufficient for your use case. 

</Callout>
<br>


<div class="featured-demo">

```gjs live preview
import { Progress, Shadowed } from 'ember-primitives';
import { cell, resource } from 'ember-resources';

const randomValue = resource(({on}) => {
  let value = cell(randomPercent());
  let interval = setInterval(() => value.current = randomPercent(), 3000);
  on.cleanup(() => clearInterval(interval));

  return value;
}); 

const randomPercent = () => Math.random() * 100;
const translate = (v) => -(100 - v);

<template>
  <Shadowed>
    <Progress @value={{(randomValue)}} aria-label="demo" as |x|>
      <span>{{Math.round x.value}}%</span>
      <x.Indicator style="transform: translateX({{translate x.percent}}%);" />
    </Progress>

    <style>
      [role="progressbar"] {
        margin: 0 auto;
        width: 60%;
        height: 1.5rem;
        border-radius: 0.75rem;
        position: relative;
        overflow: hidden;
        background: conic-gradient(at -20% 15%, white 0%, #aaccff 72%);
      }
      [role="progressbar"] > div {
        background: linear-gradient(45deg, #5E0091FF 0%, #004976FF 100%);
        width: 100%;
        height: 100%;
        border-radius: 1rem;
        transition: transform 700ms cubic-bezier(0.65, 0, 0.35, 1);
      }
      [role="progressbar"] > span {
        line-height: 1.5rem;
        text-align: center;
        width: 100%;
        color: white;
        mix-blend-mode: difference;
        position: absolute;
        z-index: 1;
      }
    </style>
  </Shadowed>
</template>
```

</div>

<div class="featured-demo">

```gjs live preview
import { Progress, Shadowed } from 'ember-primitives';
import { cell, resource } from 'ember-resources';

const randomValue = resource(({on}) => {
  let value = cell(randomPercent());
  let interval = setInterval(() => value.current = randomPercent(), 3000);
  on.cleanup(() => clearInterval(interval));

  return value;
}); 

const randomPercent = () => Math.random() * 100;
const r = 60;
const size = Math.PI * 2 * r;
const toOffset = (x) => ((100 - x) / 100) * size;

const RandomProgress = 
<template>
  <Shadowed>
    <Progress @value={{(randomValue)}} ...attributes as |x|>
      <x.Indicator class="progress" />
      <svg width="200" height="200" viewPort="0 0 100 100">
        <circle 
          r={{r}} cx="100" cy="100" 
          fill="transparent" 
          stroke-dasharray={{size}} stroke-dashoffset="0"></circle>
        <circle
          r={{r}} cx="100" cy="100" 
          fill="transparent" 
          style="stroke: {{@color}}"
          stroke-linecap="round"
          stroke-dasharray={{size}} stroke-dashoffset="{{toOffset x.percent}}"></circle>
      </svg>
    </Progress>

    <style>
      [role="progressbar"] { position: relative; }
      svg circle {
        transition: stroke-dashoffset 0.5s linear;
        stroke: #555;
        stroke-width: 1rem;
      }
      .progress {
        height: 200px;
        width: 200px;
        position: absolute;
        text-align: center;
      }
      .progress:after {
        content: attr(data-percent)"%";
        line-height: 200px;
        font-size: 1.5rem;
      }
    </style>
  </Shadowed>
</template>;

<template>
  <div style="display: flex; gap: 1.5rem">
    <RandomProgress @color="#FF1E7D" aria-label="demo-pink" />
    <RandomProgress @color="#1EFF7D" aria-label="demo-green" />
  </div>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/progress.gts" />
```

## Features

* Provides context for assistive technology to read the progress of a task.


## Anatomy

```js 
import { Progress } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { Progress } from 'ember-primitives/components/progress';
```


```gjs 
import { Progress } from 'ember-primitives';

<template>
  <Progress aria-label="example" as |x|>
    <x.Indicator />
    <x.Indicator>
      with text
    </x.Indicator>

    text can go out here, too
  </Switch>
</template>
```

## Accessibility

Adheres to the [`progressbar` role requirements](https://www.w3.org/WAI/ARIA/apg/patterns/meter).

Note that a progressbar is [required to have a name](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/progressbar_role#associated_wai-aria_roles_states_and_properties).

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/progress" 
    @name="Signature" />
</template>
```

### State Attributes

<br>

#### `<Progress>`

| key | description |  
| :---: | :----------- |  
| `data-state` | `'complete' \| 'indeterminate' \| 'loading'` | 
| `data-value` | The current value. Will never be less than 0, and never more than `@max` 
| `data-max` | The max value 
| `data-min` | Always 0 
| `data-percent` | The current value, rounded to two decimal places


#### `<Indicator>`

| key | description |  
| :---: | :----------- |  
| `data-state` | `'complete' \| 'indeterminate' \| 'loading'` | 
| `data-value` | The current value. Will never be less than 0, and never more than `@max` 
| `data-max` | The max value 
| `data-percent` | The current value, rounded to two decimal places


---

# Rating

Ratings are used for displaying a score within a given range.

When interactive, the underlying implementation is a radio button for maximum accessibility.

<div class="featured-demo">


```gjs live preview
import { Rating } from 'ember-primitives';
import { cell } from 'ember-resources';

const capturedValue = cell(2);

<template>
  Current Value: {{capturedValue.current}}<br>
  <Rating 
    @step={{0.5}} {{! enabling half ratings, omit @step for whole ratings }} 
    @value={{capturedValue.current}} 
    @onChange={{capturedValue.set}}
    {{! Using CSS only for icons in this demo 
        -- use your design system's star icons here if you need }}
    @icon=""
  >
    <:label>Rate me</:label>
  </Rating>

  <style>
    @import "/demo-support/utilities.css";

    @scope {

    /* Some CSS borrowed from https://play.tailwindcss.com/SMXsGVXaHO */
    .ember-primitives__rating__items {
      display: inline-flex;
      flex-direction: row;
      justify-content: flex-end;
      border: none;
      position: relative; /* so the focus ring pseudo-element can be positioned */

      /* visually hide radio input, but leave accessible to screen readers */
      input {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border-width: 0;
      }

      --star-size: 2rem;
      --star-gap: 0.33rem;
      --star-height: var(--star-size);
      --star-width: calc(var(--star-size) / 2);
      --star-width-plus-gap: calc(var(--star-width) + var(--star-gap));

      .ember-primitives__rating__item {
        label {
          display: block;
          height: var(--star-height);
          width: var(--star-width);
          background-color: currentColor;
        }

        &:nth-of-type(even) label {
          mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 264 512"><path d="M0 0c12.2.1 23.3 7 28.6 18L93 150.3l143.6 21.2c12 1.8 22 10.2 25.7 21.7 3.7 11.5.7 24.2-7.9 32.7L150.2 329l24.6 145.7c2 12-3 24.2-12.9 31.3-9.9 7.1-23 8-33.8 2.3L0 439.8V0Z"/></svg>') no-repeat;
          width: var(--star-width-plus-gap);
          mask-size: var(--star-width);
        }

        &:nth-of-type(odd) label {
          mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 264 512"><path d="M264 0c-12.2.1-23.3 7-28.6 18L171 150.3 27.4 171.5c-12 1.8-22 10.2-25.7 21.7-3.7 11.5-.7 24.2 7.9 32.7L113.8 329 89.2 474.7c-2 12 3 24.2 12.9 31.3 9.9 7.1 23 8 33.8 2.3L264 439.8V0Z"/></svg>') no-repeat;
        }

        &:first-of-type label {
          width: var(--star-width);
        }

        &:has(:checked) label,
        &:has(~ span :checked) label {
          background-color: gold;
        }

        label:hover,
        &:has(~ span label:hover) label {
          background-color: #ffbb00;
        }
      }

      /* Focus ring for keyboard users: around the whole rating group */
      &:has(:focus-visible)::after {
        content: '';
        position: absolute;
        inset: -4px; /* offset so the ring sits outside the stars */
        border-radius: 9999px;
        pointer-events: none;
        box-shadow: 0 0 0 0 transparent;
      }

      &:has(:focus-visible)::after {
        box-shadow:
          0 0 0 2px #000,
          0 0 0 4px rgb(224 78 57);
      }
    }
    }
  </style>
</template>
```


</div>

<details><summary>defaults</summary>
<div class="featured-demo">


```gjs live preview
import { Rating, Shadowed } from 'ember-primitives';
import { cell } from 'ember-resources';

const capturedValue = cell(2);

<template>
  <Shadowed>
    Current Value: {{capturedValue.current}}<br><hr>
    <Rating @value={{capturedValue.current}} @onChange={{capturedValue.set}}>
      <:label>Rate me</:label>
    </Rating>

    <style>
      @import "/demo-support/utilities.css";

      .ember-primitives__rating__items {
        width: fit-content;
        display: grid;
        gap: 0.5rem;
        grid-auto-flow: column;
        justify-content: center;
        align-items: center;
        height: 4rem;
      }

      .ember-primitives__rating__item {
        font-size: 3rem;
        line-height: 1em;
        transition: all 0.1s;
        transform-origin: center;
        aspect-ratio: 1 / 1;
        user-select: none;
        width: 3rem;
        text-align: center;
        border-radius: 1.5rem;

        label:hover {
          cursor: pointer;
        }

        &:has(input:focus-visible) {
          --tw-ring-opacity: 1;
          --tw-ring-offset-color: #000;
          --tw-ring-offset-width: 2px;
          --tw-ring-color: rgb(224 78 57 / var(--tw-ring-opacity, 1));
          --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
          --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
          box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
          outline: 2px solid transparent;
          outline-offset: 2px;
        }

        input {
          appearance: none;
          position: absolute;

          &:focus-visible, &:focus {
            outline: none;
            box-shadow: none;
          }
        }

        &[data-selected] {
          color: gold;
        }
      } 

      .ember-primitives__rating__item:hover {
          transform: rotate3d(0, 0, 1, 15deg) scale(1.05);
      } 
    </style>
  </Shadowed>
</template>
```


</div>
</details>
<details><summary>non-interactive (display-only) mode</summary>
<div class="featured-demo">

```gjs live preview 
import { Rating, Shadowed } from 'ember-primitives';

<template>
  <Shadowed>
    <Rating @value={{2}} @interactive={{false}} />
    <Rating @value={{4}} @interactive={{false}}>
      <:label as |rating|>
        {{rating.value}} of {{rating.total}}
      </:label>
    </Rating>
    <Rating @value={{3}} @interactive={{false}} as |rating|>
      <rating.Stars />
      {{rating.value}} of {{rating.total}}
    </Rating>

    <style>
      @import "/demo-support/utilities.css";

      .ember-primitives__rating {
        width: fit-content;
        display: grid;
        gap: 0.5rem;
        grid-auto-flow: column;
        justify-content: center;
        align-items: center;
        height: 4rem;
      }

      .ember-primitives__rating__item {
          font-size: 3rem;
          line-height: 3rem;
          transition: all 0.1s;
          transform-origin: center;
          aspect-ratio: 1 / 1;
          cursor: pointer;
          user-select: none;

        input {
            display: none;
        }

        &[data-selected] {
          color: gold;
        }
      } 

      .ember-primitives__rating__item:hover {
          transform: rotate3d(0, 0, 1, 15deg) scale(1.05);
      } 
    </style>
  </Shadowed>
</template>
```

</div>
</details>
<details><summary>Custom Component / Icons</summary>
<div class="featured-demo">

```gjs live preview 
import { Rating, Shadowed } from 'ember-primitives';

const Icon = <template>
  <div ...attributes style={{if @isSelected "transform:rotate(180deg)"}}>
    {{@value}}
  </div>
</template>;


<template>
  <Shadowed>
    <Rating @icon={{Icon}} />

    <style>
      @import "/demo-support/utilities.css";

      .ember-primitives__rating__items {
        width: fit-content;
        display: grid;
        gap: 0.5rem;
        grid-auto-flow: column;
        justify-content: center;
        align-items: center;
        height: 4rem;
      }

      .ember-primitives__rating__item {
        font-size: 3rem;
        line-height: 1em;
        transition: all 0.1s;
        transform-origin: center;
        aspect-ratio: 1 / 1;
        user-select: none;
        width: 3rem;
        text-align: center;
        border-radius: 1.5rem;

        label:hover {
          cursor: pointer;
        }

        &:has(input:focus-visible) {
          --tw-ring-opacity: 1;
          --tw-ring-offset-color: #000;
          --tw-ring-offset-width: 2px;
          --tw-ring-color: rgb(224 78 57 / var(--tw-ring-opacity, 1));
          --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
          --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
          box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
          outline: 2px solid transparent;
          outline-offset: 2px;
        }

        input {
          appearance: none;
          position: absolute;

          &:focus-visible, &:focus {
            outline: none;
            box-shadow: none;
          }
        }

        &[data-selected] {
          color: gold;
        }
      } 

      .ember-primitives__rating__item:hover {
          transform: rotate3d(0, 0, 1, 15deg) scale(1.05);
      } 
    </style>
  </Shadowed>
</template>
```

</div>
</details>
<details><summary>No Styles</summary>
<div class="featured-demo">

```gjs live preview
import { Rating, Shadowed } from 'ember-primitives';

const Star = <template>
    <div class="item">
        <span class="icon">★</span>
    </div>
  </template>;

<template>
  <Shadowed>
    <Rating as |rating|>
      {{rating.value}} of {{rating.total}}<br>
      <rating.Stars @icon={{Star}} />
    </Rating>

    <style>
      /* just layout, since we don't want to use all the vertical space */
      .ember-primitives__rating__items {
        display: flex;
        gap: 1rem;
      }
    </style>
  </Shadowed>
</template>
```

</div>
</details>

## Install

```hbs live
<SetupInstructions @src="components/rating.gts" />
```

## Features

- Any shape can be used
- All styles / directions possible via CSS
- Full componets can be passed for the rating items / stars, and will have all the same information is available (properties, state, etc). This allows for custom icons, svgs, or some more complex pattern.
- Half-ratings and fractional values supported via the `@step` property

## Accessibility

Keyboard users can always change the star rating as every variant of the component has individually selectable elements.

Screen reader users will have a summary of the state of the component read to them as "Rated $Current of $Total"

### Keyboard

Using this component works the same as a radio group. 
- Tab to focus the group as a whole
- Arrow keys to select
- Space toggles

## Testing

```gts
import * as primitiveHelpers from 'ember-primitives/test-support';

const rating = primitiveHelpers.rating();


test('example', async function (assert) {
  await render(
    <template>
      <Rating />
    </template>
  );

  assert.strictEqual(rating.value, 0);
  assert.strictEqual(rating.isReadonly, false);

  await rating.select(3);
  assert.strictEqual(rating.value, 3);
  assert.strictEqual(rating.stars, '★ ★ ★ ☆ ☆');

  // Toggle
  await rating.select(3);
  assert.strictEqual(rating.value, 0);
  assert.strictEqual(rating.stars, '☆ ☆ ☆ ☆ ☆');
});
```

<details><summary>Multiple on a page</summary>

```gts
test('multiple', async function (assert) {
  await render(
    <template>
      <Rating data-test-first />
      <Rating data-test-second />
    </template>
  );

  const first = createTestHelper('[data-test-first]');
  const second = createTestHelper('[data-test-second]');

  assert.strictEqual(first.value, 0, 'first Rating has no selection');
  assert.strictEqual(second.value, 0, 'second Rating has no selection');

  await first.select(3);
  assert.strictEqual(first.value, 3, 'first Rating now has 3 stars');
  assert.strictEqual(second.value, 0, 'second Rating is still unchanged');

  await second.select(4);
  assert.strictEqual(second.value, 4, 'second Rating now has 4 stars');
  assert.strictEqual(first.value, 3, 'first Rating is still unchanged (at 3)');

  // Toggle First
  await first.select(3);
  assert.strictEqual(first.value, 0, 'first Rating is toggled from 3 to 0');
  assert.strictEqual(second.value, 4, 'second Rating is still unchanged (at 4)');

  // Toggle Second
  await second.select(4);
  assert.strictEqual(second.value, 0, 'second Rating is toggled from 4 to 0');
  assert.strictEqual(first.value, 0, 'first Rating is still unchanged (at 0)');
});
```

</details>



## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/rating/rating" 
    @name="Rating" />
</template>
```

### Classes & Attributes

All these classes do nothing on their own, but offer a way for folks authoring CSS to set their styles. 

- `.ember-primitives__rating`

    This is the class on the root-level element, the `<fieldset>`. This element has some data attributes representing the overall state of the rating component. 

    - `[data-total]` number.
    - `[data-value]` number.
    - `[data-readonly]` boolean.


- `.ember-primitives__rating__items`

    The wrapping element around all of the individual items (stars by default). This is placed on a `div`.

- `.ember-primitives__rating__item`

    Each item (star by default) is wrapped in a `span` with tihs class. This element also has the some data-attributes representing the state of an individual item / star. 

    - `[data-number]` number. Which numer of the total this item is.
    - `[data-selected]` boolean.
    - `[data-percent-selected]` number (0-100). The percentage of selection for this item. Useful for styling half-stars or fractional ratings.
    - `[data-readonly]` boolean.
    - `[data-disabled]` boolean.

    Every item underneath this element with the `*item` class has unique elements or other selectors. Generally DOM is private API for JavaScript access, but styling with CSS may need access to both the `label` and the `input`.

- `.ember-primitives__rating__label`

    This is the class used by default when no `<:label>` block/slot is provided. It says "Rated $Value of $Total", but is visually hidden, as sighted users can see the star rating visuals. This class is not used for any other reason. 



---

# Separator

A component for rendering both **semantic** separators and **decorative** separators.

The `Separator` is **80% documentation and 20% boilerplate reduction**.

- By default it renders a semantic separator (`<hr>`) that is exposed to assistive technology.
- For purely visual glyph separators (like `/` in breadcrumbs), use `@decorative={{true}}` to apply `aria-hidden="true"`.

<div class="featured-demo">

```gjs live preview
import { Separator } from "ember-primitives";

<template>
  <nav aria-label="First Demo">
    <ol class="breadcrumb-list">
      <li><a href="/">Home</a></li>
      <Separator @as="li" @decorative={{true}}>/</Separator>
      <li><a href="/docs">Docs</a></li>
      <Separator @as="li" @decorative={{true}}>/</Separator>
      <li aria-current="page">Separator</li>
    </ol>
  </nav>

  <style>
    @scope {
      nav {
        user-select: none;
        background: var(--color-page-background);
        border-radius: 0.25rem;
        filter: drop-shadow(0 0 0.75rem rgba(0, 0, 0, 0.2));
        padding: 0.25rem 1rem;
        width: min-content;

        ol { list-style-type: none; }
      }

      .breadcrumb-list {
        list-style: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding: 0;
        margin: 0;
      }

      a {
        color: #0066cc;
        text-decoration: none;
      }

      a:hover {
        text-decoration: underline;
      }

      li[aria-current="page"] {
        color: #666;
        font-weight: 600;
      }

      li[aria-hidden] {
        color: #999;
        user-select: none;
      }
    }
  </style>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/separator.gts" />
```

Introduced in [0.52.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.52.0-ember-primitives)

## What It Does

The `Separator` component is primarily a **documentation and readability tool**. It:

- Makes the code more readable by clearly marking separator elements
- Renders a semantic separator (`<hr>`) by default
- Provides an explicit `@decorative` mode for purely visual separators (adds `aria-hidden="true"`)
- Provides a consistent pattern across your codebase
- Allows customizing the element tag via the `@as` argument

This `Separator` is intended for **non-interactive** use-cases (semantic `<hr>` and decorative glyph separators). It does **not** implement focus/drag/keyboard behaviors for splitter-style UI.

## The `@as` Argument

By default, the Separator renders as an `<hr>` element.

When you are using `@decorative={{true}}` (for glyph separators like `/`), you typically want a non-void element so you can provide visible content. In lists, use `@as="li"` so separators are siblings to `<li>` elements:

```gjs
<ol>
  <li>Item 1</li>
  <Separator @as="li" @decorative={{true}}>/</Separator>
  <li>Item 2</li>
</ol>
```

This is important because in HTML, `<ol>` and `<ul>` elements should only have `<li>` children. Using `<span>` elements as siblings to `<li>` elements is invalid HTML.

## Plain HTML Alternative

**Using plain HTML is just as easy!**

- For a semantic separator, use `<hr>`.
- For a decorative glyph separator in breadcrumbs, use an element with `aria-hidden="true"`.

```gjs live preview
<template>
  <nav aria-label="Demo with plain HTML">
    <ol class="breadcrumb-list">
      <li><a href="/">Home</a></li>
      <li aria-hidden="true">/</li>
      <li><a href="/docs">Docs</a></li>
      <li aria-hidden="true">/</li>
      <li aria-current="page">Plain HTML</li>
    </ol>
  </nav>

  <style>
    @scope {
      nav {
        user-select: none;
        background: var(--color-page-background);
        border-radius: 0.25rem;
        filter: drop-shadow(0 0 0.75rem rgba(0, 0, 0, 0.2));
        padding: 0.25rem 1rem;
        width: min-content;

        ol { list-style-type: none; }
      }

      .breadcrumb-list {
        list-style: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding: 0;
        margin: 0;
      }

      a {
        color: #0066cc;
        text-decoration: none;
      }

      a:hover {
        text-decoration: underline;
      }

      li[aria-current="page"] {
        color: #666;
        font-weight: 600;
      }

      li[aria-hidden] {
        color: #999;
        user-select: none;
      }
    }
  </style>
</template>
```

Both approaches are equally valid. Choose whichever feels more natural for your codebase!

## Anatomy

```gjs
import { Separator } from "ember-primitives/components/separator";

<template>
  {{! Default: semantic separator (renders as <hr>) }}
  <Separator />

  {{! Decorative glyph separator (renders as <span aria-hidden="true">) }}
  <Separator @as="span" @decorative={{true}}>/</Separator>

  {{! Decorative glyph separator in lists (renders as <li aria-hidden="true">) }}
  <Separator @as="li" @decorative={{true}}>/</Separator>
</template>
```

## Example: In Breadcrumbs

When using with Breadcrumb, the yielded `b.Separator` is automatically configured with `@as="li"`:

```gjs live preview
import { Breadcrumb } from "ember-primitives";

<template>
  <Breadcrumb class="not-prose" as |b|>
    <li><a href="/">Home</a></li>
    <b.Separator>/</b.Separator>
    <li><a href="/docs">Docs</a></li>
    <b.Separator>/</b.Separator>
    <li aria-current="page">Current</li>
  </Breadcrumb>

  <style>
    @scope {
      nav {
        background: var(--color-page-background);
        padding: 0.25rem 1rem;
        ol { list-style-type: none; }
      }

      nav ol {
        list-style: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding: 0;
        margin: 0;
      }

      nav a {
        color: #0066cc;
        text-decoration: none;
      }

      nav a:hover {
        text-decoration: underline;
      }

      nav li[aria-current="page"] {
        color: #666;
        font-weight: 600;
      }

      nav li[aria-hidden] {
        color: #999;
      }
    }
  </style>
</template>
```

## Example: Custom Separators

You can use any content as a separator, including icons or symbols:

```gjs live preview
import { Separator } from "ember-primitives";

<template>
  <nav aria-label="Custom Separator component usage">
    <ol class="breadcrumb-list">
      <li><a href="/">Home</a></li>
      <Separator @as="li" @decorative={{true}}>&gt;</Separator>
      <li><a href="/products">Products</a></li>
      <Separator @as="li" @decorative={{true}}>→</Separator>
      <li aria-current="page">Details</li>
    </ol>
  </nav>

  <style>
    @scope {
      nav {
        user-select: none;
        background: var(--color-page-background);
        border-radius: 0.25rem;
        padding: 0.25rem 1rem;
        width: min-content;
      }

      .breadcrumb-list {
        list-style: none;
        display: flex;
        align-items: center;
        gap: 0.5rem;
        padding: 0;
        margin: 0;
      }

      a {
        color: #0066cc;
        text-decoration: none;
      }

      a:hover {
        text-decoration: underline;
      }

      li[aria-current="page"] {
        color: #666;
      }

      li[aria-hidden] {
        color: #999;
      }
    }
  </style>
</template>
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from "kolay";

<template>
  <ComponentSignature
    @package="ember-primitives"
    @module="declarations/components/separator"
    @name="Signature"
  />
</template>
```

## Accessibility

When used as a semantic separator (`<Separator />`), the separator is exposed to assistive technology.

When used as a decorative glyph separator (`@decorative={{true}}`), the component adds `aria-hidden="true"` so screen reader users don't hear unnecessary characters like "/" or ">".

### Best Practices

- Use `<Separator />` (semantic) when the separation itself is meaningful structure.
- Use `@decorative={{true}}` only for visual separators.
- When decorative, the content is hidden from screen readers, so don't use it for meaningful content.
- Ensure sufficient color contrast between separators and background.


---

# Switch

The Switch component is a user interface element used for toggling between two states. It consists of a track and a handle that can be interacted with to change the state. The Switch is commonly used in forms, settings screens, and preference panels to control features or settings.

`<Switch />` can be used in any design system.

## Examples

<details><summary><h3>Draggable</h3></summary>
<br>

```gjs live preview
import { Switch } from 'ember-primitives/components/switch';
import { Shadowed } from 'ember-primitives/components/shadowed';
import { modifier } from 'ember-modifier';

const draggableSwitch = modifier((element) => {
  const control = element.querySelector('[role="switch"]');

  let pointerId = null;
  let rect = null;

  const getIsOn = () => {
    const ariaChecked = control.getAttribute('aria-checked');
    if (ariaChecked !== null) {
      return ariaChecked === 'true';
    }

    if ('checked' in control && typeof control.checked === 'boolean') {
      return control.checked;
    }

    return control.getAttribute('data-state') === 'on';
  };

  const updateThumb = (clientX) => {
    if (!rect) return 0;

    let fraction = (clientX - rect.left) / rect.width;
    if (!Number.isFinite(fraction)) fraction = 0;

    const clamped = Math.min(Math.max(fraction, 0), 1);
    const percent = clamped * 100;

    element.style.setProperty('--thumb-translate', `${percent}%`);

    return clamped;
  };

  const handlePointerMove = (event) => {
    if (event.pointerId !== pointerId) return;
    updateThumb(event.clientX);
  };

  const handlePointerUpOrCancel = (event) => {
    if (event.pointerId !== pointerId) return;

    updateThumb(event.clientX); // 0..1

    element.style.removeProperty('--thumb-translate');
    element.classList.remove('is-dragging');

    element.releasePointerCapture?.(pointerId);
    element.removeEventListener('pointermove', handlePointerMove);
    element.removeEventListener('pointerup', handlePointerUpOrCancel);
    element.removeEventListener('pointercancel', handlePointerUpOrCancel);

    pointerId = null;
    rect = null;
  };

  const handlePointerDown = (event) => {
    if (event.button !== 0) return; 

    pointerId = event.pointerId;
    rect = element.getBoundingClientRect();

    element.classList.add('is-dragging');
    element.setPointerCapture?.(pointerId);

    element.addEventListener('pointermove', handlePointerMove);
    element.addEventListener('pointerup', handlePointerUpOrCancel);
    element.addEventListener('pointercancel', handlePointerUpOrCancel);

    updateThumb(event.clientX);
  };

  element.addEventListener('pointerdown', handlePointerDown);

  return () => {
    element.removeEventListener('pointerdown', handlePointerDown);
    element.removeEventListener('pointermove', handlePointerMove);
    element.removeEventListener('pointerup', handlePointerUpOrCancel);
    element.removeEventListener('pointercancel', handlePointerUpOrCancel);
  };
});

<template>
  <Shadowed>
    <Switch as |s|>
      <s.Label
        class="switch"
        data-state={{if s.isChecked "on" "off"}}
        {{draggableSwitch}}
      >
        {{! Words reversed because the control is _over_  the word }}
        On
        <s.Control />
        Off
      </s.Label>
      <br><br>
      Result: {{s.isChecked}}
    </Switch>

    <style>

.switch {
  --switch-width: 90px;
  --switch-height: 32px;
  --switch-border: 2px;
  --thumb-translate: 0%; /* 0% = off, 100% = on */

  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;

  width: var(--switch-width);
  height: var(--switch-height);

  padding: 0 10px; /* space for "Off" / "On" text */

  border-radius: 999px;
  border: var(--switch-border) solid #4b5563;

  background: transparent;
  color: #e5e7eb;
  font-size: 0.75rem;
  font-weight: 500;

  cursor: pointer;
  user-select: none;
  touch-action: pan-x;

  &[data-state="on"] {
    border-color: #22ff66;
    color: #22ff66;
}

}

.switch[data-state="on"] {
  --thumb-translate: 100%;
}

.switch::before {
  content: "";
  position: absolute;
  inset: var(--switch-border); 
  border-radius: inherit;
  background: #4b5563;
  opacity: 0.6;
  z-index: 0;
}

.switch::after {
  content: "";
  position: absolute;

  /* start right inside the border */
  top: var(--switch-border);
  left: var(--switch-border);
  width: calc(50% - var(--switch-border));
  height: calc(100% - 2 * var(--switch-border));

  border-radius: inherit;
  background: #f9fafb;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);

  transform: translateX(var(--thumb-translate));
  transition:
    transform 120ms ease-out,
    background 120ms ease-out;
  z-index: 0;
}

.switch.is-dragging::after {
  transition: none;
}

.switch > * {
  position: relative;
  z-index: 1;
}

.switch span {
  flex: 1;
  text-align: center;
}

.switch input,
.switch [role="switch"] {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: inherit;
}

.switch:focus-within {
  outline: 2px solid #e5e7eb;
  outline-offset: 2px;

  &[data-state="on"] {
    outline: 2px solid #aaffaa;
  }
}
    </style>
  </Shadowed>
</template>
```

</details>
<details open><summary><h3>Dark/Light Theme Switch</h3></summary>

CSS inspired/taken from [this Codepen](https://codepen.io/Umer_Farooq/pen/eYJgKGN?editors=1100)

```gjs live preview
import { Switch, Shadowed } from 'ember-primitives';
import { on } from '@ember/modifier';

const toggleTheme = (e) =>
  e.target.closest('div').classList.toggle("dark");

<template>
  <Shadowed>
    <Switch as |s|>
      <s.Control {{on 'change' toggleTheme}} />
      <s.Label>
        <span class="sr-only">Toggle between light and dark mode</span>
        <span class="ball" data-state={{if s.isChecked "on" "off"}}>
          {{#if s.isChecked}}
            <Moon />
          {{else}}
            <Sun />
          {{/if}}
      </span>
      </s.Label>
    </Switch>

    <style>
      * {box-sizing: border-box;}

      @scope {
        div {
          padding: 1rem;
          background-color: #eee;
          display: flex;
          justify-content: center;
          align-items: center;
          flex-direction: column;
          text-align: center;
          margin: 0;
          transition: background 0.2s linear;
          width: 100%;
        }

        div.dark {background-color: #292c35;}
        div.dark label { background-color: #9b59b6; }


        input[type='checkbox'][role='switch'] {
          touch-action: pan-y;
          opacity: 0;
          position: absolute;
        }

        .sr-only {
          width: 0px;
          max-width: 0px;
          height: 0px;
          max-height: 0px;
          overflow: hidden;
          margin-left: -0.5rem;
        }

        label {
          background-color: #aaaaff;
          border: 1px solid;
          width: 60px;
          height: 32px;
          border-radius: 50px;
          position: relative;
          padding: 5px;
          cursor: pointer;
          display: flex;
          justify-content: space-between;
          align-items: center;
          gap: 0.5rem;
        }

        svg { fill: currentColor; position: absolute; top: 3px; left: 3px; }
        .moon { color: #f1c4ff; }
        .sun { color: #f39c12; }

        label .ball {
          background-color: #111;
          width: 26px;
          height: 26px;
          position: absolute;
          left: 2px;
          top: 2px;
          border-radius: 50%;
          transition-property: transform filter;
          transition-duration: 0.2s;
          transition-timing-function: linear(0, 0.1, 0.25, 0.5, 0.68, 0.8, 0.88, 0.94, 0.98, 0.995, 1);;
          border: 2px solid #f1c40f;

          &[data-state="on"] {
            border: 2px solid #f1c4ff;
          }
        }

        label:hover .ball {
          filter: drop-shadow(0 0 3px #f1c40f);
        }
        label:active .ball {
          filter: drop-shadow(0 0 10px #f1c40f);
        }
        input[type='checkbox'][role='switch']:checked + label .ball {
          transform: translateX(28px);
        }
        input[type='checkbox'][role='switch']:checked:hover + label .ball {
          filter: drop-shadow(0 0 3px #f1c4ff);
        }
        input[type='checkbox'][role='switch']:checked:active + label .ball {
          filter: drop-shadow(0 0 10px #f1c4ff);
        }
      }
    </style>
  </Shadowed>
</template>

// 🎵 It's raining, it's pouring, ... 🎵
// https://www.youtube.com/watch?v=ll5ykbAumD4
const Sun = <template>
  <svg
    class="sun"
    xmlns="http://www.w3.org/2000/svg"
    width="16"
    height="16"
    viewBox="0 0 16 16"
    fill="none"
    stroke="currentColor"
    stroke-width="1.5"
    stroke-linecap="round"
    stroke-linejoin="round"
    aria-hidden="true"
  >
    <circle cx="8" cy="8" r="3.25" />
    <line x1="8" y1="1" x2="8" y2="3" />
    <line x1="8" y1="13" x2="8" y2="15" />
    <line x1="1" y1="8" x2="3" y2="8" />
    <line x1="13" y1="8" x2="15" y2="8" />
    <line x1="3.05" y1="3.05" x2="4.47" y2="4.47" />
    <line x1="11.53" y1="11.53" x2="12.95" y2="12.95" />
    <line x1="11.53" y1="4.47" x2="12.95" y2="3.05" />
    <line x1="3.05" y1="12.95" x2="4.47" y2="11.53" />
  </svg>
</template>;

const Moon = <template>
<svg
  xmlns="http://www.w3.org/2000/svg"
  class="moon"
  width="16"
  height="16"
  viewBox="0 0 16 16"
  fill="none"
  stroke="currentColor"
  stroke-width="1.5"
  stroke-linecap="round"
  stroke-linejoin="round"
  aria-hidden="true"
>
  <path
    transform="translate(-1 0)"
    d="M11.5 2a5.5 5.5 0 1 0 2 9.5 4.5 4.5 0 0 1 -2 -9.5z"
  />
</svg>
</template>;
```

</details>
<details><summary><h3>Bootstrap</h3></summary>

See [Bootstrap Switch](https://getbootstrap.com/docs/5.3/forms/checks-radios/#switches) docs.

```gjs live preview
import { Switch } from 'ember-primitives/components/switch';
import { Shadowed } from 'ember-primitives/components/shadowed';

<template>
  <Shadowed>
    <div class="p-4">
      <Switch class="form-check form-switch" as |s|>
        <s.Control class="form-check-input" />
        <s.Label class="form-check-label">
          Toggle on or off
        </s.Label>
      </Switch>
    </div>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
  </Shadowed>
</template>
```

</details>

## Install

```hbs live
<SetupInstructions @src="components/switch.gts" />
```

## Features 

* Full keyboard navigation 
* Can be controlled or uncontrolled

## Anatomy


```js 
import { Switch } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { Switch } from 'ember-primitives/components/switch';
```


```gjs 
import { Switch } from 'ember-primitives';

<template>
  <Switch as |s|>
    <s.Control class="form-check-input" />
    <s.Label class="form-check-label">
      Toggle on or off
    </s.Label>
  </Switch>
</template>
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/switch" 
    @name="Signature" />
</template>
```

### State Attributes

| key | description |  
| :---: | :----------- |  
| checked | attribute will be present when the underlying input is checked |  
| data-state | attribute will be "on" or "off", depending on the state of the toggle button |  

No custom data attributes are needed. From the root element, you may use the `:has` selector, to change the state of the container.

```gjs live preview
import { Switch } from 'ember-primitives';

<template>
  <style>
    /* styles for the root element when checked */
    .my-switch:has(:checked) {
      font-style: italic;
    }
    .my-switch:has([data-state=on]) {
      font-weight: bold;
    }
  </style>

  <Switch class="my-switch" as |s|>
    <s.Control class="form-check-input" />
    <s.Label class="form-check-label">
      Toggle on or off
    </s.Label>
  </Switch>
</template>
```


## Accessibility 

Adheres to the `switch` [role requirements](https://www.w3.org/WAI/ARIA/apg/patterns/switch)

### Keyboard Interactions 

| key | description |  
| :---: | :----------- |  
| <kbd>Space</kbd> | Toggles the component's state |  
| <kbd>Enter</kbd> | Toggles the component's state |  

In addition, a label is required so that users know what the switch is for.

## References

- https://web.dev/articles/building/a-switch-component
- https://getbootstrap.com/docs/5.3/forms/checks-radios/#switches
- https://web.dev/articles/building/a-switch-component


---

# Tabs

A set of layered sections of content, known as tab panels, that are displayed one at a time.


<Callout>

  Tabs _can_ be implemented without JavaScript, using "[plain HTML][css-only-tabs]", however, in order to do so, you must render all tabpanels at once (even if hidden). A defining characteristic of JavaScript-enabled tabs is that the DOM has less content rendered at once, improving page load.

  This still requires that subsequent tab loads are fast so that there is no visible UI lag.

[css-only-tabs]: https://jsfiddle.net/eu81273/812ehkyf/ 

</Callout>


<div class="featured-demo">

<!-- tabster doesn't work across the shadow boundary -->
```gjs live preview no-shadow
import { Tabs } from 'ember-primitives/components/tabs';

<template>
  <Tabs @label="Install with your favorite package-manager" as |Tab|>
    <Tab @label="npm">npm add ember-primitives</Tab>
    <Tab @label="pnpm">pnpm add ember-primitives</Tab>

    <Tab as |Label Content|>
      <Label>yarn</Label>
      <Content>
        yarn add ember-primitives
      </Content>
    </Tab>
  </Tabs>

  <style>
    /* https://caniuse.com/css-cascade-scope */
    @scope {
    [role="tablist"] {
        min-width: 100%;
      }

      [role="tab"] {
        color: black;
        display: inline-block;
        padding: 0.25rem 0.5rem; 
        background: hsl(220deg 20% 94%);
        outline: none;
        font-weight: bold;
        cursor: pointer;
        box-shadow: inset 0 -1px 1px black;
      }

      [role="tab"][aria-selected="true"] {
        background: white;
        box-shadow: inset 0 -4px 0px orange;
      }

      [role="tab"]:first-of-type {
        border-top-left-radius: 0.25rem;
      }
      [role="tab"]:last-of-type {
        border-top-right-radius: 0.25rem;
      }

      [role="tabpanel"] {
        color: black;
        padding: 1rem;
        border-radius: 0 0.25rem 0.25rem;
        background: white; 
        width: 100%;
        overflow: auto;
        font-family: ui-monospace monospace;
      }
    }
  </style>
</template>
```

</div>

## Customizing Layout

Feel free to inspect element here to see how the styles are done.

<div class="not-prose featured-demo inline-tabs">

<!-- tabster doesn't work across the shadow boundary -->
```gjs live 
import { Demo } from '#public/3-ui/tabs/layout-demo';

<template>
  <Demo />
</template>
```

</div>


Because the  [tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/) involves a fair bit of boilerplate HTML for satisfying aria structure, not every element created by `<Tabs>` is exposed for direct manipulation by the caller. However, all possible Tabs layouts are possible with both CSS and [tailwind](https://tailwindcss.com/docs/styling-with-utility-classes#complex-selectors) alike. Note though that it's recommended to use [scoped CSS](https://github.com/auditboard/ember-scoped-css/) as this will provide the best ergonomics for styling HTML in the component.


## Features

* Can be controlled or uncontrolled 
* Supports any layout (via CSS, horizontal, vertical, reversed, etc) 
* Full keyboard navigation 
* Tabs may be buttons or links[^tab-links]
* Configurable activation behavior
* Supports nesting

[^tab-links]: when tabs are links there is no customizable associated content with the tab, because a navigation would occur. Use the routing system to place content in the `{{outlet}}` provided for you in the implicit content area.

## Installation

```hbs live
<SetupInstructions @src="components/tabs.gts" />
```

Introduced in [0.41.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.41.0-ember-primitives)

## Anatomy

All content can either be a argument or block to better suit your invocation and styling needs.

```gjs
import { Tabs } from 'ember-primitives/components/tabs';

<template>
  {{! Label as argument (may be a component) }}
  <Tabs @label="text here" as TabList>...</Tabs>

  {{! Label as block }}
  <Tabs as |Tab|>
    <Tab.Label>text here</Tab.Label>
    ...
  </Tabs>

  {{! tab as arguments (each arg may also be a component) }}
  <Tabs as |Tab|>
    <Tab @label="Banana" @content="something about bananas" />
  </Tabs>

  {{! tab content as blocks }}
  <Tabs as |Tab|>
    <Tab @label="Banana">
      something about bananas
    </Tab>
  </Tabs>

  {{! tab content and label as components }}
  <Tabs as |Tab|>
    <Tab as |Label Content|>
      <Label>
        Banana
      </Label>

      <Content>
        something about bananas
      </Content>
    </Tab>
  </Tabs>
</template>
```

## Accessibility

- Follows the [WAI-ARIA](https://www.w3.org/WAI/ARIA/apg/patterns/tabs) pattern for Tabs.
- Keyboard interaction provided by [tabster](https://tabster.io/). 

### Keyboard Interactions 

| key | description |
| :---: | :----------- |  
| <kbd>Tab</kbd> | When focus moves on to the tabs, the first tab is focused |  
| <kbd>ArrowLeft</kbd> | Moves focus to the previous tab |  
| <kbd>ArrowRight</kbd> | Moves focus to the next tab |  
| <kbd>ArrowDown</kbd> | Moves focus to the next tab |  
| <kbd>ArrowUp</kbd> | Moves focus to the previous tab |  
| <kbd>Home</kbd> | Moves focus to the first tab |  
| <kbd>End</kbd> | Moves focus to the last tab |  



## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/tabs" 
    @name="Signature" />
</template>
```

### Classes & Attributes

All these classes do nothing on their own, but offer a way for folks authoring CSS to set their styles (especially since `@scope` isn't available everywhere (yet?)).


- `ember-primitives__tabs`

  This is the class on the root-level element. This element has data attributes representing the overall state of the tabs component

  - `data-active` string. Will represent the id or (if provided), the value of the active tab.

- `ember-primitives__tabs__label`
  
  The element around the label text, whether passed as an argument or block content.

- `ember-primitives__tabs__tabpanel`

  The containing element of the active tab content. This element has `[role='tabpanel']`
  

- `ember-primitives__tabs__tablist`

  The containing element of each of the tabs. This element has `[role='tablist']`






---

# ToggleGroup

A group of two-state buttons that can be toggled on or off.


<div class="featured-demo">

```gjs live preview no-shadow
import { ToggleGroup } from 'ember-primitives';

<template>
  <div class="demo">
    <ToggleGroup class="toggle-group" as |t|>
      <t.Item @value="left" aria-label="Left align">
        <AlignLeft />
      </t.Item> 
      <t.Item @value="center" aria-label="Center align">
        <AlignCenter />
      </t.Item> 
      <t.Item @value="right" aria-label="Right align">
        <AlignRight />
      </t.Item> 
    </ToggleGroup>

    <ToggleGroup @type="multi" class="toggle-group" as |t|>
      <t.Item @value="bold" aria-label="Bold text">
        B
      </t.Item> 
      <t.Item @value="italic" aria-label="Italicize text">
        I
      </t.Item> 
      <t.Item @value="underline" aria-label="Underline text">
        U
      </t.Item> 
    </ToggleGroup>
  </div>

  <style>
    button[aria-label="Bold text"] { font-weight: bold; }
    button[aria-label="Italicize text"] { font-style: italic; }
    button[aria-label="Underline text"] { text-decoration: underline; } 

    .demo { 
      display: flex; 
      justify-content: center; 
      align-items: center; 
      gap: 1rem; 
    }

    .toggle-group {
      display: inline-flex;
      background-color: #fff;
      border-radius: 0.25rem;
      filter: drop-shadow(0 2px 2px rgba(0,0,0,0.5));
    }

    .toggle-group > button {
      background-color: white;
      color: #000;
      height: 35px;
      width: 35px;
      display: flex;
      font-size: 15px;
      padding: 0.5rem;
      line-height: 1;
      align-items: center;
      justify-content: center;
      margin-left: 1px;
      border: 0;
    }
    .toggle-group > button:first-child {
      margin-left: 0;
      border-top-left-radius: 4px;
      border-bottom-left-radius: 4px;
    }
    .toggle-group > button:last-child {
      border-top-right-radius: 4px;
      border-bottom-right-radius: 4px;
    }
    .toggle-group > button:hover {
      background-color: #eee;
    }
    .toggle-group > button[aria-pressed='true'] {
      background-color: #ddf;
      color: black;
    }
    .toggle-group > button:focus {
      position: relative;
      box-shadow: 0 0 0 2px black;
    }
  </style>
</template>

const AlignLeft = <template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M288 64c0 17.7-14.3 32-32 32H32C14.3 96 0 81.7 0 64S14.3 32 32 32H256c17.7 0 32 14.3 32 32zm0 256c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H256c17.7 0 32 14.3 32 32zM0 192c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg>
</template>;
const AlignCenter = <template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M352 64c0-17.7-14.3-32-32-32H128c-17.7 0-32 14.3-32 32s14.3 32 32 32H320c17.7 0 32-14.3 32-32zm96 128c0-17.7-14.3-32-32-32H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H416c17.7 0 32-14.3 32-32zM0 448c0 17.7 14.3 32 32 32H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H32c-17.7 0-32 14.3-32 32zM352 320c0-17.7-14.3-32-32-32H128c-17.7 0-32 14.3-32 32s14.3 32 32 32H320c17.7 0 32-14.3 32-32z"/></svg>
</template>;
const AlignRight = <template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M448 64c0 17.7-14.3 32-32 32H192c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32zm0 256c0 17.7-14.3 32-32 32H192c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32zM0 192c0-17.7 14.3-32 32-32H416c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32zM448 448c0 17.7-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32H416c17.7 0 32 14.3 32 32z"/></svg>
</template>;
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/toggle-group.gts" />
```

## Features

* Full keyboard navigation
* Supports horizontal / vertical orientation
* Support single and multiple pressed buttons
* Can be controlled or uncontrolled

## Anatomy

```js 
import { ToggleGroup } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { ToggleGroup } from 'ember-primitives/components/toggle';
```


```gjs 
import { ToggleGroup } from 'ember-primitives';

<template>
  <ToggleGroup as |t|>
    <t.Item /> 
  </ToggleGroup>
</template>
```

## API Reference: `@type='single'` (default)

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/toggle-group" 
    @name="SingleSignature" />
</template>
```

## API Reference: `@type='multi'` 

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/toggle-group" 
    @name="MultiSignature" />
</template>
```


<hr>

## API Reference: `Item`

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/toggle-group" 
    @name="ItemSignature" />
</template>
```

## Accessibility

Uses [roving tabindex](https://www.w3.org/TR/wai-aria-practices-1.2/examples/radio/radio.html) to manage focus movement among items using the [tabster](https://tabster.io/) library.

As a caveat to using [tabster](https://tabster.io/), a "tabster-root" will need to be established separately if this component is used within a shadow-root, which escapes the parent-DOM's tabster instance.

For more information, see [The Accessibility guide](/2-accessibility/index.md).

### Keyboard Interactions

| key | description |  
| :---: | :----------- |  
| <kbd>Tab</kbd> | Moves focus to the first item in the group |  
| <kbd>Space</kbd> | Toggles the item's state |  
| <kbd>Enter</kbd> | Toggles the item's state |  
| <kbd>ArrowDown</kbd> | Moves focus to the next item in the group |  
| <kbd>ArrowRight</kbd> | Moves focus to the next item in the group |  
| <kbd>ArrowDown</kbd> | Moves focus to the previous item in the group |  
| <kbd>ArrowLeft</kbd> | Moves focus to the previous item in the group |  
| <kbd>Home</kbd> | Moves focus to the first item in the group |  
| <kbd>End</kbd> | Moves focus to the last item in the group |  


---

# Toggle

A two-state button that can be either on or off.

This type of button could be used to enable or disable a feature, activate or deactivate a mode, or show or hide a particular element on a webpage.

`<Toggle />` can be used in any design system.

## Examples


<details open><summary><h3>Bold Text Toggle</h3></summary>


See [Bootstrap Toggle Button](https://getbootstrap.com/docs/5.3/forms/checks-radios/#toggle-buttons) docs.

```gjs live preview
import { Toggle } from 'ember-primitives';

<template>
  <Toggle aria-label="Toggle Bold Text" class="bold-toggle">
    B
  </Toggle>

  <style>
    .bold-toggle {
      border: 1px solid;
      padding: 5px 10px;
      font-size: 1.25rem;
      line-height: 1rem;
    }
    .bold-toggle[aria-pressed="true"] {
      font-weight: bold;
      backdrop-filter: blur(25px);
      background: gray;
    }
  </style>
  
</template>
```

</details>

## Install

```hbs live
<SetupInstructions @src="components/toggle.gts" />
```

## Features 

* Full keyboard navigation 
* Can be controlled or uncontrolled


## Anatomy

```js 
import { Toggle } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { Toggle } from 'ember-primitives/components/toggle';
```


```gjs 
import { Toggle } from 'ember-primitives';

<template>
  <Toggle aria-label="Toggle Bold Text">
    B
  </Toggle>
</template>
```


## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/toggle" 
    @name="Signature" 
  />
</template>
```

### State Attributes

| key | description |  
| :---: | :----------- |  
| aria-pressed | "true" or "false", depending on the state of the toggle button |  


## Accessibility

Uses [`aria-pressed`](https://www.w3.org/TR/wai-aria-1.2/#aria-pressed) but with only two possible states.

### Keyboard Interactions

| key | description |  
| :---: | :----------- |  
| <kbd>Space</kbd> | Toggles the component's state |  
| <kbd>Enter</kbd> | Toggles the component's state |  

In addition, a label is required so that users know what the toggle is for.


---

# Zoetrope

A slider style element built using browser native scrolling to create netflix style carousels.

<div class="hidden">
```gjs live preview
<!-- demo styles -->
<template>
  <style>
    .featured-demo h2 { margin: 0; } .featured-demo .glimdown-render { padding-left: 0; padding-right: 0; }
    .card { background: red; color: #fff; height: 150px; padding: 24px; text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); width: 200px; }
    .card:nth-child(1n) { background: blue; } .card:nth-child(2n) { background: green; }
    .card:nth-child(3n) { background: yellow; } .card:nth-child(4n) { background: purple; }
    .card:nth-child(5n) { background: orange; } .card:nth-child(6n) { background: pink; }
    .card:nth-child(7n) { background: brown; } .card:nth-child(8n) { background: black; }
    .card:nth-child(9n) { background: mediumaquamarine; } .card:nth-child(10n) { background: gray; }
  </style>
</template>
```
</div>

<div data-tabster='{"uncontrolled": {}}'>
<div class="featured-demo auto-height">

```gjs live preview no-shadow
import { Zoetrope } from "ember-primitives";

<template>
  <Zoetrope @gap={{8}} @offset={{40}}>
    <:header>
      <h2>Heading</h2>
    </:header>

    <:content>
      <a href="#" class="card">1</a>
      <a href="#" class="card">2</a>
      <a href="#" class="card">3</a>
      <a href="#" class="card">4</a>
      <a href="#" class="card">5</a>
      <a href="#" class="card">6</a>
      <a href="#" class="card">7</a>
      <a href="#" class="card">8</a>
      <a href="#" class="card">9</a>
      <a href="#" class="card">10</a>
      <a href="#" class="card">11</a>
      <a href="#" class="card">12</a>
      <a href="#" class="card">13</a>
      <a href="#" class="card">14</a>
      <a href="#" class="card">15</a>
      <a href="#" class="card">16</a>
      <a href="#" class="card">17</a>
      <a href="#" class="card">18</a>
      <a href="#" class="card">19</a>
      <a href="#" class="card">20</a>
      <a href="#" class="card">21</a>
      <a href="#" class="card">22</a>
      <a href="#" class="card">23</a>
      <a href="#" class="card">24</a>
      <a href="#" class="card">25</a>
      <a href="#" class="card">26</a>
      <a href="#" class="card">27</a>
      <a href="#" class="card">28</a>
      <a href="#" class="card">29</a>
      <a href="#" class="card">30</a>
      <a href="#" class="card">31</a>
      <a href="#" class="card">32</a>
      <a href="#" class="card">33</a>
      <a href="#" class="card">34</a>
      <a href="#" class="card">35</a>
      <a href="#" class="card">36</a>
      <a href="#" class="card">37</a>
      <a href="#" class="card">38</a>
      <a href="#" class="card">39</a>
      <a href="#" class="card">40</a>
      <a href="#" class="card">41</a>
      <a href="#" class="card">42</a>
      <a href="#" class="card">43</a>
      <a href="#" class="card">44</a>
      <a href="#" class="card">45</a>
      <a href="#" class="card">46</a>
      <a href="#" class="card">47</a>
      <a href="#" class="card">48</a>
      <a href="#" class="card">49</a>
      <a href="#" class="card">50</a>
      <a href="#" class="card">51</a>
      <a href="#" class="card">52</a>
      <a href="#" class="card">53</a>
      <a href="#" class="card">54</a>
      <a href="#" class="card">55</a>
      <a href="#" class="card">56</a>
      <a href="#" class="card">57</a>
      <a href="#" class="card">58</a>
      <a href="#" class="card">59</a>
      <a href="#" class="card">60</a>
    </:content>
  </Zoetrope>

  <style>
    /* some basic button styles */ .ember-primitives__zoetrope__controls button { background: #fff;
    padding: 0.5rem; border-radius: 0.25rem; color: #333; } .ember-primitives__zoetrope__controls
    button:disabled { opacity: 0.5; }
  </style>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/zoetrope.ts" />
```

## Features

- Automatic page size detection.
- Provide your own custom control buttons.
- CSS variables to control gap and offset.
- Keyboard navigation.

## Custom Controls

You can pass your own control buttons to the zoetrope component. Use the `.ember-primitives__zoetrope__controls` class to place them in the default position, or style them as you wish.

<div class="featured-demo auto-height">

```gjs live preview no-shadow
import { Zoetrope } from "ember-primitives";
import { on } from "@ember/modifier";

<template>
  <Zoetrope @gap={{8}} @offset={{40}}>
    <:header>
      <h2>Default Placement</h2>
    </:header>

    <:controls as |z|>
      <div class="ember-primitives__zoetrope__controls">
        <button
          type="button"
          {{on "click" z.scrollLeft}}
          disabled={{z.cannotScrollLeft}}
        >&lt;</button>

        <button
          type="button"
          {{on "click" z.scrollRight}}
          disabled={{z.cannotScrollRight}}
        >&gt;</button>
      </div>
    </:controls>

    <:content>
      <a href="#" class="card">1</a>
      <a href="#" class="card">2</a>
      <a href="#" class="card">3</a>
      <a href="#" class="card">4</a>
      <a href="#" class="card">5</a>
      <a href="#" class="card">6</a>
      <a href="#" class="card">7</a>
      <a href="#" class="card">8</a>
      <a href="#" class="card">9</a>
      <a href="#" class="card">10</a>
      <a href="#" class="card">11</a>
      <a href="#" class="card">12</a>
    </:content>
  </Zoetrope>
</template>
```

```gjs live preview no-shadow
import { Zoetrope } from "ember-primitives";
import { on } from "@ember/modifier";

<template>
  <Zoetrope @gap={{8}} @offset={{40}}>
    <:header>
      <h2>Custom Placement</h2>
    </:header>

    <:controls as |z|>
      <div class="my-controls">
        <button
          type="button"
          {{on "click" z.scrollLeft}}
          disabled={{z.cannotScrollLeft}}
        >&lt;</button>

        <button
          type="button"
          {{on "click" z.scrollRight}}
          disabled={{z.cannotScrollRight}}
        >&gt;</button>
      </div>
    </:controls>

    <:content>
      <a href="#" class="card">1</a>
      <a href="#" class="card">2</a>
      <a href="#" class="card">3</a>
      <a href="#" class="card">4</a>
      <a href="#" class="card">5</a>
      <a href="#" class="card">6</a>
      <a href="#" class="card">7</a>
      <a href="#" class="card">8</a>
      <a href="#" class="card">9</a>
      <a href="#" class="card">10</a>
      <a href="#" class="card">11</a>
      <a href="#" class="card">12</a>
    </:content>
  </Zoetrope>

  <style>
    .my-controls { display: flex; position: absolute; top: 50%; transform: translateY(-50%); height:
    40%; width: 100%; padding-top: 2rem; pointer-events: none; } .my-controls button { height: 100%;
    width: var(--zoetrope-offset); background: rgb(0 0 0 / 50%); pointer-events: auto; }
    .my-controls button:nth-child(2) { margin-left: auto; } .my-controls button:disabled {
    visibility: hidden; }

  </style>
</template>
```

</div>

## Scroll Behavior

The component takes a `@scrollBehavior` argument that can be used to control the scroll behavior. The default value is `smooth`.

<div class="featured-demo auto-height">

```gjs live preview no-shadow
import { Zoetrope } from "ember-primitives";

<template>
  <Zoetrope @gap={{8}} @offset={{40}} @scrollBehavior="instant">
    <:header>
      <h2>Instant Scroll</h2>
    </:header>

    <:content>
      <a href="#" class="card">1</a>
      <a href="#" class="card">2</a>
      <a href="#" class="card">3</a>
      <a href="#" class="card">4</a>
      <a href="#" class="card">5</a>
      <a href="#" class="card">6</a>
      <a href="#" class="card">7</a>
      <a href="#" class="card">8</a>
      <a href="#" class="card">9</a>
      <a href="#" class="card">10</a>
      <a href="#" class="card">11</a>
      <a href="#" class="card">12</a>
    </:content>

  </Zoetrope>
</template>
```

</div>

## Anatomy

```js
import { Zoetrope } from "ember-primitives";
```

or for non-tree-shaking environments:

```js
import { Zoetrope } from "ember-primitives/components/zoetrope";
```

```gjs
import { Zoetrope } from "ember-primitives";

<template>
  <Zoetrope @gap={{8}} @offset={{40}}>
    <:header>
      <h2>Heading</h2>
    </:header>

    <:content>
      <a href="#" class="card">1</a>
      <a href="#" class="card">2</a>
      <a href="#" class="card">3</a>
      <a href="#" class="card">4</a>
      <a href="#" class="card">5</a>
      <a href="#" class="card">6</a>
    </:content>
  </Zoetrope>
</template>
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from "kolay";

<template>
  <ComponentSignature
    @package="ember-primitives"
    @module="declarations/components/zoetrope/types"
    @name="Signature"
  />
</template>
```

</div>

## Test Support

We provide a test support module that can be used to interact with the zoetrope component in your tests.

Use the `ZoetropeHelper` class to interact with the zoetrope component in your tests. It takes an optional `selector` argument that can be used to target a specific zoetrope component.

It has the following methods:

- `visibleItems()`: Returns the visible items as an array of DOM items.
- `visibleItemCount()`: Returns the count of visible items.
- `scrollLeft()`: Scrolls the zoetrope component to the left.
- `scrollRight()`: Scrolls the zoetrope component to the right.

```gjs no-shadow
import { render } from "@ember/test-helpers";
import { module, test } from "qunit";
import { setupRenderingTest } from "ember-qunit";

import { Zoetrope } from "ember-primitives";

import { ZoetropeHelper } from "ember-primitives/test-support";

// setup helper
const zoetrope = new ZoetropeHelper();

module("<Zoetrope />", function (hooks) {
  setupRenderingTest(hooks);

  test("basic usage renders", async function (assert) {
    await render(
      <template>
        <Zoetrope>
          <:content>
            <a href="#">Card</a>
            <a href="#">Card</a>
            <a href="#">Card</a>
            <a href="#">Card</a>
          </:content>
        </Zoetrope>
      </template>,
    );

    const visibleItemsArray = zoetrope.visibleItems();

    assert.strictEqual(zoetrope.visibleItemCount(), 2);

    await zoetrope.scrollRight();

    await zoetrope.scrollLeft();
  });
});
```


---

# ExternalLink

The `<ExternalLink />` component is a light wrapper around the [Anchor element][mdn-a], which will always make your link an external link.


[mdn-a]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a

Unlike `<Link />`, `<ExternalLink />` uses the native `href` attribute.

This component always has `target=_blank` and `rel='noreferrer noopener'`.

## Example 

```gjs live preview
import { ExternalLink } from 'ember-primitives';

<template>
  <ExternalLink href="https://developer.mozilla.org">
    MDN ➚
  </ExternalLink>
</template>
```


## Install

```hbs live
<SetupInstructions @src="components/external-link.gts" />
```


---

# Link

```gjs live
import { Comment } from '#src/api-docs';

<template>
  <Comment @name="Link" @declaration="components/link" />
</template>
```

<Callout>

Most of the time, for in-app navigations especially, you'll want to use the [native `<a>`][mdn-a] element.

However, for consistent usage and behavior within a design system, it'll be beneficial to lint against `<a>`, and use a design-system-provided version of `<Link />`, which wraps _this_ `<Link />`.

</Callout>

The `<Link />` component provides additional behavior and utilities for styling and providing additional context, such as within `<nav>` or other UI patterns which persist across multiple page navigations.

`<Link />` will automatically externalize a `href` which specify different domains (add `target='_blank'` and `rel='noreferrer noopener'`)



[mdn-a]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a

## Example


```gjs live preview
import { Link } from 'ember-primitives';

<template>
  <Link @href="/">Home</Link>  &nbsp;&nbsp;

  <Link @href="https://developer.mozilla.org" as |a|>
    MDN

    {{#if a.isExternal}}
      ➚
    {{/if}}
  </Link>
</template>
```

## Install

```hbs live
<SetupInstructions @src="components/link.gts" />
```

## Features

* Full keyboard navigation
* Active state
* "Just an `<a>`" 

## Anatomy

_requires usage of [`@properLinks`](/4-routing/proper-links)_


```js 
import { Link } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { Link } from 'ember-primitives/components/link';
```


```gjs 
import { Link } from 'ember-primitives';

<template>
  <Link @href="..." as |a|>
    {{if a.isActive "active!"}}
    {{if a.isExternal "external!"}}
  </Link>
</template>
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/link" 
    @name="Signature" />
</template>
```

### State Attributes


| key | description |  
| :---: | :----------- |  
| data-active | attribute will be "true" or "false", depending on if the `@href` matches the current URL |  

<br>

```gjs live preview
import { Link } from 'ember-primitives';

<template>
  <style>
    @scope {
      [data-active] {
        color: red;
      }
      a { padding: 0.25rem 0.5rem; }
    }
  </style>

  <Link @href="/4-routing/link" as |a|>
    This page
  </Link>
  |
  <Link @href="/4-routing/proper-links" as |a|>
    Related page
  </Link>
</template>
```




---

# Proper Links

Enables usage of plain `<a>` tags.
You no longer need to use a component to have single-page-app navigation 🎉.

## Install

```hbs live
<SetupInstructions @src="proper-links.ts" />
```

## Setup

import `properLinks` and apply it to your Router.

```diff
  // app/router.js
  import EmberRouter from "@ember/routing/router";

  import config from "docs-app/config/environment";
+ import { properLinks } from "ember-primitives/proper-links";

+ @properLinks
  export default class Router extends EmberRouter {
    location = config.locationType;
    rootURL = config.rootURL;
  }
```

## Example

Once `@properLinks` is installed and setup, you can use plain `<a>` tags for navigation like this

```gjs live preview
<template>
  <nav id="example" style="display: flex; gap: 0.5rem">
    <a href="/">Home</a>
    <a href="#example">Link using a hash</a>
    <a href="/4-routing/link">Link docs</a>
    <a href="/4-routing/external-link">ExternalLink docs</a>
    <a href="https://developer.mozilla.org">MDN ➚</a>
  </nav>
</template>
```




---

# anchorTo 

The `anchorTo` modifier provides a wrapper for using [Floating UI](https://floating-ui.com/), for associating a floating element to an anchor element (such as for menus, popovers, etc). 

<Callout>

The usage of a 3rd-party library will be removed when [CSS Anchor Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) lands and is widely supported (This component and modifier will still exist for the purpose of wiring up the ids between anchor and target). 

</Callout>

Several of Floating UI's functions and [middleware](https://floating-ui.com/docs/middleware) are used to create an experience out of the box that is useful and expected.
See Floating UI's [documentation](https://floating-ui.com/docs/getting-started) for more information on any of the following included functionality.

## Install


```hbs live
<SetupInstructions @src="floating-ui.ts" />
```


## `{{anchorTo}}`

The main modifier for creating floating UIs with any elements.

Requires you to maintain a unique ID for every invocation. 


<div class="featured-demo">

```gjs live preview no-shadow
import { anchorTo } from 'ember-primitives/floating-ui';
import { InViewport } from 'ember-primitives/viewport';

<template>
  <InViewport>
    <button id="reference" popovertarget="floating">Click the reference element</button>
    <menu popover id="floating" {{anchorTo "#reference"}}>Here is <br> floating element</menu>
  </InViewport>

  <style>
    menu#floating {
      width: max-content;
      position: absolute;
      top: 0;
      left: 0;
      background: #222;
      color: white;
      font-weight: bold;
      padding: 2rem;
      border-radius: 4px;
      font-size: 90%;
      filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.4));
      z-index: 10;
    }
    button#reference {
      padding: 0.5rem;
      border: 1px solid;
      display: inline-block;
      background: white;
      color: black;
      border-radius: 0.25rem;

      &:hover {
        background: #ddd;
      }
    }
  </style>
</template>
```

</div>

Note that in this demo thare are _two_ sets of ids. One pair for the floating behavior, and another pair for the [popover](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover) wiring.  The component below handles the floating id, but to avoid needing to maintain _unique_ pairs of ids for each floating-ui you may be interested in the [Popover](/5-floaty-bits/popover.md) component (which also includes arrow support).

### API Reference for `{{anchorTo}}`

```gjs live no-shadow
import { ModifierSignature } from 'kolay';

<template>
  <ModifierSignature 
    @package="ember-primitives" 
    @module="declarations/floating-ui/modifier" 
    @name="Signature"
  />
</template>
```




---

# Drawer

A drawer is a sliding panel that appears from the side of the screen, typically used for navigation, filters, or additional content.
It is built on top of the native `<dialog>` element, similar to modals, but designed to slide in from a specific side of the viewport.

<Callout>

This pattern of a sliding drawer / sheet can be done with only the native `<dialog>` and some `<button>` to open it. This component is a _very_ small wrapper for this pattern -- primarily a state container for easily controlling the open state of the `<dialog>` element without wiring it up yourself.

</Callout>


<div class="featured-demo">

```gjs live preview no-shadow
import { Drawer } from 'ember-primitives/components/drawer';
import { on } from '@ember/modifier';

<template>
  <Drawer as |d|>
    <button {{on 'click' d.open}}>Open Drawer</button>

    <d.Drawer class="not-prose">
      <header>
        <h2>Example Drawer</h2>
        <button {{on 'click' d.close}}>Close</button>
      </header>
      <form method="dialog">
        <main>
          Drawer content here
          <br>
          Because this is a native dialog element, it captures focus,
          and pressing escape will close the dialog and return focus to the 
          original button.
        </main>

        <footer>
          <button type="submit" value="confirm">Confirm</button>
          <button type="submit" value="create">Create</button>
        </footer>
      </form>
    </d.Drawer>
  </Drawer>

  <link rel="stylesheet" href="https://unpkg.com/open-props/easings.min.css"/>
  <style>
    @scope {
      dialog {
        transition: 
          display 0.125s allow-discrete, 
          overlay 0.125s allow-discrete;
        animation: close 0.125s forwards;

        &[open] {
          animation: open 0.25s forwards;
        }
      }

      @keyframes open {
        from {
          opacity: 0;
          transform: translate(0, 100%);
        }
        to {
          opacity: 1;
          transform: translate(0, 0);
        }
      }

      @keyframes close {
        from {
          opacity: 1;
          transform: translate(0, 0);
        }
        to {
          opacity: 0;
          transform: translate(0, 100%);
        }
      }

    }

    @scope {
      [data-repl-output] {
        display: block;
      }
      button {
        padding: 0.5rem 1rem;
        border-radius: 0.25rem;;

        background-color: #2563eb;   /* blue */
        color: #ffffff;

        transition:
          background-color 120ms ease-out,
          box-shadow 120ms ease-out,
          transform 120ms ease-out;
      }

      button:hover {
        background-color: #1d4ed8;
      }

      button:active {
        transform: translateY(1px);
        box-shadow: 0 1px 2px rgba(15, 23, 42, 0.3) inset;
      }
    }

    @scope {
      /* Basic reset for the dialog */
      dialog {
        position: fixed;
        top: unset;
        bottom: 0px;
        color: black;
        background: white;
        margin: 0 auto;
        min-width: 80dvw;
        border-top-right-radius: 0.5rem;
        border-top-left-radius: 0.5rem;

        main {
          padding: 1rem;
        }

        header {
          display: flex;
          align-items: center;
          justify-content: space-between;
          padding: 0.5rem 1rem;
          border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent);

          h2 {
            font-size: 1.5rem;
          }
        }

        footer {
          display: flex;
          justify-content: flex-end;
          gap: 2rem;
          padding: 1rem;
        }
      }

      /* backdrop */
      dialog::backdrop {
        background: color-mix(in srgb, #020617 60%, transparent);
      }
    }
  </style>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/drawer.gts" />
```

Introduced in [0.51.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.51.0-ember-primitives)

## Anatomy

```gjs 
import { Drawer } from 'ember-primitives/components/drawer';

<template>
  <Drawer as |d|>

    d.isOpen - boolean
    d.open   - function
    d.close  - function

    <d.Drawer ...attributes>
      this is just the HTMLDialogElement
    </d.Drawer>
  </Drawer>
</template>
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/drawer" 
    @name="Signature" />
</template>
```

### State Attributes

There is no root element for this component, so there are no state attributes to use.
Since this component uses the `<dialog>` element, it will still use the `open` attribute.

## Accessibility

Once the drawer is open, the browser will focus on the first button (in this case, it's our close button on the drawer header) or any button with the autofocus attribute within the drawer. When you close the drawer, the browser restores the focus on the button we used to open it.

### Keyboard Interactions

Because this builds on `<dialog>`, all behaviors are already built in to the browser. 

Pressing the <kbd>esc</kbd> key closes the dialog, focusing the last-focused element before the dialog was opened. 

## References

- [MDN HTMLDialogElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)
- [web.dev : Building a dialog component](https://web.dev/building-a-dialog-component/)




---

# Floating UI

The `FloatingUI` component provides a wrapper for using [Floating UI](https://floating-ui.com/), for associating a floating element to an anchor element (such as for menus, popovers, etc). 

<Callout>

The usage of a 3rd-party library will be removed when [CSS Anchor Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) lands and is widely supported (This component and modifier will still exist for the purpose of wiring up the ids between anchor and target). 

</Callout>

Several of Floating UI's functions and [middleware](https://floating-ui.com/docs/middleware) are used to create an experience out of the box that is useful and expected.
See Floating UI's [documentation](https://floating-ui.com/docs/getting-started) for more information on any of the following included functionality.

## Install


```hbs live
<SetupInstructions @src="floating-ui.ts" />
```

## `<FloatingUI>`

This component takes the [`archorTo` modifier](/5-floaty-bits/anchor-to.md) and abstracts away the need to manage the `id`-relationship between reference and floating elements -- since every ID on the page needs to be unique, it is useful to have this automatically managed for you.

This component has no DOM of its own, but provides two modifiers to attach to both reference and floating elements.

<div class="featured-demo">

```gjs live preview no-shadow
import { FloatingUI } from 'ember-primitives/floating-ui';

<template>
  <FloatingUI as |reference floating|>
    <button {{reference}} popovertarget="floating2">Click the reference element</button>
    <menu {{floating}} popover id="floating2">Here is <br> floating element</menu>
  </FloatingUI>

  <style>
    menu#floating2 {
      width: max-content;
      position: absolute;
      top: 0;
      left: 0;
      background: #222;
      color: white;
      font-weight: bold;
      padding: 2rem;
      border-radius: 4px;
      font-size: 90%;
      filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.4));
      z-index: 10;
    }
    button[popovertarget="floating2"] {
      padding: 0.5rem;
      border: 1px solid;
      display: inline-block;
      background: white;
      color: black;
      border-radius: 0.25rem;

      &:hover {
        background: #ddd;
      }
    }
  </style>
</template>
```

</div>

Note that this demo has to main a unique id/target for the popover behavior. If you'd like to not have to manage ids at all, you may be interested in the [Popover](/5-floaty-bits/popover.md) component (which also includes arrow support).

### API Reference for `<FloatingUI>`

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/floating-ui/component" 
    @name="Signature" />
</template>
```

## References

- [Positioning Anchored Popovers](https://hidde.blog/positioning-anchored-popovers/)
- [Floating UI Documentation](https://floating-ui.com/)

## Comparison to similar projects

Similar projects include:

* [ember-popperjs](https://github.com/NullVoxPopuli/ember-popperjs)
* [ember-popper-modifier](https://github.com/adopted-ember-addons/ember-popper-modifier)

The above projects both use [Popper](https://popper.js.org/). In contrast, Ember Velcro uses Floating UI. Floating UI is the successor to Popper - see their [migration guide](https://floating-ui.com/docs/migration) for a complete comparison.

There is also:

* [ember-velcro](https://github.com/CrowdStrike/ember-velcro)

which this project is a fork up, and ditches the velcro (hook / loop) verbiage and fixes bugs and improves ergonomics.



---

# Menu

Menus are built with Popovers, with added features for keyboard navigation and accessibility. 

The placement of the menu content is handled by `<Popover>`, so `<Menu>` accepts the same arguments for positioning the dropdown.

Like `<Popover>`, the `<Menu>` component uses portals in a way that totally solves layering issues. No more worrying about tooltips on varying layers of your UI sometimes appearing behind other floaty bits. See the `<Portal>` and `<PortalTargets>` pages for more information.

<div class="featured-demo">

```gjs live preview no-shadow
import { PortalTargets, Menu } from 'ember-primitives';

<template>
  <PortalTargets />

  <Menu @offsetOptions={{8}} as |m|>
    <m.Trigger class="trigger" aria-label="Options">
      <EllipsisVertical />
    </m.Trigger>

    <m.Content class="content" as |c|>
      <c.Item>Item 1</c.Item>
      <c.Item>Item 2</c.Item>
      <c.Separator />
      <c.LinkItem class="not-prose" @href="/">Item 3</c.LinkItem>
    </m.Content>
  </Menu>

  <style>
    .content {
      all: unset;
      min-width: 180px;
      background: #fff;
      color: #111827;
      padding: 8px 0;
      border-radius: 6px;
      border: none;
      font-size: 14px;
      z-index: 10;
      box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
      display: flex;
      flex-direction: column;
    }

    .content [role="menuitem"] {
      all: unset;
      display: block;
      padding: 4px 12px;
      cursor: pointer;
    }

    .content [role="menuitem"]:focus, .trigger:hover {
      background-color: #f9fafb;
    }

    .content [role="separator"] {
      border-bottom: 1px solid rgb(17 24 39 / 0.1);
    }

    .trigger {
      display: inline-block;
      border-radius: 4px;
      border-width: 0;
      background-color: #fff;
      color: #111827;
      border-radius: 100%;
      padding: 10px;
      box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 10px;
      cursor: pointer;
    }

    .trigger svg {
      width: 15px;
      height: 15px;
      display: block;
    }
  </style>
</template>

const EllipsisVertical = <template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 512" fill="currentColor"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M64 360a56 56 0 1 0 0 112 56 56 0 1 0 0-112zm0-160a56 56 0 1 0 0 112 56 56 0 1 0 0-112zM120 96A56 56 0 1 0 8 96a56 56 0 1 0 112 0z"/></svg>
</template>;
```

</div>


Sometimes, you need to use an existing component as the trigger. `<Menu>` also yields a `trigger` modifier that you can use anywhere, even on your own components (e.g a custom button):


```hbs
<Menu as |m|>
  <MyButton {{m.trigger}}>
    <EllipsisVertical />
  </MyButton>

  <m.Content class="content" as |c|>
    <c.Item>Item 1</c.Item>
    <c.Item>Item 2</c.Item>
    <c.Separator />
    <c.Item>Item 3</c.Item>
  </m.Content>
</Menu>
```

Keep in mind that for the modifier to do its work, your custom component must use [`...attributes`](https://guides.emberjs.com/v5.7.0/components/component-arguments-and-html-attributes/#toc_html-attributes) in some HTML element.


## Install


```hbs live
<SetupInstructions @src="components/menu.gts" />
```


## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/menu" 
    @name="Signature" />
</template>
```

## Accessibility

Adheres to the [Menu Button WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/).

### Keyboard Interactions

| key | description |
| :---: | :----------- |
| <kbd>Space</kbd> <kbd>Enter</kbd>  | When focus is on `Trigger`, opens the menu and focuses the first item. When focus is on an `Item`, activates the focused item. |
| <kbd>ArrowDown</kbd> <kbd>ArrowRight</kbd> | When `Content` is open, moves to the next item.  |
| <kbd>ArrowUp</kbd> <kbd>ArrowLeft</kbd> | When `Content` is open, moves to the previous item.  |
| <kbd>Esc</kbd> | Closes the menu and moves focus to `Trigger`. |


---

# Modal `<dialog>`

A modal in is a temporary overlay that appears on top of the main content of a webpage. 
It is used to present important information, prompt for input, or require the user to make a decision. 

Modals create a focused and isolated interaction, often with a darker background overlay, to draw attention and prevent interactions with the underlying page until the modal is dismissed. 

## Example

<div class="featured-demo">

```gjs live preview no-shadow
import { Modal } from 'ember-primitives';

import { on } from '@ember/modifier';
import { cell } from 'ember-resources';
import { loremIpsum } from 'lorem-ipsum';

const returnValue = cell('');

<template>
  <Modal @onClose={{returnValue.set}} as |m|>
    <button {{on 'click' m.open}} {{m.focusOnClose}}>Open Modal</button>

    <br><br>
    isOpen: {{m.isOpen}}<br>
    return: {{returnValue.current}}

    <m.Dialog>
      <div>
        <header>
          <h2>Example Modal</h2>

          <button {{on 'click' m.close}}>Close</button>
        </header>

        <form method="dialog">
          <main>
            Modal content here
            <br>

           {{loremIpsum 1}}
          </main>

          <footer>
            <button type="submit" value="confirm">Confirm</button>
            <button type="submit" value="create">Create</button>
            <button type="reset" value="close" {{on 'click' m.close}}>Reset</button>
          </footer>
        </form>
      </div>
    </m.Dialog>
  </Modal>


  <link rel="stylesheet" href="https://unpkg.com/open-props/easings.min.css"/>
  <link rel="stylesheet" href="https://unpkg.com/open-props/animations.min.css"/>
  <style>
    dialog {
      border-radius: 0.25rem;
      animation: var(--animation-slide-in-up), var(--animation-fade-in);
      animation-timing-function: var(--ease-out-5);
      animation-duration: 0.2s;
    }
    dialog > div {
      display: grid;
      gap: 1rem;
    }
    dialog::backdrop {
      backdrop-filter: blur(1px);
    }
    dialog header { 
      display: flex;
      justify-content: space-between;
    }
    dialog h2 {
      margin: 0;
    }

    dialog main {
      max-width: 300px;
    }
    form {
      display: grid;
      gap: 1rem; 
    }
  </style>
</template>
```

</div>

Note that animations on `<dialog>` elements do not work within a [Shadow Dom](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM).

### Adding a focus-trap

`<Modal />` doesn't provide a focus-trap by default. 
The `<dialog>` element already traps focus for your webpage -- however, 
`<dialog>` does not trap focus from tabbing to the browser (address bar, tabs, etc). 
This is, in part, so that focus behavior is consistent in and out of a modal, 
and that keyboard users retain the ability to escape the webpage without being forced to close the modal -- though,
if keyboard users are also power-users, they may know about <kbd>ctrl</kbd> + <kbd>l</kbd> which escapes all focus traps, focusing the address bar, which would then allow them to tab to the back, forward, refresh, etc buttons in their browsers UI.

This browser defined, yet consistent across all browser behavior, conflicts with the [W3 ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) recommendation:

> If focus is on the last tabbable element inside the dialog, moves focus to the first tabbable element inside the dialog.

_However_, the [example](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/) does not use the `<dialog>` element, and instead uses divs.

The behavior of trapping all focus is [proposed here](https://github.com/whatwg/html/issues/8339), and we are reminded that the ARIA patterns are _guidelines_.


If you wish to follow this guideline, it can be achieved via the `focusTrap` modifier.

```bash
pnpm add ember-focus-trap
```

<div class="featured-demo">

```gjs live preview no-shadow
import { Modal } from 'ember-primitives';

import { on } from '@ember/modifier';
import { focusTrap } from 'ember-focus-trap';
import { loremIpsum } from 'lorem-ipsum';


<template>
  <Modal as |m|>
    <button {{on 'click' m.open}}>Open Modal</button>

    <br><br>
    isOpen: {{m.isOpen}}<br>

    <m.Dialog {{focusTrap isActive=m.isOpen}}>
      <div>
        <header>
          <h2>Example Modal</h2>

          <button {{on 'click' m.close}}>Close</button>
        </header>

        <form method="dialog">
          <main>
            Modal content here
            <br>

           {{loremIpsum 1}}
          </main>

          <footer>
            <button type="submit" value="confirm">Confirm</button>
            <button type="reset" value="close" {{on 'click' m.close}}>Reset</button>
          </footer>
        </form>
      </div>
    </m.Dialog>
  </Modal>


  <link rel="stylesheet" href="https://unpkg.com/open-props/easings.min.css"/>
  <link rel="stylesheet" href="https://unpkg.com/open-props/animations.min.css"/>
  <style>
    dialog {
      border-radius: 0.25rem;
      animation: var(--animation-slide-in-up), var(--animation-fade-in);
      animation-timing-function: var(--ease-out-5);
      animation-duration: 0.2s;
    }
    dialog > div {
      display: grid;
      gap: 1rem;
      padding: 1rem;
    }
    dialog::backdrop {
      backdrop-filter: blur(1px);
    }
    dialog header { 
      display: flex;
      justify-content: space-between;
    }
    dialog h2 {
      margin: 0 !important;
    }

    dialog main {
      max-width: 300px;
    }
    form {
      display: grid;
      gap: 1rem; 
    }
    .glimdown-render {
      button { border: 1px solid; padding: 0.5rem; }
    }
  </style>
</template>
```

</div>

### Using as a Routeable Modal

To use the modal as a routeable modal, you can set the `@open` and `@onClose` keys, like so:
```gjs
import { Modal } from 'ember-primitives';
import Component from '@glimmer/component';
import { service } from '@ember/service';

export default class RouteableModal extends Component {
  <template>
    <Modal @open={{true}} @onClose={{this.handleClose}} as |m|>

      <m.Dialog>
        <form method="dialog">
          <button type="submit" value="confirm">Confirm</button>
          <button type="submit" value="create">Create</button>
          <button type="reset" value="close" {{on 'click' m.close}}>Reset</button>
        </form>
      </m.Dialog>
    </Modal>
  </template>

  @service router;

  handleClose = (reason) => {
    switch (reason) {
      case 'create': return this.router.transitionTo('place/when/created');
      case 'confirm': return this.router.transitionTo('place/when/confirmed');
      default:
        /**
          * there is no reason when ESC is pressed, 
          * or a type=reset button is clicked
          */
        return this.router.transitionTo('place/when/cancelled');
    }
  }

}
```

### Using an external trigger

To use an external trigger, you have to use a side-effect to do it, like so: 

<div class="featured-demo">

```gjs live preview no-shadow
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { Modal } from "ember-primitives";
import { waitForPromise } from "@ember/test-waiters";

import { loremIpsum } from "lorem-ipsum";

export default class ExternalTrigger extends Component {
  @tracked _isOpen = false;

  openModal = () => {
    this._isOpen = true;
  };

  closeModal = () => {
    this._isOpen = false;
  };

  <template>
    <button onclick={{this.openModal}}>Open Modal</button>

    <br /><br />
    isOpen :
    {{this._isOpen}}

    <ModalWrapper @isOpen={{this._isOpen}} @onClose={{this.closeModal}} as |m|>

      {{loremIpsum 1}}

      <button onclick={{m.close}}>Close</button>
    </ModalWrapper>
  </template>
}

const ModalWrapper = <template>
  <Modal @onClose={{@onClose}} as |m|>
    {{(sideEffect toggle @isOpen m)}}

    <m.Dialog>
      {{yield m}}
    </m.Dialog>
  </Modal>
</template>;

function toggle(wantsOpen, { open, close, isOpen }) {
  if (wantsOpen) {
    if (isOpen) return;
    open();
    return;
  }

  if (!isOpen) return;

  close();
}

function sideEffect(func, ...args) {
  waitForPromise(
    (async () => {
      // auto tracking is synchronous.
      // This detaches from tracking frames.
      await Promise.resolve();
      func(...args);
    })(),
  );
}
```

</div>

## Enabling automatic body-scroll lock

You'll need page-wide CSS similar to this:
```css
html {
  overflow: hidden;
}

body {
  overflow: auto;
  height: 100dvh;
}
```
This is also a common technique for controlling which element scrolls when doing custom layouts.
Constraining the height of an element to the dynamic vertical height works for desktop and mobile where some elements of the browser may not always be visible.

The scrollable element doesn't have to be the `body` either, it could be a `<div>`, as you'll see in layouts where scroll-content may be wholly underneath a header.

## Install

```hbs live
<SetupInstructions @src="components/dialog.gts" />
```

## Anatomy

```js 
import { Modal, Dialog /* alias */ } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { Modal, Dialog /* alias */ } from 'ember-primitives/components/dialog';
```


```gjs 
import { Modal } from 'ember-primitives';

<template>
  <Modal as |m|>

    m.isOpen
    m.open
    m.close

    <m.Dialog ...attributes>
      this is just the HTMLDialogElement
    </m.Dialog>
  </Modal>
</template>
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/dialog" 
    @name="Signature" />
</template>
```

### State Attributes

There is no root element for this component, so there are no state attributes to use.
Since the this component uses the `<dialog>` element, it will still use the `open` attribute.

## Accessibility

Once the dialog is open, the browser will focus on the first button (in this case, it's our close button on the dialog header) or any button with the autofocus attribute within the dialog. When you close the dialog, the browser restores the focus on the button we used to open it.

### Keyboard Interactions

The dialog element handles ESC (escape) key events automatically, hence reducing the burdens and efforts required to provide an effortless experience for users while working with dialogs.

However, if you want to add an animation effect on _closing_ and opening dialog programmatically, note that you will lose this built-in feature support and have to implement the tab navigation focus yourself.

When the dialog is closed, you can refocus the opening button when the `{{m.focusOnClose}}` modifier is applied to that button.


## References

- [MDN HTMLDialogElement](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)
- [web.dev : Building a dialog component](https://web.dev/building-a-dialog-component/)
- [Exploring the HTML Dialog element](https://mayashavin.com/articles/build-a-dialog-with-dialog-element)[^a11y]
- [Open Props](https://open-props.style)[^animations]

<hr>

[^a11y]: The Accessibility text was copied from this blog. 
[^animations]: Animations in the examples are provided by Open Props 


---

# Popover

Popovers are built with [Floating UI][docs-floating-ui], a set of utilities for making floating elements relate to each other with minimal configuration. 


The `<Popover>` component uses portals in a way that totally solves layering issues. No more worrying about tooltips on varying layers of your UI sometimes appearing behind other floaty bits. See the `<Portal>` and `<PortalTargets>` pages for more information.

One thing to note is that the position of the popover can _escape_ the boundary of a [ShadowDom][docs-shadow-dom] -- all demos on this docs site for `ember-primitives` use a `ShadowDom` to allow for isolated CSS usage within the demos.

[docs-floating-ui]: /5-floaty-bits/floating-ui.md
[docs-floating]: https://floating-ui.com/
[docs-popper]: https://popper.js.org/
[docs-shadow-dom]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM

<div class="featured-demo">

```gjs live preview
import { PortalTargets, Popover } from 'ember-primitives';
import { array, hash } from '@ember/helper';
import { loremIpsum } from 'lorem-ipsum';

<template>
  <PortalTargets />

  <div class="scroll-content" tabindex="0">
    {{loremIpsum (hash count=1 units="sentence")}}

    <Popover @placement="top" @offsetOptions={{8}} as |p|>
      <div class="hook" {{p.reference}}>
        the hook / anchor of the popover.
        <br> it sticks the boundary of this element.
      </div>
      <p.Content class="floatybit">
        The floaty bit here
        <div class="arrow" {{p.arrow}}></div>
      </p.Content>
    </Popover>

    {{loremIpsum (hash count=2 units="paragraphs")}}
  </div>

  <style>
    @scope {
      .floatybit {
        width: max-content;
        position: absolute;
        background: #222;
        color: white;
        font-weight: bold;
        padding: 5px;
        border-radius: 4px;
        font-size: 90%;
        filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.4));
        z-index: 10;
      }
      .arrow {
        position: absolute;
        background: #222;
        width: 8px;
        height: 8px;
        transform: rotate(45deg);
      }
      .hook {
        padding: 0.5rem;
        border: 1px solid;
        display: inline-block;
        color: black;
      }
      .scroll-content {
        max-height: 150px;
        overflow-y: auto;
        border: 1px solid;
        padding: 0.5rem;
      }
    }
  </style>
</template>
```

</div>

## Usage within a header

It's often common to provide popover-using UIs in site headers, such as a settings menu, or navigation.


<div class="featured-demo">

```gjs live preview
import { PortalTargets, Popover } from 'ember-primitives';
import { hash } from '@ember/helper';
import { loremIpsum } from 'lorem-ipsum';
import { cell } from 'ember-resources';
import { on } from '@ember/modifier';
import { focusTrap } from 'ember-focus-trap';

const settings = cell(false);

<template>
    <div class="site not-prose">
      <PortalTargets />

        <header>
            <span style="color: black">My App: click settings -&gt;</span>

            <Popover @offsetOptions={{8}} as |p|>
              <button class="hook" {{p.reference}} {{on 'click' settings.toggle}}>
                Settings
              </button>

              {{#if settings.current}}
                <p.Content @as="dialog" open class="floatybit">
                  <PortalTargets />
                  <ul>
                    <li>a</li>
                    <li>not so big list</li>
                    <li>of</li>
                    <li>
                      things<br>

                      <Popover @placement="left" @offsetOptions={{16}} as |pp|>
                        <button {{pp.reference}}>view profile</button>

                        <pp.Content class="floatybit">
                          View or edit your profile settings
                          <div class="arrow" {{pp.arrow}}></div>
                        </pp.Content>
                      </Popover>
                    </li>
                  </ul>
                  <div class="arrow" {{p.arrow}}></div>
                </p.Content>
              {{/if}}
            </Popover>

        </header>

        <div class="main">
          {{loremIpsum (hash count=2 units="paragraphs")}}
        </div>
    </div>

  <style>
    @scope {
      .floatybit {
        width: 200px;
        position: absolute;
        background: #222;
        color: white;
        font-weight: bold;
        padding: 1rem;
        border-radius: 4px;
        font-size: 90%;
        filter: drop-shadow(0 0 0.75rem rgba(0,0,0,0.4));
        z-index: 10;
        border: 1px solid rgba(0, 255, 255, 0.6);
      }
      .floatybit .floatybit {
        background: #eee;
        color: black;
      }
      .floatybit .floatybit .arrow {
        background: #eee;
        transform: translateY(-1rem) rotate(45deg);

        &::before {
          background: #eee;
          border-top: 1px solid rgba(0, 255, 255, 0.6);
          transform: rotate(45deg) translateY(7px) translateX(-8px);
          border-left: none;
        }
      }
      ul {
        padding-left: 1rem;
        margin: 0;
      }
      .arrow {
        position: absolute;
        background: #222;
        width: 8px;
        height: 8px;
        transform: rotate(45deg);
        border: 1px solid rgba(0, 255, 255, 0.6);

        &::before {
          content: '';
          position: absolute;
          display: block;
          width: 1rem;
          height: 1rem;
          background: #222;
          transform: rotate(45deg);
          border-left: 1px solid rgba(0, 255, 255, 0.6);

        }
      }
      .hook {
        padding: 0.5rem;
        border: 1px solid;
        display: inline-block;
        color: black;
      }
      header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        background: white;
        position: sticky;
        top: 0;
        width: 100%;
        padding: 0.25rem;
        filter: drop-shadow(0px 3px 6px #000000aa);
      }
      .main {
        padding: 0.5rem;
      }
      .site {
        max-height: 200px;
        overflow-y: auto;
        border: 1px solid;
      }
      * { box-sizing: border-box; }
    }
  </style>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/popover.gts" />
```


## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/popover" 
    @name="Signature" />
</template>
```

## Accessibility

The `Content` of a popover is focusable, so that keyboard (and screenreader) users can interact with the Popover content. Generally this is great for modals, but also extends to things like tooltips, so that folks can copy the content out.

Since a `Popover` isn't an explicit design pattern provided by W3, but instead, `Popover` is a low level primitive that could be used to build the W3 examples of
- [Modal Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/dialog/)
- [Date Picker Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/)
- [Date Picker Combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-datepicker/)
- [Select-Only Combobox](https://www.w3.org/WAI/ARIA/apg/patterns/combobox/examples/combobox-select-only/)
- [Menu Button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/)
- and more


---

# PortalTargets

The portal targets are stable element references that can be nested over and over again to get around layering issues. 

For example, given a z-index order like 
 - `1` popover
 - `2` tooltip
 - `3` modal

 What happens if you want to display a tooltip within a modal?
 Normally this would end up rendering behind the modal, but since `<PortalTargets />` can be used multiple times, anything that is targeting a particular portal will render in to the nearest portal target within the render tree, thus eliminating all layering issues.


The following example proposes a potential series of features that would benefit from this behavior.

```gjs 
import { on } from '@ember/modifier';
import { cell } from 'ember-resources';
import { PortalTargets, /* Portal, PORTALS */ } from 'ember-primitives';

const isOpen = cell(false);

<template>
  {{! Application-level targets }}
  <PortalTargets />

  <button {{on 'click' isOpen.toggle}}>
    Toggle
    <Tooltip> {{! uses <Portal @to={{PORTALS.tooltip}}> }}
      Toggles the modal state
    </Tooltip>
  </button>

  {{#if isOpen.current}}
    <Modal> {{! uses <Portal @to={{PORTALS.modal}}> }}
      <:body>
        <PortalTargets />

        <p>
          this is some text
          <Tooltip> {{! uses <Portal @to={{PORTALS.tooltip}}> }}
            This is still positioned above the Modal
            because the nearest portal targets are within the modal.
          </Tooltip>
        </p>
      </:body>
    </Modal>
  {{/if}}
</template>
 ```

## Install

```hbs live
<SetupInstructions @src="components/portal-targets.gts" />
```


## Anatomy

```js 
import { PortalTargets } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { PortalTargets } from 'ember-primitives/components/portal-targets';
```

```gjs 
import { PortalTargets } from 'ember-primitives/components/portal-targets';

<template>
  <PortalTargets />
</template>
```

## API Reference

### `PortalTargets`

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/portal-targets" 
    @name="Signature" 
  />
</template>
```

### `PortalTarget`

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/portal-targets" 
    @name="PortalTarget" 
  />
</template>
```


---

# Portal

A `<Portal>` allows teleporting elements to another place in the DOM tree. This can be used for components altering the layout of the page, or getting around z-index issues with modals, popovers, etc.

`<Portal>` must be combined with `<PortalTargets>`, or your own portal targets that match the requirements of portalling.  Additionally, a `<Portal>` will render in to the nearest `<PortalTargets>` it can find, allowing for UI layering, e.g.: Modals have their own `<PortalTargets>` so they can have their own tooltips and popovers.  _For use with popovers_, this portalling can be a way to support [popover](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover) on older browsers. But there are many other use cases outside of popovers as well.

<h2 visually-hidden>Usage</h2>

The following example demonstrates Portal-nesting:

<div class="featured-demo">

```gjs live preview
import { Portal } from 'ember-primitives/components/portal';

<template>
  <div class="style-wrapper">
    <div data-portal-target></div>

    <button>main content</button>

    <Portal @to="[data-portal-target]">
      <div class="popover">
        <div data-portal-target></div>
        first layer

        <Portal @to="[data-portal-target]">
          <div class="popover">
            <div data-portal-target></div>
            second layer 

            <Portal @to="[data-portal-target]">
              <div class="popover">
                tooltip / third layer
              </div>
            </Portal>
          </div>
        </Portal>

      </div>
    </Portal>
  </div>

  <style>
    .popover {
      position: absolute;
      width: max-content;
      border: 1px solid;
      padding: 1rem;
      color: black;
      margin: 1.25rem;
      background: #fefefe;
      filter: invert(1)  drop-shadow(0 0 0.75rem rgba(0,0,0,0.2));
      border-radius: 4px;
    }
    .button {
      padding: 0.5rem 1rem;
    }
    .style-wrapper { 
      height: 100px;
      position: relative;
    }
  </style>
</template>
```

</div>

<Callout>

When using a popover pattern, you'll want to use the native [`popover`](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API/Using#nested_popovers) capabilities of browsers. When coupled with [Floating UI](/5-floaty-bits/floating-ui.md), you also can correctly position a popover to a target element.

</Callout>

### Component-controlled Layout

Using Portals enables you to be able to have sub-components (or even sub-routes render into elements that live outside of their hierarchy. 

<div class="featured-demo">

```gjs live preview
import { Portal } from 'ember-primitives/components/portal';


<template>
  <div class="layout">
    <div class="header-container"></div>
    <div class="main-container"></div>
    <div class="sidebar-container"></div>
    <div class="footer-container"></div>
  </div>

  <ExampleA />
  <ExampleB />
  {{!-- or yield or outlet --}}

  <style>
    .header-container { grid-area: header; }
    .main-container { grid-area: main; }
    .sidebar-container { grid-area: sidebar; }
    .footer-container { grid-area: footer; }
    .layout {
      display: grid;
      grid-template-columns: 1fr 1fr 50px 1fr;
      grid-template-rows: auto;
      grid-template-areas: 
        "header header header header"
        "main main . sidebar"
        "footer footer footer footer";
      gap: 0.5rem;
    }


    .header-container, .main-container, .sidebar-container, .footer-container {
      border: 1px solid;
    }
  </style>
</template>

// Var used for hoisting, so that the 
// layout component can be seen first
var ExampleA = <template>
  <Portal @to=".main-container" @append={{true}}>
    ExampleA main
  </Portal>
  <Portal @to=".header-container" @append={{true}}>
    ExampleA header
  </Portal>
  <Portal @to=".sidebar-container" @append={{true}}>
    ExampleA sidebar
  </Portal>
</template>;


var ExampleB = <template>
  <Portal @to=".main-container" @append={{true}}>
    ExampleB main
  </Portal>
  <Portal @to=".sidebar-container" @append={{true}}>
    ExampleB sidebar
  </Portal>
  <Portal @to=".footer-container" @append={{true}}>
    ExampleB footer
  </Portal>
</template>;
```

</div>

### Why not just `in-element`?

The [`in-element`](https://api.emberjs.com/ember/6.5/classes/Ember.Templates.helpers/methods/in-element?anchor=in-element) utility is built in to Ember, but it requires the user handle how to find the element that is passed to in-element's first positional argument.

```hbs
{{#in-element (getElementSomehow)}}
    content portaled to the element
{{/in-element}}
```

This Portal and other implementatiosn solve this in various ways.
- [ember-wormhole](https://github.com/yapplabs/ember-wormhole/blob/master/addon/components/ember-wormhole.js) - created before `in-element` existed, so it contains _a lot_ of code (but can be used as a sortof polyfill for _very_ old ember versions).
- [ember-stargate](https://github.com/simonihmig/ember-stargate)  - builds on top of in-element and uses one of the techniques that this Portal component uses 

### `@to` can be an element

The element can be created dynamically (for rendering off screen), or any other element obtained by other means.

<div class="featured-demo">

```gjs live preview
import { Portal } from 'ember-primitives/components/portal';

let element = document.createElement('div');

<template>
  <fieldset class="border">
    <legend>origin</legend>
    <Portal @to={{element}}>
      content
    </Portal>
  </fieldset>

  element is wherever we want:
  {{element}}
</template>
```

</div>

### ember-wormhole

If you're migrating from [ember-wormhole](https://github.com/yapplabs/ember-wormhole/), a migration path using this Portal component would look like this:

<div class="featured-demo">

```gjs live preview no-shadow
import { Portal } from 'ember-primitives/components/portal';
import { InViewport } from 'ember-primitives/viewport';

<template>
  <InViewport>
    <fieldset class="border">
      <legend>origin</legend>
      Two portals:
      <Portal @wormhole="wormhole-target">
        content
      </Portal>

      <Portal @wormhole="#wormhole-target">
        extra content
      </Portal>
    </fieldset>

    element is wherever we want:
    <div id="wormhole-target"></div>
  </InViewport>
</template>
```

</div>

The wormhole compatibility mode assumes that an element will be rendered in to the DOM before the next cycle of the runloop. This approach is non-reactive, and doesn't handle portal nesting -- which is why it can't use the same `@to` argument as the other usages (the code that polyfills ember-wormhole is _very small_).

Additionally, ember-wormhole allows passing a plain `id` as in `#selector` without the leading `#`. But because we don't want to only support ids, any valid CSS selector is also allowed.

And an improvement upon ember-wormhole is that if the element can already be found in the DOM, the portaled contents will render right away, and not wait until the next render cycle.
Of note, the default behavior here is to append to the portal target instead of replace. The other usages do not default to this behavior.

A major limitation of the wormhole approach is that it does not work well within shadowdom nor does it work with off-canvas rendering (fragments not yet part of the DOM tree (which is a strategy that [repl-sdk](https://limber.glimdown.com/docs/repl-sdk) employs)).


### ember-stargate

ember-stargate uses reactive registration for the portal targets. 

Note that when doing this, you also get the nested portals functionality demo'd at the top of this page.

Here is what using ember-primitive's Portal in an ember-stargate style would look like:

<div class="featured-demo">

```gjs live preview no-shadow
import { Portal } from 'ember-primitives/components/portal';
import { PortalTarget } from 'ember-primitives/components/portal-targets';

<template>
  <fieldset class="border">
    <legend>origin</legend>

    <Portal @to="name-here">
      content
    </Portal>
  </fieldset>

  <PortalTarget @name="name-here" />
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/portal.gts" />
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/portal" 
    @name="Signature" 
  />
</template>
```

## Accessibility

When portalling content, we break away from how the browser natively handles focus, and returning focus to certain elements. This is important in Modals and other Popovers that can be made with portalling. During the rendering phase of portaled elements, you'll want to store the `activeElement` so that it can be restored when that portaled content is discarded. 


---

# Body class

A utility template-helper for updating the `class` on the `document.body`, based on the rendered content. When `{{bodyClass ".."}}` is unrendered, the specified body classes will also be removed.

## Example

```gjs
import { bodyClass } from 'ember-primitives/helpers/body-class';

// When rendered: "prevent-scrolling" is added to the body class
// When unrendered: "prevent-scrolling" is removed from the body class
<template>
  {{bodyClass "prevent-scrolling"}}
</template>
```

## Install

```hbs live
<SetupInstructions @src="helpers/body-class.ts" />
```

## API Reference


```gjs live no-shadow
import { HelperSignature } from 'kolay';

<template>
  <HelperSignature 
    @package="ember-primitives" 
    @module="declarations/helpers/body-class" 
    @name="Signature" />
</template>
```

## Reference

- originally from [set-body-class](https://github.com/ef4/ember-set-body-class)


---

# Sync Color Scheme 

With the introduction of [`prefers-color-scheme`][mdn-prefers-color-scheme], we can better serve our users preferences and help them feel comfortable in our applications.

However, when using prefers-color-scheme, there are some rough edges around the whole feature of color schemes in browsers:

- the scrollbars do not change unless the [`color-scheme`][mdn-color-scheme] variable is set on the root element
- default browsers styles do not change based on `prefers-color-scheme`, and instead are only reactive to `color-scheme`
- a user's `prefers-color-scheme` preference does not set `color-scheme` on the root element 
- it's not possible to, from CSS, query the value of `color-scheme`

So, we need to run some JavaScript to synchronize all this.

[mdn-prefers-color-scheme]: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme
[mdn-color-scheme]: https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme 


## App-based theme preference?

The `color-scheme` property can be used at any nesting level in the DOM, so if the user wishes to set their preferred color scheme to "dark mode" in your app, even though their browser reports that their `prefers-color-scheme` value is "light mode", dark mode is what we want to render. This is persisted across refreshes.

```gjs live preview 
import { 
  sync, colorScheme, prefers, getColorScheme, localPreference
} from 'ember-primitives/color-scheme';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';

// reads the user's browser preferences
sync();

function gatherSchemePreferences() {
  return {
    prefers: {
      dark: prefers.dark(),
      light: prefers.light(),
      synthWave: prefers.custom('synthwave'),
    },
    // colorScheme is a reactive value
    activeTheme: colorScheme.current,
    otherSources: {
      'documentElement': getColorScheme(), 
      localStorage: localPreference.read(),
    }
  };
}

<template>
  <button {{on 'click' (fn colorScheme.update 'dark')}}>Dark mode</button>
  <button {{on 'click' (fn colorScheme.update 'light')}}>Light mode</button>

  <pre>{{JSON.stringify (gatherSchemePreferences) null 4}}</pre>
</template>
```

Linking to external stylesheets can be made reactive by  reading from the `colorScheme` object:

Here is how this docs site swaps out the CSS theme for highlight.js, used for syntax highlighting:

```gjs 
import { colorScheme } from 'ember-primitives/color-scheme';

function isDark() {
  return colorScheme.current === 'dark';
}

<template>
  {{#if (isDark)}}
    <link 
      rel="stylesheet" 
      href="https://cdn.jsdelivr.net/npm/highlight.js@11.8.0/styles/atom-one-dark.css" />
  {{else}}
    <link 
      rel="stylesheet" 
      href="https://cdn.jsdelivr.net/npm/highlight.js@11.8.0/styles/atom-one-light.css" />
  {{/if}}
</template>
```

Sometimes, you may wish to control the CSS via a class property on `<body>` or similar element.

To do that, you need an effect:

```gjs 
import { colorScheme } from 'ember-primitives/color-scheme';

function syncBodyClass() {
  if (colorScheme.current === 'dark') {
    document.body.classList.remove('theme-light');
    document.body.classList.add('theme-dark');
  } else {
    document.body.classList.remove('theme-dark');
    document.body.classList.add('theme-light');
  }
}

<template>
  {{ (syncBodyClass) }}
</template>
```

## Install

```hbs live
<SetupInstructions @src="color-scheme.gts" />
```

## API Reference

```hbs live
<APIDocs @declaration="color-scheme" @name="colorScheme" />
<APIDocs @declaration="color-scheme" @name="sync" />
<APIDocs @declaration="color-scheme" @name="prefers" />
<APIDocs @declaration="color-scheme" @name="localPreference" />
<APIDocs @declaration="color-scheme" @name="getColorScheme" />
<APIDocs @declaration="color-scheme" @name="setColorScheme" />
<APIDocs @declaration="color-scheme" @name="removeColorScheme" />
```


---

# createAsyncService

This utility will create a service using a class definition similar to what is described in [RFC#502](https://github.com/emberjs/rfcs/pull/502) for explicit service injection -- no longer using strings. This allows module graphs to shake out services that aren't used until they are _needed_.

The difference with `createService` is that `createAsyncService` takes a _stable reference_ to a function that will eventually return a class definition. 

This can be useful for importing services that may themselves import many things, or large dependencies.

[gh-polaris-service]: https://github.com/chancancode/ember-polaris-service

<Callout>

  `createAsyncService` is not meant to be a replacement for `@service`, but a specific tool for asynchronously loading services when the hosting class has to be synchronously known.

  Another approach to dynamically loading services would be to dynamically load all components that use the serivce, such as via the [`load`](/6-utils/load.md) utility, and calling the service with etiher [`createService`](/6-utils/createService.md) or an [ember-polaris-service][gh-polaris-service].

  So `createAsyncService` shifts the resoponsibility of where the async state is being handled, but it should be handled (error, loading states), in either situation.



</Callout>


## Install

```hbs live
<SetupInstructions @src="service.ts" />
```


Introduced in [0.47.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.47.0-ember-primitives)


## Usage

`createAsyncService` doesn't expose the service _directly_, but instead it is wrapped in a reactive Promise State (in particular, the state returend by [getPromiseState](https://reactive.nullvoxpopuli.com/functions/get-promise-state.getPromiseState.html)).

When using async services, you have to be concerned with loading and error states, like this:

```gjs
import { createAsyncService } from 'ember-primitives/service';

class Demo extends Component {
  state = createAsyncService(this, /* ... */)

  <template>
    {{#if this.state.isLoading}}
    ... pending ...
    {{else if this.state.error}}
      oh no!
      {{this.state.error}}
    {{else if this.state.resolved}}

      Here is where you can access the full service
      And you'll have the same shared instance for the whole application -- a singleton.
      {{this.state.resolve.foo}}
    {{/if}}
  </template>
}


```

### In a component

```js
import { createAsyncService } from 'ember-primitives/service';

// This function is the key that all consumers should use to get the same instance
const getService = async () => {
  let module = await import('./service/from/somewhere');
  return module.MyState;
}

class Demo extends Component {
  state = createAsyncService(this, getService);
}
```

### With Arguments

```js
import { createAsyncService } from 'ember-primitives/service';

class MyState {
  constructor(/* .. */ ) { /* ... */ }
}

// in another file

// This function is the key that all consumers should use to get the same instance
const getService = async (/* args here */ ) => {
  let module = await import('./service/from/somewhere');
  return () => new module.MyState(/* args */ );
}

class Demo extends Component {
  state = createAsyncService(this, () => getService(/* ... */));
}
```


### Accessing Services and handling cleanup 

Like with [`link`][reactiveweb-link], use of services and `registerDestructor` is valid:
```js
import { service } from '@ember/service';
import { createAsyncService } from 'ember-primitives/service';

class MyState {
  @service router;

  constructor(/* .. */) { 
    registerDestructor(this, () => {
      // cleanup runs when Demo is torn down
    });  
  }
}

// in another file
class Demo extends Component {
  state = createAsyncService(this, getService);
}
```

However, note that the same restrictions as with `link` apply: services may not be accessed in the constructor.

And even that caveat can be undone if what you need is passed in to your service's constructor.

[reactiveweb-link]: https://reactive.nullvoxpopuli.com/functions/link.link.html 


## Testing

Testing with `createAsyncService` is a little different from  the `@service` from ember. In particular, `createAsyncService` takes the approach that the whole implementation is a black box, hidden, or private from the caller.
You can still unit test the service as you would any other class. But for consmers, mocking is not supported. If there is a state that needs to be tested, the component(s) owning the service should manipulate the service in to getting in to that state.

If the service is performing operations with the network, you'll want to mock the network -- with tools such as [MSW](https://mswjs.io/), or [WarpDrive](https://warp-drive.io/)'s holodeck.




---

# createService

This utility will create a _private_ service using a class definition similar to what is described in [RFC#502](https://github.com/emberjs/rfcs/pull/502) for explicit service injection -- no longer using strings. This allows module graphs to shake out services that aren't used until they are _needed_.

[rfc-502]: https://github.com/emberjs/rfcs/pull/502
[gh-polaris-service]: https://github.com/chancancode/ember-polaris-service
[createStore]: /6-utils/createStore.md

<Callout>

`createService` is not meant to be a replacement for `@service`, but more so a _very small_ utility (one line?) that gets you the spirit of [RFC#502][rfc-502], but without covering all the needed use cases for something that would be built in to the framework.

  For a more robust and whollistic approach to services using the class definition as a key, you may be interested in [ember-polaris-service][gh-polaris-service].
  You can do this yourself with:

  You can do this yourself with:
  ```js
  let serviceInstance = createStore(findOwner(this), ClassDefinition);
  ```

</Callout>


## Install

```hbs live
<SetupInstructions @src="service.ts" />
```


Introduced in [0.47.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.47.0-ember-primitives)


## Usage

```js
import { createService } from 'ember-primitives/service';

class MyState {
  // @services are allowed here
}

class Demo extends Component {
  // lazyily created upon access of `foo`
  get foo() {
    return createService(this, MyState);
  }

  // or eagrly created when `Demo` is created
  state = createService(this, MyState);
}
```

### With Arguments

```js
import { createService } from 'ember-primitives/service';

class MyState {
  constructor(/* .. */ ) { /* ... */ }
}

class Demo extends Component {
  get state() {
    return createService(this, () => new MyState(1, 2));
  }
}
```

### Accessing Services and handling cleanup 

Like with [`link`][reactiveweb-link], use of services and `registerDestructor` is valid:
```js
import { service } from '@ember/service';
import { createService } from 'ember-primitives/service';

class MyState {
  @service router;

  constructor(/* .. */) { 
    registerDestructor(this, () => {
      // cleanup runs when Demo is torn down
    });  
  }
}

class Demo extends Component {
  // or 
  get foo() {
    return createService(this, () => new MyState(/* ... */));
  }
}
```

However, note that the same restrictions as with `link` apply: services may not be accessed in the constructor.

And even that caveat can be undone if what you need is passed in to your service's constructor.

[reactiveweb-link]: https://reactive.nullvoxpopuli.com/functions/link.link.html 


## Testing

Testing with `createService` is a little different from  the `@service` from ember. In particular, `createService` takes the approach that the whole implementation is a black box, hidden, or private from the caller.
You can still unit test the service as you would any other class. But for consmers, mocking is not supported. If there is a state that needs to be tested, the component(s) owning the service should manipulate the service in to getting in to that state.

If the service is performing operations with the network, you'll want to mock the network -- with tools such as [MSW](https://mswjs.io/), or [WarpDrive](https://warp-drive.io/)'s holodeck.




---

# createStore

This utility will create a stable instance or singleton hosted on any context (component, application, etc). The lifetime of the store is determined by the parent's object's lifetime. Can be used to create private services that don't live in the registry, or any lazily created state private to certain components. Can also be combined wih DOM hierachy crawling to create DOM-based context/contextual state. 

Ownership and destroyable linkage is handled (via [`link`][reactiveweb-link]).

[reactiveweb-link]: https://reactive.nullvoxpopuli.com/functions/link.link.html 

<Callout>

When using `createStore` with the `owner` as the key, you effectively have lazyily included services, as per [RFC #502, "Explicit Service Injection"](https://github.com/emberjs/rfcs/pull/502)

</Callout>


## Install

```hbs live
<SetupInstructions @src="store.ts" />
```


Introduced in [0.38.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.38.0-ember-primitives)

## Usage

 In this example, `MyState` is created once per instance of the component.
 repeat accesses to `this.foo` return a stable reference _as if_ `@cached` were used.

```js
import { createStore } from 'ember-primitives/store';

class MyState {}

class Demo extends Component {
  // lazyily created upon access of `foo`
  get foo() {
    return createStore(this, MyState);
  }

  // or eagrly created when `Demo` is created
  foo = createStore(this, MyState);
}
```

### Usage in a component

```js
import { createStore } from 'ember-primitives/store';

class MyState {}

class Demo extends Component {
  // this is a stable reference
  get foo() {
    return createStore(this, MyState);
  }
}
```

Functions may also be passed. Note however that each function passed is its own key, so `() => new MyClass()` and another `() => new MyClass()` elsewhere are separate arrow functions, and it's not possible to treat them as the same.

### With Arguments

```js
import { createStore } from 'ember-primitives/store';

class MyState {
  constructor(/* .. */ ) { /* ... */ }
}

class Demo extends Component {
  // or 
  get foo() {
    return createStore(this, () => new MyState(1, 2));
  }
}
```

### Accessing Services and handling cleanup 

Like with [`link`][reactiveweb-link], use of services and `registerDestructor` is valid:
```js
import { createStore } from 'ember-primitives/store';
import { service } from '@ember/service';

class MyState {
  @service router;

  constructor(/* .. */) { 
    registerDestructor(this, () => {
      // cleanup runs when Demo is torn down
    });  
  }
}

class Demo extends Component {
  // or 
  get foo() {
    return createStore(this, () => new MyState(/* ... */));
  }
}
```

However, note that the same restrictions as with `link` apply: services may not be accessed in the constructor.

### Application Singletons

Up until this point, the above examples have all been tied to the lifetime of _the component instance_ -- in that multiple instances of `Demo` would still have different instances of `MyState`.

To address this, the application instance needs to be passed instead of `this`.

For example:

```js
import { createStore } from 'ember-primitives/store';
import { getOwner } from '@ember/owner';

class MyState {}

class Demo extends Component {
  // lazyily created upon access of `foo`.
  // will be the same instance everywhere in the application 
  // 
  get foo() {
    return createStore(getOwner(this), MyState);
  }
}
```

It may be helpful to make a little utility to wrap up the now boilerplate:
```js
import { createStore } from 'ember-primitives/store';
import { getOwner } from '@ember/owner';

class MyState {}

function createState(context) {
  let owner = getOwner(this);
  return createStore(owner, MyState);
}

class Demo extends Component {
  // lazyily created upon access of `foo`.
  // will be the same instance everywhere in the application 
  get foo() {
    return createState(this);
  }
}
```

In this way, you may create private services (useful for libraries) without exposing your service to public usage via the `@service` decorator or registry means -- additionally helpful when consuming ember apps forgo the automatic file-based structure and registrations.


---

{
  "title": "Data from Form Events 📦"
}


---

# Data from `<form>`

A utility function for extracting the FormData as an object from the native `<form>` 
element, allowing more ergonomic of usage of _The Platform_'s default form/fields usage.

Each input within your `<form>` should have a `name` attribute.
(or else the `<form>` element doesn't know what inputs are relevant)

This will provide values for all types of controls/fields,
- input: text, checkbox, radio, etc
- select
  - behavior is fixed from browser default behavior, where
    only the most recently selected value comes through in
    the FormData. This fix only affects `<select multiple>`

## Example

Try filling out some data in the form below, and click submit.

<div class="featured-demo">

```gjs live preview no-shadow 
import { cell } from 'ember-resources';
import { dataFromEvent } from 'ember-primitives/components/form';

const dataPreview = cell({});

function handleSubmit(event) {
  event.preventDefault();
  dataPreview.set(dataFromEvent(event));
}

<template>
  <div id="formData-demo">
    <form onsubmit={{handleSubmit}}>
      <label>
        First Name
        <input type="text" name="firstName" value="NVP" />
      </label>
      <label> 
        Are you a human?
        <input type="checkbox" name="isHuman" value="nah" />
      </label>
      <fieldset>
        <legend>Favorite Race</legend>
        <label>Zerg<input type="radio" name="bestRace" value="zerg" checked /></label>
        <label>Protoss<input type="radio" name="bestRace" value="protoss" /></label>
        <label>Terran<input type="radio" name="bestRace" value="terran" /></label>
      </fieldset>

      <label>
        Worst Race
        <select multiple name="worstRace">
          <option value="zerg" disabled>Zerg</option>
          <option value="protoss" selected>Protoss</option>
          <option value="terran" selected>Terran</option>
        </select>
      </label>
      <button type="submit">Submit</button>
    </form>

    <pre>{{JSON.stringify dataPreview.current null 3}}</pre>
  </div>

  <style>
    #formData-demo {
      display: flex;
      gap: 1rem;
      justify-content: space-between;
      align-items: start;

      form {
        display: flex;
        flex-direction: column;
        gap: 0.5rem;

        button {
          border: 1px solid;
        }
        input {
          color: black;
        }
      }

      pre {
        margin: 0;
      }
    }
    .featured-demo .glimdown-render {
      max-height: unset;
    }
  </style>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @name="form-data-utils" />
```


otherwise, this is included with ember-primitives when using the `<Form />` component.

## Anatomy

These are aliases of each other
```js
import { dataFrom } from 'form-data-utils';
```
and 
```js
import { dataFromEvent } from 'ember-primitives/components/form';
```

`form-data-utils` was extracted from `ember-primitives` -- the re-export is kept for convenience.


## API Reference

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs @package="ember-primitives" @module="declarations/components/form" @name="dataFromEvent" />
</template>
```


---

# DOM Context

DOM Context provides a way to share data between components through the DOM hierarchy using `<Provide>` and `<Consume>` components. This enables React-style context patterns in Ember applications with full fine-grained reactivity support, built on top of public APIs for maximum stability.

Unlike event-based context systems, DOM Context follows the DOM tree synchronously, allowing for proper fine-grained reactivity where consumers automatically update when the provided data changes.

## Install

```hbs live
<SetupInstructions @src="dom-context.gts" />
```


Introduced in [0.40.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.40.0-ember-primitives)

## Usage

A `<Consume>` component must be a child of `<Provide>`.

`<Provide>` will instatiate and link up the class with an owner and destroyable linkage so that the class may use services and have proper lifetime.
`<Consume>`'s `@key` should match the constructing object passed to `<Provide>` for free type inference (for typescript users). The `@key` can be a string, but then you have to type the yielded content another way.

<div class="featured-demo">

```gjs live preview
import { Provide, Consume } from 'ember-primitives/dom-context';

class State {
  greeting = 'hello';
  name = 'world';
}

<template>
  <Provide @data={{State}}>
    <Consume @key={{State}} as |context|>
      <p>{{context.data.greeting}}, {{context.data.name}}!</p>
    </Consume>
  </Provide>
</template>
```

</div>


### Reactive Data with Classes

DOM Context correctly interops with reactive data:

<div class="featured-demo">

```gjs live preview
import { Provide, Consume } from 'ember-primitives/dom-context';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

class Counter {
  @tracked count = 0;
  
  increment = () => {
    this.count++;
  }
  
  decrement = () => {
    this.count--;
  }
}

<template>
  <Provide @data={{Counter}}>  
    <Consume @key={{Counter}} as |context|>
      <div class="counter">
        <button {{on "click" context.data.decrement}} class="btn">-</button>
        <span class="count">{{context.data.count}}</span>
        <button {{on "click" context.data.increment}} class="btn">+</button>
      </div>
    </Consume>
    
    <p>Multiple consumers see the same state:</p>
    <Consume @key={{Counter}} as |context|>
      Count is: {{context.data.count}}
    </Consume>
  </Provide>

  <style>
    .counter {
      display: flex;
      align-items: center;
      gap: 1rem;
      margin: 1rem 0;
    }
    .btn {
      background: #3b82f6;
      color: white;
      border: none;
      padding: 0.5rem 1rem;
      border-radius: 4px;
      cursor: pointer;
      font-size: 1.2rem;
      min-width: 2.5rem;
    }
    .btn:hover {
      background: #2563eb;
    }
    .count {
      font-size: 1.5rem;
      font-weight: bold;
      min-width: 3rem;
      text-align: center;
    }
  </style>
</template>
```

</div>

### Multiple Independent Providers

Different providers with the same key remain completely independent:

```gjs live preview
import { Provide, Consume } from 'ember-primitives/dom-context';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

class Incrementer {
  @tracked count = 2;
  doit = () => this.count++;
}

class Doubler {
  @tracked count = 2;
  doit = () => this.count *= 2;
}

const StoreConsumer = <template>
  <Consume @key="store" as |context|>
    {{yield context.data}}
  </Consume>
</template>;

<template>
  <div class="demo">
    <div class="provider-group">
      <h4>Incrementer Store</h4>
      <Provide @data={{Incrementer}} @key="store">
        <div class="store-container">
          <StoreConsumer as |store|>
            <div class="store-display">
              Count: {{store.count}}
              <button {{on "click" store.doit}} class="btn">Increment</button>
            </div>
          </StoreConsumer>
        </div>
      </Provide>
    </div>

    <div class="provider-group">
      <h4>Doubler Store</h4>
      <Provide @data={{Doubler}} @key="store">
        <div class="store-container">
          <StoreConsumer as |store|>
            <div class="store-display">
              Count: {{store.count}}
              <button {{on "click" store.doit}} class="btn">Double</button>
            </div>
          </StoreConsumer>
        </div>
      </Provide>
    </div>
  </div>

  <style>
    .demo {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
      padding: 1rem;
    }
    .provider-group h4 {
      margin-top: 0;
      color: #374151;
    }
    .store-container {
      padding: 1rem;
      border: 2px solid #10b981;
      border-radius: 8px;
      background: #ecfdf5;
    }
    .store-display {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
    }
    .btn {
      background: #10b981;
      color: white;
      border: none;
      padding: 0.5rem 1rem;
      border-radius: 4px;
      cursor: pointer;
    }
    .btn:hover {
      background: #059669;
    }
  </style>
</template>
```

### Functions as Data

You can also provide functions that return data. Functions are called once and cached:

```gjs live preview
import { Provide, Consume } from 'ember-primitives/dom-context';
import { registerDestructor } from '@ember/destroyable';
import { tracked } from '@glimmer/tracking';

function createTimer() {
  return new class Timer {
    @tracked seconds = 0;
    
    constructor() {
      let interval = setInterval(() => {
        this.seconds++;
      }, 1000);

      registerDestructor(this, () => clearInterval(interval));
    }
  }();
}

<template>
  <div class="demo">
    <Provide @data={{createTimer}}>
      <div class="container">
        <h3>Timer Example</h3>
        
        <Consume @key={{createTimer}} as |context|>
          <div class="timer">
            Seconds elapsed: {{context.data.seconds}}
          </div>
        </Consume>
        
        <Consume @key={{createTimer}} as |context|>
          <div class="timer-alt">
            Also showing: {{context.data.seconds}}s
          </div>
        </Consume>
      </div>
    </Provide>
  </div>

  <style>
    .demo { padding: 1rem; }
    .container {
      padding: 1.5rem;
      border: 2px solid #ef4444;
      border-radius: 8px;
      background: #fef2f2;
    }
    .container h3 {
      margin-top: 0;
      color: #dc2626;
    }
    .timer, .timer-alt {
      padding: 0.5rem;
      margin: 0.5rem 0;
      border-radius: 4px;
      font-weight: 500;
    }
    .timer {
      background: #fecaca;
    }
    .timer-alt {
      background: #fed7d7;
    }
  </style>
</template>
```

## API Reference

### `<Provide />`

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/dom-context" 
    @name="Provide" />
</template>
```

### `<Consume />`

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/dom-context" 
    @name="Consume" />
</template>
```


---

{
  "title": "Heading Level 📦"
}


---

# Heading Level 📦

Used by the [`<Heading>`](/3-ui/heading.md) component, and available for use in all frameworks (React, Svelte, Ember, etc). `getSectionHeadingLevel` is the primary function exported by this utility library and correctly determines which of the `<h1>` through `<h6>` [Section Heading][mdn-h] elements to use, where the **level** is determined _automatically_ based on how the DOM has rendered.

This enables distributed teams to correctly produce appropriate section heading levels without knowledge of where their work will be rendered in the overall document -- and extra helpful for design systems teams where is _is not possible_ to know the appropriate heading level ahead of time.

[mdn-h]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/Heading_Elements

## Install

```hbs live
<SetupInstructions @name="which-heading-do-i-need" />
```


## Usage

In your app, you can use any of `<section>`, `<article>`, and `<aside>` elements to denote when the [_Section Heading_][mdn-h] element should change its level.

In this example, we dynamically create a TextNode and Element, where, since the TextNode is rendered first, the Element can traverse from the TextNode up the existing DOM to determine which h-level to use -- all in a single render pass.


```gjs live
import { REPL } from 'limber-ui';

const code = `import Component from "@glimmer/component";
import { element } from "ember-element-helper";
import { getSectionHeadingLevel } from "which-heading-do-i-need";

class Heading extends Component {
  markerNode = document.createTextNode("");

  get hLevel() {
    return \`h\${getSectionHeadingLevel(this.markerNode)}\`;
  }

  <template>
    {{this.markerNode}}

    {{#let (element this.hLevel) as |El|}}
      <El ...attributes>
        {{yield}}
      </El>
    {{/let}}
  </template>
}

// Usage / output
<template>
  <Heading>hello there</Heading>

  <section>
    <Heading>hello there</Heading>
  </section>

  <style>
    /* These styles are completely optional */
    h1, h2, h3, h4, h5, h6 { margin: 0; }

    h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
      position: absolute;
      margin-left: -1.2em;
      font-size: 0.7em;
      text-align: right;
      opacity: 0.8;
    }

    h1 { font-size: 2.5rem; }
    h2 { font-size: 2.25rem; }
    h3 { font-size: 2rem; }
    h4 { font-size: 1.5rem; }
    h5 { font-size: 1.25rem; }
    h6 { font-size: 1rem; }
    a { color: white; }

    h1::before { content: 'h1'; }
    h2::before { content: 'h2'; }
    h3::before { content: 'h3'; }
    h4::before { content: 'h4'; }
    h5::before { content: 'h5'; }
    h6::before { content: 'h6'; }

    article, section, aside, nav, main, footer {
      border: 1px dotted;
      padding: 0.25rem 1.5rem;
      padding-left: 2rem;
      padding-right: 0.5rem;
      position: relative;

      &::before {
        position: absolute;
        right: 0.5rem;
        top: -1rem;
        font-size: 0.7rem;
        text-align: right;
        opacity: 0.8;
      }
    }

    section, article {
      display: flex;
      flex-direction: column;
      gap: 0.75rem;
    }

    article::before { content: '<article>'; }
    section::before { content: '<section>'; }
    aside::before { content: '<aside>'; }
    nav::before { content: '<nav>'; }
    main::before { content: '<main>'; }
    footer::before { content: '<footer>'; }

    main {
      display: grid;
      gap: 2.5rem;
      grid-template-columns: max-content 1fr;
      grid-template-areas:
        "heading heading"
        "nav content"
        "nav content"
        "nav content";

    }

    >:first-child { grid-area: heading; }
    nav { grid-area: nav; }
    article { grid-area: content; }

  </style>
</template>  
`;

<template>
  <REPL 
    @code={{code}} 
    @format="gjs"
    @lines={{14}} 
    @editorLoad="force"
    @editor="65v"
    style="width:100%; height: 500px; border-radius: 0.5rem;" 
  />
</template>
```

<br>
<details><summary>using Ember</summary>

This version:
  - caller can pass attributes to the generated heading
  - only element generated is the heading
  - synchronous, so there are no extra renders

```gjs
import Component from "@glimmer/component";
import { getSectionHeadingLevel } from "which-heading-do-i-need";

class Heading extends Component {
  markerNode = document.createTextNode("");

  get hLevel() {
    return `h${getSectionHeadingLevel(this.markerNode)}`;
  }

  <template>
    {{this.markerNode}}
    {{#let (element this.hLevel) as |El|}}
      <El ...attributes>{{yield}}</El>
    {{/let}}
  </template>
}
```

</details>
<details><summary>using Svelte</summary>

  downside to this approach is that it requires two renders.
  first time is a span, second the span is replaced with the heading.

  See [Feature: Bind to text nodes with `svelte:text`](https://github.com/sveltejs/svelte/issues/7424) on svelte's GitHub.

  [See it in the Svelte Playground here](https://svelte.dev/playground/45e9e53c9b5b4d0bbaaf7e71acde7893?version=5.45.2)

```svelte
<script>
	import { getSectionHeadingLevel } from "which-heading-do-i-need";

	let { children } = $props();

	let ref = $state();
	const hLevel = $derived(ref ? `h${getSectionHeadingLevel(ref)}` : 'span');
</script>

<svelte:element this={hLevel} bind:this={ref}>
	{@render children?.()}
</svelte:element>
```

</details>
<details><summary>using Vue</summary>

This version:
  - caller can pass attributes, props, etc to the generated heading
  - only element generated is the heading

But:
  - this implementation needs two render passes -- once to get the reference in the DOM, and another to set the h-level

[See it in the Vue Playground here](https://play.vuejs.org/#eNp9Vdtu2kAQ/ZWRWxUjgS01faKAlEaJ0ja9KEHqQ10JFw/Yqb1r7a6BCPHvnb0ZO6V5gd3ZuZw5ZwYOwWVdR9sGg0kwlStR1AokqqaeJ6yoai4U3GKaFWwDa8ErGESxu+ugwfuEJWwa20AKoYvCqi5ThXQDMBYXMF8UqsSpj7feEleq4ExfVOv40PxW//qSQ9cb4ORvzWAKQJhfDHuRqpPaXsngYcKrDNdpU6r5JeMqRwHnsnWaMtGd9IQj7uDq9uEz3uEWS3jrmexG92N7hYJRYBUYV2kdPUrOSKODYdU9yCSYgLFo2y4vVvk4t6nHGR8XY4aYaZ8kyJWq5SSOUVaRzOP/+upkx4QdqbiSK87WxeZZ6RWv6qJE8a3WsPsQ0rLku0/GpkSDI29f5bj6c8b+KPcW3neBEsUWk6B9U6nYoLLP1w9fcU/n9rHiWVOS9wuP9yh52WiM1u1DwzKC3fEzaD8aJomFhbzeK2TSN6WBGjaMfxLQuF+90PoJ7kX0rsNiZ1s6K1ambDOjIMpxWrQDUAY3fS7MTs7R7t4LEtMetklongtmsHKGTI2gkbhwY3WP6xHciHRTmZd8BFrORmE28lUG7V6T/FKBwPVnfIIZDBTRzHiGA/2Ie1POLc/zoqHhhaUVTmDgehkQkdpqfl4mENaC13JEgFOlBB1kyRV9YVUoOA5hNvfkWhz0SfIoAmIcI1c5HNqs3k0DpC7Jrd/2dEHo56Htpo2JY/iRIyOWBdCqgm7RpIBCQsUbKknM6BfNk2kNdkVZEisCWUZRu0LlkILTBFS66aIpjX6zluUwNI05kNE2LRuEN29gmb8+nBc/7PkOj8sWuyAeBfMkga3lhhvAsutvlmJ/c1S2d025PdPQ2onX37ZDUurgcnulnHI+zRnJHLafvkIenobuoEdq4ufqOHTVnBxXnGWFJoG28wl2XpylQbDUstS8brSoJIzTwKM3CW642KUiA4qH28WXu2egU5YZvBIIPSdpKaOmuMeiliQPXdMHiKLIJaGTy0Mn23bLg25EZ/jlt3+o//86f47B8S9Tj4Ii)

```vue
<script lang="ts">
import { getSectionHeadingLevel } from "which-heading-do-i-need";
import { defineComponent, useTemplateRef, Fragment, h, computed, } from 'vue';

const refKey = 'textnode'

export default defineComponent({
  name: 'Heading',

  setup: (props, { attrs, slots, emit }) => {
    const content = slots.default()

    const nodeRef = useTemplateRef<Text>(refKey)

    // Whenever the text node is mounted, the component will rerender with a heading tag
    const level = computed(() => nodeRef.value && `h${getSectionHeadingLevel(nodeRef.value)}`)

    return {
      level,
      props,
      attrs,
      content,
      emit
    }
  },

  render: ({ level, attrs, props, content, emit }) => {
    return [
      h(Fragment, { ref: refKey }),

      // Conditionally whenever `level` is populated, render it
      // Forward all HTML attrs, props, and emits onto this node
      level && h(level, { ...attrs, ...props, ...emit }, content),
    ]
  }
})  
</script>
```

</details>

## API Reference

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs @package="which-heading-do-i-need" @module="getSectionHeadingLevel" @name="getSectionHeadingLevel" />
</template>
```


---

# Iframe

Utilities for working with IFrames.


```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs @package="ember-primitives" @module="declarations/iframe" @name="inIframe" />
</template>
```

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs @package="ember-primitives" @module="declarations/iframe" @name="notInIframe" />
</template>
```


## Install

```hbs live
<SetupInstructions @src="iframe.ts" />
```



---

# In Head

Utility component to place elements in the document `<head>`

The component will appropriate clean up the `<head>` when unrendered.

## Example

```gjs live preview no-shadow
import { InHead } from 'ember-primitives/head';
import { cell } from 'ember-resources';

// This changes the styles of the whole site
const useBootstrap = cell(false);

<template>
  <button 
    onclick={{useBootstrap.toggle}}
    type="button" class="btn btn-primary">Toggle Bootstrap</button>
  &nbsp;
  <button 
    onclick={{useBootstrap.toggle}}
    type="button" class="btn btn-success">Toogle Bootstrap</button>

  {{#if useBootstrap.current}}
    <InHead>
      <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"></script>
      <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css">
    </InHead>
  {{/if}}
</template>
```



## Install

```hbs live
<SetupInstructions @src="head.gts" />
```



---

# InViewport

A component that defers rendering its content until the element is visible or near the viewport.

This is useful for optimizing performance by not rendering expensive components until they're actually needed. Each demo on this site renders off-canvas, which can sometimes cause problems if the DOM is queried during render.

## Install

```hbs live
<SetupInstructions @src="viewport/in-viewport.gts" />
```


## Usage

You'll need to inspect-element to see that the component is not rendered when scrolled off screen.

<div class="featured-demo">

```gjs live preview
import { InViewport } from 'ember-primitives/viewport';

<template>
  <div style="height: 160px; overflow: auto; border: 1px solid gray; padding: 1rem;">
    <div style="height: 800px;" tabindex="0">
      <div style="display:grid; gap:4rem;">
        <span>Scroll down</span>
        <span>Scroll down</span>
        <span>Scroll down</span>
      </div>
      
      <InViewport @mode="contain">
        <div style="background: black; padding: 2rem;">
          This content is rendered only when near the viewport!
        </div>
      </InViewport>

      <div style="height: 400px;"></div>
    </div>
  </div>
</template>
```

</div>

## Modes

### Contain Mode (Default)

In `contain` mode, the placeholder element wraps the yielded content once rendered. The element remains in the DOM structure.

<div class="featured-demo">

```gjs live preview
import { InViewport } from 'ember-primitives/viewport';

<template>
  <div style="height: 170px; overflow: auto; border: 1px solid gray; padding: 1rem;" tabindex="0">
    <div style="height: 600px;">
      <div style="display:grid; gap:4rem;">
        <span>Scroll down</span>
        <span>Scroll down</span>
        <span>Scroll down</span>
      </div>

      <InViewport @mode="contain">
        <div style="background: black; padding: 1rem;">
          Content in contain mode
        </div>
      </InViewport>
    </div>
  </div>
</template>
```

</div>

### Replace Mode

In `replace` mode, the placeholder element is replaced entirely by the yielded content once rendered.

<div class="featured-demo">

```gjs live preview
import { InViewport } from 'ember-primitives/viewport';

<template>
  <div style="height: 170px; overflow: auto; border: 1px solid gray; padding: 1rem;">
    <div style="height: 600px;" tabindex="0">
      <div style="display:grid; gap:4rem;">
        <span>Scroll down</span>
        <span>Scroll down</span>
        <span>Scroll down</span>
      </div>

      <InViewport @mode="replace">
        <div style="background: black; padding: 1rem;">
          This replaces the placeholder
        </div>
      </InViewport>
    </div>
  </div>
</template>
```

</div>

## Custom Tag Name

You can specify a custom tag name for the placeholder element:

```gjs live preview
import { InViewport } from 'ember-primitives/viewport';

<template>
  <InViewport @tagName="section">
    <div style="background: lightblue; padding: 1rem;">
      Content wrapped in a section element
    </div>
  </InViewport>
</template>
```

## How It Works

The `InViewport` component uses the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to detect when an element is near the viewport.

1. A placeholder element is rendered initially
2. An IntersectionObserver watches the element
3. When the element enters the viewport (or the configured rootMargin area), the content is rendered
4. The observer is destroyed - this is a one-time optimization
5. The content remains rendered even if scrolled out of view

This approach is ideal for:
- Pages with many heavy components
- Off-canvas renders that may query the DOM
- Implementing "virtual scrolling" patterns
- Progressive enhancement strategies

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/viewport" 
    @name="InViewportSignature" />
</template>
```


---

# Load

A utility for creating a component that handles the loading of a promise or function.
Can be use for manual, import-based, bundle splittiing (or any other async activity.


This is a very tiny wrapper around reactiveweb's [`getPromiseState`](https://reactive.nullvoxpopuli.com/functions/get-promise-state.getPromiseState.html)

## Install

```hbs live
<SetupInstructions @src="load.gts" />
```


## Usage

```gjs
import { load } from 'ember-primitives/load';

const Loader = load(() => import('./routes/sub-route.gts'));

<template>
   <Loader>
     <:loading> ... loading ... </:loading>
     <:error as |error|> ... error! {{error.reason}} </:error>
     <:success as |component|> <component /> </:success>
   </Loader>
</template>
```




---

# on-resize

Utility for efficiently interacting with a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) on any element.
No matter how many times `{{onResize}}` is used within your application, only one `ResizeObserver` will exist.

This utility also handles the (uncachable) ["ResizeObserver loop limit exceeded"](https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded) error that can happen when resize event happens too quickly for the _browser_ to handle.


## Install

```hbs live
<SetupInstructions @src="on-resize.ts" />
```


Introduced in [0.32.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.32.0-ember-primitives)

## Usage

<div class="featured-demo">

```gjs live preview
import { onResize } from 'ember-primitives/on-resize';
import { cell } from 'ember-resources';

const inner = cell();

function handleResize(entry) {
  const { width, height } = entry.contentRect;

  inner.current = `${width} x ${height}`;
}

<template>
  Inner Dimensions: {{inner.current}}<br>

  <div class="resizable" {{onResize handleResize}}>
    Resize me
  </div>
  
  <style>
    .resizable {
      border: 2px black dashed;
      resize: both;
      overflow: auto;
      padding: 0.5rem;
    }
  </style>
</template>
```

</div>

## API Reference

```gjs live no-shadow
import { ModifierSignature } from 'kolay';

<template>
  <ModifierSignature 
    @package="ember-primitives" 
    @module="declarations/on-resize" 
    @name="Signature" />
</template>
```


## Reference

- originally from [ember-on-resize-modifier](https://github.com/PrecisionNutrition/ember-resize-kitchen-sink/tree/main/packages/ember-on-resize-modifier)


---

# Query Params 

Utilities for accessing query-params without the need for creating a class-component, or injecting the [router service][router-service].

[router-service]: https://api.emberjs.com/ember/release/classes/routerservice/

## Install

```hbs live
<SetupInstructions @src="qp.ts" />
```


## API Reference

There are a few exports from `ember-primitives/qp`

### `{{qp 'qp-name'}}`

Grabs a query-param off the current route from the router service.

```gjs
import { qp } from 'ember-primitives/qp';

<template>
 {{qp "query-param"}}
</template>
```

```gjs live no-shadow
import { HelperSignature } from 'kolay';

<template>
  <HelperSignature 
    @package="ember-primitives" 
    @module="declarations/qp" 
    @name="qp" />
</template>
```

### `{{withQP 'qp-name' value}}`

Returns a string for use as an `href` on `<a>` tags, updated with the passed query param

```gjs
import { withQP } from 'ember-primitives/qp';

<template>
  <a href={{withQP "foo" "2"}}>
    ...
  </a>
</template>
```

```gjs live no-shadow
import { HelperSignature } from 'kolay';

<template>
  <HelperSignature 
    @package="ember-primitives" 
    @module="declarations/qp" 
    @name="withQP" />
</template>
```

### `castToBoolean`

Cast a query-param string value to a boolean.

The following values are considered "false"
- `undefined`
- `""`
- `"0"`
- `"false"`
- `"f"`
- `"off"`
- `"no"`
- `"null"`
- `"undefined"`

All other values are considered truthy

```gjs live no-shadow
import { HelperSignature } from 'kolay';

<template>
  <HelperSignature 
    @package="ember-primitives" 
    @module="declarations/qp" 
    @name="castToBoolean" />
</template>
```



---

# ResizeObserver

Utility for managing a singleton [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) for efficient CPU and Memory usage.

Using one `ResizeObserver` (instead of multiple) results in dramatically better performance of your application. [See discussion here](https://github.com/WICG/resize-observer/issues/59#issuecomment-408098151), and direct link to a discussion within the [Chromium Forums](https://groups.google.com/a/chromium.org/g/blink-dev/c/z6ienONUb5A/m/F5-VcUZtBAAJ).


This utility also handles the (uncatchable) ["ResizeObserver loop limit exceeded"](https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded) error that can happen when resize event happens too quickly for the _browser_ to handle.

## Install

```hbs live
<SetupInstructions @src="resize-observer.ts" />
```


Introduced in [0.42.0](https://github.com/universal-ember/ember-primitives/releases/tag/v0.42.0-ember-primitives)

## Usage


<div class="featured-demo">

```gjs live preview
import { resizeObserver } from 'ember-primitives/resize-observer';

import Component from '@glimmer/component';
import { registerDestructor } from '@ember/destroyable';

export default class Demo extends Component {
  #resizeObserver = resizeObserver(this);
  element = document.createElement('div');

  constructor(owner, args) {
    super(owner, args);

    this.#resizeObserver.observe(this.element, this.handleResize);
    registerDestructor(this, () => {
      this.#resizeObserver.unobserve(this.element, this.handleResize);
    });
  }

  handleResize = (entry) => {
    const { width, height } = entry.contentRect;
    this.element.textContent = `( ${width} x ${height} )`;
  }

  
  <template>
    {{this.element}}

    <style>
      @scope {
        div {
          border: 2px black dashed;
          resize: both;
          overflow: auto;
          padding: 0.5rem;
        }
      }
    </style>
  </template>
}
```

</div>



## API Reference

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs 
    @package="ember-primitives" 
    @module="declarations/resize-observer" 
    @name="resizeObserver" />
</template>
```


## Reference

- originally from [ember-resize-observer-service](https://github.com/PrecisionNutrition/ember-resize-kitchen-sink/tree/main/packages/ember-resize-observer-service)


---

# Scroller

```gjs live no-shadow
import { CommentQuery } from 'kolay';

<template>
  <CommentQuery @package="ember-primitives" @module="declarations/components/scroller" @name="Scroller" />
</template>
```


<div class="featured-demo">

```gjs live preview no-shadow
import { Scroller } from 'ember-primitives';
import { on } from '@ember/modifier';
import { hash, fn } from '@ember/helper';
import { loremIpsum } from 'lorem-ipsum';

// set during render
let scrollers = {};
const setScrollers = (s) => scrollers = s;
const click = (methodName) => scrollers[methodName]();

<template>
  <div class="demo">
    <Scroller class="container" as |s|>
      {{ (setScrollers s) }}

      <div class="big-content">
        {{loremIpsum (hash count=10 units="paragraphs")}}
      </div>
    </Scroller>

    <div class="fixed-button-set">
      <button {{on "click" (fn click "scrollToLeft")}}>⬅️</button>
      <button {{on "click" (fn click "scrollToBottom")}}>⬇️</button>
      <button {{on "click" (fn click "scrollToTop")}}>⬆️</button>
      <button {{on "click" (fn click "scrollToRight")}}>➡️</button>
    </div>
  </div>

  <style>
    .demo { position: relative; }
    .container {
      overflow: auto;
      height: 200px;
      scroll-behavior: smooth;
    } 
    .big-content {
      width: 200%;
    }
    .fixed-button-set {
      position: absolute;
      top: 0rem;
      right: 0rem;
      margin-top: -4rem;
      filter: drop-shadow(0 2px 3px #555);
    }
    button { padding: 0; font-size: 2rem; line-height: 2rem;}
  </style>
</template>
```

</div>


## Install

```hbs live
<SetupInstructions @src="components/scroller.gts" />
```


## Anatomy

```js 
import { Scroller } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { Scroller } from 'ember-primitives/components/scroller';
```


```gjs 
import { Scroller } from 'ember-primitives';

<template>
  <Scroller as |s|>

    {{ (s.scrollToTop) }}
    {{ (s.scrollToBottom) }}
    {{ (s.scrollToLeft) }}
    {{ (s.scrolltoRight) }}

  </Scroller>
</template>
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/scroller" 
    @name="Scroller" />
</template>
```

## Accessibility

The wrapping `<div>` is scrollable, and is required to have keyboard access, via [AXE: Scrollable Region Must have Keyboard Access](https://dequeuniversity.com/rules/axe/4.8/scrollable-region-focusable?application=axeAPI).


---

# Service

This helper enables typed access to services without a backing class or even a component.

## Example 

```gjs live preview 
import { service } from 'ember-primitives/helpers/service';

<template>
  {{#let (service 'router') as |router|}}

    {{router.currentURL}} : <br/>
    <pre>{{JSON.stringify router.currentRoute.attributes null 2}}</pre>

  {{/let}}
</template>
```


## Install

```hbs live
<SetupInstructions @src="helpers/service.ts" />
```



---

# Shadowed

```gjs live no-shadow
import { CommentQuery } from 'kolay';

<template>
  <CommentQuery @module="components/shadowed" @name="Shadowed" />
</template>
```

## Example

Almost all demos within these docs are rendered within a `<Shadowed />` wrapper.

```gjs live preview 
import { Shadowed } from 'ember-primitives';

<template>
  <style> 
    p {
      border: 1px solid;
      padding: 0.75rem;
      transform: skew(5deg, 5deg); 
      width: 100px;
    }
  </style>

  <p>
    This element is affected by the global styles
  </p>

  <Shadowed>
    <p>
      This element is not affected by global sytles
    </p>
  </Shadowed>
</template>
```


## Install

```hbs live
<SetupInstructions @src="components/shadowed.gts" />
```


## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/shadowed" 
    @name="Shadowed" 
  />
</template>
```


---

{
  "title": "Should Handle Link 📦"
}


---

# `should-handle-link`

The utility provided from [should-handle-link](https://github.com/NullVoxPopuli/should-handle-link/) is what powers the decisions for `properLinks`.

Before we pass off the the ember router to determine if a `href` link is part of your app, we have to first determine if the browser should handle the click instead.

## Install

```hbs live
<SetupInstructions @name="should-handle-link" />
```


## Usage 

```ts
import { shouldHandle } from 'should-handle-link';

function handler(event) {
    let anchor = getAnchor(event);

    if (!shouldHandle(location.href, anchor, event)) {
        return;
    }

    event.preventDefault();
    event.stopImmediatePropagation();
    event.stopPropagation();
    // Do single-page-app routing, 
    // or some other manual handling of the clicked anchor element
}

document.body.addEventListener('click', handler);

function getAnchor(event) {
  /**
   * Using composed path in case the link is removed from the DOM
   * before the event handler evaluates
   */
  let composedPath = event.composedPath();

  for (let element of composedPath) {
    if (element instanceof HTMLAnchorElement) {
      return element;
    }
  }
}
```


---

# Viewport

Utility for managing a singleton [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) for efficient CPU and Memory usage when detecting element visibility in the viewport.

Using one `IntersectionObserver` (instead of multiple) results in dramatically better performance of your application, especially when observing many elements.

## Install

```hbs live
<SetupInstructions @src="viewport/viewport.ts" />
```


Introduced in [0.49.0](https://github.com/universal-ember/ember-primitives/releases)

## Usage

You'll need to inspect-element to see that the text is changing when leaving view.


<div class="featured-demo">

```gjs live preview
import { viewport } from 'ember-primitives/viewport';

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { modifier } from 'ember-modifier';

export default class Demo extends Component {
  @tracked isVisible = false;
  @tracked intersectionRatio = 0;

  get #viewport() {
    return viewport(this);
  }

  observeIntersection = modifier((element) => {
    this.#viewport.observe(element, this.handleIntersection);

    return () => this.#viewport.unobserve(element, this.handleIntersection);
  });

  handleIntersection = (entry) => {
    this.isVisible = entry.isIntersecting;
    this.intersectionRatio = entry.intersectionRatio;
  }

  <template>
    <div class="scroll-container" tabindex="0">
      <div class="spacer">Scroll down to see the element enter the viewport</div>
      
      <div {{this.observeIntersection}} class="observed-element">
        {{#if this.isVisible}}
          ✓ Element is visible in viewport ({{this.intersectionRatio}})
        {{else}}
          ✗ Element is not visible
        {{/if}}
      </div>
      
      <div class="spacer">Scroll up to see it leave</div>
    </div>

    <style>
      @scope {
        .scroll-container {
          height: 160px;
          overflow-y: scroll;
          border: 2px dashed black;
          padding: 2rem;
        }
        
        .spacer {
          height: 180px;
          display: flex;
          align-items: center;
          justify-content: center;
        }
        
        .observed-element {
          padding: 1rem;
          background: linear-gradient(135deg, #66aeea 0%, #862ba2 100%);
          color: white;
          border-radius: 8px;
          text-align: center;
          font-weight: bold;
          display: flex;
          align-items: center;
          justify-content: center;
        }
      }
    </style>
  </template>
}
```

</div>

## Advanced Usage

You can provide options to configure the intersection observer:

```gjs
import { viewport } from 'ember-primitives/viewport';
import Component from '@glimmer/component';
import { modifier } from 'ember-modifier';

export default class Demo extends Component {
  get #viewport() {
    return viewport(this);
  }

  observeIntersection = modifier((element) => {
    // Observe with custom options
    this.#viewport.observe(element, this.handleIntersection, {
      // Trigger 100px before entering viewport
      rootMargin: '100px',
      // Trigger at 0%, 50%, and 100% visibility
      threshold: [0, 0.5, 1.0]
    });

    return () => this.#viewport.unobserve(element, this.handleIntersection);
  });

  handleIntersection = (entry) => {
    console.log('Intersection ratio:', entry.intersectionRatio);
    console.log('Is intersecting:', entry.isIntersecting);
  }

  <template>
    <div {{this.observeIntersection}}>
      Observed content
    </div>
  </template>
}
```

## API Reference

```gjs live no-shadow
import { APIDocs } from 'kolay';

<template>
  <APIDocs 
    @package="ember-primitives" 
    @module="declarations/viewport/viewport" 
    @name="viewport" />
</template>
```

## See Also

- [`InViewport`](/6-utils/in-viewport) - A component built on top of this utility for declarative viewport-based rendering
- [ResizeObserver](/6-utils/resize-observer) - Similar pattern for observing element size changes


---

# VisuallyHidden

Hides content from the screen in an accessible way.

Can be used as an attribute, `visually-hidden` on any element, or a Component. 

## Example

```gjs live live-preview no-shadow
import { VisuallyHidden } from 'ember-primitives';

<template>
  Visually <span visually-hidden>secrets!</span>
  seen

  <VisuallyHidden>
    This is visually hidden 
  </VisuallyHidden>
  Visually seen
</template>
```


## Install

```hbs live
<SetupInstructions @src="components/visually-hidden.gts" />
```


## Features

- Visually hides content while preserving it for assistive technology.
- Just an attribute on any element, component optional.

## Anatomy

Using the attribute

```hbs
<span visually-hidden>...</span>
```

the `visually-hidden` attribute becomes available after importing the component (below), or including this import somewherer in your app:

```js 
import 'ember-primitives/styles.css';
```


Using the component:

```js 
import { VisuallyHidden } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { VisuallyHidden } from 'ember-primitives/components/dialog';
```


```gjs 
import { VisuallyHidden } from 'ember-primitives';

<template>
  <VisuallyHidden>
    text here to hide visually
  </VisuallyHidden>
</template>
```

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/visually-hidden" 
    @name="VisuallyHidden" 
  />
</template>
```

## Accessibility

This is useful in certain scenarios as an alternative to traditional labelling with `aria-label` or `aria-labelledby`.


---

# Hero

The hero pattern is for featuring large, eye-catching content, such as an image or video, along with a clear, easy to read headline or call to action.

## Features 

* Covers the full browser height and width, including proper dimensionality on dynamically sized viewports, such as on mobile.



## Install

```hbs live
<SetupInstructions @src="components/layout/hero.gts" />
```


## Anatomy

```js 
import { Hero } from 'ember-primitives/layout/hero';
```

```gjs 
import { Hero } from 'ember-primitives/layout/hero';

<template>
  <Hero>
    content
  </Hero>
</template>
```

There is 1 BEM-style class on the element to enable further customization or styling.
```css
/* 
  the containing element. 
  this is not the same element that `...attributes` is on.
*/ 
.ember-primitives__hero__wrapper
```

It has `position: relative` on it.
So elements within can be sticky or absolutely positioned along the outside, if needed (such as for headers and footers).

## Accessibility

This component provides no accessibility patterns and using `<main>` / `<footer>` or not is up to the use case of the `<Hero />` component.

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/layout/hero" 
    @name="Hero" 
  />
</template>
```

### State Attributes

This component has no state.


---

{
  "title": "Sidebar 📦"
}


---

# Sidebar (Off-Canvas Menu)

For implementing an off-canvas sidebar menu (also known as a drawer or hamburger menu), we recommend using [ember-mobile-menu](https://nickschot.github.io/ember-mobile-menu).

<Callout>

This library doesn't need to implement an off-canvas sidebar because [ember-mobile-menu](https://nickschot.github.io/ember-mobile-menu) already exists and provides a robust, accessible solution. We document it here as a recommended tool for this common UI pattern. But please see [their docs](https://nickschot.github.io/ember-mobile-menu/) for more thorough examples.

</Callout>

## Features

* Good for use on non-touch devices as well as touch devices
* **Draggable sidebar** - Users can drag the sidebar open and closed
* **Touch-friendly** - Optimized for mobile devices with touch gestures
* **Multiple positions** - Support for left, right, top, and bottom sidebars
* **Multiple Menus** - have left right, etc simultaneously
* **Customizable** - Flexible styling and behavior options
* **Accessible** - Built with accessibility in mind
* **Shadow backdrop** - Optional backdrop overlay when sidebar is open

## Install

```hbs live
<SetupInstructions @name="ember-mobile-menu" />
```

## Example

<div class="featured-demo">

```gjs live preview
  import { Shadowed } from 'ember-primitives/components/shadowed';
import MobileMenuWrapper from 'ember-mobile-menu/components/mobile-menu-wrapper';
import { on } from '@ember/modifier';

<template>
  <Shadowed>
    <div style="height:150px">
      <MobileMenuWrapper @embed={{true}} as |mmw|>
        <mmw.MobileMenu as |mm|>
          <button  {{on "click" mm.actions.close}}>close</button>
        </mmw.MobileMenu>

        <mmw.Content>
          <mmw.Toggle>Menu</mmw.Toggle>

          content here
        </mmw.Content>
      </MobileMenuWrapper>
    </div>

    <style>

    /* The default theme included with the library */
  .mobile-menu {
    position: fixed;
    top: 0;
    width: 0;
  }

  .mobile-menu.mobile-menu--left {
    left: 0;
  }
  .mobile-menu.mobile-menu--right {
    right: 0;
  }

  /* variants */
  .mobile-menu--default {
    z-index: var(--mobile-menu-z-index);
  }
  .mobile-menu--squeeze, .mobile-menu--push {
    z-index: 2;
  }
  .mobile-menu--ios, .mobile-menu--reveal, .mobile-menu--squeeze-reveal {
    display: none;
    z-index: -1;
  }
  .mobile-menu--ios.mobile-menu--dragging, .mobile-menu--ios.mobile-menu--transitioning, .mobile-menu--ios.mobile-menu--open, .mobile-menu--reveal.mobile-menu--dragging, .mobile-menu--reveal.mobile-menu--transitioning, .mobile-menu--reveal.mobile-menu--open, .mobile-menu--squeeze-reveal.mobile-menu--dragging, .mobile-menu--squeeze-reveal.mobile-menu--transitioning, .mobile-menu--squeeze-reveal.mobile-menu--open {
    display: block;
    z-index: unset;
  }
  .mobile-menu--ios .mobile-menu__mask, .mobile-menu--reveal .mobile-menu__mask, .mobile-menu--squeeze-reveal .mobile-menu__mask {
    z-index: 1;
  }
  .mobile-menu--ios.mobile-menu--open .mobile-menu__mask, .mobile-menu--reveal.mobile-menu--open .mobile-menu__mask, .mobile-menu--squeeze-reveal.mobile-menu--open .mobile-menu__mask {
    display: none;
  }

  .mobile-menu-wrapper--embedded .mobile-menu {
    position: absolute;
  }
  .mobile-menu-wrapper--embedded .mobile-menu__mask, .mobile-menu-wrapper--embedded .mobile-menu.mobile-menu--open, .mobile-menu-wrapper--embedded .mobile-menu.mobile-menu--transitioning, .mobile-menu-wrapper--embedded .mobile-menu.mobile-menu--dragging {
    width: 100%;
  }
  .mobile-menu-wrapper--embedded .mobile-menu, .mobile-menu-wrapper--embedded .mobile-menu__mask,
  .mobile-menu-wrapper--embedded .mobile-menu .mobile-menu__tray {
    height: var(--mobile-menu-height);
  }

  .mobile-menu__toggle {
    cursor: pointer;
  }

  @layer ember-mobile-menu {
    :root {
      --mobile-menu-wrapper-width: 100%;
      --mobile-menu-wrapper-min-height: 100vh;

      --mobile-menu-height: 100vh;
      --mobile-menu-z-index: 2000;
    }
  }

  body.mobile-menu--prevent-scroll {
    overflow: hidden;
  }

  .mobile-menu-wrapper {
    overflow: hidden;
    width: var(--mobile-menu-wrapper-width);
    min-height: var(--mobile-menu-wrapper-min-height);
    /* Avoid Chrome to see Safari hack */
  }
  @supports (-webkit-touch-callout: none) {
    .mobile-menu-wrapper {
      /* The hack for Safari */
      min-height: -webkit-fill-available;
    }
  }

  .mobile-menu-wrapper--embedded {
    position: relative;
    min-height: 100%;
    min-width: 100%;
    overflow: hidden;
  }
  .mobile-menu-wrapper--embedded .mobile-menu-wrapper__content {
    min-height: 100%;
  }

  .mobile-menu-wrapper__content {
    color: black;
    padding: 0.5rem;
    min-height: var(--mobile-menu-wrapper-min-height);
    position: relative;
    background: #FFF;
    will-change: transform, margin-left, margin-right;
    z-index: 1;
    touch-action: pan-y;
  }
  .mobile-menu-wrapper__content--shadow {
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
  }
  .mobile-menu-wrapper__content--ios, .mobile-menu-wrapper__content--reveal, .mobile-menu-wrapper__content--squeeze-reveal {
    z-index: 2;
  }

  .mobile-menu__mask {
    position: absolute;
    top: 0;
    left: 0;
    border: none;
    border-radius: 0;
    margin: 0;
    padding: 0;
    width: 100vw;
    height: 100vh;
    /* Avoid Chrome to see Safari hack */
    background: rgba(0, 0, 0, 0.3);
    opacity: 0;
    transition: none;
    touch-action: pan-y;
    will-change: opacity;
    visibility: hidden;
    outline: none;
    -webkit-tap-highlight-color: transparent;
  }
  @supports (-webkit-touch-callout: none) {
    .mobile-menu__mask {
      /* The hack for Safari */
      height: -webkit-fill-available;
    }
  }

  .mobile-menu__tray {
    position: absolute;
    top: 0;
    height: var(--mobile-menu-height);
    /* Avoid Chrome to see Safari hack */
    overflow-y: auto;
    touch-action: pan-y;
    background: #FFF;
    will-change: transform;
  }
  @supports (-webkit-touch-callout: none) {
    .mobile-menu__tray {
      /* The hack for Safari */
      height: -webkit-fill-available;
    }
  }

  @layer ember-mobile-menu {
    :root {
      --mobile-menu-header-bg:                 #E04E39;

      --mobile-menu-item-color:                #333;
      --mobile-menu-item-active-bg:            #EEE;
      --mobile-menu-item-link-disabled-color:  #6C757D;
    }
  }

  .mobile-menu__tray .mobile-menu__header {
    min-height: 150px;
    background: var(--mobile-menu-header-bg);
    color: #FFF;
    margin-bottom: 8px;
  }
  .mobile-menu__tray .mobile-menu__header .header__text {
    padding: 16px;
  }
  .mobile-menu__tray .mobile-menu__header .header__btn {
    padding: 16px;
    color: #FFF;
    text-decoration: none;
  }

  .mobile-menu__tray .mobile-menu__nav {
    list-style: none;
    padding: 0;
    margin: 0;
  }
  .mobile-menu__tray .mobile-menu__nav .mobile-menu__nav-item a {
    display: block;
    font-size: 12px;
    font-weight: bold;
    color: var(--mobile-menu-item-color);
    line-height: 1.5;
    text-decoration: none !important;

    padding: 12px;
  }
  .mobile-menu__tray .mobile-menu__nav .mobile-menu__nav-item a.mobile-menu__nav-link.disabled {
    color: var(--mobile-menu-item-link-disabled-color);
  }
  .mobile-menu__tray .mobile-menu__nav .mobile-menu__nav-item a.active {
    background: var(--mobile-menu-item-active-bg);
  }
  .mobile-menu__tray .mobile-menu__nav .mobile-menu__nav-divider {
    margin: 8px 0;
    height: 0;
    border-bottom: 1px solid var(--mobile-menu-item-active-bg);
  }

    </style>
  </Shadowed>
</template>
```

</div>

## Styling

ember-mobile-menu provides default styles but allows for customization. You can override the default styles by targeting the component's CSS classes or by providing your own styles.

## Accessibility

ember-mobile-menu handles keyboard navigation and focus management automatically. The sidebar can be closed with the Escape key, and focus is properly managed when opening and closing.

## Resources

- [ember-mobile-menu Documentation](https://nickschot.github.io/ember-mobile-menu)
- [GitHub Repository](https://github.com/nickschot/ember-mobile-menu)
- [NPM Package](https://www.npmjs.com/package/ember-mobile-menu)

## Use Cases

Off-canvas sidebars are commonly used for:

* **Mobile navigation** - Primary navigation on mobile devices
* **Settings panels** - Side panels with filters or settings
* **Shopping carts** - Slide-out cart summaries in e-commerce
* **User profiles** - Quick access to user information
* **Notification panels** - Displaying notifications or messages

ember-mobile-menu provides a production-ready solution for all these use cases and more.


---

# Sticky Footer

A common UI pattern is to have a footer area at the bottom of the page that always sticks to the bottom of the window, regardless of how little content there is on the page, yet allows scrolling and being pushed out of frame when there is a lot of content, as the footer is not "main" content on the page, but typically more "reference" material, or serving a navigation purpose.

This component implements a CSS/markup-only pattern for the above-described layout-pattern.

<div class="featured-demo auto-height">

```gjs live preview no-shadow
import { StickyFooter } from 'ember-primitives';
import { on } from '@ember/modifier';
import { TrackedArray } from 'tracked-built-ins';
import { loremIpsum } from 'lorem-ipsum';

const content = new TrackedArray();
const addContent = () => content.push(loremIpsum({ count: 1, units: 'paragraph' }));
const removeContent = () => content.splice(-1);

<template>
  <div class="fake-window">
    <StickyFooter>
      <:content>
        <button {{on 'click' addContent}}>Add Content</button>
        <button {{on 'click' removeContent}}>Remove Content</button>
        <br>

        {{#each content as |paragraph|}}
          {{paragraph}}
        {{/each}}
      </:content>
      <:footer>
        <footer>
          This is the footer
        </footer>
      </:footer>
    </StickyFooter>
  </div>
  <style>
    /* styles for demo, not required */
    footer { border: 1px solid; }
    .fake-window {
      height: 200px;
      border: 1px solid;
      overflow: auto;
      padding: 1rem;
    }
  </style>
</template>
```

</div>

## Example: perma sticky / revealing footer

In this example, there is an extra footer at the bottom, and we want the sticky footer to always show above that, but then reveal more information when we scroll to the bottom

<div class="featured-demo auto-height">

```gjs live preview no-shadow
import { StickyFooter } from 'ember-primitives';
import { on } from '@ember/modifier';
import { TrackedArray } from 'tracked-built-ins';
import { loremIpsum } from 'lorem-ipsum';

const content = new TrackedArray();
const addContent = () => content.push(loremIpsum({ count: 1, units: 'paragraph' }));
const removeContent = () => content.splice(-1);

<template>
  <div class="fake-window2">
    <StickyFooter class="container">
      <:content>
        <button {{on 'click' addContent}}>Add Content</button>
        <button {{on 'click' removeContent}}>Remove Content</button>
        <br>

        {{#each content as |paragraph|}}
          {{paragraph}}
        {{/each}}
      </:content>
      <:footer>
        <footer class="sticky-footer">
          This is the footer
          <br><br>
          some information can be hidden until scrolled to.
        </footer>
      </:footer>
    </StickyFooter>

    <footer class="site-footer">
      site-wide footer
    </footer>
  </div>
  <style>
    /* styles that demonstrate the UX */
    .container {
      max-height: 200px;
      overflow: auto;
      position: relative;
      padding-bottom: 60px;
    }
    .ember-primitives__sticky-footer__footer {
      position: sticky;
      bottom: -38px;
    }
    footer.sticky-footer, footer.site-footer { 
      border: 1px solid; background: #333; 
    }
    footer.site-footer { height: 38px; position: absolute; bottom: 0; left: 0; right: 0; }
    .fake-window2 {
      padding-bottom: 38px;
      min-height: 150px;
      max-height: 200px;
      position: relative;
      border: 1px solid;
      padding: 1rem;
      overflow: hidden;
    }
  </style>
</template>
```

</div>


## Install

```hbs live
<SetupInstructions @src="components/layout/sticky-footer.gts" />
```


## Features

* Footer sticks to the bottom of the window when there is less than a screen's worth of content
* Footer sits below the content when there is enough content to overflow the containing element / body

## Anatomy

```js 
import { StickyFooter } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { StickyFooter } from 'ember-primitives/layout/sticky-footer';
```

```gjs 
import { StickyFooter } from 'ember-primitives';

<template>
  <StickyFooter>
    <:content>
      content here
    </:content>
    <:footer>
      footer content here
    </:footer>
  </StickyFooter>
</template>
```

There are 3 BEM-style classes on the elements to enable further customization or styling.
```css
/* 
  the containing element of both <:content> and <:footer> 
  this is not the same element that `...attributes` is on.
*/ 
.ember-primitives__sticky-footer__container
  /* for the <:content> block's containing element */ 
  .ember-primitives__sticky-footer__content
  /* for the <:footer> block's containing element */ 
  .ember-primitives__sticky-footer__footer
```

## Accessibility

This component provides no accessibility patterns and using `<main>` / `<footer>` or not is up to the use case of the `<StickyFooter />` component.

## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/layout/sticky-footer" 
    @name="StickyFooter" 
  />
</template>
```

### State Attributes

This component has no state.


---

{
  // default sort order provided by the filesystem: Alphabetical
  "order": ["otp", "otp-input"],
}


---

{
  "componentName": "OTPInput"
}


---

# One-Time Password Input

This `<OTPInput>` is a low-level component used in the `<OTP>` component. It provides only the collective input field for embedding in broader forms.


<Callout>

Before reaching for this component, consider if the [`WebOTP` API](https://developer.mozilla.org/en-US/docs/Web/API/WebOTP_API) and/or the [native `<input>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) with [`autocomplete="one-time-code"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#browser_compatibility) is sufficient for your use case. 

</Callout>
<br>


<div class="featured-demo">

```gjs live preview
import { OTPInput } from 'ember-primitives';
import { cell } from 'ember-resources';
import { uniqueId } from '@ember/helper';

const currentCode = cell();
const update = ({ code }) => currentCode.current = code;

<template>
  <pre>code: {{currentCode}}</pre>

  Please enter the OTP<br>

  <OTPInput @onChange={{update}} />

  <style>
    fieldset {
      border: none;
      padding: 0;
    }
    input {
      border: 1px solid;
      font-size: 2rem;
      width: 2.5rem;
      height: 3rem;
      text-align: center;
    }
  </style>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/one-time-password.gts" />
```

## Features

* Auto-advance focus between inputs as characters are typed
* Pasting into the collective field will fill all inputs with the pasted value
* backspace / arrow key support
* number keyboard on [mobile](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode)

## Anatomy

```js 
import { OTPInput } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { OTPInput } from 'ember-primitives/components/one-time-password';
```


```gjs 
import { OTPInput } from 'ember-primitives';

<template>

  <label>
    Enter OTP

    <OTPInput />
  </label>
</template>
```

There is also an alternate block-fork you can use if you want to place additional information
within the `fieldset`
```gjs 
import { OTPInput } from 'ember-primitives';

<template>
    <OTPInput as |Fields|>
        <Fields />
    </OTPInput>
</template>
```


## Accessibility

Every field making up the collective input already has a screen-reader friendly label.
Developers are encouraged to provide a visible label via `<legend>` or other means.

Keyboard interactions try to mimic select interactions from a single input (arrows, backspace, etc).


## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/one-time-password/input" 
    @name="OTPInput" 
  />
</template>
```
### State Attributes

none

[^sms]: noting that SMS is not the *most secure* form of 2FA, and for applications that truly need secure logic, you'll want an authenticator app. See [this article for a high level overview of the reasoning](https://www.securemac.com/news/is-sms-for-2fa-insecure)


---

{
  "componentName": "OTP"
}


---

# One-Time Password

The `<OTP>` component provides a way to manage one-time-password entry using the common pattern where each character entry for the one-time-password is its own field, while also managing accessibility requirements of a multi-field input. 

For more information on OTP patterns, see [web.dev's SMS OTP Form](https://web.dev/sms-otp-form/)[^sms]

<Callout>

Before reaching for this component, consider if the [`WebOTP` API](https://developer.mozilla.org/en-US/docs/Web/API/WebOTP_API) and/or the [native `<input>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) with [`autocomplete="one-time-code"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#browser_compatibility) is sufficient for your use case. 

</Callout>
<br>

<div class="featured-demo">

```gjs live preview
import { OTP } from 'ember-primitives';
import { cell } from 'ember-resources';

const submittedCode = cell();
const handleSubmit = ({ code }) => submittedCode.current = code;

<template>
  <pre>submitted: {{submittedCode}}</pre>

  <OTP @onSubmit={{handleSubmit}} as |x|>
    <x.Input class="fields" />
    <br>
    <x.Submit>Submit</x.Submit>
    <x.Reset>Reset</x.Reset>
  </OTP>

  <style>
    .fields { 
      display: grid;
      grid-auto-flow: column;
      gap: 0.5rem;
      width: min-content;
      border: none;
      padding: 0;

      input {
        border: 1px solid;
        font-size: 2rem;
        width: 2.5rem;
        height: 3rem;
        text-align: center;
        appearance: textfield;
      }
    }
  </style>
</template>
```

</div>

## Install

```hbs live
<SetupInstructions @src="components/one-time-password.gts" />
```

## Features

* Auto-advance focus between inputs as characters are typed
* Pasting into the collective field will fill all inputs with the pasted value
* Standalone form, allowing for easily creating OTP-entry screens
* Optional reset button
* Pressing enter submits the code
* backspace / arrow key support
* number keyboard on [mobile](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/inputmode)


## Anatomy

```js 
import { OTP } from 'ember-primitives';
```

or for non-tree-shaking environments:
```js 
import { OTP } from 'ember-primitives/components/one-time-password';
```


```gjs 
import { OTP } from 'ember-primitives';

<template>
  <OTP as |x|>
    text can go here, or in between the below components

    <x.Input />
    <x.Submit> submit text </x.Submit>
    <x.Reset> reset text </x.Reset>

    text can go here as well
  </OTP>
</template>
```

Additionally, the customization from `<OTPInput>` is available as well

```gjs 
import { OTP } from 'ember-primitives';

<template>
  <OTP as |x|>
    <x.Input as |Fields|>
        <Fields />
    </x.Input>

    <x.Submit> submit text </x.Submit>
  </OTP>
</template>
```


## Accessibility

This component complies with all `<form>` and `<input>` accessibility guidelines.
It is rendered within a fieldset to convey that all of the character inputs are related.


## API Reference

```gjs live no-shadow
import { ComponentSignature } from 'kolay';

<template>
  <ComponentSignature 
    @package="ember-primitives" 
    @module="declarations/components/one-time-password/otp" 
    @name="OTP" 
  />
</template>
```
### State Attributes

none

[^sms]: noting that SMS is not the *most secure* form of 2FA, and for applications that truly need secure logic, you'll want an authenticator app. See [this article for a high level overview of the reasoning](https://www.securemac.com/news/is-sms-for-2fa-insecure)


---

import { Tabs } from 'ember-primitives/components/tabs';

export const BottomTabs = <template>
  <Tabs @label="Install with your favorite package-manager" as |Tab|>
    <Tab @label="npm">npm add ember-primitives</Tab>
    <Tab @label="pnpm">pnpm add ember-primitives</Tab>
    <Tab @label="yarn">yarn add ember-primitives</Tab>
  </Tabs>
  <style>
    /* https://caniuse.com/css-cascade-scope */
    @scope {
      .ember-primitives__tabs {
        display: grid;
        grid-template-areas:
          "label label"
          "tabpanel tabpanel"
          "tablist tablist";
      }
      .ember-primitives__tabs__label {
        grid-area: label;
      }
      .ember-primitives__tabs__tabpanel {
        grid-area: tabpanel;
        display: flex;
        border: 1px solid;
      }

      [role="tablist"] {
        grid-area: tablist;
        display: flex;
        flex-wrap: wrap;
      }

      [role="tab"] {
        border-radius: 0;
        color: black;
        padding: 0.25rem 0.5rem;
        background: hsl(220deg 20% 94%);
        outline: none;
        font-weight: bold;
        cursor: pointer;
        box-shadow: inset 1px 0px 1px black;

        &:focus-visible {
          z-index: 1;
        }
      }

      [role="tab"][aria-selected="true"] {
        background: #efefef;
        box-shadow: inset 0 4px 0px orange;
      }

      [role="tab"]:first-of-type {
        border-bottom-left-radius: 0.25rem;
      }
      [role="tab"]:last-of-type {
        border-bottom-right-radius: 0.25rem;
      }

      [role="tabpanel"] {
        flex-grow: 1;
        color: black;
        padding: 1rem;
        border-radius: 0.25rem 0.25rem 0.25rem 0;
        background: white;
        overflow: auto;
        font-family: ui-monospace monospace;
      }
    }
  </style>
</template>;


---

import { Tabs } from 'ember-primitives/components/tabs';

import { BottomTabs } from './bottom-tabs.gjs';
import { LeftTabs } from './left-tabs.gjs';
import { RightTabs } from './right-tabs.gjs';

export const Demo = <template>
  <Tabs class="left-tabs inline-tabs" as |Tab|>
    <Tab @label="Tabs on Left">
      <LeftTabs />
    </Tab>
    <Tab @label="Tabs on Right">
      <RightTabs />
    </Tab>
    <Tab @label="Tabs on Bottom">
      <BottomTabs />
    </Tab>
  </Tabs>

  <style>
    /* https://caniuse.com/css-cascade-scope */
    @scope {
      .inline-tabs {
        > [role="tablist"] {
          min-width: 100%;

          > [role="tab"] {
            color: black;
            display: inline-block;
            padding: 0.25rem 0.5rem;
            background: hsl(220deg 20% 94%);
            outline: none;
            font-weight: bold;
            cursor: pointer;
            box-shadow: inset 0 -1px 1px black;
          }

          > [role="tab"][aria-selected="true"] {
            background: #efefef;
            box-shadow: inset 0 -4px 0px orange;
          }

          > [role="tab"]:first-of-type {
            border-top-left-radius: 0.25rem;
          }
          > [role="tab"]:last-of-type {
            border-top-right-radius: 0.25rem;
          }
        }

        > .ember-primitives__tabs__tabpanel {
          > [role="tabpanel"] {
            color: black;
            padding: 1rem;
            border-radius: 0 0.25rem 0.25rem;
            background: white;
            width: 100%;
            overflow: auto;
            font-family: ui-monospace monospace;
          }
        }
      }
    }
  </style>
</template>;


---

import { Tabs } from 'ember-primitives/components/tabs';

export const LeftTabs = <template>
  <Tabs @label="Install with your favorite package-manager" as |Tab|>
    <Tab @label="npm">npm add ember-primitives</Tab>
    <Tab @label="pnpm">pnpm add ember-primitives</Tab>
    <Tab @label="yarn">yarn add ember-primitives</Tab>
  </Tabs>

  <style>
    /* https://caniuse.com/css-cascade-scope */
    @scope {
      .ember-primitives__tabs {
        display: grid;
        grid-template-areas:
          "label label"
          "tablist tabpanel"
          "tablist tabpanel";
        grid-template-columns: min-content;
      }
      .ember-primitives__tabs.reversed {
        grid-template-areas:
          "label label"
          "tabpanel tablist"
          "tabpanel tablist";
        grid-template-columns: max-content min-content;
      }
      .ember-primitives__tabs__label {
        grid-area: label;
      }
      .ember-primitives__tabs__tabpanel {
        grid-area: tabpanel;
        display: flex;
        border: 1px solid;
      }

      [role="tablist"] {
        grid-area: tablist;
        flex-direction: column;
        display: flex;
        flex-wrap: wrap;
      }

      [role="tab"] {
        border-radius: 0;
        color: black;
        padding: 0.25rem 0.5rem;
        background: hsl(220deg 20% 94%);
        outline: none;
        font-weight: bold;
        cursor: pointer;
        box-shadow: inset -1px 0px 1px black;

        &:focus-visible {
          z-index: 1;
        }
      }

      [role="tab"][aria-selected="true"] {
        background: white;
        box-shadow: inset -4px -0px 0px orange;
      }

      [role="tab"]:first-of-type {
        border-top-left-radius: 0.25rem;
      }
      [role="tab"]:last-of-type {
        border-bottom-left-radius: 0.25rem;
      }

      [role="tabpanel"] {
        flex-grow: 1;
        color: black;
        padding: 1rem;
        border-radius: 0 0.25rem 0.25rem 0;
        background: white;
        overflow: auto;
        font-family: ui-monospace monospace;
      }
    }
  </style>
</template>;


---

import { Tabs } from 'ember-primitives/components/tabs';

export const RightTabs = <template>
  <Tabs @label="Install with your favorite package-manager" as |Tab|>
    <Tab @label="npm">npm add ember-primitives</Tab>
    <Tab @label="pnpm">pnpm add ember-primitives</Tab>
    <Tab @label="yarn">yarn add ember-primitives</Tab>
  </Tabs>
  <style>
    /* https://caniuse.com/css-cascade-scope */
    @scope {
      .ember-primitives__tabs {
        display: grid;
        grid-template-areas:
          "label label"
          "tabpanel tablist"
          "tabpanel tablist";
        grid-template-columns: 1fr min-content;
      }
      .ember-primitives__tabs__label {
        grid-area: label;
      }
      .ember-primitives__tabs__tabpanel {
        grid-area: tabpanel;
        display: flex;
        border: 1px solid;
      }

      [role="tablist"] {
        grid-area: tablist;
        flex-direction: column;
        display: flex;
        flex-wrap: wrap;
      }

      [role="tab"] {
        border-radius: 0;
        color: black;
        padding: 0.25rem 0.5rem;
        background: hsl(220deg 20% 94%);
        outline: none;
        font-weight: bold;
        cursor: pointer;
        box-shadow: inset 1px 0px 1px black;

        &:focus-visible {
          z-index: 1;
        }
      }

      [role="tab"][aria-selected="true"] {
        background: #efefef;
        box-shadow: inset 4px -0px 0px orange;
      }

      [role="tab"]:first-of-type {
        border-top-right-radius: 0.25rem;
      }
      [role="tab"]:last-of-type {
        border-bottom-right-radius: 0.25rem;
      }

      [role="tabpanel"] {
        flex-grow: 1;
        color: black;
        padding: 1rem;
        border-radius: 0.25rem 0 0 0.25rem;
        background: white;
        overflow: auto;
        font-family: ui-monospace monospace;
      }
    }
  </style>
</template>;


---

/**
 * @internal
 */
export const PRIMITIVES = Symbol.for('ember-primitives-globals');


---

import { waitForPromise } from '@ember/test-waiters';

import { cell } from 'ember-resources';

const _colorScheme = cell<string | undefined>();

let callbacks: Set<(colorScheme: string) => void> = new Set();

async function runCallbacks(theme: string) {
  await Promise.resolve();

  for (const callback of callbacks.values()) {
    callback(theme);
  }
}

/**
 * Object for managing the color scheme
 */
export const colorScheme = {
  /**
   * Set's the current color scheme to the passed value
   */
  update: (value: string) => {
    colorScheme.current = value;

    void waitForPromise(runCallbacks(value));
  },

  on: {
    /**
     * register a function to be called when the color scheme changes.
     */
    update: (callback: (colorScheme: string) => void) => {
      callbacks.add(callback);
    },
  },
  off: {
    /**
     * unregister a function that would have been called when the color scheme changes.
     */
    update: (callback: (colorScheme: string) => void) => {
      callbacks.delete(callback);
    },
  },

  /**
   * the current valuel of the "color scheme"
   */
  get current(): string | undefined {
    return _colorScheme.current;
  },
  set current(value: string | undefined) {
    _colorScheme.current = value;

    if (!value) {
      localPreference.delete();

      return;
    }

    localPreference.update(value);
    setColorScheme(value);
  },

  get isDark() {
    return _colorScheme.current === 'dark';
  },
  get isLight() {
    return _colorScheme.current !== 'dark';
  },
};

/**
 * Synchronizes state of `colorScheme` with the users preferences as well as reconciles with previously set theme in local storage.
 *
 * This may only be called once per app.
 */
export function sync() {
  /**
   * reset the callbacks
   */
  callbacks = new Set();

  /**
   * If local prefs are set, then we don't care what prefers-color-scheme is
   */
  const userPreference = localPreference.read();

  if (userPreference) {
    setColorScheme(userPreference);
    _colorScheme.current = userPreference;

    return;
  }

  if (prefers.dark()) {
    setColorScheme('dark');
    _colorScheme.current = 'dark';
  } else if (prefers.light()) {
    setColorScheme('light');
    _colorScheme.current = 'light';
  }
}

const queries = {
  dark: window.matchMedia('(prefers-color-scheme: dark)'),
  light: window.matchMedia('(prefers-color-scheme: light)'),
  none: window.matchMedia('(prefers-color-scheme: no-preference)'),
};

queries.dark.addEventListener('change', (e) => {
  const mode = e.matches ? 'dark' : 'light';

  colorScheme.update(mode);
});

/**
 * Helper methods to determining what the user's preferred color scheme is
 * based on the system preferences rather than the users explicit preference.
 */
export const prefers = {
  dark: () => queries.dark.matches,
  light: () => queries.light.matches,
  none: () => queries.none.matches,
  custom: (name: string) => window.matchMedia(`(prefers-color-scheme: ${name})`).matches,
};

const LOCAL_PREF_KEY = 'ember-primitives/color-scheme#local-preference';

/**
 * Helper methods for working with the color scheme preference in local storage
 */
export const localPreference = {
  isSet: () => Boolean(localPreference.read()),
  read: () => localStorage.getItem(LOCAL_PREF_KEY),
  update: (value: string) => localStorage.setItem(LOCAL_PREF_KEY, value),
  delete: () => localStorage.removeItem(LOCAL_PREF_KEY),
};

/**
 * For the given element, returns the `color-scheme` of that element.
 */
export function getColorScheme(element?: HTMLElement) {
  const style = styleOf(element);

  return style.getPropertyValue('color-scheme');
}

export function setColorScheme(element: HTMLElement, value: string): void;
export function setColorScheme(value: string): void;

export function setColorScheme(...args: [string] | [HTMLElement, string]): void {
  if (typeof args[0] === 'string') {
    styleOf().setProperty('color-scheme', args[0]);

    return;
  }

  if (typeof args[1] === 'string') {
    styleOf(args[0]).setProperty('color-scheme', args[1]);

    return;
  }

  throw new Error(`Invalid arity, expected up to 2 args, received ${args.length}`);
}

/**
 * Removes the `color-scheme` from the given element
 */
export function removeColorScheme(element?: HTMLElement) {
  const style = styleOf(element);

  style.removeProperty('color-scheme');
}

function styleOf(element?: HTMLElement) {
  if (element) {
    return element.style;
  }

  return document.documentElement.style;
}

sync();


---

import Component from "@glimmer/component";
import { cached, tracked } from "@glimmer/tracking";
import { assert } from "@ember/debug";

import { isElement } from "./narrowing.ts";
import { createStore } from "./store.ts";

import type { Newable } from "./type-utils";
import type Owner from "@ember/owner";

/**
 * IMPLEMENTATION NOTE:
 *   we don't use https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md
 *   because it is not inherently reactive.
 *
 *   Its *event* based, which opts you out of fine-grained reactivity.
 *   We want minimal effort fine-grained reactivity.
 *
 * This Technique follows the DOM tree, and is synchronous,
 * allowing correct fine-grained signals-based reactivity.
 *
 * We *could* do less work to find Providers,
 * but only if we forgoe DOM-tree scoping.
 * We must traverse the DOM hierarchy to validate that we aren't accessing providers from different subtrees.
 */
const LOOKUP = new WeakMap<Text | Element, [unknown, () => unknown]>();

export class Provide<Data extends object> extends Component<{
  /**
   * The Element is not customizable
   * (and also sometimes doesn't exist (depending on the `@element` value))
   */
  Element: null;
  Args: {
    /**
     * What data do you want to provide to the DOM subtree?
     *
     * If this is a function or class, it will be instantiated and given an
     * owner + destroyable linkage via `createStore`
     */
    data: Data | (() => Data) | Newable<Data>;

    /**
     * Optionally, you may use string-based keys to reference the data in the Provide.
     *
     * This is not recommended though, because when using a class or other object-like structure,
     * the type in the `<Consume>` component can be derived from that class or object-like structure.
     * With string keys, the `<Consume>` type will be unknown.
     */
    key?: string;

    /**
     * Can be used to either customize the element tag ( defaults to div )
     * If set to `false`, we won't use an element for the Provider boundary.
     *
     * Setting this to `false` changes the DOM Node containing the Provider's data to be a text node -- which can be useful when certain CSS situations are needed.
     *
     * But setting to `false` has a hazard: it allows subsequent sibling subtrees to access adjacent providers.
     *
     * There is no way around caveat in library land, and in a framework implementation of context,
     * it can only be solved by having render-tree context implemented, and ignoring DOM
     *  (which then makes the only difference between DOM-Context and Context be whether or not
     *    the context punches through Portals)
     */
    element?: keyof HTMLElementTagNameMap | false | undefined;
  };
  Blocks: {
    /**
     * The content that this component will _provide_ data to the entire hierarchy.
     */
    default: [];
  };
}> {
  get data() {
    assert(`@data is missing in <Provide>. Please pass @data.`, "data" in this.args);

    /**
     * This covers both classes and functions
     */
    if (typeof this.args.data === "function") {
      return createStore<Data>(this, this.args.data);
    }

    /**
     * Non-instantiable value
     */
    return this.args.data;
  }

  element: Text | HTMLElement;

  constructor(
    owner: Owner,
    args: {
      data: Data | (() => Data) | Newable<Data>;
      key?: string;
    },
  ) {
    super(owner, args);

    assert(
      `@element may only be \`false\` or a string (or undefined (default when not set))`,
      this.args.element === undefined ||
        this.args.element === false ||
        typeof this.args.element === "string",
    );

    if (this.useElementProvider) {
      this.element = document.createElement(this.args.element || "div");

      // This tells the browser to ignore everything about this element when it comes to styling
      this.element.style.display = "contents";
    } else {
      this.element = document.createTextNode("");
    }

    const key = this.args.key ?? this.args.data;

    LOOKUP.set(this.element, [key, () => this.data]);
  }

  get useElementProvider() {
    return this.args.element !== false;
  }

  <template>
    {{#if (isElement this.element)}}
      {{this.element}}

      {{#in-element this.element}}
        {{yield}}
      {{/in-element}}

    {{else}}
      {{! NOTE! This type of provider will _allow_ non-descendents using the same key to find the provider and use it.

        For example:
          Provider
            Consumer

          Consumer (finds Provider)
      }}

      {{this.element}}
      {{yield}}

    {{/if}}
  </template>
}

/**
 * How this works:
 * - starting at some deep node (Text, Element, whatever),
 *   start crawling up the ancenstry graph (of DOM Nodes).
 *
 * - This algo "tops out" (since we traverse upwards (otherwise this would be "bottoming out")) at the HTMLDocument (parent of the HTML Tag)
 *
 */
function findForKey<Data>(startElement: Text, key: string | object): undefined | (() => Data) {
  let parent: ParentNode | Text | null | undefined = startElement;

  while (parent) {
    let target: ParentNode | ChildNode | Text | null | undefined = parent;

    while (target) {
      if (!(target instanceof Element) && !(target instanceof Text)) {
        target = target?.previousSibling;
        continue;
      }

      const maybe = LOOKUP.get(target);

      target = target?.previousSibling;

      if (!maybe) {
        continue;
      }

      if (maybe[0] === key) {
        return maybe[1] as () => Data;
      }
    }

    parent = parent.parentElement;
  }
}

type DataForKey<Key> = Key extends string
  ? unknown
  : Key extends Newable<infer T>
    ? T
    : Key extends () => infer T
      ? T
      : Key;

export class Consume<Key extends object | string> extends Component<{
  Args: {
    key: Key;
  };
  Blocks: {
    default: [
      context: {
        data: DataForKey<Key>;
      },
    ];
  };
}> {
  // SAFETY: We do a runtime assert in the getter below.
  @tracked getData!: () => DataForKey<Key>;

  element: Text;

  constructor(owner: Owner, args: { key: Key }) {
    super(owner, args);

    this.element = document.createTextNode("");
  }

  @cached
  get context() {
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    const self = this;

    return {
      get data(): DataForKey<Key> {
        const getData = findForKey<Key>(self.element, self.args.key);

        assert(
          `Could not find provided context in <Consume>. Please assure that there is a corresponding <Provide> component before using this <Consume> component`,
          getData,
        );

        // SAFETY: return type handled by getter's signature
        // eslint-disable-next-line @typescript-eslint/no-unsafe-return
        return getData() as any;
      },
    };
  }

  <template>
    {{this.element}}

    {{yield this.context}}
  </template>
}


---

export { FloatingUI } from './floating-ui/component.gts';
export { anchorTo } from './floating-ui/modifier.ts';


---

import type { TOC } from "@ember/component/template-only";

export interface Signature {
  Blocks: {
    /**
     * Content to render in to the `<head>` element
     */
    default: [];
  };
}

function getHead() {
  return document.head;
}

/**
 * Utility component to place elements in the document `<head>`
 *
 * When this component is unrendered, its contents will be removed as well.
 *
 * @example
 * ```js
 * import { InHead } from 'ember-primitives/head';
 *
 * <template>
 *   {{#if @useBootstrap}}
 *     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"></script>
 *     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css">
 *   {{/if}}
 * </template>
 * ```
 */
export const InHead: TOC<Signature> = <template>
  {{#in-element (getHead) insertBefore=null}}
    {{yield}}
  {{/in-element}}
</template>;


---

export { link } from './helpers/link.ts';
export { service } from './helpers/service.ts';


---

/**
 * Returns true if the current frame is within an iframe.
 *
 * ```gjs
 * import { inIframe } from 'ember-primitives/iframe';
 *
 * <template>
 *   {{#if (inFrame)}}
 *     only show content in an iframe
 *   {{/if}}
 * </template>
 * ```
 */
export const inIframe = () => window.self !== window.top;

/**
 * Returns true if the current frame is not within an iframe.
 *
 * ```gjs
 * import { notInIframe } from 'ember-primitives/iframe';
 *
 * <template>
 *   {{#if (notInIframe)}}
 *     only show content when not in an iframe
 *     This is also the default if your site/app
 *     does not use iframes
 *   {{/if}}
 * </template>
 * ```
 */
export const notInIframe = () => !inIframe();


---

/**
 * DANGER: this is a *barrel file*
 *
 * It forces the whole library to be loaded and all dependencies.
 *
 * If you have a small app, you probably don't want to import from here -- instead import from each sub-path.
 */
import { importSync, isDevelopingApp, macroCondition } from '@embroider/macros';

if (macroCondition(isDevelopingApp())) {
  importSync('./components/violations.css');
}

export { Accordion } from './components/accordion.gts';
export type {
  AccordionContentExternalSignature,
  AccordionHeaderExternalSignature,
  AccordionItemExternalSignature,
  AccordionTriggerExternalSignature,
} from './components/accordion/public.ts';
export { Avatar } from './components/avatar.gts';
export { Breadcrumb } from './components/breadcrumb.gts';
export { Dialog, Dialog as Modal } from './components/dialog.gts';
export { Drawer } from './components/drawer.gts';
export { ExternalLink } from './components/external-link.gts';
export { Form } from './components/form.gts';
export { Key, KeyCombo } from './components/keys.gts';
export { StickyFooter } from './components/layout/sticky-footer.gts';
export { Link } from './components/link.gts';
export { Menu } from './components/menu.gts';
export { OTP, OTPInput } from './components/one-time-password.gts';
export { Popover } from './components/popover.gts';
export { Portal } from './components/portal.gts';
export { PortalTargets } from './components/portal-targets.gts';
export { TARGETS as PORTALS } from './components/portal-targets.gts';
export { Progress } from './components/progress.gts';
export { Rating } from './components/rating.gts';
export { Scroller } from './components/scroller.gts';
export { Separator } from './components/separator.gts';
export { Shadowed } from './components/shadowed.gts';
export { Switch } from './components/switch.gts';
export { Toggle } from './components/toggle.gts';
export { ToggleGroup } from './components/toggle-group.gts';
export { VisuallyHidden } from './components/visually-hidden.gts';
export { Zoetrope } from './components/zoetrope.ts';
export * from './helpers.ts';


---

import { setComponentTemplate } from "@ember/component";
import templateOnly from "@ember/component/template-only";
// Have to use these until min ember version is like 6.3 or something
import { precompileTemplate } from "@ember/template-compilation";

import { getPromiseState } from "reactiveweb/get-promise-state";

import type { ComponentLike } from "@glint/template";

interface LoadSignature<
  Expected = {
    Args: any;
  },
> {
  Blocks: {
    loading: [];
    error: [
      {
        original: unknown;
        reason: string;
      },
    ];
    success?: [component: ComponentLike<Expected>];
  };
}

/**
 * Loads a value / promise / function providing state for the lifetime of that value / promise / function.
 *
 * Can be used for manual bundle splitting via await importing components.
 *
 * @example
 * ```gjs
 * import { load } from 'ember-primitives/load';
 *
 * const Loader = load(() => import('./routes/sub-route.gts'));
 *
 * <template>
 *   <Loader>
 *     <:loading> ... loading ... </:loading>
 *     <:error as |error|> ... error! {{error.reason}} </:error>
 *     <:success as |component|> <component /> </:success>
 *   </Loader>
 * </template>
 * ```
 */
export function load<ExpectedSignature, Value>(
  fn: Value | Promise<Value> | (() => Promise<Value>) | (() => Value),
): ComponentLike<LoadSignature<ExpectedSignature>> {
  return setComponentTemplate(
    precompileTemplate(
      `{{#let (getPromiseState fn) as |state|}}
  {{#if state.isLoading}}
    {{yield to="loading"}}
  {{else if state.error}}
    {{yield state.error to="error"}}
  {{else if state.resolved}}
    {{#if (has-block "success")}}
      {{yield state.resolved to="success"}}
    {{else}}
      <state.component />
    {{/if}}
  {{/if}}
{{/let}}`,
      {
        strictMode: true,
        /**
         * The old setComponentTemplate + precompileTemplate combo
         * does not allow defining things in this scope object,
         * we _have_ to use the shorthand.
         */
        scope: () => ({ fn, getPromiseState }),
      },
    ),
    templateOnly(),
  ) as ComponentLike<LoadSignature<ExpectedSignature>>;
}


---

export function isString(x: unknown): x is string {
  return typeof x === 'string';
}

export function isElement(x: unknown): x is Element {
  return x instanceof Element;
}


---

import { assert } from '@ember/debug';
import { registerDestructor } from '@ember/destroyable';

import Modifier, { type ArgsFor } from 'ember-modifier';

import { resizeObserver } from './resize-observer.ts';

import type Owner from '@ember/owner';

// re-export provided for convenience
export { ignoreROError } from './resize-observer.ts';

export interface Signature {
  /**
   * Any element that is resizable can have onResize attached
   */
  Element: Element;
  Args: {
    Positional: [
      /**
       * The ResizeObserver callback will only receive
       * one entry per resize event.
       *
       * See: [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry)
       */
      callback: (entry: ResizeObserverEntry) => void,
    ];
  };
}

class OnResize extends Modifier<Signature> {
  #callback: ((entry: ResizeObserverEntry) => void) | null = null;
  #element: Element | null = null;

  #resizeObserver = resizeObserver(this);

  constructor(owner: Owner, args: ArgsFor<Signature>) {
    super(owner, args);

    registerDestructor(this, () => {
      if (this.#element && this.#callback) {
        this.#resizeObserver.unobserve(this.#element, this.#callback);
      }
    });
  }

  modify(element: Element, [callback]: [callback: (entry: ResizeObserverEntry) => void]) {
    assert(
      `{{onResize}}: callback must be a function, but was ${callback as unknown as string}`,
      typeof callback === 'function'
    );

    if (this.#element && this.#callback) {
      this.#resizeObserver.unobserve(this.#element, this.#callback);
    }

    this.#resizeObserver.observe(element, callback);

    this.#callback = callback;
    this.#element = element;
  }
}

export const onResize = OnResize;


---

import { assert } from '@ember/debug';
import { registerDestructor } from '@ember/destroyable';
import { getOwner } from '@ember/owner';

import { getAnchor, shouldHandle } from 'should-handle-link';

import type { Newable } from './type-utils.ts';
import type EmberRouter from '@ember/routing/router';
import type RouterService from '@ember/routing/router-service';

export { shouldHandle } from 'should-handle-link';

export interface Options {
  ignore?: string[];
}

export function properLinks(
  options: Options
): <Instance extends object, Klass = { new (...args: any[]): Instance }>(klass: Klass) => Klass;

export function properLinks<Instance extends object, Klass = { new (...args: any[]): Instance }>(
  klass: Klass
): Klass;
/**
 * @internal
 */
export function properLinks<Instance extends object, Klass = { new (...args: any[]): Instance }>(
  options: Options,
  klass: Klass
): Klass;

export function properLinks<Instance extends object, Klass = { new (...args: any[]): Instance }>(
  ...args: [Options] | [Klass] | [Options, Klass]
): Klass | ((klass: Klass) => Klass) {
  let options: Options = {};

  let klass: undefined | Klass = undefined;

  if (args.length === 2) {
    options = args[0];
    klass = args[1];
  } else if (args.length === 1) {
    if (typeof args[0] === 'object') {
      // TODO: how to get first arg type correct?
      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
      return (klass: Klass) => properLinks(args[0] as any, klass);
    } else {
      klass = args[0];
    }
  }

  const ignore = options.ignore || [];

  assert(`klass was not defined. possibile incorrect arity given to properLinks`, klass);

  return class RouterWithProperLinks extends (klass as unknown as Newable<EmberRouter>) {
    // SAFETY: we literally do not care about the args' type here,
    //         because we just call super
    constructor(...args: any[]) {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
      super(...args);

      setup(this, ignore);
    }
  } as unknown as Klass;
}

/**
 * Setup proper links without a decorator.
 * This function only requires that a framework object with an owner is passed.
 */
export function setup(parent: object, ignore?: string[]) {
  const handler = (event: MouseEvent) => {
    /**
     * event.target may not be an anchor,
     * it may be a span, svg, img, or any number of elements nested in <a>...</a>
     */
    const interactive = getAnchor(event);

    if (!interactive) return;

    const owner = getOwner(parent);

    assert('owner is not present', owner);

    const routerService = owner.lookup('service:router');

    handle(routerService, interactive, ignore ?? [], event);
  };

  document.body.addEventListener('click', handler, false);

  registerDestructor(parent, () => document.body.removeEventListener('click', handler));
}

export function handle(
  router: RouterService,
  element: HTMLAnchorElement,
  ignore: string[],
  event: MouseEvent
) {
  if (!shouldHandle(location.href, element, event, ignore)) {
    return;
  }

  const url = new URL(element.href);

  const fullHref = `${url.pathname}${url.search}${url.hash}`;

  const rootURL = router.rootURL;

  let withoutRootURL = fullHref.slice(rootURL.length);

  // re-add the "root" sigil
  // we removed it when we chopped off the rootURL,
  // because the rootURL often has this attached to it as well
  if (!withoutRootURL.startsWith('/')) {
    withoutRootURL = `/${withoutRootURL}`;
  }

  try {
    const routeInfo = router.recognize(fullHref);

    if (routeInfo) {
      event.preventDefault();
      event.stopImmediatePropagation();
      event.stopPropagation();

      router.transitionTo(withoutRootURL);

      return false;
    }
  } catch (e) {
    if (e instanceof Error && e.name === 'UnrecognizedURLError') {
      return;
    }

    throw e;
  }
}


---

import Helper from '@ember/component/helper';
import { assert } from '@ember/debug';
import { service } from '@ember/service';

import type RouterService from '@ember/routing/router-service';

interface Signature {
  Args: {
    Positional: [string];
  };
  Return: string | undefined;
}

/**
 * Grabs a query-param off the current route from the router service.
 *
 * ```gjs
 * import { qp } from 'ember-primitives/qp';
 *
 * <template>
 *  {{qp "query-param"}}
 * </template>
 * ```
 */
export class qp extends Helper<Signature> {
  @service declare router: RouterService;

  compute([name]: [string]): string | undefined {
    assert('A queryParam name is required', name);

    return this.router.currentRoute?.queryParams?.[name] as string | undefined;
  }
}

/**
 * Returns a string for use as an `href` on `<a>` tags, updated with the passed query param
 *
 * ```gjs
 * import { withQP } from 'ember-primitives/qp';
 *
 * <template>
 *   <a href={{withQP "foo" "2"}}>
 *     ...
 *   </a>
 * </template>
 * ```
 */
export class withQP extends Helper<{ Args: { Positional: [string, string] }; Return: string }> {
  @service declare router: RouterService;

  compute([qpName, nextValue]: [string, string]) {
    const existing = this.router.currentURL;

    assert('A queryParam name is required', qpName);
    assert('There is no currentURL', existing);

    const url = new URL(existing, location.origin);

    url.searchParams.set(qpName, nextValue);

    return url.href;
  }
}

/**
 * Cast a query-param string value to a boolean
 *
 * ```gjs
 * import { castToBoolean, qp } from 'ember-primitives/qp';
 *
 * <template>
 *  {{#if (castToBoolean (qp 'the-qp'))}}
 *    ...
 *  {{/if}}
 * </template>
 * ```
 *
 * The following values are considered "false"
 * - undefined
 * - ""
 * - "0"
 * - false
 * - "f"
 * - "off"
 * - "no"
 * - "null"
 * - "undefined"
 *
 * All other values are considered truthy
 */
export function castToBoolean(x: string | undefined) {
  if (!x) return false;

  const isFalsey =
    x === '0' ||
    x === 'false' ||
    x === 'f' ||
    x === 'null' ||
    x === 'off' ||
    x === 'undefined' ||
    x === 'no';

  if (isFalsey) return false;

  // All other values are considered truthy
  return true;
}


---

import { assert } from '@ember/debug';
import { registerDestructor } from '@ember/destroyable';

import { createStore } from './store.ts';
import { findOwner } from './utils.ts';

/**
 * Creates or returns the ResizeObserverManager.
 *
 * Only one of these will exist per owner.
 *
 * Has only two methods:
 * - observe(element, callback: (resizeObserverEntry) => void)
 * - unobserve(element, callback: (resizeObserverEntry) => void)
 *
 * Like with the underlying ResizeObserver API (and all event listeners),
 * the callback passed to unobserved must be the same reference as the one
 * passed to observe.
 */
export function resizeObserver(context: object) {
  const owner = findOwner(context);

  assert(
    `Could not find owner on the passed context (to resizeObserver). resizeObserver can only be used on an object whos lifetime is in someone entangled with the application (which incidentally has an "owner").`,
    owner
  );

  return createStore(owner, ResizeObserverManager);
}

class ResizeObserverManager {
  #callbacks = new WeakMap<Element, Set<(entry: ResizeObserverEntry) => unknown>>();

  #handleResize = (entries: ResizeObserverEntry[]) => {
    for (const entry of entries) {
      const callbacks = this.#callbacks.get(entry.target);

      if (callbacks) {
        for (const callback of callbacks) {
          callback(entry);
        }
      }
    }
  };
  #observer = new ResizeObserver(this.#handleResize);

  constructor() {
    ignoreROError();

    registerDestructor(this, () => {
      this.#observer?.disconnect();
    });
  }

  /**
   * Initiate the observing of the `element` or add an additional `callback`
   * if the `element` is already observed.
   *
   * @param {object} element
   * @param {function} callback The `callback` is called whenever the size of
   *    the `element` changes. It is called with `ResizeObserverEntry` object
   *    for the particular `element`.
   */
  observe(element: Element, callback: (entry: ResizeObserverEntry) => unknown) {
    const callbacks = this.#callbacks.get(element);

    if (callbacks) {
      callbacks.add(callback);
    } else {
      this.#callbacks.set(element, new Set([callback]));
      this.#observer.observe(element);
    }
  }

  /**
   * End the observing of the `element` or just remove the provided `callback`.
   *
   * It will unobserve the `element` if the `callback` is not provided
   * or there are no more callbacks left for this `element`.
   *
   * @param {object} element
   * @param {function?} callback - The `callback` to remove from the listeners
   *   of the `element` size changes.
   */
  unobserve(element: Element, callback: (entry: ResizeObserverEntry) => unknown) {
    const callbacks = this.#callbacks.get(element);

    if (!callbacks) {
      return;
    }

    callbacks.delete(callback);

    if (!callback || !callbacks.size) {
      this.#callbacks.delete(element);
      this.#observer.unobserve(element);
    }
  }
}

const errorMessages = [
  'ResizeObserver loop limit exceeded',
  'ResizeObserver loop completed with undelivered notifications.',
];

/**
 * Ignores "ResizeObserver loop limit exceeded" error in Ember tests.
 *
 * This "error" is safe to ignore as it is just a warning message,
 * telling that the "looping" observation will be skipped in the current frame,
 * and will be delivered in the next one.
 *
 * For some reason, it is fired as an `error` event at `window` failing Ember
 * tests and exploding Sentry with errors that must be ignored.
 */
export function ignoreROError() {
  if (typeof window.onerror !== 'function') {
    return;
  }

  const onError = window.onerror;

  window.onerror = (...args) => {
    const [message] = args;

    if (typeof message === 'string') {
      if (errorMessages.includes(message)) return true;
    }

    onError(...args);
  };
}


---

import { assert } from '@ember/debug';

import { getPromiseState } from 'reactiveweb/get-promise-state';

import { createStore } from './store.ts';
import { findOwner } from './utils.ts';

import type { Newable } from './type-utils.ts';

/*
import type { Newable } from './type-utils.ts';
import type { Registry } from '@ember/service';
import type Service from '@ember/service';

type Decorator = ReturnType<typeof emberService>;

// export function service<Key extends keyof Registry>(
//   context: object,
//   serviceName: Key
// ): Registry[Key] & Service;
export function service<Class extends object>(
  context: object,
  serviceDefinition: Newable<Class>
): Class;
export function service<Class extends object>(serviceDefinition: Newable<Class>): Decorator;
export function service<Key extends keyof Registry>(serviceName: Key): Decorator;
export function service(prototype: object, name: string | symbol, descriptor: unknown): void;
export function service<Value, Result>(
  context: object,
  fn: Parameters<typeof getPromiseState<Value, Result>>[0]
): ReturnType<typeof getPromiseState<Value, Result>>;
export function service<Value, Result>(
  fn: Parameters<typeof getPromiseState<Value, Result>>[0]
): Decorator;
*/

/**
 * Instantiates a class once per application instance.
 *
 *
 */
export function createService<Instance extends object>(
  context: object,
  theClass: Newable<Instance> | (() => Instance)
): Instance {
  const owner = findOwner(context);

  assert(
    `Could not find owner / application instance. Cannot create a instance tied to the application lifetime without the application`,
    owner
  );

  return createStore(owner, theClass);
}

const promiseCache = new WeakMap<() => any, unknown>();

/**
 * Lazily instantiate a service.
 *
 * This is a replacement / alternative API for ember's `@service` decorator from `@ember/service`.
 *
 * For example
 * ```js
 * import { service } from 'ember-primitives/service';
 *
 * const loader = () => {
 *   let module = await import('./foo/file/with/class.js');
 *   return () => new module.MyState();
 * }
 *
 * class Demo extends Component {
 *   state = createAsyncService(this, loader);
 * }
 * ```
 *
 * The important thing is for repeat usage of `createAsyncService` the second parameter,
 * (loader in this case), must be shared between all usages.
 *
 * This is an alternative to using `createStore` inside an await'd component,
 * or a component rendered with [`getPromiseState`](https://reactive.nullvoxpopuli.com/functions/get-promise-state.getPromiseState.html)
 * ```
 */
export function createAsyncService<Instance extends object>(
  context: object,
  theClass: () => Promise<Newable<Instance> | (() => Instance)>
): ReturnType<typeof getPromiseState<unknown, Instance>> {
  let existing = promiseCache.get(theClass);

  if (!existing) {
    existing = async () => {
      const result = await theClass();

      // Pay no attention to the lies, I don't know what the right type is here
      return createStore(context, result as Newable<Instance>);
    };

    promiseCache.set(theClass, existing);
  }

  // Pay no attention to the TS inference crime here
  return getPromiseState<unknown, Instance>(existing);
}


---

import { link } from 'reactiveweb/link';

import { isNewable } from './utils.ts';

import type { Newable } from './type-utils.ts';

/**
 * context => { class => instance }
 */
const contextCache = new WeakMap<object, Map<object, object>>();

/**
 * Creates a singleton for the given context and links the lifetime of the created class to the passed context
 *
 * Note that this function is _not_ lazy. Calling `createStore` will create an instance of the passed class.
 * When combined with a getter though, creation becomes lazy.
 *
 * In this example, `MyState` is created once per instance of the component.
 * repeat accesses to `this.foo` return a stable reference _as if_ `@cached` were used.
 * ```js
 * class MyState {}
 *
 * class Demo extends Component {
 *   // this is a stable reference
 *   get foo() {
 *     return createStore(this, MyState);
 *   }
 *
 *   // or
 *   bar = createStore(this, MyState);
 *
 *  // or
 *  three = createStore(this, () => new MyState(1, 2));
 * }
 * ```
 *
 * If arguments need to be configured during construction, the second argument may also be a function
 * ```js
 * class MyState {}
 *
 * class Demo extends Component {
 *   // this is a stable reference
 *   get foo() {
 *     return createStore(this, MyState);
 *   }
 * }
 * ```
 */
export function createStore<Instance extends object>(
  context: object,
  theClass: Newable<Instance> | (() => Instance)
): Instance {
  let cache = contextCache.get(context);

  if (!cache) {
    cache = new Map();
    contextCache.set(context, cache);
  }

  let existing = cache.get(theClass);

  if (!existing) {
    const instance = isNewable(theClass) ? new theClass() : theClass();

    link(instance, context);

    cache.set(theClass, instance);
    existing = instance;
  }

  return existing as Instance;
}


---

/**
 * Styles that are always needed, but their components
 * may not be are included here.
 */
import './components/visually-hidden.css';


---

import { registerDestructor } from '@ember/destroyable';

export async function setupTabster(
  /**
   * A destroyable object.
   * This is needed so that when the app (or tests) or unmounted or ending,
   * the tabster instance can be disposed of.
   */
  context: object,
  {
    tabster,
    setTabsterRoot,
  }: {
    /**
     * Let this setup function initalize tabster.
     * https://tabster.io/docs/core
     *
     * This should be done only once per application as we don't want
     * focus managers fighting with each other.
     *
     * Defaults to `true`,
     *
     * Will fallback to an existing tabster instance automatically if `getTabster` returns a value.
     *
     * If `false` is explicitly passed here, you'll also be in charge of teardown.
     */
    tabster?: boolean;
    setTabsterRoot?: boolean;
  } = {}
) {
  const { createTabster, getDeloser, getMover, getTabster, disposeTabster } =
    await import('tabster');

  tabster ??= true;
  setTabsterRoot ??= true;

  if (!tabster) {
    return;
  }

  const existing = getTabster(window);
  const primitivesTabster = existing ?? createTabster(window);

  getMover(primitivesTabster);
  getDeloser(primitivesTabster);

  if (setTabsterRoot) {
    document.body.setAttribute('data-tabster', '{ "root": {} }');
  }

  registerDestructor(context, () => {
    disposeTabster(primitivesTabster);
  });
}


---

// Easily allow apps, which are not yet using strict mode templates, to consume your Glint types, by importing this file.
// Add all your components, helpers and modifiers to the template registry here, so apps don't have to do this.
// See https://typed-ember.gitbook.io/glint/using-glint/ember/authoring-addons

import type { Accordion } from './components/accordion';
import type { AccordionContent } from './components/accordion/content';
import type { AccordionHeader } from './components/accordion/header';
import type { AccordionItem } from './components/accordion/item';
import type { AccordionTrigger } from './components/accordion/trigger';
import type { Dialog } from './components/dialog';
import type { ExternalLink } from './components/external-link';
import type { Link } from './components/link';
import type { Popover } from './components/popover';
import type { Portal } from './components/portal';
import type { PortalTargets } from './components/portal-targets';
import type { Shadowed } from './components/shadowed';
import type { Switch } from './components/switch';
import type { Toggle } from './components/toggle';
import type { service } from './helpers/service';

// import type MyComponent from './components/my-component';

// Remove this once entries have been added! 👇

export default interface Registry {
  // components
  Accordion: typeof Accordion;
  AccordionItem: typeof AccordionItem;
  AccordionHeader: typeof AccordionHeader;
  AccordionContent: typeof AccordionContent;
  AccordionTrigger: typeof AccordionTrigger;
  Dialog: typeof Dialog;
  ExternalLink: typeof ExternalLink;
  Link: typeof Link;
  Popover: typeof Popover;
  PortalTargets: typeof PortalTargets;
  Portal: typeof Portal;
  Shadowed: typeof Shadowed;
  Switch: typeof Switch;
  Toggle: typeof Toggle;

  // helpers
  service: typeof service;
}


---

export { setupTabster } from "./test-support/a11y.ts";
export { findInFirstShadow, findInShadow, findShadow, hasShadowRoot } from "./test-support/dom.ts";
export { fillOTP } from "./test-support/otp.ts";
export { rating } from "./test-support/rating.ts";
export { getRouter, setupRouting } from "./test-support/routing.ts";
export { ZoetropeHelper } from "./test-support/zoetrope.ts";


---

export type Newable<T extends object = object> = { new (...args: any[]): T };


---

import { getOwner } from '@ember/owner';

import type Owner from '@ember/owner';

// this is copy pasted from https://github.com/emberjs/ember.js/blob/60d2e0cddb353aea0d6e36a72fda971010d92355/packages/%40ember/-internals/glimmer/lib/helpers/unique-id.ts
// Unfortunately due to https://github.com/emberjs/ember.js/issues/20165 we cannot use the built-in version in template tags
export function uniqueId(): string {
  // @ts-expect-error this one-liner abuses weird JavaScript semantics that
  // TypeScript (legitimately) doesn't like, but they're nonetheless valid and
  // specced.
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-unsafe-member-access
  return ([3e7] + -1e3 + -4e3 + -2e3 + -1e11).replace(/[0-3]/g, (a) =>
    ((a * 4) ^ ((Math.random() * 16) >> (a & 2))).toString(16)
  );
}

export function isNewable(x: any): x is new (...args: unknown[]) => NonNullable<object> {
  // TypeScript has really bad prototype support -- they don't really
  // want folks using this sort of stuff -- but it's handy for perf and all that.
  //
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
  return x.prototype?.constructor === x;
}

/**
 * Loose check for an "ownerish" API.
 * only the ".lookup" method is required.
 *
 * The requirements for what an "owner" is are sort of undefined,
 * as the actual owner in ember applications has too much on it,
 * and the long term purpose of the owner will be questioned once we
 * eliminate the need to have a registry (what lookup looks in to),
 * but we'll still need "Something" to represent the lifetime of the application.
 *
 * Technically, the owner could be any object, including `{}`
 */
export function isOwner(x: unknown): x is Owner {
  if (!isNonNullableObject(x)) return false;

  return 'lookup' in x && typeof x.lookup === 'function';
}

export function isNonNullableObject(x: unknown): x is NonNullable<object> {
  if (typeof x !== 'object') return false;
  if (x === null) return false;

  return true;
}

/**
 * Can receive the class instance or the owner itself, and will always return return the owner.
 *
 * undefined will be returned if the Owner does not exist on the passed object
 *
 * Can be useful when combined with `createStore` to then create "services",
 * which don't require string lookup.
 */
export function findOwner(contextOrOwner: unknown): Owner | undefined {
  if (isOwner(contextOrOwner)) return contextOrOwner;

  // _ENSURE_ that we have an object, else getOwner makes no sense to call
  if (!isNonNullableObject(contextOrOwner)) return;

  const maybeOwner = getOwner(contextOrOwner);

  if (isOwner(maybeOwner)) return maybeOwner;

  if ('owner' in contextOrOwner) {
    const maybeOwner = contextOrOwner.owner;

    if (isOwner(maybeOwner)) return maybeOwner;
  }

  return;
}


---

export { InViewport, type InViewportSignature } from './viewport/in-viewport.gts';
export { viewport, type ViewportOptions } from './viewport/viewport.ts';


---

import Component from "@glimmer/component";
import { assert } from "@ember/debug";
import { hash } from "@ember/helper";

// temp
//  https://github.com/tracked-tools/tracked-toolbox/issues/38
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { localCopy } from "tracked-toolbox";

import AccordionItem from "./accordion/item.gts";

import type { WithBoundArgs } from "@glint/template";

type AccordionSingleArgs = {
  /**
   * The type of accordion. If `single`, only one item can be selected at a time. If `multiple`, multiple items can be selected at a time.
   */
  type: "single";
  /**
   * Whether the accordion is disabled. When `true`, all items cannot be expanded or collapsed.
   */
  disabled?: boolean;
  /**
   * When type is `single`, whether the accordion is collapsible. When `true`, the selected item can be collapsed by clicking its trigger.
   */
  collapsible?: boolean;
} & (
  | {
      /**
       * The currently selected value. To be used in a controlled fashion in conjunction with `onValueChange`.
       */
      value: string;
      /**
       * A callback that is called when the selected value changes. To be used in a controlled fashion in conjunction with `value`.
       */
      onValueChange: (value: string | undefined) => void;
      /**
       * Not available in a controlled fashion.
       */
      defaultValue?: never;
    }
  | {
      /**
       * Not available in an uncontrolled fashion.
       */
      value?: never;
      /**
       * Not available in an uncontrolled fashion.
       */
      onValueChange?: never;
      /**
       * The default value of the accordion. To be used in an uncontrolled fashion.
       */
      defaultValue?: string;
    }
);

type AccordionMultipleArgs = {
  /**
   * The type of accordion. If `single`, only one item can be selected at a time. If `multiple`, multiple items can be selected at a time.
   */
  type: "multiple";
  /**
   * Whether the accordion is disabled. When `true`, all items cannot be expanded or collapsed.
   */
  disabled?: boolean;
} & (
  | {
      /**
       * The currently selected values. To be used in a controlled fashion in conjunction with `onValueChange`.
       */
      value: string[];
      /**
       * A callback that is called when the selected values change. To be used in a controlled fashion in conjunction with `value`.
       */
      onValueChange: (value?: string[]) => void;
      /**
       * Not available in a controlled fashion.
       */
      defaultValue?: never;
    }
  | {
      /**
       * Not available in an uncontrolled fashion.
       */
      value?: never;
      /**
       * Not available in an uncontrolled fashion.
       */
      onValueChange?: never;
      /**
       * The default values of the accordion. To be used in an uncontrolled fashion.
       */
      defaultValue?: string[];
    }
);

export class Accordion extends Component<{
  Element: HTMLDivElement;
  Args: AccordionSingleArgs | AccordionMultipleArgs;
  Blocks: {
    default: [
      {
        /**
         * The AccordionItem component.
         */
        Item: WithBoundArgs<typeof AccordionItem, "selectedValue" | "toggleItem" | "disabled">;
      },
    ];
  };
}> {
  <template>
    <div data-disabled={{@disabled}} ...attributes>
      {{yield
        (hash
          Item=(component
            AccordionItem
            selectedValue=this.selectedValue
            toggleItem=this.toggleItem
            disabled=@disabled
          )
        )
      }}
    </div>
  </template>

  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
  @localCopy("args.defaultValue") declare _internallyManagedValue?: string | string[];

  get selectedValue() {
    return this.args.value ?? this._internallyManagedValue;
  }

  toggleItem = (value: string) => {
    if (this.args.disabled) {
      return;
    }

    if (this.args.type === "single") {
      this.toggleItemSingle(value);
    } else if (this.args.type === "multiple") {
      this.toggleItemMultiple(value);
    }
  };

  toggleItemSingle = (value: string) => {
    assert("Cannot call `toggleItemSingle` when `disabled` is true.", !this.args.disabled);
    assert(
      "Cannot call `toggleItemSingle` when `type` is not `single`.",
      this.args.type === "single",
    );

    if (value === this.selectedValue && !this.args.collapsible) {
      return;
    }

    const newValue = value === this.selectedValue ? undefined : value;

    if (this.args.onValueChange) {
      this.args.onValueChange(newValue);
    } else {
      this._internallyManagedValue = newValue;
    }
  };

  toggleItemMultiple = (value: string) => {
    assert("Cannot call `toggleItemMultiple` when `disabled` is true.", !this.args.disabled);
    assert(
      "Cannot call `toggleItemMultiple` when `type` is not `multiple`.",
      this.args.type === "multiple",
    );

    const currentValues = (this.selectedValue as string[] | undefined) ?? [];
    const indexOfValue = currentValues.indexOf(value);
    let newValue: string[];

    if (indexOfValue === -1) {
      newValue = [...currentValues, value];
    } else {
      newValue = [
        ...currentValues.slice(0, indexOfValue),
        ...currentValues.slice(indexOfValue + 1),
      ];
    }

    if (this.args.onValueChange) {
      this.args.onValueChange(newValue);
    } else {
      this._internallyManagedValue = newValue;
    }
  };
}

export default Accordion;


---

import { hash } from "@ember/helper";

import { ReactiveImage } from "reactiveweb/image";
import { WaitUntil } from "reactiveweb/wait-until";

import type { TOC } from "@ember/component/template-only";
import type { WithBoundArgs } from "@glint/template";

const Fallback: TOC<{
  Blocks: { default: [] };
  Args: {
    /**
     * The number of milliseconds to wait for the image to load
     * before displaying the fallback
     */
    delayMs?: number;
    /**
     * @private
     * Bound internally by ember-primitives
     */
    isLoaded: boolean;
  };
}> = <template>
  {{#unless @isLoaded}}
    {{#let (WaitUntil @delayMs) as |delayFinished|}}
      {{#if delayFinished}}
        {{yield}}
      {{/if}}
    {{/let}}
  {{/unless}}
</template>;

const Image: TOC<{
  Element: HTMLImageElement;
  Args: {
    /**
     * @private
     * The `src` value for the image.
     *
     * Bound internally by ember-primitives
     */
    src: string;
    /**
     * @private
     * Bound internally by ember-primitives
     */
    isLoaded: boolean;
  };
}> = <template>
  {{#if @isLoaded}}
    <img alt="__missing__" ...attributes src={{@src}} />
  {{/if}}
</template>;

export const Avatar: TOC<{
  Element: HTMLSpanElement;
  Args: {
    /**
     * The `src` value for the image.
     */
    src: string;
  };
  Blocks: {
    default: [
      avatar: {
        /**
         * The image to render. It will only render when it has loaded.
         */
        Image: WithBoundArgs<typeof Image, "src" | "isLoaded">;
        /**
         * An element that renders when the image hasn't loaded.
         * This means whilst it's loading, or if there was an error.
         * If you notice a flash during loading,
         * you can provide a delayMs prop to delay its rendering so it only renders for those with slower connections.
         */
        Fallback: WithBoundArgs<typeof Fallback, "isLoaded">;
        /**
         * true while the image is loading
         */
        isLoading: boolean;
        /**
         * If the image fails to load, this will be `true`
         */
        isError: boolean;
      },
    ];
  };
}> = <template>
  {{#let (ReactiveImage @src) as |imgState|}}
    <span
      data-prim-avatar
      ...attributes
      data-loading={{imgState.isLoading}}
      data-error={{imgState.isError}}
    >
      {{yield
        (hash
          Image=(component Image src=@src isLoaded=imgState.isResolved)
          Fallback=(component Fallback isLoaded=imgState.isResolved)
          isLoading=imgState.isLoading
          isError=imgState.isError
        )
      }}
    </span>
  {{/let}}
</template>;

export default Avatar;


---

import { hash } from "@ember/helper";

import { Separator } from "./separator.gts";

import type { TOC } from "@ember/component/template-only";
import type { WithBoundArgs } from "@glint/template";

export interface Signature {
  Element: HTMLElement;
  Args: {
    /**
     * The accessible label for the breadcrumb navigation.
     * Defaults to "Breadcrumb"
     */
    label?: string;
  };
  Blocks: {
    default: [
      {
        /**
         * A separator component to place between breadcrumb items.
         * Typically renders as "/" or ">" and is decorative (aria-hidden="true").
         * Pre-configured to render as an `<li>` element for proper HTML structure.
         */
        Separator: WithBoundArgs<typeof Separator, "as" | "decorative">;
      },
    ];
  };
}

/**
 * A breadcrumb navigation component that displays the current page's location within a navigational hierarchy.
 *
 * Breadcrumbs help users understand their current location and provide a way to navigate back through the hierarchy.
 *
 * For example:
 *
 * ```gjs live preview
 * import { Breadcrumb } from 'ember-primitives';
 *
 * <template>
 *   <Breadcrumb as |b|>
 *     <li>
 *       <a href="/">Home</a>
 *     </li>
 *     <b.Separator>/</b.Separator>
 *     <li>
 *       <a href="/docs">Docs</a>
 *     </li>
 *     <b.Separator>/</b.Separator>
 *     <li aria-current="page">
 *       Breadcrumb
 *     </li>
 *   </Breadcrumb>
 * </template>
 * ```
 */
export const Breadcrumb: TOC<Signature> = <template>
  <nav aria-label={{if @label @label "Breadcrumb"}} ...attributes>
    <ol>
      {{yield (hash Separator=(component Separator as="li" decorative=true))}}
    </ol>
  </nav>
</template>;

export default Breadcrumb;


---

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { assert } from "@ember/debug";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";

import { modifier as eModifier } from "ember-modifier";
// temp
//  https://github.com/tracked-tools/tracked-toolbox/issues/38
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import { localCopy } from "tracked-toolbox";

import type { TOC } from "@ember/component/template-only";
import type { ModifierLike, WithBoundArgs } from "@glint/template";

const DialogElement: TOC<{
  Element: HTMLDialogElement;
  Args: {
    /**
     * @internal
     */
    open: boolean | undefined;
    /**
     * @internal
     */
    onClose: () => void;

    /**
     * @internal
     */
    register: ModifierLike<{ Element: HTMLDialogElement }>;
  };
  Blocks: { default: [] };
}> = <template>
  <dialog ...attributes open={{@open}} {{on "close" @onClose}} {{@register}}>
    {{yield}}
  </dialog>
</template>;

export interface Signature {
  Args: {
    /**
     * Optionally set the open state of the `<dialog>`
     * The state will still be managed internally,
     * so this does not need to be a maintained value, but whenever it changes,
     * the dialog element will reflect that change accordingly.
     */
    open?: boolean;
    /**
     * When the `<dialog>` is closed, this function will be called
     * and the `<dialog>`'s `returnValue` will be passed.
     *
     * This can be used to determine which button was clicked to close the modal
     *
     * Note though that this value is only populated when using
     * `<form method='dialog'>`
     */
    onClose?: (returnValue: string) => void;
  };
  Blocks: {
    default: [
      {
        /**
         * Represents the open state of the `<dialog>` element.
         */
        isOpen: boolean;

        /**
         * Closes the `<dialog>` element
         * Will throw an error if `Dialog` is not rendered.
         */
        close: () => void;

        /**
         * Opens the `<dialog>` element.
         * Will throw an error if `Dialog` is not rendered.
         */
        open: () => void;

        /**
         * This modifier should be applied to the button that opens the Dialog so that it can be re-focused when the dialog closes.
         *
         * Example:
         *
         * ```gjs
         * <template>
         *   <Modal as |m|>
         *     <button {{m.focusOnClose}} {{on "click" m.open}}>Open</button>
         *
         *     <m.Dialog>...</m.Dialog>
         *   </Modal>
         * </template>
         * ```
         */
        focusOnClose: ModifierLike<{ Element: HTMLElement }>;

        /**
         * This is the `<dialog>` element (with some defaults pre-wired).
         * This is required to be rendered.
         */
        Dialog: WithBoundArgs<typeof DialogElement, "onClose" | "register" | "open">;
      },
    ];
  };
}

class ModalDialog extends Component<Signature> {
  <template>
    {{yield
      (hash
        isOpen=this.isOpen
        open=this.open
        close=this.close
        focusOnClose=this.refocus
        Dialog=(component DialogElement open=@open onClose=this.handleClose register=this.register)
      )
    }}
  </template>

  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
  @localCopy("args.open") declare _isOpen: boolean;

  get isOpen() {
    /**
     * Always fallback to false (closed)
     */
    return this._isOpen ?? false;
  }
  set isOpen(val: boolean) {
    this._isOpen = val;
  }

  #lastIsOpen = false;
  refocus = eModifier((element) => {
    assert(`focusOnClose is only valid on a HTMLElement`, element instanceof HTMLElement);

    if (!this.isOpen && this.#lastIsOpen) {
      element.focus();
    }

    this.#lastIsOpen = this.isOpen;
  });

  @tracked declare dialogElement: HTMLDialogElement | undefined;

  register = eModifier((element: HTMLDialogElement) => {
    /**
     * This is very sad.
     *
     * But we need the element to be 'root state'
     * so that when we read things like "isOpen",
     * when the dialog is finally rendered, all the
     * downstream properties render.
     *
     * This has to be an async / delayed a bit, so that
     * the tracking frame can exit, and we don't infinite loop
     */
    void (async () => {
      await Promise.resolve();

      this.dialogElement = element;
    })();
  });

  /**
   * Closes the dialog -- this will throw an error in development if the dialog element was not rendered
   */
  close = () => {
    assert(
      "Cannot call `close` on <Dialog> without rendering the dialog element.",
      this.dialogElement,
    );

    /**
     * If the element is already closed, don't run all this again
     */
    if (!this.dialogElement.hasAttribute("open")) {
      return;
    }

    /**
     * removes the `open` attribute
     * handleClose will be called because the dialog has bound the `close` event.
     */
    this.dialogElement.close();
  };

  /**
   * @internal
   *
   * handles the <dialog> element's native close behavior.
   * listened to via addEventListener('close', ...);
   */
  handleClose = () => {
    assert(
      "Cannot call `handleDialogClose` on <Dialog> without rendering the dialog element. This is likely a bug in ember-primitives. Please open an issue <3",
      this.dialogElement,
    );

    this.isOpen = false;
    this.args.onClose?.(this.dialogElement.returnValue);
    // the return value ends up staying... which is annoying
    this.dialogElement.returnValue = "";
  };

  /**
   * Opens the dialog -- this will throw an error in development if the dialog element was not rendered
   */
  open = () => {
    assert(
      "Cannot call `open` on <Dialog> without rendering the dialog element.",
      this.dialogElement,
    );

    /**
     * If the element is already open, don't run all this again
     */
    if (this.dialogElement.hasAttribute("open")) {
      return;
    }

    /**
     * adds the `open` attribute
     */
    this.dialogElement.showModal();
    this.isOpen = true;
  };
}

export const Modal = ModalDialog;
export const Dialog = ModalDialog;

export default ModalDialog;


---

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { assert } from "@ember/debug";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";

import { modifier as eModifier } from "ember-modifier";
// temp
//  https://github.com/tracked-tools/tracked-toolbox/issues/38
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import { localCopy } from "tracked-toolbox";

import type { TOC } from "@ember/component/template-only";
import type { ModifierLike, WithBoundArgs } from "@glint/template";

const DrawerElement: TOC<{
  Element: HTMLDialogElement;
  Args: {
    /**
     * @internal
     */
    open: boolean | undefined;
    /**
     * @internal
     */
    onClose: () => void;

    /**
     * @internal
     */
    register: ModifierLike<{ Element: HTMLDialogElement }>;
  };
  Blocks: { default: [] };
}> = <template>
  <dialog ...attributes open={{@open}} {{on "close" @onClose}} {{@register}}>
    {{yield}}
  </dialog>
</template>;

export interface Signature {
  Args: {
    /**
     * Optionally set the open state of the drawer
     * The state will still be managed internally,
     * so this does not need to be a maintained value, but whenever it changes,
     * the drawer element will reflect that change accordingly.
     */
    open?: boolean;
    /**
     * When the drawer is closed, this function will be called
     * and the drawer's `returnValue` will be passed.
     *
     * This can be used to determine which button was clicked to close the drawer
     *
     * Note though that this value is only populated when using
     * `<form method='dialog'>`
     */
    onClose?: (returnValue: string) => void;
  };
  Blocks: {
    default: [
      {
        /**
         * Represents the open state of the drawer element.
         */
        isOpen: boolean;

        /**
         * Closes the drawer element
         * Will throw an error if `Drawer` is not rendered.
         */
        close: () => void;

        /**
         * Opens the drawer element.
         * Will throw an error if `Drawer` is not rendered.
         */
        open: () => void;

        /**
         * This modifier should be applied to the button that opens the Drawer so that it can be re-focused when the drawer closes.
         *
         * Example:
         *
         * ```gjs
         * <template>
         *   <Drawer as |d|>
         *     <button {{d.focusOnClose}} {{on "click" d.open}}>Open</button>
         *
         *     <d.Drawer>...</d.Drawer>
         *   </Drawer>
         * </template>
         * ```
         */
        focusOnClose: ModifierLike<{ Element: HTMLElement }>;

        /**
         * This is the `<dialog>` element (with some defaults pre-wired).
         * This is required to be rendered.
         */
        Drawer: WithBoundArgs<typeof DrawerElement, "onClose" | "register" | "open">;
      },
    ];
  };
}

class DrawerDialog extends Component<Signature> {
  <template>
    {{yield
      (hash
        isOpen=this.isOpen
        open=this.open
        close=this.close
        focusOnClose=this.refocus
        Drawer=(component DrawerElement open=@open onClose=this.handleClose register=this.register)
      )
    }}
  </template>

  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
  @localCopy("args.open") declare _isOpen: boolean;

  get isOpen() {
    /**
     * Always fallback to false (closed)
     */
    return this._isOpen ?? false;
  }
  set isOpen(val: boolean) {
    this._isOpen = val;
  }

  #lastIsOpen = false;
  refocus = eModifier((element) => {
    assert(`focusOnClose is only valid on a HTMLElement`, element instanceof HTMLElement);

    if (!this.isOpen && this.#lastIsOpen) {
      element.focus();
    }

    this.#lastIsOpen = this.isOpen;
  });

  @tracked declare drawerElement: HTMLDialogElement | undefined;

  register = eModifier((element: HTMLDialogElement) => {
    /**
     * This is very sad.
     *
     * But we need the element to be 'root state'
     * so that when we read things like "isOpen",
     * when the drawer is finally rendered, all the
     * downstream properties render.
     *
     * This has to be an async / delayed a bit, so that
     * the tracking frame can exit, and we don't infinite loop
     */
    void (async () => {
      await Promise.resolve();

      this.drawerElement = element;
    })();
  });

  /**
   * Closes the drawer -- this will throw an error in development if the drawer element was not rendered
   */
  close = () => {
    assert(
      "Cannot call `close` on <Drawer> without rendering the drawer element.",
      this.drawerElement,
    );

    /**
     * If the element is already closed, don't run all this again
     */
    if (!this.drawerElement.hasAttribute("open")) {
      return;
    }

    /**
     * removes the `open` attribute
     * handleClose will be called because the drawer has bound the `close` event.
     */
    this.drawerElement.close();
  };

  /**
   * @internal
   *
   * handles the <dialog> element's native close behavior.
   * listened to via addEventListener('close', ...);
   */
  handleClose = () => {
    assert(
      "Cannot call `handleClose` on <Drawer> without rendering the drawer element. This is likely a bug in ember-primitives. Please open an issue <3",
      this.drawerElement,
    );

    this.isOpen = false;
    this.args.onClose?.(this.drawerElement.returnValue);
    // the return value ends up staying... which is annoying
    this.drawerElement.returnValue = "";
  };

  /**
   * Opens the drawer -- this will throw an error in development if the drawer element was not rendered
   */
  open = () => {
    assert(
      "Cannot call `open` on <Drawer> without rendering the drawer element.",
      this.drawerElement,
    );

    /**
     * If the element is already open, don't run all this again
     */
    if (this.drawerElement.hasAttribute("open")) {
      return;
    }

    /**
     * adds the `open` attribute
     */
    this.drawerElement.showModal();
    this.isOpen = true;
  };
}

export const Drawer = DrawerDialog;

export default DrawerDialog;


---

import type { TOC } from "@ember/component/template-only";

export const ExternalLink: TOC<{
  Element: HTMLAnchorElement;
  Blocks: {
    default: [];
  };
}> = <template>
  <a target="_blank" rel="noreferrer noopener" href="##missing##" ...attributes>
    {{yield}}
  </a>
</template>;

export default ExternalLink;


---

import { fn } from "@ember/helper";
import { on } from "@ember/modifier";

import { dataFrom } from "form-data-utils";

import type { TOC } from "@ember/component/template-only";

type Data = ReturnType<typeof dataFrom>;

export const dataFromEvent = dataFrom;

const handleInput = (
  onChange: (data: Data, eventType: "input" | "submit", event: Event) => void,
  event: Event | SubmitEvent,
  eventType: "input" | "submit" = "input",
) => {
  const data = dataFrom(event);

  onChange(data, eventType, event);
};

const handleSubmit = (
  onChange: (data: Data, eventType: "input" | "submit", event: Event | SubmitEvent) => void,
  event: SubmitEvent,
) => {
  event.preventDefault();
  handleInput(onChange, event, "submit");
};

export interface Signature {
  Element: HTMLFormElement;
  Args: {
    /**
     *  Any time the value of any field is changed this function will be called.
     */
    onChange: (
      /**
       * The data from the form as an Object of `{ [field name] => value }` pairs.
       * This is generated from the native [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
       *
       * Additional fields/inputs/controls can be added to this data by specifying a
       * "name" attribute.
       */
      data: Data,
      /**
       * Indicates whether the `onChange` function was called from the `input` or `submit` event handlers.
       */
      eventType: "input" | "submit",
      /**
       * The raw event, if needed.
       */
      event: Event | SubmitEvent,
    ) => void;
  };
  Blocks: {
    /**
     * The main content for the form. This is where inputs / fields / controls would go.
     * Within the `<form>` content, `<button type="submit">` will submit the form, which
     * triggers the `@onChange` event.
     */
    default: [];
  };
}

export const Form: TOC<Signature> = <template>
  <form
    {{on "input" (fn handleInput @onChange)}}
    {{on "submit" (fn handleSubmit @onChange)}}
    ...attributes
  >
    {{yield}}
  </form>
</template>;

export default Form;


---

import Component from "@glimmer/component";

import { element } from "ember-element-helper";
import { getSectionHeadingLevel } from "which-heading-do-i-need";

import type Owner from "@ember/owner";

export class Heading extends Component<{
  Element: HTMLElement;
  Blocks: { default: [] };
}> {
  headingScopeAnchor: Text;
  constructor(owner: Owner, args: object) {
    super(owner, args);

    this.headingScopeAnchor = document.createTextNode("");
  }

  get level() {
    return getSectionHeadingLevel(this.headingScopeAnchor);
  }

  get hLevel() {
    return `h${this.level}`;
  }

  <template>
    {{this.headingScopeAnchor}}

    {{#let (element this.hLevel) as |El|}}
      <El ...attributes>
        {{yield}}
      </El>
    {{/let}}
  </template>
}


---

import type { TOC } from "@ember/component/template-only";

const isLast = (collection: unknown[], index: number) => index === collection.length - 1;
const isNotLast = (collection: unknown[], index: number) => !isLast(collection, index);
const isMac = navigator.userAgent.indexOf("Mac OS") >= 0;

function split(str: string) {
  const keys = str.split("+").map((x) => x.trim());

  return keys;
}

function getKeys(keys: string[] | string, mac?: string[] | string) {
  const normalKeys = Array.isArray(keys) ? keys : split(keys);

  if (!mac) {
    return normalKeys;
  }

  const normalMac = Array.isArray(mac) ? mac : split(mac);

  return isMac ? normalMac : normalKeys;
}

export interface KeyComboSignature {
  Element: HTMLElement;
  Args: {
    keys: string[] | string;
    mac?: string[] | string;
  };
}

export const KeyCombo: TOC<KeyComboSignature> = <template>
  <span class="ember-primitives__key-combination" ...attributes>
    {{#let (getKeys @keys @mac) as |keys|}}
      {{#each keys as |key i|}}
        <Key>{{key}}</Key>
        {{#if (isNotLast keys i)}}
          <span class="ember-primitives__key-combination__separator">+</span>
        {{/if}}
      {{/each}}
    {{/let}}
  </span>
</template>;

export interface KeySignature {
  Element: HTMLElement;
  Blocks: { default?: [] };
}

export const Key: TOC<KeySignature> = <template>
  <kbd class="ember-primitives__key" ...attributes>{{yield}}</kbd>
</template>;


---

/**
 * TODO: make template-only component,
 * and use class-based modifier?
 *
 * This would require that modifiers could run pre-render
 */
import { hash } from '@ember/helper';
import { on } from '@ember/modifier';

import { link } from '../helpers/link.ts';
import { ExternalLink } from './external-link.gts';

import type { TOC } from '@ember/component/template-only';

export interface Signature {
  Element: HTMLAnchorElement;
  Args: {
    /**
     * the `href` string value to set on the anchor element.
     */
    href: string;
    /**
     * When calculating the "active" state of the link, you may decide
     * whether or not you want to _require_ that all query params be considered (true)
     * or specify individual query params, ignoring anything not specified.
     *
     * For example:
     *
     * ```gjs live preview
     * import { Link } from 'ember-primitives';
     *
     * <template>
     *   <Link @href="/" @includeActiveQueryParams={{true}} as |a|>
     *     ...
     *   </Link>
     * </template>
     * ```
     *
     * the data-active state here will only be "true" on
     * - `/`
     * - `/?foo=2`
     * - `/?foo=&bar=`
     *
     */
    includeActiveQueryParams?: true | string[];
    /**
     * When calculating the "active" state of the link, you may decide
     * whether or not you want to consider sub paths to be active when
     * child routes/urls are active.
     *
     * For example:
     *
     * ```gjs live preview
     * import { Link } from 'ember-primitives';
     *
     * <template>
     *   <Link @href="/forum/1" @activeOnSubPaths={{true}} as |a|>
     *     ...
     *   </Link>
     * </template>
     * ```
     *
     * the data-active state here will be "true" on
     * - `/forum/1`
     * - `/forum/1/posts`
     * - `/forum/1/posts/comments`
     * - `/forum/1/*etc*`
     *
     * if `@activeOnSubPaths` is set to false or left off
     * the data-active state here will only be "true" on
     * - `/forum/1`
     *
     */
    activeOnSubPaths?: true;
  };
  Blocks: {
    default: [
      {
        /**
         * Indicates if the passed `href` is pointing to an external site.
         * Useful if you want your links to have additional context for when
         * a user is about to leave your site.
         *
         * For example:
         *
         * ```gjs live preview
         * import { Link } from 'ember-primitives';
         *
         * const MyLink = <template>
         *   <Link @href={{@href}} as |a|>
         *     {{yield}}
         *     {{#if a.isExternal}}
         *       ➚
         *     {{/if}}
         *   </Link>
         * </template>;
         *
         * <template>
         *   <MyLink @href="https://developer.mozilla.org">MDN</MyLink> &nbsp;&nbsp;
         *   <MyLink @href="/">Home</MyLink>
         *  </template>
         * ```
         */
        isExternal: boolean;
        /**
         * Indicates if the passed `href` is *active*, or the user is on the same basepath.
         * This allows consumers to style their link if they wish or style their text.
         * The active state will also be present on a `data-active` attribute on the generated anchor tag.
         *
         *
         * For example
         * ```gjs
         * import { Link, service } from 'ember-primitives';
         *
         * const MyLink = <template>
         *   <Link @href="..."> as |a|>
         *     <span class="{{if a.isActive 'underline'}}">
         *     {{yield}}
         *     </span>
         *   </Link>
         * </template>
         *
         * <template>
         * {{#let (service 'router') as |router|}}
         *     <MyLink @href={{router.currentURL}}>Ths page</MyLink> &nbsp;&nbsp;
         *     <MyLink @href="/">Home</MyLink>
         *   {{/let}}
         *  </template>
         * ```
         *
         * By default, the query params are omitted from `isActive` calculation, but you may
         * configure the query params to be included if you wish
         * See: `@includeActiveQueryParams`
         *
         * By default, only the exact route/url is considered for the `isActive` calculation,
         * but you may configure sub routes/paths to also be considered active
         * See: `@activeOnSubPaths`
         *
         * Note that external links are never active.
         */
        isActive: boolean;
      },
    ];
  };
}

/**
 * A light wrapper around the [Anchor element][mdn-a], which will appropriately make your link an external link if the passed `@href` is not on the same domain.
 *
 *
 * [mdn-a]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a
 */
export const Link: TOC<Signature> = <template>
  {{#let (link @href includeActiveQueryParams=@includeActiveQueryParams activeOnSubPaths=@activeOnSubPaths) as |l|}}
    {{#if l.isExternal}}
      <ExternalLink href={{@href}} ...attributes>
        {{yield (hash isExternal=true isActive=false)}}
      </ExternalLink>
    {{else}}
      <a
        data-active={{l.isActive}}
        href={{if @href @href "##missing##"}}
        {{on "click" l.handleClick}}
        ...attributes
      >
        {{yield (hash isExternal=false isActive=l.isActive)}}
      </a>
    {{/if}}
  {{/let}}
</template>;

export default Link;


---

import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { guidFor } from "@ember/object/internals";

import { modifier as eModifier } from "ember-modifier";
import { cell } from "ember-resources";
import { getTabster, getTabsterAttribute, MoverDirections, setTabsterAttribute } from "tabster";

import { Link, type Signature as LinkSignature } from "./link.gts";
import { Popover, type Signature as PopoverSignature } from "./popover.gts";

import type { TOC } from "@ember/component/template-only";
import type { WithBoundArgs } from "@glint/template";

type Cell<V> = ReturnType<typeof cell<V>>;
type LinkArgs = LinkSignature["Args"];
type PopoverArgs = PopoverSignature["Args"];
type PopoverBlockParams = PopoverSignature["Blocks"]["default"][0];

const TABSTER_CONFIG_CONTENT = getTabsterAttribute(
  {
    mover: {
      direction: MoverDirections.Both,
      cyclic: true,
    },
    deloser: {},
  },
  true,
);

const TABSTER_CONFIG_TRIGGER = {
  deloser: {},
};

export interface Signature {
  Args: PopoverArgs;
  Blocks: {
    default: [
      {
        arrow: PopoverBlockParams["arrow"];
        trigger: WithBoundArgs<
          typeof trigger,
          "triggerElement" | "contentId" | "isOpen" | "setReference"
        >;
        Trigger: WithBoundArgs<typeof Trigger, "triggerModifier">;
        Content: WithBoundArgs<
          typeof Content,
          "triggerElement" | "contentId" | "isOpen" | "PopoverContent"
        >;
        isOpen: boolean;
      },
    ];
  };
}

export interface SeparatorSignature {
  Element: HTMLDivElement;
  Blocks: { default: [] };
}

const Separator: TOC<SeparatorSignature> = <template>
  <div role="separator" ...attributes>
    {{yield}}
  </div>
</template>;

/**
 * We focus items on `pointerMove` to achieve the following:
 *
 * - Mouse over an item (it focuses)
 * - Leave mouse where it is and use keyboard to focus a different item
 * - Wiggle mouse without it leaving previously focused item
 * - Previously focused item should re-focus
 *
 * If we used `mouseOver`/`mouseEnter` it would not re-focus when the mouse
 * wiggles. This is to match native menu implementation.
 */
function focusOnHover(e: PointerEvent) {
  const item = e.currentTarget;

  if (item instanceof HTMLElement) {
    item?.focus();
  }
}

interface PrivateItemSignature {
  Element: HTMLButtonElement;
  Args: { onSelect?: (event: Event) => void; toggle: () => void };
  Blocks: { default: [] };
}

export interface ItemSignature {
  Element: PrivateItemSignature["Element"];
  Args: Omit<PrivateItemSignature["Args"], "toggle">;
  Blocks: PrivateItemSignature["Blocks"];
}

const Item: TOC<PrivateItemSignature> = <template>
  {{! @glint-expect-error }}
  {{#let (if @onSelect (modifier on "click" @onSelect)) as |maybeClick|}}
    <button
      type="button"
      role="menuitem"
      {{! @glint-expect-error }}
      {{maybeClick}}
      {{on "click" @toggle}}
      {{on "pointermove" focusOnHover}}
      ...attributes
    >
      {{yield}}
    </button>
  {{/let}}
</template>;

interface LinkItemArgs extends LinkArgs {
  toggle: () => void;
}

interface PrivateLinkItemSignature {
  Element: HTMLAnchorElement;
  Args: LinkItemArgs;
  Blocks: { default: [] };
}

export interface LinkItemSignature {
  Element: PrivateLinkItemSignature["Element"];
  Args: LinkArgs;
  Blocks: PrivateLinkItemSignature["Blocks"];
}

const LinkItem: TOC<PrivateLinkItemSignature> = <template>
  <Link
    role="menuitem"
    @href={{@href}}
    @includeActiveQueryParams={{@includeActiveQueryParams}}
    @activeOnSubPaths={{@activeOnSubPaths}}
    {{on "click" @toggle}}
    {{on "pointermove" focusOnHover}}
    ...attributes
  >
    {{yield}}
  </Link>
</template>;

const installContent = eModifier<{
  Element: HTMLElement;
  Args: {
    Named: {
      isOpen: Cell<boolean>;
      triggerElement: Cell<HTMLElement>;
    };
  };
}>((element, _: [], { isOpen, triggerElement }) => {
  // focus first focusable element on the content
  const tabster = getTabster(window);
  const firstFocusable = tabster?.focusable.findFirst({
    container: element,
  });

  firstFocusable?.focus();

  // listen for "outside" clicks
  function onDocumentClick(e: MouseEvent) {
    if (
      isOpen.current &&
      e.target &&
      !element.contains(e.target as HTMLElement) &&
      !triggerElement.current?.contains(e.target as HTMLElement)
    ) {
      isOpen.current = false;
    }
  }

  // listen for the escape key
  function onDocumentKeydown(e: KeyboardEvent) {
    if (isOpen.current && e.key === "Escape") {
      isOpen.current = false;
    }
  }

  document.addEventListener("click", onDocumentClick);
  document.addEventListener("keydown", onDocumentKeydown);

  return () => {
    document.removeEventListener("click", onDocumentClick);
    document.removeEventListener("keydown", onDocumentKeydown);
  };
});

interface PrivateContentSignature {
  Element: HTMLDivElement;
  Args: {
    triggerElement: Cell<HTMLElement>;
    contentId: string;
    isOpen: Cell<boolean>;
    PopoverContent: PopoverBlockParams["Content"];
  };
  Blocks: {
    default: [
      {
        Item: WithBoundArgs<typeof Item, "toggle">;
        LinkItem: WithBoundArgs<typeof LinkItem, "toggle">;
        Separator: typeof Separator;
      },
    ];
  };
}

export interface ContentSignature {
  Element: PrivateContentSignature["Element"];
  Blocks: PrivateContentSignature["Blocks"];
}

const Content: TOC<PrivateContentSignature> = <template>
  {{#if @isOpen.current}}
    <@PopoverContent
      id={{@contentId}}
      role="menu"
      data-tabster={{TABSTER_CONFIG_CONTENT}}
      tabindex="0"
      {{installContent isOpen=@isOpen triggerElement=@triggerElement}}
      ...attributes
    >
      {{yield
        (hash
          Item=(component Item toggle=@isOpen.toggle)
          LinkItem=(component LinkItem toggle=@isOpen.toggle)
          Separator=Separator
        )
      }}
    </@PopoverContent>
  {{/if}}
</template>;

interface PrivateTriggerModifierSignature {
  Element: HTMLElement;
  Args: {
    Named: {
      triggerElement: Cell<HTMLElement>;
      isOpen: Cell<boolean>;
      contentId: string;
      setReference: PopoverBlockParams["setReference"];
      stopPropagation?: boolean;
      preventDefault?: boolean;
    };
  };
}

export interface TriggerModifierSignature {
  Element: PrivateTriggerModifierSignature["Element"];
}

const trigger = eModifier<PrivateTriggerModifierSignature>(
  (
    element,
    _: [],
    { triggerElement, isOpen, contentId, setReference, stopPropagation, preventDefault },
  ) => {
    element.setAttribute("aria-haspopup", "menu");

    if (isOpen.current) {
      element.setAttribute("aria-controls", contentId);
      element.setAttribute("aria-expanded", "true");
    } else {
      element.removeAttribute("aria-controls");
      element.setAttribute("aria-expanded", "false");
    }

    setTabsterAttribute(element, TABSTER_CONFIG_TRIGGER);

    const onTriggerClick = (event: MouseEvent) => {
      if (stopPropagation) {
        event.stopPropagation();
      }

      if (preventDefault) {
        event.preventDefault();
      }

      isOpen.toggle();
    };

    element.addEventListener("click", onTriggerClick);

    triggerElement.current = element;

    setReference(element);

    return () => {
      element.removeEventListener("click", onTriggerClick);
    };
  },
);

interface PrivateTriggerSignature {
  Element: HTMLButtonElement;
  Args: {
    triggerModifier: WithBoundArgs<
      typeof trigger,
      "triggerElement" | "contentId" | "isOpen" | "setReference"
    >;
    stopPropagation?: boolean;
    preventDefault?: boolean;
  };
  Blocks: { default: [] };
}

export interface TriggerSignature {
  Element: PrivateTriggerSignature["Element"];
  Blocks: PrivateTriggerSignature["Blocks"];
}

const Trigger: TOC<PrivateTriggerSignature> = <template>
  <button
    type="button"
    {{@triggerModifier stopPropagation=@stopPropagation preventDefault=@preventDefault}}
    ...attributes
  >
    {{yield}}
  </button>
</template>;

const IsOpen = () => cell<boolean>(false);
const TriggerElement = () => cell<HTMLElement>();

export class Menu extends Component<Signature> {
  contentId = guidFor(this);

  <template>
    {{#let (IsOpen) (TriggerElement) as |isOpen triggerEl|}}
      <Popover
        @flipOptions={{@flipOptions}}
        @middleware={{@middleware}}
        @offsetOptions={{@offsetOptions}}
        @placement={{@placement}}
        @shiftOptions={{@shiftOptions}}
        @strategy={{@strategy}}
        @inline={{@inline}}
        as |p|
      >
        {{#let
          (modifier
            trigger
            triggerElement=triggerEl
            isOpen=isOpen
            contentId=this.contentId
            setReference=p.setReference
          )
          as |triggerModifier|
        }}
          {{yield
            (hash
              trigger=triggerModifier
              Trigger=(component Trigger triggerModifier=triggerModifier)
              Content=(component
                Content
                PopoverContent=p.Content
                isOpen=isOpen
                triggerElement=triggerEl
                contentId=this.contentId
              )
              arrow=p.arrow
              isOpen=isOpen.current
            )
          }}
        {{/let}}
      </Popover>
    {{/let}}
  </template>
}

export default Menu;


---

export { OTPInput } from "./one-time-password/input.gts";
export { OTP } from "./one-time-password/otp.gts";


---

import { hash } from "@ember/helper";

import { arrow } from "@floating-ui/dom";
import { element } from "ember-element-helper";
import { modifier as eModifier } from "ember-modifier";
import { cell } from "ember-resources";

import { FloatingUI } from "../floating-ui.ts";
import { Portal } from "./portal.gts";
import { TARGETS } from "./portal-targets.gts";

import type { Signature as FloatingUiComponentSignature } from "../floating-ui/component.ts";
import type { Signature as HookSignature } from "../floating-ui/modifier.ts";
import type { TOC } from "@ember/component/template-only";
import type { ElementContext, Middleware } from "@floating-ui/dom";
import type { ModifierLike, WithBoundArgs } from "@glint/template";

export interface Signature {
  Args: {
    /**
     * See the Floating UI's [flip docs](https://floating-ui.com/docs/flip) for possible values.
     *
     * This argument is forwarded to the `<FloatingUI>` component.
     */
    flipOptions?: HookSignature["Args"]["Named"]["flipOptions"];
    /**
     * Array of one or more objects to add to Floating UI's list of [middleware](https://floating-ui.com/docs/middleware)
     *
     * This argument is forwarded to the `<FloatingUI>` component.
     */
    middleware?: HookSignature["Args"]["Named"]["middleware"];
    /**
     * See the Floating UI's [offset docs](https://floating-ui.com/docs/offset) for possible values.
     *
     * This argument is forwarded to the `<FloatingUI>` component.
     */
    offsetOptions?: HookSignature["Args"]["Named"]["offsetOptions"];
    /**
     * One of the possible [`placements`](https://floating-ui.com/docs/computeposition#placement). The default is 'bottom'.
     *
     * Possible values are
     * - top
     * - bottom
     * - right
     * - left
     *
     * And may optionally have `-start` or `-end` added to adjust position along the side.
     *
     * This argument is forwarded to the `<FloatingUI>` component.
     */
    placement?: `${"top" | "bottom" | "left" | "right"}${"" | "-start" | "-end"}`;
    /**
     * See the Floating UI's [shift docs](https://floating-ui.com/docs/shift) for possible values.
     *
     * This argument is forwarded to the `<FloatingUI>` component.
     */
    shiftOptions?: HookSignature["Args"]["Named"]["shiftOptions"];
    /**
     * CSS position property, either `fixed` or `absolute`.
     *
     * Pros and cons of each strategy are explained on [Floating UI's Docs](https://floating-ui.com/docs/computePosition#strategy)
     *
     * This argument is forwarded to the `<FloatingUI>` component.
     */
    strategy?: HookSignature["Args"]["Named"]["strategy"];

    /**
     * By default, the popover is portaled.
     * If you don't control your CSS, and the positioning of the popover content
     * is misbehaving, you may pass "@inline={{true}}" to opt out of portalling.
     *
     * Inline may also be useful in nested menus, where you know exactly how the nesting occurs
     */
    inline?: boolean;
  };
  Blocks: {
    default: [
      {
        reference: FloatingUiComponentSignature["Blocks"]["default"][0];
        setReference: FloatingUiComponentSignature["Blocks"]["default"][2]["setReference"];
        Content: WithBoundArgs<typeof Content, "floating">;
        data: FloatingUiComponentSignature["Blocks"]["default"][2]["data"];
        arrow: ModifierLike<{ Element: HTMLElement }>;
      },
    ];
  };
}

function getElementTag(tagName: undefined | string) {
  return tagName || "div";
}

/**
 * Allows lazy evaluation of the portal target (do nothing until rendered)
 * This is useful because the algorithm for finding the portal target isn't cheap.
 */
const Content: TOC<{
  Element: HTMLDivElement;
  Args: {
    floating: ModifierLike<{ Element: HTMLElement }>;
    inline?: boolean;
    /**
     * By default the popover content is wrapped in a div.
     * You may change this by supplying the name of an element here.
     *
     * For example:
     * ```gjs
     * <Popover as |p|>
     *  <p.Content @as="dialog">
     *    this is now focus trapped
     *  </p.Content>
     * </Popover>
     * ```
     */
    as?: string;
  };
  Blocks: { default: [] };
}> = <template>
  {{#let (element (getElementTag @as)) as |El|}}
    {{#if @inline}}
      {{! @glint-ignore
            https://github.com/tildeio/ember-element-helper/issues/91
            https://github.com/typed-ember/glint/issues/610
      }}
      <El {{@floating}} ...attributes>
        {{yield}}
      </El>
    {{else}}
      <Portal @to={{TARGETS.popover}}>
        {{! @glint-ignore
              https://github.com/tildeio/ember-element-helper/issues/91
              https://github.com/typed-ember/glint/issues/610
        }}
        <El {{@floating}} ...attributes>
          {{yield}}
        </El>
      </Portal>
    {{/if}}
  {{/let}}
</template>;

interface AttachArrowSignature {
  Element: HTMLElement;
  Args: {
    Named: {
      arrowElement: ReturnType<typeof ArrowElement>;
      data:
        | undefined
        | {
            placement: string;
            middlewareData?: {
              arrow?: { x?: number; y?: number };
            };
          };
    };
  };
}

const arrowSides = {
  top: "bottom",
  right: "left",
  bottom: "top",
  left: "right",
};

type Direction = "top" | "bottom" | "left" | "right";
type Placement = `${Direction}${"" | "-start" | "-end"}`;

const attachArrow: ModifierLike<AttachArrowSignature> = eModifier<AttachArrowSignature>(
  (element, _: [], named) => {
    if (element === named.arrowElement.current) {
      if (!named.data) return;
      if (!named.data.middlewareData) return;

      const { arrow } = named.data.middlewareData;
      const { placement } = named.data;

      if (!arrow) return;
      if (!placement) return;

      const { x: arrowX, y: arrowY } = arrow;
      const otherSide = (placement as Placement).split("-")[0] as Direction;
      const staticSide = arrowSides[otherSide];

      Object.assign(named.arrowElement.current.style, {
        left: arrowX != null ? `${arrowX}px` : "",
        top: arrowY != null ? `${arrowY}px` : "",
        right: "",
        bottom: "",
        [staticSide]: "-4px",
      });

      return;
    }

    void (async () => {
      await Promise.resolve();
      named.arrowElement.set(element);
    })();
  },
);

const ArrowElement: () => ReturnType<typeof cell<HTMLElement>> = () => cell<HTMLElement>();

function maybeAddArrow(middleware: Middleware[] | undefined, element: Element | undefined) {
  const result = [...(middleware || [])];

  if (element) {
    result.push(arrow({ element }));
  }

  return result;
}

function flipOptions(options: HookSignature["Args"]["Named"]["flipOptions"]) {
  return {
    elementContext: "reference" as ElementContext,
    ...options,
  };
}

export const Popover: TOC<Signature> = <template>
  {{#let (ArrowElement) as |arrowElement|}}
    <FloatingUI
      @placement={{@placement}}
      @strategy={{@strategy}}
      @middleware={{maybeAddArrow @middleware arrowElement.current}}
      @flipOptions={{flipOptions @flipOptions}}
      @shiftOptions={{@shiftOptions}}
      @offsetOptions={{@offsetOptions}}
      as |reference floating extra|
    >
      {{#let (modifier attachArrow arrowElement=arrowElement data=extra.data) as |arrow|}}
        {{yield
          (hash
            reference=reference
            setReference=extra.setReference
            Content=(component Content floating=floating inline=@inline)
            data=extra.data
            arrow=arrow
          )
        }}
      {{/let}}
    </FloatingUI>
  {{/let}}
</template>;

export default Popover;


---

import { assert } from "@ember/debug";
import { isDevelopingApp, macroCondition } from "@embroider/macros";

import { modifier } from "ember-modifier";
import { TrackedMap, TrackedSet } from "tracked-built-ins";

import type { TOC } from "@ember/component/template-only";

const cache = new TrackedMap<string, Set<Element>>();

export const TARGETS = Object.freeze({
  popover: "ember-primitives__portal-targets__popover",
  tooltip: "ember-primitives__portal-targets__tooltip",
  modal: "ember-primitives__portal-targets__modal",
});

export function findNearestTarget(origin: Element, name: string): Element | undefined {
  assert(`first argument to \`findNearestTarget\` must be an element`, origin instanceof Element);
  assert(`second argument to \`findNearestTarget\` must be a string`, typeof name === `string`);

  let element: Element | undefined | null = null;

  let parent = origin.parentNode;

  const manuallyRegisteredSet = cache.get(name);
  const manuallyRegistered: Element[] | null = manuallyRegisteredSet?.size
    ? [...manuallyRegisteredSet]
    : null;

  /**
   * For use with <PortalTarget @name="hi" />
   */
  function findRegistered(host: ParentNode): Element | undefined {
    return manuallyRegistered?.find((element) => {
      if (host.contains(element)) {
        return element;
      }
    });
  }

  const selector = Object.values(TARGETS as Record<string, string>).includes(name)
    ? `[data-portal-name=${name}]`
    : name;

  /**
   * Default portals / non-registered -- here we match a query selector instead of an element
   */
  function findDefault(host: ParentNode): Element | undefined {
    return host.querySelector(selector) as Element;
  }

  const finder = manuallyRegistered ? findRegistered : findDefault;

  /**
   * Crawl up the ancestry looking for our portal target
   */
  while (!element && parent) {
    element = finder(parent);
    if (element) break;
    parent = parent.parentNode;
  }

  if (macroCondition(isDevelopingApp())) {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
    (window as any).prime0 = origin;
  }

  if (name.startsWith("ember-primitives")) {
    assert(
      `Could not find element by the given name: \`${name}\`.` +
        ` The known names are ` +
        `${Object.values(TARGETS).join(", ")} ` +
        `-- but any name will work as long as it is set to the \`data-portal-name\` attribute ` +
        `(or if the name has been specifically registered via the <PortalTarget /> component). ` +
        `Double check that the element you're wanting to portal to is rendered. ` +
        `The element passed to \`findNearestTarget\` is stored on \`window.prime0\` ` +
        `You can debug in your browser's console via ` +
        `\`document.querySelector('[data-portal-name="${name}"]')\``,
      element,
    );
  }

  return element ?? undefined;
}

const register = modifier((element: Element, [name]: [name: string]) => {
  assert(`@name is required when using <PortalTarget>`, name);

  void (async () => {
    // Bad TypeScript lint.
    // eslint-disable-next-line @typescript-eslint/await-thenable
    await 0;

    let existing = cache.get(name);

    if (!existing) {
      existing = new TrackedSet<Element>();
      cache.set(name, existing);
    }

    existing.add(element);
  })();

  return () => {
    cache.delete(name);
  };
});

export interface Signature {
  Element: null;
}

export const PortalTargets: TOC<Signature> = <template>
  <div data-portal-name={{TARGETS.popover}}></div>
  <div data-portal-name={{TARGETS.tooltip}}></div>
  <div data-portal-name={{TARGETS.modal}}></div>
</template>;

/**
 * For manually registering a PortalTarget for use with Portal
 */
export const PortalTarget: TOC<{
  Element: HTMLDivElement;
  Args: {
    /**
     * The name of the PortalTarget
     *
     * This exact string may be passed to `Portal`'s `@to` argument.
     */
    name: string;
  };
}> = <template>
  <div {{register @name}} ...attributes></div>
</template>;

export default PortalTargets;


---

import { assert } from "@ember/debug";
import { schedule } from "@ember/runloop";
import { buildWaiter } from "@ember/test-waiters";

import { modifier } from "ember-modifier";
import { cell, resource, resourceFactory } from "ember-resources";

import { isElement } from "../narrowing.ts";
import { findNearestTarget, type TARGETS } from "./portal-targets.gts";

import type { TOC } from "@ember/component/template-only";

type Targets = (typeof TARGETS)[keyof typeof TARGETS];

interface ToSignature {
  Args: {
    to: string;
    append?: boolean;
  };
  Blocks: {
    default: [];
  };
}
interface ElementSignature {
  Args: {
    to: Element;
    append?: boolean;
  };
  Blocks: {
    default: [];
  };
}

export interface Signature {
  Args: {
    /**
     * The name of the PortalTarget to render in to.
     * This is the value of the `data-portal-name` attribute
     * of the element you wish to render in to.
     *
     * This can also be an Element which pairs nicely with query-utilities such as the platform-native `querySelector`
     */
    to?: (Targets | (string & {})) | Element;

    /**
     * Set to true to append to the portal instead of replace
     *
     * Default: false
     */
    append?: boolean;
    /**
     * For ember-wormhole style behavior, this argument may be an id,
     * or a selector.
     * This can also be an element, in which case the behavior is identical to `@to`
     */
    wormhole?: string | Element;
  };
  Blocks: {
    /**
     * The portaled content
     */
    default: [];
  };
}

/**
 * Polyfill for ember-wormhole behavior
 *
 * Example usage:
 * ```gjs
 * import { wormhole, Portal } from 'ember-primitives/components/portal';
 *
 * <template>
 *   <div id="the-portal"></div>
 *
 *   <Portal @to={{wormhole "the-portal"}}>
 *     content renders in the above div
 *   </Portal>
 * </template>
 *
 * ```
 */
export function wormhole(query: string | null | undefined | Element) {
  assert(`Expected query/element to be truthy.`, query);

  if (isElement(query)) {
    return query;
  }

  let found = document.getElementById(query);

  found ??= document.querySelector(query);

  return found;
}

const anchor = modifier(
  (element: Element, [to, update]: [string, ReturnType<typeof ElementValue>["set"]]) => {
    const found = findNearestTarget(element, to);

    update(found);
  },
);

const ElementValue = () => cell<Element | ShadowRoot | null | undefined>();

const waiter = buildWaiter("ember-primitives:portal");

function wormholeCompat(selector: string | Element) {
  const target = wormhole(selector);

  if (target) return target;

  return resource(() => {
    const target = cell<Element | undefined | null>();

    const token = waiter.beginAsync();

    // eslint-disable-next-line ember/no-runloop
    schedule("afterRender", () => {
      const result = wormhole(selector);

      waiter.endAsync(token);
      target.current = result;
      assert(
        `Could not find element with id/selector \`${typeof selector === "string" ? selector : "<Element>"}\``,
        result,
      );
    });

    return () => target.current;
  });
}

resourceFactory(wormholeCompat);

export const Portal: TOC<Signature> = <template>
  {{#if (isElement @to)}}
    <ToElement @to={{@to}} @append={{@append}}>
      {{yield}}
    </ToElement>
  {{else if @wormhole}}
    {{#let (wormholeCompat @wormhole) as |target|}}
      {{#if target}}
        {{#in-element target insertBefore=null}}
          {{yield}}
        {{/in-element}}
      {{/if}}
    {{/let}}
  {{else if @to}}
    <Nestable @to={{@to}} @append={{@append}}>
      {{yield}}
    </Nestable>
  {{else}}
    {{assert "either @to or @wormhole is required. Received neither"}}
  {{/if}}
</template>;

const ToElement: TOC<ElementSignature> = <template>
  {{#if @append}}
    {{#in-element @to insertBefore=null}}
      {{yield}}
    {{/in-element}}
  {{else}}
    {{#in-element @to}}
      {{yield}}
    {{/in-element}}
  {{/if}}
</template>;

const Nestable: TOC<ToSignature> = <template>
  {{#let (ElementValue) as |target|}}
    {{! This div is always going to be empty,
          because it'll either find the portal and render content elsewhere,
          it it won't find the portal and won't render anything.
      }}
    {{! template-lint-disable no-inline-styles }}
    <div style="display:contents;" {{anchor @to target.set}}>
      {{#if target.current}}
        {{#if @append}}
          {{#in-element target.current insertBefore=null}}
            {{yield}}
          {{/in-element}}
        {{else}}
          {{#in-element target.current}}
            {{yield}}
          {{/in-element}}
        {{/if}}
      {{/if}}
    </div>
  {{/let}}
</template>;

export default Portal;


---

import Component from "@glimmer/component";
import { hash } from "@ember/helper";

import type { TOC } from "@ember/component/template-only";
import type { WithBoundArgs } from "@glint/template";

export interface Signature {
  Element: HTMLDivElement;
  Args: {
    /**
     * The current progress
     * This may be less than 0 or more than `max`,
     * but the resolved value (managed internally, and yielded out)
     * does not exceed the range [0, max]
     */
    value: number;
    /**
     * The max value, defaults to 100
     */
    max?: number;
  };
  Blocks: {
    default: [
      {
        /**
         * The indicator element with some state applied.
         * This can be used to style the progress of bar.
         */
        Indicator: WithBoundArgs<typeof Indicator, "value" | "max" | "percent">;
        /**
         * The value as a percent of how far along the indicator should be
         * positioned, between 0 and 100.
         * Will be rounded to two decimal places.
         */
        percent: number;
        /**
         * The value as a percent of how far along the indicator should be positioned,
         * between 0 and 1
         */
        decimal: number;
        /**
         * The resolved value within the limits of the progress bar.
         */
        value: number;
      },
    ];
  };
}

type ProgressState = "indeterminate" | "complete" | "loading";

const DEFAULT_MAX = 100;

/**
 * Non-negative, non-NaN, non-Infinite, positive, rational
 */
function isValidProgressNumber(value: number | undefined | null): value is number {
  if (typeof value !== "number") return false;
  if (!Number.isFinite(value)) return false;

  return value >= 0;
}

function progressState(value: number | undefined | null, maxValue: number): ProgressState {
  return value == null ? "indeterminate" : value === maxValue ? "complete" : "loading";
}

function getMax(userMax: number | undefined | null): number {
  return isValidProgressNumber(userMax) ? userMax : DEFAULT_MAX;
}

function getValue(userValue: number | undefined | null, maxValue: number): number {
  const max = getMax(maxValue);

  if (!isValidProgressNumber(userValue)) {
    return 0;
  }

  if (userValue > max) {
    return max;
  }

  return userValue;
}

function getValueLabel(value: number, max: number) {
  return `${Math.round((value / max) * 100)}%`;
}

const Indicator: TOC<{
  Element: HTMLDivElement;
  Args: { max: number; value: number; percent: number };
  Blocks: { default: [] };
}> = <template>
  <div
    ...attributes
    data-max={{@max}}
    data-value={{@value}}
    data-state={{progressState @value @max}}
    data-percent={{@percent}}
  >
    {{yield}}
  </div>
</template>;

export class Progress extends Component<Signature> {
  get max() {
    return getMax(this.args.max);
  }

  get value() {
    return getValue(this.args.value, this.max);
  }

  get valueLabel() {
    return getValueLabel(this.value, this.max);
  }

  get decimal() {
    return this.value / this.max;
  }

  get percent() {
    return Math.round(this.decimal * 100 * 100) / 100;
  }

  <template>
    <div
      ...attributes
      aria-valuemax={{this.max}}
      aria-valuemin="0"
      aria-valuenow={{this.value}}
      aria-valuetext={{this.valueLabel}}
      role="progressbar"
      data-value={{this.value}}
      data-state={{progressState this.value this.max}}
      data-max={{this.max}}
      data-min="0"
      data-percent={{this.percent}}
    >

      {{yield
        (hash
          Indicator=(component Indicator value=this.value max=this.max percent=this.percent)
          value=this.value
          percent=this.percent
          decimal=this.decimal
        )
      }}
    </div>
  </template>
}

export default Progress;


---

export { Rating } from "./rating/rating.gts";

import type { ComponentIcons } from "./rating/public-types.ts";

export type IconType = ComponentIcons["icon"];


---

import Component from "@glimmer/component";
import { isDestroyed, isDestroying } from "@ember/destroyable";
import { hash } from "@ember/helper";

import { modifier } from "ember-modifier";

/**
 * Utility component for helping with scrolling in any direction within
 * any of the 4 directions: up, down, left, right.
 *
 * This can be used to auto-scroll content as new content is inserted into the scrollable area, or possibly to bring focus to something on the page.
 */
export class Scroller extends Component<{
  /**
   * A containing element is required - in this case, a div.
   * It must be scrollable for this component to work, but can be customized.
   *
   * By default, this element will have some styling applied:
   *   overflow: auto;
   *
   * By default, this element will have tabindex="0" to support keyboard usage.
   *
   * The scroll-behavior is "auto", which can be controlled via CSS
   * https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior
   *
   */
  Element: HTMLDivElement;
  Blocks: {
    default: [
      {
        /**
         * Scroll the content to the bottom
         *
         * ```gjs
         * import { Scroller } from 'ember-primitives';
         *
         * <template>
         *   <Scroller as |s|>
         *      ...
         *
         *      {{ (s.scrollToBottom) }}
         *   </Scroller>
         * </template>
         * ```
         */
        scrollToBottom: () => void;
        /**
         * Scroll the content to the top
         *
         * ```gjs
         * import { Scroller } from 'ember-primitives';
         *
         * <template>
         *   <Scroller as |s|>
         *      ...
         *
         *      {{ (s.scrollToTop) }}
         *   </Scroller>
         * </template>
         * ```
         */
        scrollToTop: () => void;
        /**
         * Scroll the content to the left
         *
         * ```gjs
         * import { Scroller } from 'ember-primitives';
         *
         * <template>
         *   <Scroller as |s|>
         *      ...
         *
         *      {{ (s.scrollToLeft) }}
         *   </Scroller>
         * </template>
         * ```
         */
        scrollToLeft: () => void;
        /**
         * Scroll the content to the right
         *
         * ```gjs
         * import { Scroller } from 'ember-primitives';
         *
         * <template>
         *   <Scroller as |s|>
         *      ...
         *
         *      {{ (s.scrollToRight) }}
         *   </Scroller>
         * </template>
         * ```
         */
        scrollToRight: () => void;
      },
    ];
  };
}> {
  declare withinElement: HTMLDivElement;

  ref = modifier((el: HTMLDivElement) => {
    this.withinElement = el;
  });

  #frame?: number;

  scrollToBottom = () => {
    if (this.#frame) {
      cancelAnimationFrame(this.#frame);
    }

    this.#frame = requestAnimationFrame(() => {
      if (isDestroyed(this) || isDestroying(this)) return;

      this.withinElement.scrollTo({
        top: this.withinElement.scrollHeight,
        behavior: "auto",
      });
    });
  };

  scrollToTop = () => {
    if (this.#frame) {
      cancelAnimationFrame(this.#frame);
    }

    this.#frame = requestAnimationFrame(() => {
      if (isDestroyed(this) || isDestroying(this)) return;

      this.withinElement.scrollTo({
        top: 0,
        behavior: "auto",
      });
    });
  };

  scrollToLeft = () => {
    if (this.#frame) {
      cancelAnimationFrame(this.#frame);
    }

    this.#frame = requestAnimationFrame(() => {
      if (isDestroyed(this) || isDestroying(this)) return;

      this.withinElement.scrollTo({
        left: 0,
        behavior: "auto",
      });
    });
  };

  scrollToRight = () => {
    if (this.#frame) {
      cancelAnimationFrame(this.#frame);
    }

    this.#frame = requestAnimationFrame(() => {
      if (isDestroyed(this) || isDestroying(this)) return;

      this.withinElement.scrollTo({
        left: this.withinElement.scrollWidth,
        behavior: "auto",
      });
    });
  };

  <template>
    <div tabindex="0" ...attributes {{this.ref}}>
      {{yield
        (hash
          scrollToBottom=this.scrollToBottom
          scrollToTop=this.scrollToTop
          scrollToLeft=this.scrollToLeft
          scrollToRight=this.scrollToRight
        )
      }}
    </div>
  </template>
}


---

import { element } from "ember-element-helper";

import type { TOC } from "@ember/component/template-only";

type Orientation = "horizontal" | "vertical";

function normalizeTagName(tagName: string) {
  return tagName.trim().toLowerCase();
}

function getElementTag(tagName: undefined | string) {
  if (tagName) return tagName;

  return "hr";
}

function roleFor(tagName: string, decorative: undefined | boolean) {
  if (decorative) return undefined;

  // <hr> already has implicit role="separator".
  if (normalizeTagName(tagName) === "hr") return undefined;

  return "separator";
}

function ariaHiddenFor(decorative: undefined | boolean) {
  return decorative ? "true" : undefined;
}

function ariaOrientationFor(orientation: undefined | Orientation, decorative: undefined | boolean) {
  if (decorative) return undefined;

  // `separator` has an implicit aria-orientation of horizontal.
  // Only specify when authors opt in (e.g. vertical separators).
  return orientation;
}

function shouldYield(decorative: undefined | boolean, tagName: string) {
  // `<hr>` is a void element and must not have children.
  if (normalizeTagName(tagName) === "hr") return false;

  // Content inside a `separator` is presentational to AT; only yield for decorative
  // separators so consumers don't accidentally rely on it for semantics.
  return Boolean(decorative);
}

export interface Signature {
  Element: HTMLElement;
  Args: {
    /**
     * The tag name to use for the separator element.
     * Defaults to `<hr>` for non-decorative separators.
     * You can override this (e.g. `"li"` in menus, or `"span"` for inline separators).
     *
     * For example, in breadcrumbs where separators are siblings to `<li>` elements:
     * ```gjs
     * <Separator @as="li" @decorative={{true}}>/</Separator>
     * ```
     */
    as?: string;

    /**
     * When true, hides the separator from assistive technologies.
     *
     * Use this for purely decorative separators, such as breadcrumb slashes.
     */
    decorative?: boolean;

    /**
     * Sets `aria-orientation`. `separator` has an implicit orientation of `horizontal`.
     * Provide this when the separator is vertical.
     */
    orientation?: Orientation;
  };
  Blocks: {
    default: [];
  };
}

/**
 * A separator component that follows the ARIA `separator` role guidance.
 *
 * By default, this component renders a semantic separator (`<hr>`). When using a
 * non-`hr` tag via `@as`, it adds `role="separator"`.
 *
 * For purely decorative separators (e.g. breadcrumb slashes), set `@decorative={{true}}`
 * to apply `aria-hidden="true"`.
 *
 * For example:
 *
 * ```gjs live preview
 * import { Separator } from 'ember-primitives';
 *
 * <template>
 *   <nav>
 *     <ol style="display: flex; gap: 0.5rem; list-style: none; padding: 0;">
 *       <li><a href="/">Home</a></li>
 *       <Separator @as="li" @decorative={{true}}>/</Separator>
 *       <li><a href="/docs">Docs</a></li>
 *       <Separator @as="li" @decorative={{true}}>/</Separator>
 *       <li>Current</li>
 *     </ol>
 *   </nav>
 * </template>
 * ```
 */
export const Separator: TOC<Signature> = <template>
  {{#let (getElementTag @as) as |tagName|}}
    {{#let (element tagName) as |El|}}
      <El
        aria-hidden={{ariaHiddenFor @decorative}}
        role={{roleFor tagName @decorative}}
        aria-orientation={{ariaOrientationFor @orientation @decorative}}
        ...attributes
      >
        {{#if (shouldYield @decorative tagName)}}
          {{yield}}
        {{/if}}
      </El>
    {{/let}}
  {{/let}}
</template>;

export default Separator;


---

import Component from "@glimmer/component";

import type Owner from "@ember/owner";

// index.html has the production-fingerprinted references to these links
// Ideally, we'd have some pre-processor scan everything for references to
// assets in public, but idk how to set that up
const getStyles = () => [...document.querySelectorAll("link")].map((link) => link.href);

/**
 * style + native @import
 * is the only robust way to load styles in a shadowroot.
 *
 * link is only valid in the head element.
 */
const Styles = <template>
  <style>
    {{#each (getStyles) as |styleHref|}}

      @import "{{styleHref}}";

    {{/each}}
  </style>
</template>;

/**
 * Render content in a shadow dom, attached to a div.
 *
 * Uses the [shadow DOM][mdn-shadow-dom] API.
 *
 * [mdn-shadow-dom]: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM
 *
 * This is useful when you want to render content that escapes your app's styles.
 */
export class Shadowed extends Component<{
  /**
   * The shadow dom attaches to a div element.
   * You may specify any attribute, and it'll be applied to this host element.
   */
  Element: HTMLDivElement;
  Args: {
    /**
     * @public
     *
     * By default, shadow-dom does not include any styles.
     * Setting this to true will include all the `<style>` tags
     * that are present in the `<head>` element.
     */
    includeStyles?: boolean;
  };
  Blocks: {
    /**
     * Content to be placed within the ShadowDOM
     */
    default: [];
  };
}> {
  shadow: HTMLDivElement;
  host: HTMLDivElement;
  /**
   * ember-source 5.6 broke the ability to in-element
   * natively into a shadowroot.
   *
   * We have two or three more dives than we should have here.
   *
   *
   * See these ember-source bugs:
   * - https://github.com/emberjs/ember.js/issues/20643
   * - https://github.com/emberjs/ember.js/issues/20642
   * - https://github.com/emberjs/ember.js/issues/20641
   *
   * Ideally, shadowdom should be built in.
   * Couple paths forward:
   *  - (as the overall template tag)
   *     <template shadowrootmode="open">
   *     </template>
   *
   *  - Build a component into the framework that does the above ^
   *  - add additional parsing in content-tag to allow
   *    nested <template>
   *
   */
  constructor(owner: Owner, args: { includeStyles?: boolean }) {
    super(owner, args);

    const element = document.createElement("div");
    const shadowRoot = element.attachShadow({ mode: "open" });
    const div = document.createElement("div");

    shadowRoot.appendChild(div);
    this.host = element;
    this.shadow = div;
  }

  <template>
    <div ...attributes>{{this.host}}</div>

    {{#in-element this.shadow}}

      {{#if @includeStyles}}
        <Styles />
      {{/if}}

      {{yield}}

    {{/in-element}}
  </template>
}

export default Shadowed;


---

import { fn, hash } from "@ember/helper";
import { on } from "@ember/modifier";

import { cell } from "ember-resources";

import { uniqueId } from "../utils.ts";
import { Label } from "./-private/typed-elements.gts";
import { toggleWithFallback } from "./-private/utils.ts";

import type { TOC } from "@ember/component/template-only";
import type { WithBoundArgs } from "@glint/template";

export interface Signature {
  Element: HTMLInputElement;
  Args: {
    /**
     * The initial checked value of the Switch.
     * This value is reactive, so if the value that
     * `@checked` is set to updates, the state of the Switch will also update.
     */
    checked?: boolean;
    /**
     * Callback when the Switch state is toggled
     */
    onChange?: (checked: boolean, event: Event) => void;
  };
  Blocks: {
    default?: [
      {
        /**
         * The current state of the Switch.
         *
         * ```gjs
         * import { Switch } from 'ember-primitives/components/switch';
         *
         * <template>
         *   <Switch as |s|>
         *     {{s.isChecked}}
         *   </Switch>
         * </template>
         * ```
         */
        isChecked: boolean;
        /**
         * The Switch Element.
         * It has a pre-wired `id` so that the relevant Label is
         * appropriately associated via the `for` property of the Label.
         *
         * ```gjs
         * import { Switch } from 'ember-primitives/components/switch';
         *
         * <template>
         *   <Switch as |s|>
         *     <s.Control />
         *   </Switch>
         * </template>
         * ```
         */
        Control: WithBoundArgs<typeof Checkbox, "checked" | "id" | "onChange">;
        /**
         * The Switch element requires a label, and this label already has
         * the association to the Control by setting the `for` attribute to the `id` of the Control
         *
         * ```gjs
         * import { Switch } from 'ember-primitive/components/switchs';
         *
         * <template>
         *   <Switch as |s|>
         *     <s.Label />
         *   </Switch>
         * </template>
         * ```
         */
        Label: WithBoundArgs<typeof Label, "for">;
      },
    ];
  };
}

interface ControlSignature {
  Element: HTMLInputElement;
  Args: { id: string; checked?: ReturnType<typeof cell<boolean>>; onChange: () => void };
}

const Checkbox: TOC<ControlSignature> = <template>
  <input
    id={{@id}}
    type="checkbox"
    role="switch"
    checked={{@checked.current}}
    aria-checked={{@checked.current}}
    data-state={{if @checked.current "on" "off"}}
    {{on "click" (fn toggleWithFallback @checked.toggle @onChange)}}
    ...attributes
  />
</template>;

function defaultFalse(value: unknown) {
  return value ?? false;
}

/**
 * @public
 */
export const Switch: TOC<Signature> = <template>
  <div ...attributes data-prim-switch>
    {{#let (uniqueId) as |id|}}
      {{#let (cell (defaultFalse @checked)) as |checked|}}
        {{! @glint-nocheck }}
        {{yield
          (hash
            isChecked=checked.current
            Control=(component Checkbox checked=checked id=id onChange=@onChange)
            Label=(component Label for=id)
          )
        }}
      {{/let}}
    {{/let}}
  </div>
</template>;

export default Switch;


---

/**
 * References:
 * - https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/tablist_role
 * - https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
 *
 *
 * Keyboard behaviors (optionally) provided by tabster
 */

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { isDestroyed, isDestroying } from "@ember/destroyable";
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { next } from "@ember/runloop";

import { getTabsterAttribute, MoverDirections } from "tabster";

import { uniqueId } from "../utils.ts";
import Portal from "./portal.gts";

import type { TOC } from "@ember/component/template-only";
import type Owner from "@ember/owner";
import type { ComponentLike, WithBoundArgs } from "@glint/template";

const UNSET = Symbol.for("ember-primitives:tabs:unset");

const TABSTER_CONFIG = getTabsterAttribute(
  {
    mover: {
      direction: MoverDirections.Both,
      cyclic: true,
      memorizeCurrent: true,
    },
    deloser: {},
  },
  true,
);

const TabLink: TOC<{
  Element: HTMLAnchorElement;
  Args: {
    /**
     * @internal
     * for linking of aria
     */
    id: string;
    /**
     * @internal
     * for linking of aria
     */
    panelId: string;
  };
  Blocks: { default: [] };
}> = <template>
  <a href="##missing##" ...attributes role="tab" aria-controls={{@panelId}} id={{@id}}>
    {{yield}}
  </a>
</template>;

export type ButtonType = ComponentLike<ButtonSignature>;
export interface ButtonSignature {
  Element: HTMLButtonElement;
  Blocks: {
    default: [];
  };
}

const TabButton: TOC<{
  Args: {
    /**
     * @internal
     * for linking of aria
     */
    id: string;
    /**
     * @internal
     * for linking of aria
     */
    panelId: string;

    /**
     * @internal
     * for managing state
     */
    handleClick: () => void;

    /**
     * @internal
     * for managing state
     */
    value: string | undefined;

    /**
     * @internal
     */
    state: TabState;
  };
  Blocks: {
    default: [];
  };
}> = <template>
  <button
    ...attributes
    role="tab"
    type="button"
    aria-controls={{@panelId}}
    aria-selected={{String (@state.isActive @id @value)}}
    id={{@id}}
    {{on "click" @handleClick}}
    {{! The Types for modifier are wrong }}
    {{! @glint-expect-error}}
    {{(if @state.isAutomatic (modifier on "focus" @handleClick))}}
  >
    {{yield}}
  </button>
</template>;

export type ContentType = ComponentLike<ContentSignature>;
export interface ContentSignature {
  /**
   * the [role=tabpanel] element
   */
  Element: HTMLDivElement;
  Blocks: {
    default: [];
  };
}

const TabContent: TOC<{
  Element: HTMLDivElement;
  Args: {
    /**
     * @internal
     * for linking of aria
     */
    id: string;
    /**
     * @internal
     * for linking of aria
     */
    tabId: string;
    /**
     * @internal
     */
    state: TabState;
  };
  Blocks: {
    default: [];
  };
}> = <template>
  <Portal @to="#{{@state.tabpanelId}}" @append={{true}}>
    {{#if (@state.isActive @tabId)}}
      <div ...attributes role="tabpanel" aria-labelledby={{@tabId}} id={{@id}}>
        {{yield}}
      </div>
    {{/if}}
  </Portal>
</template>;

function isString(x: unknown): x is string {
  return typeof x === "string";
}

function makeTab(tabButton: any, tabLink: any): any {
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
  tabButton.Link = tabLink;

  return tabButton;
}

export type ContainerType = ComponentLike<ContainerSignature>;
export type ContainerSignature =
  | {
      Blocks: {
        default: [];
      };
    }
  | {
      Args: {
        label: string | ComponentLike;
        content: string | ComponentLike;
      };
    }
  | {
      Args: {
        label: string | ComponentLike;
      };
      Blocks: {
        /**
         * The content for the tab
         */
        default: [];
      };
    };

class TabContainer extends Component<{
  Args: {
    /**
     * @internal
     */
    state: TabState;

    /**
     * When opting for a "controlled component",
     * the value will be needed to make sense of the selected tab.
     *
     * The default value used for communication within the Tabs component (and eventually emitted via the @onChange argument) is a unique random id.
     * So while that could still be used for controlling the tabs component, it may be more easy to grok with user-managed values.
     */
    value?: string;

    /**
     * optional user-passable label
     */
    label?: string | ComponentLike;

    /**
     * optional user-passable content.
     */
    content?: string | ComponentLike;
  };
  Blocks: {
    default: [
      Label: WithBoundArgs<typeof TabButton, "state" | "id" | "panelId" | "handleClick" | "value">,
      Content: WithBoundArgs<typeof TabContent, "state" | "id" | "tabId">,
    ];
  };
}> {
  id = `ember-primitives__tab-${uniqueId()}`;

  get tabId() {
    return `${this.id}__tab`;
  }

  get panelId() {
    return `${this.id}__panel`;
  }

  get label() {
    return this.args.label ?? this.tabId;
  }

  <template>
    {{#if @label}}
      <TabButton
        @state={{@state}}
        @id={{this.tabId}}
        @value={{@value}}
        @panelId={{this.panelId}}
        @handleClick={{fn @state.handleChange this.tabId @value}}
      >
        {{#if (isString @label)}}
          {{@label}}
        {{else}}
          <@label />
        {{/if}}
      </TabButton>

      <TabContent @state={{@state}} @id={{this.panelId}} @tabId={{this.tabId}}>
        {{#if @content}}
          {{#if (isString @content)}}
            {{@content}}
          {{else}}
            <@content />
          {{/if}}
        {{else}}
          {{yield}}
        {{/if}}
      </TabContent>
    {{else}}
      {{yield
        (makeTab
          (component
            TabButton
            state=@state
            value=@value
            id=this.tabId
            panelId=this.panelId
            handleClick=(fn @state.handleChange this.tabId @value)
          )
          (component TabLink state=@state id=this.tabId panelId=this.panelId)
        )
        (component TabContent state=@state id=this.panelId tabId=this.tabId)
      }}
    {{/if}}
  </template>
}

const Label: TOC<{
  /**
   * The label wiring (id, aria, etc) are handled for you.
   * If you'd like to use a heading element (h3, etc), place that in the block content
   * when invoking this Label component.
   */
  Element: null;
  Args: {
    /**
     * @internal
     */
    state: TabState;
  };
  Blocks: { default: [] };
}> = <template>
  <Portal @to="#{{@state.labelId}}">
    {{yield}}
  </Portal>
</template>;

export interface Signature {
  /**
   * The wrapping element for the overall Tabs component.
   * This should be used for styling the layout of the tabs.
   */
  Element: HTMLDivElement;
  Args: {
    /**
     * Sets the active tab.
     * If not passed, the first tab will be selected
     */
    activeTab?: string;

    /**
     * Optional label for the overall TabList
     */
    label?: string | ComponentLike;

    /**
     * When the tab changes, this function will be called.
     * The function receives both the newly selected tab as well as the previous tab.
     *
     * However, if the tabs are not configured with names, these values will be null.
     */
    onChange?: (selectedTab: string, previousTab: string | null) => void;

    /**
     * When activationMode is set to "automatic", tabs are activated when receiving focus. When set to "manual", tabs are activated when clicked (or when "enter" is pressed via the keyboard).
     */
    activationMode?: "automatic" | "manual";
  };
  Blocks: {
    default: [
      Tab: WithBoundArgs<typeof TabContainer, "state"> & {
        Label: WithBoundArgs<typeof Label, "state">;
      },
    ];
  };
}

/**
 * We're doing old skool hax with this, so we don't need to care about what the types think, really
 */
function makeAPI(tabContainer: any, labelComponent: any): any {
  // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
  tabContainer.Label = labelComponent;

  return tabContainer;
}

import { buildWaiter } from "@ember/test-waiters";

const stateWaiter = buildWaiter("ember-primitives:tabs");

/**
 * State bucket passed around to all the sub-components.
 *
 * Sort of a "Context", but with a bit of prop-drilling (which is more efficient than dom-context)
 */
class TabState {
  declare args: {
    activeTab?: string;
    activationMode?: "automatic" | "manual";
    onChange?: (selected: string, previous: string | null) => void;
  };

  @tracked _active: string | null = null;

  @tracked _label: string | undefined;

  #first: string | null = null;
  id: string;
  labelId: string;
  tabpanelId: string;
  #token: unknown;

  constructor(args: { activeTab?: string; onChange?: () => void }) {
    this.args = args;

    this.id = `ember-primitives-${uniqueId()}`;
    this.labelId = `${this.id}__label`;
    this.tabpanelId = `${this.id}__tabpanel`;
  }

  get activationMode() {
    return this.args.activationMode ?? "automatic";
  }

  get isAutomatic() {
    return this.activationMode === "automatic";
  }

  /**
   * This function relies on the fact that during rendering,
   * the first component to be rendered will be first,
   * and it will be the one to set the secret first value,
   * which means all other tabs will not be first.
   *
   */
  isActive = (tabId: string, tabValue: undefined | string) => {
    /**
     * When users pass the @value to a tab, we use that for managing
     * the "active state" instead of the DOM ID.
     *
     * NOTE: DOM IDs must be unique across the whole document, but @value
     *     does not need to be unqiue.
     *          `@value` *should* be unique for the Tabs component though
     */
    const isSelected = (x: string) => {
      if (tabValue) return x === tabValue;

      return x === tabId;
    };

    if (this.active === UNSET) {
      if (this.#first) return isSelected(this.#first);

      this.#first = tabValue ?? tabId;
      this.#token = stateWaiter.beginAsync();

      // eslint-disable-next-line ember/no-runloop
      next(() => {
        if (!this.#token) return;
        stateWaiter.endAsync(this.#token);
        if (this._active) return;
        if (isDestroyed(this) || isDestroying(this)) return;

        this._label = tabValue ?? tabId;
      });

      return true;
    }

    return isSelected(this.active);
  };

  get active() {
    return this._active ?? this.args.activeTab ?? UNSET;
  }

  get activeLabel() {
    /**
     * This is only needed during the first set
     * because we prioritize rendering first, and then updating metadata later
     * (next render)
     *
     * NOTE: this does not mean that the a11y tree is updated later.
     *       it is correct on initial render
     */
    if (this._label) {
      return this._label;
    }

    if (this.active === UNSET) {
      return "Pending";
    }

    return this.active;
  }

  handleChange = (tabId: string, tabValue: string | undefined) => {
    const previous = this.active;
    const next = tabValue ?? tabId;

    // No change, no need to be noisy
    if (next === previous) return;

    this._active = this._label = next;

    this.args.onChange?.(next, previous === UNSET ? null : previous);
  };
}

export class Tabs extends Component<Signature> {
  state: TabState;

  // eslint-disable-next-line @typescript-eslint/no-empty-object-type
  constructor(owner: Owner, args: {}) {
    super(owner, args);

    this.state = new TabState(args);
  }

  <template>
    <div class="ember-primitives__tabs" ...attributes data-active={{this.state.activeLabel}}>
      {{! This element will be portaled in to and replaced if tabs.Label is invoked }}
      <div class="ember-primitives__tabs__label" id={{this.state.labelId}}>
        {{#if (isString @label)}}
          {{@label}}
        {{else}}
          <@label />
        {{/if}}
      </div>
      <div
        class="ember-primitives__tabs__tablist"
        role="tablist"
        aria-labelledby={{this.state.labelId}}
        data-tabster={{TABSTER_CONFIG}}
      >
        {{yield
          (makeAPI (component TabContainer state=this.state) (component Label state=this.state))
        }}
      </div>
      {{!
        Tab's contents are portaled in to this element
      }}
      <div class="ember-primitives__tabs__tabpanel" id={{this.state.tabpanelId}}></div>
    </div>
  </template>
}


---

import Component from "@glimmer/component";
import { cached } from "@glimmer/tracking";
import { hash } from "@ember/helper";

import { getTabsterAttribute, MoverDirections } from "tabster";
import { TrackedSet } from "tracked-built-ins";
// The consumer will need to provide types for tracked-toolbox.
// Or.. better yet, we PR to trakcked-toolbox to provide them
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { localCopy } from "tracked-toolbox";

import { Toggle } from "./toggle.gts";

import type { ComponentLike } from "@glint/template";

const TABSTER_CONFIG = getTabsterAttribute(
  {
    mover: {
      direction: MoverDirections.Both,
      cyclic: true,
    },
  },
  true,
);

export interface ItemSignature<Value = any> {
  /**
   * The button element will have aria-pressed="true" on it when the button is in the pressed state.
   */
  Element: HTMLButtonElement;
  Args: {
    /**
     * When used in a group of Toggles, this option will be helpful to
     * know which toggle was pressed if you're using the same @onChange
     * handler for multiple toggles.
     */
    value?: Value;
  };
  Blocks: {
    default: [
      /**
       * the current pressed state of the toggle button
       *
       * Useful when using the toggle button as an uncontrolled component
       */
      pressed: boolean,
    ];
  };
}

export type Item<Value = any> = ComponentLike<ItemSignature<Value>>;

export interface SingleSignature<Value> {
  Element: HTMLDivElement;
  Args: {
    /**
     * Optionally set the initial toggle state
     */
    value?: Value;
    /**
     * Callback for when the toggle-group's state is changed.
     *
     * Can be used to control the state of the component.
     *
     *
     * When none of the toggles are selected, undefined will be passed.
     */
    onChange?: (value: Value | undefined) => void;
  };
  Blocks: {
    default: [
      {
        /**
         * The Toggle Switch
         */
        Item: Item;
      },
    ];
  };
}

export interface MultiSignature<Value = any> {
  Element: HTMLDivElement;
  Args: {
    /**
     * Optionally set the initial toggle state
     */
    value?: Value[] | Set<Value> | Value;
    /**
     * Callback for when the toggle-group's state is changed.
     *
     * Can be used to control the state of the component.
     *
     *
     * When none of the toggles are selected, undefined will be passed.
     */
    onChange?: (value: Set<Value>) => void;
  };
  Blocks: {
    default: [
      {
        /**
         * The Toggle Switch
         */
        Item: Item;
      },
    ];
  };
}

interface PrivateSingleSignature<Value = any> {
  Element: HTMLDivElement;
  Args: {
    type?: "single";

    /**
     * Optionally set the initial toggle state
     */
    value?: Value;
    /**
     * Callback for when the toggle-group's state is changed.
     *
     * Can be used to control the state of the component.
     *
     *
     * When none of the toggles are selected, undefined will be passed.
     */
    onChange?: (value: Value | undefined) => void;
  };
  Blocks: {
    default: [
      {
        Item: Item;
      },
    ];
  };
}

interface PrivateMultiSignature<Value = any> {
  Element: HTMLDivElement;
  Args: {
    type: "multi";
    /**
     * Optionally set the initial toggle state
     */
    value?: Value[] | Set<Value> | Value;
    /**
     * Callback for when the toggle-group's state is changed.
     *
     * Can be used to control the state of the component.
     *
     *
     * When none of the toggles are selected, undefined will be passed.
     */
    onChange?: (value: Set<Value>) => void;
  };
  Blocks: {
    default: [
      {
        Item: Item;
      },
    ];
  };
}

function isMulti(x: "single" | "multi" | undefined): x is "multi" {
  return x === "multi";
}

export class ToggleGroup<Value = any> extends Component<
  PrivateSingleSignature<Value> | PrivateMultiSignature<Value>
> {
  // See: https://github.com/typed-ember/glint/issues/715
  <template>
    {{#if (isMulti this.args.type)}}
      <MultiToggleGroup
        @value={{this.args.value}}
        @onChange={{this.args.onChange}}
        ...attributes
        as |x|
      >
        {{yield x}}
      </MultiToggleGroup>
    {{else}}
      <SingleToggleGroup
        @value={{this.args.value}}
        @onChange={{this.args.onChange}}
        ...attributes
        as |x|
      >
        {{yield x}}
      </SingleToggleGroup>
    {{/if}}
  </template>
}

class SingleToggleGroup<Value = any> extends Component<SingleSignature<Value>> {
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
  @localCopy("args.value") activePressed?: Value;

  handleToggle = (value: Value) => {
    if (this.activePressed === value) {
      this.activePressed = undefined;

      return;
    }

    this.activePressed = value;

    this.args.onChange?.(this.activePressed);
  };

  isPressed = (value: Value | undefined) => value === this.activePressed;

  <template>
    <div data-tabster={{TABSTER_CONFIG}} ...attributes>
      {{yield (hash Item=(component Toggle onChange=this.handleToggle isPressed=this.isPressed))}}
    </div>
  </template>
}

class MultiToggleGroup<Value = any> extends Component<MultiSignature<Value>> {
  /**
   * Normalizes @value to a Set
   * and makes sure that even if the input Set is reactive,
   * we don't mistakenly dirty it.
   */
  @cached
  get activePressed(): TrackedSet<Value> {
    const value = this.args.value;

    if (!value) {
      return new TrackedSet();
    }

    if (Array.isArray(value)) {
      return new TrackedSet(value);
    }

    if (value instanceof Set) {
      return new TrackedSet(value);
    }

    return new TrackedSet([value]);
  }

  handleToggle = (value: Value) => {
    if (this.activePressed.has(value)) {
      this.activePressed.delete(value);
    } else {
      this.activePressed.add(value);
    }

    this.args.onChange?.(new Set<Value>(this.activePressed.values()));
  };

  isPressed = (value: Value) => this.activePressed.has(value);

  <template>
    <div data-tabster={{TABSTER_CONFIG}} ...attributes>
      {{yield (hash Item=(component Toggle onChange=this.handleToggle isPressed=this.isPressed))}}
    </div>
  </template>
}


---

// import Component from '@glimmer/component';
import { fn } from "@ember/helper";
import { on } from "@ember/modifier";

import { cell } from "ember-resources";

import { toggleWithFallback } from "./-private/utils.ts";

import type { TOC } from "@ember/component/template-only";

export interface Signature<Value = any> {
  Element: HTMLButtonElement;
  Args: {
    /**
     * The pressed-state of the toggle.
     *
     * Can be used to control the state of the component.
     */
    pressed?: boolean;
    /**
     * Callback for when the toggle's state is changed.
     *
     * Can be used to control the state of the component.
     *
     * if a `@value` is passed to this `<Toggle>`, that @value will
     * be passed to the `@onChange` handler.
     *
     * This can be useful when using the same function for the `@onChange`
     * handler with multiple `<Toggle>` components.
     */
    onChange?: (value: Value | undefined, pressed: boolean) => void;

    /**
     * When used in a group of Toggles, this option will be helpful to
     * know which toggle was pressed if you're using the same @onChange
     * handler for multiple toggles.
     */
    value?: Value;

    /**
     * When controlling state in a wrapping component, this function can be used in conjunction with `@value` to determine if this `<Toggle>` should appear pressed.
     */
    isPressed?: (value?: Value) => boolean;
  };
  Blocks: {
    default: [
      /**
       * the current pressed state of the toggle button
       *
       * Useful when using the toggle button as an uncontrolled component
       */
      pressed: boolean,
    ];
  };
}

function isPressed(
  pressed?: boolean,
  value?: unknown,
  isPressed?: (value?: unknown) => boolean,
): boolean {
  if (!value) return Boolean(pressed);
  if (!isPressed) return Boolean(pressed);

  return isPressed(value);
}

export const Toggle: TOC<Signature> = <template>
  {{#let (cell (isPressed @pressed @value @isPressed)) as |pressed|}}
    <button
      type="button"
      aria-pressed="{{pressed.current}}"
      {{on "click" (fn toggleWithFallback pressed.toggle @onChange @value)}}
      ...attributes
    >
      {{yield pressed.current}}
    </button>
  {{/let}}
</template>;

export default Toggle;


---

span[data-prim-avatar]:has(img[alt="__missing__"])::after,
[aria-label="__missing__"] {
  border: red;
}
label [aria-label="__missing__"] {
  border: unset;
}

/**
 * ExternalLink
 */
a[href='##missing##'],
/**
 * Avatar
 */
span[data-prim-avatar]:has(img[alt='__missing__'])::after,
/**
 * Switch
 */
div[data-prim-switch]:has(input[role="switch"]):not(:has(label)) input[role="switch"] {
  position: relative;
  border: 1px solid black;
  padding: 0.125rem 0.25rem;
  border-radius: 0.125rem;
  min-width: 10px;
}

:is(
  /**
   * ExternalLink
   */
  a[href='##missing##'],
  /**
   * Avatar
   */
  span[data-prim-avatar]:has(img[alt='__missing__'])::after,
  /**
   * Switch
   */
  div[data-prim-switch]:not(:has(label)):has(input[role="switch"]) input[role="switch"]
)::after {
  color: red;
  position: absolute;
  font-size: 0.75rem;
  font-family: monospace;
  background: black;
  padding: 0.125rem 0.25rem;
  display: flex;
  border-radius: 0.125rem;
  transform: translate(0.5rem, 1rem);
  left: 0;
  bottom: 0;
  width: max-content;
  z-index: 10000000000000000;
}

a[href="##missing##"]::after {
  content: "empty href";
}

span[data-prim-avatar]:has(img[alt="__missing__"])::after {
  content: "missing alt";
}

div[data-prim-switch]:not(:has(label)):has(input[role="switch"]) input[role="switch"]::after {
  content: "missing label";
}

@media (prefers-color-scheme: light) {
  :is(
    a[href="##missing##"],
    span[data-prim-avatar]:has(img[alt="__missing__"]),
    div[data-prim-switch]:has(input[role="switch"]):not(:has(label)) input[role="switch"]
  ) {
    border-color: black;
  }
  :is(
    a[href="##missing##"],
    span[data-prim-avatar]:has(img[alt="__missing__"]),
    div[data-prim-switch]:not(:has(label)):has(input[role="switch"]) input[role="switch"]
  ):after {
    background: white;
    border: 1px solid black;
    color: darkred;
  }
}

@media (prefers-color-scheme: dark) {
  :is(
    a[href="##missing##"],
    span[data-prim-avatar]:has(img[alt="__missing__"]),
    div[data-prim-switch]:has(input[role="switch"]):not(:has(label)) input[role="switch"]
  ) {
    border-color: red;
  }
  :is(
    a[href="##missing##"],
    span[data-prim-avatar]:has(img[alt="__missing__"]),
    div[data-prim-switch]:not(:has(label)):has(input[role="switch"]) input[role="switch"]
  ):after {
    background: #222;
    border: 1px solid red;
    color: red;
  }
}


---

import './violations.css';


---

/* See: https://github.com/twbs/bootstrap/blob/main/scss/mixins/_visually-hidden.scss */
.ember-primitives__visually-hidden,
[visually-hidden] {
  position: absolute;
  border: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  word-wrap: normal;
}


---

import "./visually-hidden.css";

import type { TOC } from "@ember/component/template-only";

export const VisuallyHidden: TOC<{
  Element: HTMLSpanElement;
  Blocks: {
    /**
     * Content to hide visually
     */
    default: [];
  };
}> = <template>
  <span class="ember-primitives__visually-hidden" ...attributes>{{yield}}</span>
</template>;


---

export { Zoetrope } from './zoetrope/index.gts';
export { default } from './zoetrope/index.gts';
export type { Signature } from './zoetrope/types.ts';


---

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";

import { modifier as eModifier } from "ember-modifier";

import { anchorTo } from "./modifier.ts";

import type { Signature as ModifierSignature } from "./modifier.ts";
import type { MiddlewareState } from "@floating-ui/dom";
import type { ModifierLike } from "@glint/template";

type ModifierArgs = ModifierSignature["Args"]["Named"];

interface ReferenceSignature {
  Element: HTMLElement | SVGElement;
}

export interface Signature {
  Args: {
    /**
     * Additional middleware to pass to FloatingUI.
     *
     * See: [The middleware docs](https://floating-ui.com/docs/middleware)
     */
    middleware?: ModifierArgs["middleware"];
    /**
     * Where to place the floating element relative to its reference element.
     * The default is 'bottom'.
     *
     * See: [The placement docs](https://floating-ui.com/docs/computePosition#placement)
     */
    placement?: ModifierArgs["placement"];
    /**
     * This is the type of CSS position property to use.
     * By default this is 'fixed', but can also be 'absolute'.
     *
     * See: [The strategy docs](https://floating-ui.com/docs/computePosition#strategy)
     */
    strategy?: ModifierArgs["strategy"];
    /**
     * Options to pass to the [flip middleware](https://floating-ui.com/docs/flip)
     */
    flipOptions?: ModifierArgs["flipOptions"];
    /**
     * Options to pass to the [hide middleware](https://floating-ui.com/docs/hide)
     */
    hideOptions?: ModifierArgs["hideOptions"];
    /**
     * Options to pass to the [shift middleware](https://floating-ui.com/docs/shift)
     */
    shiftOptions?: ModifierArgs["shiftOptions"];
    /**
     * Options to pass to the [offset middleware](https://floating-ui.com/docs/offset)
     */
    offsetOptions?: ModifierArgs["offsetOptions"];
  };
  Blocks: {
    default: [
      /**
       * A modifier to apply to the _reference_ element.
       * This is what the floating element will use to anchor to.
       *
       * Example
       * ```gjs
       * import { FloatingUI } from 'ember-primitives/floating-ui';
       *
       * <template>
       *   <FloatingUI as |reference floating|>
       *     <button {{reference}}> ... </button>
       *     ...
       *   </FloatingUI>
       * </template>
       * ```
       */
      reference: ModifierLike<ReferenceSignature>,
      /**
       * A modifier to apply to the _floating_ element.
       * This is what will anchor to the reference element.
       *
       * Example
       * ```gjs
       * import { FloatingUI } from 'ember-primitives/floating-ui';
       *
       * <template>
       *   <FloatingUI as |reference floating|>
       *     <button {{reference}}> ... </button>
       *     <menu {{floating}}> ... </menu>
       *   </FloatingUI>
       * </template>
       * ```
       */
      floating:
        | undefined
        | ModifierLike<{
            Element: HTMLElement;
            Args: {
              Named: ModifierArgs;
            };
          }>,
      /**
       * Special utilities for advanced usage
       */
      util: {
        /**
         * If you want to have a single modifier with custom behavior
         * on your reference element, you may use this `setReference`
         * function to set the reference, rather than having multiple modifiers
         * on that element.
         */
        setReference: (element: HTMLElement | SVGElement) => void;
        /**
         * Metadata exposed from floating-ui.
         * Gives you x, y position, among other things.
         */
        data?: MiddlewareState;
      },
    ];
  };
}

const ref = eModifier<{
  Element: HTMLElement | SVGElement;
  Args: {
    Positional: [setRef: (element: HTMLElement | SVGElement) => void];
  };
}>((element: HTMLElement | SVGElement, positional) => {
  const fn = positional[0];

  fn(element);
});

/**
 * A component that provides no DOM and yields two modifiers for creating
 * creating floating uis, such as menus, popovers, tooltips, etc.
 * This component currently uses [Floating UI](https://floating-ui.com/)
 * but will be switching to [CSS Anchor Positioning](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_anchor_positioning) when that lands.
 *
 * Example usage:
 * ```gjs
 * import { FloatingUI } from 'ember-primitives/floating-ui';
 *
 * <template>
 *   <FloatingUI as |reference floating|>
 *     <button {{reference}}> ... </button>
 *     <menu {{floating}}> ... </menu>
 *   </FloatingUI>
 * </template>
 * ```
 */
export class FloatingUI extends Component<Signature> {
  @tracked reference?: HTMLElement | SVGElement = undefined;
  @tracked data?: MiddlewareState = undefined;

  setData: ModifierArgs["setData"] = (data) => (this.data = data);

  setReference = (element: HTMLElement | SVGElement) => {
    this.reference = element;
  };

  <template>
    {{#let
      (modifier
        anchorTo
        flipOptions=@flipOptions
        hideOptions=@hideOptions
        middleware=@middleware
        offsetOptions=@offsetOptions
        placement=@placement
        shiftOptions=@shiftOptions
        strategy=@strategy
        setData=this.setData
      )
      as |prewiredAnchorTo|
    }}
      {{#let (if this.reference (modifier prewiredAnchorTo this.reference)) as |floating|}}
        {{! @glint-nocheck -- Excessively deep, possibly infinite }}
        {{yield
          (modifier ref this.setReference)
          floating
          (hash setReference=this.setReference data=this.data)
        }}
      {{/let}}
    {{/let}}
  </template>
}


---

import type { Middleware } from '@floating-ui/dom';

export function exposeMetadata(): Middleware {
  return {
    name: 'metadata',
    fn: (data) => {
      // https://floating-ui.com/docs/middleware#always-return-an-object
      return {
        data,
      };
    },
  };
}


---

import { assert } from '@ember/debug';

import { autoUpdate, computePosition, flip, hide, offset, shift } from '@floating-ui/dom';
import { modifier as eModifier } from 'ember-modifier';

import { exposeMetadata } from './middleware.ts';

import type {
  FlipOptions,
  HideOptions,
  Middleware,
  OffsetOptions,
  Placement,
  ShiftOptions,
  Strategy,
} from '@floating-ui/dom';

export interface Signature {
  /**
   *
   */
  Element: HTMLElement;
  Args: {
    Positional: [
      /**
       * What do use as the reference element.
       * Can be a selector or element instance.
       *
       * Example:
       * ```gjs
       * import { anchorTo } from 'ember-primitives/floating-ui';
       *
       * <template>
       *   <div id="reference">...</div>
       *   <div {{anchorTo "#reference"}}> ... </div>
       * </template>
       * ```
       */
      referenceElement: string | HTMLElement | SVGElement,
    ];
    Named: {
      /**
       * This is the type of CSS position property to use.
       * By default this is 'fixed', but can also be 'absolute'.
       *
       * See: [The strategy docs](https://floating-ui.com/docs/computePosition#strategy)
       */
      strategy?: Strategy;
      /**
       * Options to pass to the [offset middleware](https://floating-ui.com/docs/offset)
       */
      offsetOptions?: OffsetOptions;
      /**
       * Where to place the floating element relative to its reference element.
       * The default is 'bottom'.
       *
       * See: [The placement docs](https://floating-ui.com/docs/computePosition#placement)
       */
      placement?: Placement;
      /**
       * Options to pass to the [flip middleware](https://floating-ui.com/docs/flip)
       */
      flipOptions?: FlipOptions;
      /**
       * Options to pass to the [shift middleware](https://floating-ui.com/docs/shift)
       */
      shiftOptions?: ShiftOptions;
      /**
       * Options to pass to the [hide middleware](https://floating-ui.com/docs/hide)
       */
      hideOptions?: HideOptions;
      /**
       * Additional middleware to pass to FloatingUI.
       *
       * See: [The middleware docs](https://floating-ui.com/docs/middleware)
       */
      middleware?: Middleware[];
      /**
       * A callback for when data changes about the position / placement / etc
       * of the floating element.
       */
      setData?: Middleware['fn'];
    };
  };
}

/**
 * A modifier to apply to the _floating_ element.
 * This is what will anchor to the reference element.
 *
 * Example
 * ```gjs
 * import { anchorTo } from 'ember-primitives/floating-ui';
 *
 * <template>
 *   <button id="my-button"> ... </button>
 *   <menu {{anchorTo "#my-button"}}> ... </menu>
 * </template>
 * ```
 */
export const anchorTo = eModifier<Signature>(
  (
    floatingElement,
    [_referenceElement],
    {
      strategy = 'fixed',
      offsetOptions = 0,
      placement = 'bottom',
      flipOptions,
      shiftOptions,
      middleware = [],
      setData,
    }
  ) => {
    const referenceElement: null | HTMLElement | SVGElement =
      typeof _referenceElement === 'string'
        ? document.querySelector(_referenceElement)
        : _referenceElement;

    assert(
      'no reference element defined',
      referenceElement instanceof HTMLElement || referenceElement instanceof SVGElement
    );

    assert(
      'no floating element defined',
      floatingElement instanceof HTMLElement || _referenceElement instanceof SVGElement
    );

    assert(
      'reference and floating elements cannot be the same element',
      floatingElement !== _referenceElement
    );

    assert('@middleware must be an array of one or more objects', Array.isArray(middleware));

    Object.assign(floatingElement.style, {
      position: strategy,
      top: '0',
      left: '0',
    });

    const update = async () => {
      const { middlewareData, x, y } = await computePosition(referenceElement, floatingElement, {
        middleware: [
          offset(offsetOptions),
          flip(flipOptions),
          shift(shiftOptions),
          ...middleware,
          hide({ strategy: 'referenceHidden' }),
          hide({ strategy: 'escaped' }),
          exposeMetadata(),
        ],
        placement,
        strategy,
      });

      const referenceHidden = middlewareData.hide?.referenceHidden;

      Object.assign(floatingElement.style, {
        top: `${y}px`,
        left: `${x}px`,
        margin: 0,
        visibility: referenceHidden ? 'hidden' : 'visible',
      });

      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
      void setData?.(middlewareData['metadata']);
    };

    void update();

    // eslint-disable-next-line @typescript-eslint/no-misused-promises
    const cleanup = autoUpdate(referenceElement, floatingElement, update);

    /**
     * in the function-modifier manager, teardown of the previous modifier
     * occurs before setup of the next
     * https://github.com/ember-modifier/ember-modifier/blob/main/ember-modifier/src/-private/function-based/modifier-manager.ts#L58
     */
    return cleanup;
  }
);


---

/**
 * Initial inspo:
 * - https://github.com/ef4/ember-set-body-class/blob/master/addon/services/body-class.js
 * - https://github.com/ef4/ember-set-body-class/blob/master/addon/helpers/set-body-class.js
 */
import Helper from '@ember/component/helper';
import { buildWaiter } from '@ember/test-waiters';

const waiter = buildWaiter('ember-primitives:body-class:raf');

let id = 0;
const registrations = new Map<number, string[]>();
let previousRegistrations: string[] = [];

function classNames(): string[] {
  const allNames = new Set<string>();

  for (const classNames of registrations.values()) {
    for (const className of classNames) {
      allNames.add(className);
    }
  }

  return [...allNames];
}

let frame: number;
let waiterToken: unknown;

function queueUpdate() {
  waiterToken ||= waiter.beginAsync();

  cancelAnimationFrame(frame);
  frame = requestAnimationFrame(() => {
    updateBodyClass();
    waiter.endAsync(waiterToken);
    waiterToken = undefined;
  });
}

/**
 * This should only add/remove classes that we tried to maintain via the body-class helper.
 *
 * Folks can set classes in their html and we don't want to mess with those
 */
function updateBodyClass() {
  const toAdd = classNames();

  for (const name of previousRegistrations) {
    document.body.classList.remove(name);
  }

  for (const name of toAdd) {
    document.body.classList.add(name);
  }

  previousRegistrations = toAdd;
}

export interface Signature {
  Args: {
    Positional: [
      /**
       * a space-delimited list of classes to apply when this helper is called.
       *
       * When the helper is removed from rendering, the clasess will be removed as well.
       */
      classes: string,
    ];
  };
  /**
   * This helper returns nothing, as it is a side-effect that mutates and manages external state.
   */
  Return: undefined;
}

export default class BodyClass extends Helper<Signature> {
  localId = id++;

  compute([classes]: [string]): undefined {
    const classNames = classes ? classes.split(/\s+/) : [];

    registrations.set(this.localId, classNames);

    queueUpdate();
  }

  willDestroy() {
    registrations.delete(this.localId);
    queueUpdate();
  }
}

export const bodyClass = BodyClass;


---

import Helper from '@ember/component/helper';
import { assert } from '@ember/debug';
import { service } from '@ember/service';

import { handle } from '../proper-links.ts';

import type RouterService from '@ember/routing/router-service';

export interface Signature {
  Args: {
    Positional: [href: string];
    Named: {
      includeActiveQueryParams?: boolean | string[];
      activeOnSubPaths?: boolean;
    };
  };
  Return: {
    isExternal: boolean;
    isActive: boolean;
    handleClick: (event: MouseEvent) => void;
  };
}

export default class Link extends Helper<Signature> {
  @service declare router: RouterService;

  compute(
    [href]: [href: string],
    {
      includeActiveQueryParams = false,
      activeOnSubPaths = false,
    }: { includeActiveQueryParams?: boolean | string[]; activeOnSubPaths?: boolean }
  ) {
    assert('href was not passed in', href);

    const router = this.router;
    const handleClick = (event: MouseEvent) => {
      assert('[BUG]', event.currentTarget instanceof HTMLAnchorElement);

      handle(router, event.currentTarget, [], event);
    };

    return {
      isExternal: isExternal(href),
      get isActive() {
        return isActive(router, href, includeActiveQueryParams, activeOnSubPaths);
      },
      handleClick,
    };
  }
}

export const link = Link;

export function isExternal(href: string) {
  if (!href) return false;
  if (href.startsWith('#')) return false;
  if (href.startsWith('/')) return false;

  return location.origin !== new URL(href).origin;
}

export function isActive(
  router: RouterService,
  href: string,
  includeQueryParams?: boolean | string[],
  activeOnSubPaths?: boolean
) {
  if (!includeQueryParams) {
    /**
     * is Active doesn't understand `href`, so we have to convert to RouteInfo-esque
     */
    const info = router.recognize(href);

    if (info) {
      const dynamicSegments = getParams(info);
      const routeName = activeOnSubPaths ? info.name.replace(/\.index$/, '') : info.name;

      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
      return router.isActive(routeName, ...dynamicSegments);
    }

    return false;
  }

  const url = new URL(href, location.origin);
  const hrefQueryParams = new URLSearchParams(url.searchParams);
  const hrefPath = url.pathname;

  const currentPath = router.currentURL?.split('?')[0];

  if (!currentPath) return false;

  if (activeOnSubPaths ? !currentPath.startsWith(hrefPath) : hrefPath !== currentPath) return false;

  const currentQueryParams = router.currentRoute?.queryParams;

  if (!currentQueryParams) return false;

  if (includeQueryParams === true) {
    return Object.entries(currentQueryParams).every(([key, value]) => {
      return hrefQueryParams.get(key) === value;
    });
  }

  return includeQueryParams.every((key) => {
    return hrefQueryParams.get(key) === currentQueryParams[key];
  });
}

type RouteInfo = ReturnType<RouterService['recognize']>;

export function getParams(currentRouteInfo: RouteInfo) {
  let params: Record<string, unknown>[] = [];

  while (currentRouteInfo?.parent) {
    const currentParams = currentRouteInfo.params;

    params = currentParams ? [currentParams, ...params] : params;
    currentRouteInfo = currentRouteInfo.parent;
  }

  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
  return params.map(Object.values).flat();
}


---

import Helper from '@ember/component/helper';
import { assert } from '@ember/debug';
import { getOwner } from '@ember/owner';

import type { Registry } from '@ember/service';
import type Service from '@ember/service';

export interface Signature<Key extends keyof Registry> {
  Args: {
    Positional: [Key];
  };
  Return: Registry[Key] & Service;
}

export default class GetService<Key extends keyof Registry> extends Helper<Signature<Key>> {
  compute(positional: [Key]): Registry[Key] & Service {
    const owner = getOwner(this);

    assert(`Could not get owner.`, owner);

    return owner.lookup(`service:${positional[0]}`) as Registry[Key] & Service;
  }
}

export const service = GetService;


---

import { assert } from '@ember/debug';

import { setupTabster as _setupTabster } from '../tabster.ts';

import type Owner from '@ember/owner';

/**
 * Sets up all support utilities for primitive components.
 * Including the tabster root.
 */
async function setup(owner: Owner) {
  await _setupTabster(owner, { setTabsterRoot: false });

  document.querySelector('#ember-testing')?.setAttribute('data-tabster', '{ "root": {} }');
}

/**
 * A QUnit test utility for setting up the tabbing utility that a few of the components in ember-primitive use for providing enhanced keyboard support.
 *
 * ```gjs
 * import { module, test } from 'qunit';
 * import { setupRenderingTest } from 'ember-qunit';
 * import { setupTabster } from 'ember-primitives/test-support';
 *
 * module('your suite', function (hooks) {
 *   setupRenderingTest(hooks);
 *   setupTabster(hooks);
 *
 *   test('your test', async function (assert) {
 *      // ...
 *   });
 * });
 * ```
 *
 * This utility takes no options.
 */
export function setupTabster(hooks: {
  beforeEach: (callback: () => void | Promise<void>) => unknown;
}) {
  hooks.beforeEach(async function (this: { owner: object }) {
    const owner = this.owner;

    assert(
      `Test does not have an owner, be sure to use setupRenderingTest, setupTest, or setupApplicationTest (from ember-qunit (or similar))`,
      owner
    );

    await setup(this.owner as Owner);
  });
}


---

import { assert } from '@ember/debug';
import { find } from '@ember/test-helpers';

type Findable = Parameters<typeof find>[0] | Element;

/**
 * Find an element within a given element that has a shadow-root.
 *
 * If the `root` can't be found, or if there actually is no shadow root,
 * nothing will be returned.
 *
 * ```gjs
 * import { findInShadow } from 'ember-primitives/test-support';
 *
 * // ...
 *
 * test('...', async function (assert) {
 *    // ...
 *    const root = find('div.with-shadowdom');
 *    assert.dom(findInShadow(root, 'h1')).containsText('welcome');
 * });
 * ```
 */
export function findInShadow(root: Findable, query: string) {
  const rootElement = root instanceof Element ? root : find(root);

  return rootElement?.shadowRoot?.querySelector(query);
}

/**
 * Does the element have a shadow root?
 *
 * Using this utility function will only save a few characters over using its implementation directly.
 *
 * ```gjs
 * import { hasShadowRoot } from 'ember-primitives/test-support';
 *
 * // ...
 *
 * test('...', async function (assert) {
 *    // ...
 *    const el = find('div.with-shadowdom');
 *    assert.ok(hasShadowRoot(el), 'expecting el to have a shadow root');
 * });
 * ```
 */
export function hasShadowRoot(el: Element) {
  return Boolean(el.shadowRoot);
}

/**
 * Find an element within `root`, that has a shadow root.
 * The `root` param is optional, and if not provided, all of `#ember-testing` will be searched.
 *
 * This only returns the first-found shadow, so if you want a specifc shadow root,
 * you'll need to narrow down the search by specifying a `root`.
 *
 * ```gjs
 * import { findShadow } from 'ember-primitives/test-support';
 *
 * // ...
 *
 * test('...', async function (assert) {
 *    // ...
 *    const el = findShadow('div.with-shadowdom');
 *    // ...
 * });
 * ```
 */
export function findShadow(root?: Findable) {
  const rootElement = root
    ? root instanceof Element
      ? root
      : find(root)
    : document.getElementById('ember-testing');

  if (!rootElement) return;

  for (const element of rootElement.querySelectorAll('*')) {
    if (element.shadowRoot) {
      return element;
    }
  }
}

/**
 * For the first available shadow root on the page, query in to it, like you would with `querySelector`.
 *
 *
 * ```gjs
 * import { findInFirstShadow } from 'ember-primitives/test-support';
 *
 * // ...
 *
 * test('...', async function (assert) {
 *    // ...
 *    assert.dom(findInFirstShadow('h1')).containsText('welcome');
 * });
 * ```
 *
 * If there are multiple shadow roots on the page / test-render,
 * this is not the utility for you.
 *
 * For querying in specific shadow roots, you'll want to use `findInShadow`
 */
export function findInFirstShadow(query: string) {
  const host = findShadow();

  assert(`No element with a shadow root could be found`, host);

  return findInShadow(host, query);
}


---

import { assert } from '@ember/debug';
import { fillIn, find, settled } from '@ember/test-helpers';

/**
 * Fill the OTP input
 *
 * ```gjs
 * import { fillOTP } from 'ember-primitives/test-support';
 *
 * test('...', async function(assert) {
 *   // ...
 *   await fillOTP('123456');
 *   // ...
 * })
 *
 * ```
 *
 * @param {string} code the code to fill the input(s) with.
 * @param {string} [ selector ] if there are multiple OTP components on a page, this can be used to select one of them.
 */
export async function fillOTP(code: string, selector?: string) {
  const ancestor = selector ? find(selector) : document;

  assert(
    `Could not find ancestor element, does your selector match an existing element?`,
    ancestor
  );

  const fieldset =
    ancestor instanceof HTMLFieldSetElement ? ancestor : ancestor.querySelector('fieldset');

  assert(
    `Could not find containing fieldset element (this holds the OTP Input fields). Was the OTP component rendered?`,
    fieldset
  );

  const inputs = fieldset.querySelectorAll('input');

  assert(
    `code cannot be longer than the available inputs. code is of length ${code.length} but there are ${inputs.length}`,
    code.length <= inputs.length
  );

  const chars = code.split('');

  assert(`OTP Input for index 0 is missing!`, inputs[0]);
  assert(`Character at index 0 is missing`, chars[0]);

  for (let i = 0; i < chars.length; i++) {
    const input = inputs[i];
    const char = chars[i];

    assert(`Input at index ${i} is missing`, input);
    assert(`Character at index ${i} is missing`, char);

    input.value = char;
  }

  await fillIn(inputs[0], chars[0]);

  // Account for out-of-settled-system delay due to RAF debounce.
  await new Promise((resolve) => requestAnimationFrame(resolve));
  await settled();
}


---

import { assert } from '@ember/debug';
import { click, fillIn, find, findAll } from '@ember/test-helpers';

const selectors = {
  root: '.ember-primitives__rating',
  item: '.ember-primitives__rating__item',
  label: '.ember-primitives__rating__label',

  rootData: {
    total: '[data-total]',
    value: '[data-value]',
  },

  itemData: {
    number: '[data-number]',
    readonly: '[data-readonly]',
    selected: '[data-selected]',
    itemPercent: '[data-percent-selected]',
  },
};

const stars = {
  selected: '★',
  unselected: '☆',
};

/**
 * Test utility for interacting with the
 * Rating component.
 *
 * Simulates user behavior and provides high level functions so you don't need to worry about the DOM.
 *
 * Actual elements are not exposed, as the elements are private API.
 * Even as you build a design system, the DOM should not be exposed to your consumers.
 */
export function rating(selector?: string) {
  const root = `${selector ?? ''}${selectors.root}`;

  return new RatingPageObject(root);
}

class RatingPageObject {
  #root: string;

  constructor(root: string) {
    this.#root = root;
  }

  get #rootElement() {
    const element = find(this.#root);

    assert(
      `Could not find the root element for the <Rating> component. Used the selector \`${this.#root}\`. Was it rendered?`,
      element
    );

    return element;
  }

  get #labelElement() {
    const element = find(`${this.#root} ${selectors.label}`);

    assert(`Could not find the label for the <Rating> component. Was it rendered?`, element);

    return element;
  }

  get label() {
    return this.#labelElement.textContent?.replaceAll(/\s+/g, ' ').trim();
  }

  get #starElements() {
    const elements = findAll(`${this.#root} ${selectors.item}`);

    assert(
      `There are no stars/items. Is the <Rating> component misconfigured?`,
      elements.length > 0
    );

    return elements as HTMLElement[];
  }

  get stars() {
    const elements = this.#starElements;

    return elements
      .map((x) => (x.hasAttribute('data-selected') ? stars.selected : stars.unselected))
      .join(' ');
  }

  get starTexts() {
    const elements = this.#starElements;

    return elements.map((x) => x.querySelector('[aria-hidden]')?.textContent?.trim()).join(' ');
  }

  get value() {
    const value = this.#rootElement.getAttribute(`data-value`);

    assert(`data-value attribute is missing on element '${this.#root}'`, value);

    const number = parseFloat(value);

    return number;
  }

  get isReadonly() {
    return this.#starElements.every((x) => x.hasAttribute('data-readonly'));
  }

  async select(stars: number) {
    const root = this.#rootElement;

    const star = root.querySelector(`[data-number="${stars}"] input`);

    if (star) {
      await click(star);

      return;
    }

    /**
     * When we don't have an input, we require an input --
     * which is also the only way we can choose non-integer values.
     *
     * Should be able to be a number input or range input.
     */
    const input = root.querySelector('input[type="number"], input[type="range"]');

    if (input) {
      await fillIn(input, `${stars}`);

      return;
    }

    const available = [...root.querySelectorAll('[data-number]')].map((x) =>
      x.getAttribute('data-number')
    );

    assert(
      `Could not find item/star in <Rating> with value '${stars}' (or a number or range input with the same "name" value). Is the number (${stars}) correct and in-range for this component? The found available values are ${available.join(', ')}.`
    );
  }
}


---

import { assert } from '@ember/debug';
import Router from '@ember/routing/router';

import { properLinks } from '../proper-links.ts';

import type Owner from '@ember/owner';
import type { DSLCallback } from '@ember/routing/lib/dsl';
import type RouterService from '@ember/routing/router-service';

/**
 * Allows setting up routes in tests without the need to scaffold routes in the actual app,
 * allowing for iterating on many different routing scenario / configurations rapidly.
 *
 * Example:
 * ```js
 * import { setupRouting } from 'ember-primitives/test-support';
 *
 *  ...
 *
 * test('my test', async function (assert) {
 *   setupRouting(this.owner, function () {
 *     this.route('foo');
 *     this.route('bar', function () {
 *       this.route('a');
 *       this.route('b');
 *     })
 *   });
 *
 *   await visit('/bar/b');
 * });
 * ```
 *
 */
export function setupRouting(owner: Owner, map: DSLCallback, options?: { rootURL: string }) {
  if (options?.rootURL) {
    assert('rootURL must begin with a forward slash ("/")', options?.rootURL?.startsWith('/'));
  }

  @properLinks
  class TestRouter extends Router {
    rootURL = options?.rootURL ?? '/';
  }

  TestRouter.map(map);

  owner.register('router:main', TestRouter);

  // eslint-disable-next-line ember/no-private-routing-service
  const iKnowWhatIMDoing = owner.lookup('router:main');

  // We need a public testing API for this sort of stuff

  // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
  (iKnowWhatIMDoing as any).setupRouter();
}

/**
 * A small utility that only gives you a _typed_ router service.
 */
export function getRouter(owner: Owner): RouterService {
  return owner.lookup('service:router');
}


---

/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { click } from '@ember/test-helpers';

export class ZoetropeHelper {
  parentSelector = '.ember-primitives__zoetrope';

  constructor(parentSelector?: string) {
    if (parentSelector) {
      this.parentSelector = parentSelector;
    }
  }

  async scrollLeft() {
    await click(`${this.parentSelector} .ember-primitives__zoetrope__controls button:first-child`);
  }

  async scrollRight() {
    await click(`${this.parentSelector} .ember-primitives__zoetrope__controls button:last-child`);
  }

  visibleItems() {
    const zoetropeContent = document.querySelectorAll(
      `${this.parentSelector} .ember-primitives__zoetrope__scroller > *`
    );

    let firstVisibleItemIndex = -1;
    let lastVisibleItemIndex = -1;

    for (let i = 0; i < zoetropeContent.length; i++) {
      const item = zoetropeContent[i]!;
      const rect = item.getBoundingClientRect();
      const parentRect = item.parentElement!.getBoundingClientRect();

      if (rect.right >= parentRect?.left && rect.left <= parentRect?.right) {
        if (firstVisibleItemIndex === -1) {
          firstVisibleItemIndex = i;
        }

        lastVisibleItemIndex = i;
      } else if (firstVisibleItemIndex !== -1) {
        break;
      }
    }

    return Array.from(zoetropeContent).slice(firstVisibleItemIndex, lastVisibleItemIndex + 1);
  }

  visibleItemCount() {
    return this.visibleItems().length;
  }
}


---

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { assert } from "@ember/debug";

import { element } from "ember-element-helper";
import { modifier } from "ember-modifier";

import { viewport } from "./viewport.ts";

export type InViewportMode = "replace" | "contain";

/**
 * Configuration for the InViewport component
 */
export interface InViewportSignature {
  Element: HTMLElement;
  Args: {
    /**
     * The tag name for the placeholder element.
     * Can be any valid HTML tag name.
     * Default: 'div'
     */
    tagName?: string;

    /**
     * The mode determines how yielded content is rendered:
     * - 'replace': yielded content replaces the placeholder element
     * - 'contain': yielded content is rendered within the placeholder
     * Default: 'contain'
     */
    mode?: InViewportMode;
  };
  Blocks: {
    /**
     * Default block - rendered when the element is in the viewport
     */
    default: [];
  };
}

/**
 * A component that only renders its content when the element is near the viewport.
 *
 * This is useful for deferring the rendering of heavy components until they're
 * actually needed, improving performance for pages with many components.
 *
 * Example usage:
 * ```gjs
 * import { InViewport } from 'ember-primitives';
 *
 * <template>
 *   <InViewport>
 *     <ExpensiveComponent />
 *   </InViewport>
 * </template>
 * ```
 *
 * The component uses the Intersection Observer API to detect when the element
 * is near the viewport. Once detected, the observer is destroyed and the content
 * is rendered permanently.
 */
export class InViewport extends Component<InViewportSignature> {
  /**
   * Whether the element has been detected as in/near the viewport
   */
  @tracked hasIntersected = false;

  get #viewport() {
    return viewport(this);
  }

  setupObserver = modifier((element: Element) => {
    if (this.hasIntersected) {
      return;
    }

    this.#viewport.observe(element, this.handle);

    return () => this.#viewport.unobserve(element, this.handle);
  });

  handle = (entry: IntersectionObserverEntry) => {
    if (entry?.isIntersecting) {
      this.hasIntersected = true;

      this.#viewport.unobserve(entry.target, this.handle);
    }
  };

  get mode(): InViewportMode {
    assert(
      'InViewport mode must be either "replace" or "contain"',
      !this.args.mode || this.args.mode === "replace" || this.args.mode === "contain",
    );

    return this.args.mode ?? "contain";
  }

  get tagName(): string {
    return this.args.tagName ?? "div";
  }

  get hasReachedViewport(): boolean {
    return this.hasIntersected;
  }

  get isReplacing(): boolean {
    return this.mode === "replace";
  }

  <template>
    {{#let (element this.tagName) as |El|}}
      {{#if this.isReplacing}}
        {{#if this.hasReachedViewport}}
          {{yield}}
        {{else}}
          <El {{this.setupObserver}} ...attributes />
        {{/if}}
      {{else}}
        <El {{this.setupObserver}} ...attributes>
          {{#if this.hasReachedViewport}}
            {{yield}}
          {{/if}}
        </El>
      {{/if}}
    {{/let}}
  </template>
}


---

import { registerDestructor } from '@ember/destroyable';

import { createService } from '../service.ts';

/**
 * Creates or returns the ViewportObserverManager.
 *
 * Only one of these will exist per owner.
 *
 * Has only two methods:
 * - observe(element, callback: (intersectionObserverEntry) => void, options?)
 * - unobserve(element, callback: (intersectionObserverEntry) => void)
 *
 * Like with the underlying IntersectionObserver API (and all event listeners),
 * the callback passed to unobserve must be the same reference as the one
 * passed to observe.
 */
export function viewport(context: object) {
  return createService(context, ViewportObserverManager);
}

export interface ViewportOptions {
  /**
   * A margin around the root. Can have values similar to the CSS margin property.
   * The values can be percentages. This set of values serves to grow or shrink each
   * side of the root element's bounding box before computing intersections.
   * Defaults to all zeros.
   */
  rootMargin?: string;
  /**
   * Either a single number or an array of numbers which indicate at what percentage
   * of the target's visibility the observer's callback should be executed. If you only
   * want to detect when visibility passes the 50% mark, you can use a value of 0.5.
   * If you want the callback to run every time visibility passes another 25%, you would
   * specify the array [0, 0.25, 0.5, 0.75, 1]. The default is 0 (meaning as soon as
   * even one pixel is visible, the callback will be run).
   */
  threshold?: number | number[];
}

class ViewportObserverManager {
  #callbacks = new WeakMap<Element, Set<(entries: IntersectionObserverEntry) => unknown>>();

  #handleIntersection = (entries: IntersectionObserverEntry[]) => {
    for (const entry of entries) {
      const callbacks = this.#callbacks.get(entry.target);

      if (callbacks) {
        for (const callback of callbacks) {
          callback(entry);
        }
      }
    }
  };

  #observer = new IntersectionObserver(this.#handleIntersection, {
    /**
     * NOTE: clipping is unaffected by rootMargin if the intersection is with anything
     *       other than the specified "root".
     *       And since we don't specify the "root", this effectively means the window viewport.
     *       (hence the utility name: "viewport")
     */
  });

  constructor() {
    registerDestructor(this, () => {
      this.#observer?.disconnect();
    });
  }

  /**
   * Initiate the observing of the `element` or add an additional `callback`
   * if the `element` is already observed.
   *
   * @param {object} element
   * @param {function} callback The `callback` is called whenever the `element`
   *    intersects with the viewport. It is called with an `IntersectionObserverEntry`
   *    object for the particular `element`.
   */
  observe(element: Element, callback: (entry: IntersectionObserverEntry) => unknown) {
    const callbacks = this.#callbacks.get(element);

    if (callbacks) {
      callbacks.add(callback);
    } else {
      this.#callbacks.set(element, new Set([callback]));
      this.#observer.observe(element);
    }
  }

  /**
   * End the observing of the `element` or just remove the provided `callback`.
   *
   * It will unobserve the `element` if the `callback` is not provided
   * or there are no more callbacks left for this `element`.
   *
   * @param {Element | undefined | null} element
   * @param {function?} callback - The `callback` to remove from the listeners
   *   of the `element` intersection changes.
   */
  unobserve(
    element: Element | undefined | null,
    callback: (entry: IntersectionObserverEntry) => unknown
  ) {
    if (!element) {
      return;
    }

    const callbacks = this.#callbacks.get(element);

    if (!callbacks) {
      return;
    }

    callbacks.delete(callback);

    if (!callback || !callbacks.size) {
      this.#callbacks.delete(element);
      this.#observer.unobserve(element);
    }
  }
}


---

import type { TOC } from "@ember/component/template-only";

export const Div: TOC<{ Element: HTMLDivElement; Blocks: { default: [] } }> = <template>
  <div ...attributes>{{yield}}</div>
</template>;

export const Label: TOC<{
  Element: HTMLLabelElement;
  Args: { for: string };
  Blocks: { default: [] };
}> = <template>
  <label for={{@for}} ...attributes>{{yield}}</label>
</template>;


---

/**
 * If the user provides an onChange or similar function, use that,
 * otherwise fallback to the uncontrolled toggle
 */
export function toggleWithFallback(
  uncontrolledToggle: undefined | ((...args: any[]) => void) | (() => void),
  controlledToggle?: (...args: any[]) => void,
  ...args: unknown[]
) {
  if (controlledToggle) {
    return controlledToggle(...args);
  }

  uncontrolledToggle?.(...args);
}


---

import Component from "@glimmer/component";

import { getDataState } from "./item.gts";

import type { AccordionContentExternalSignature } from "./public.ts";

interface Signature extends AccordionContentExternalSignature {
  Args: {
    isExpanded: boolean;
    value: string;
    disabled?: boolean;
  };
}

export class AccordionContent extends Component<Signature> {
  <template>
    <div
      role="region"
      id={{@value}}
      data-state={{getDataState @isExpanded}}
      hidden={{this.isHidden}}
      data-disabled={{@disabled}}
      ...attributes
    >
      {{yield}}
    </div>
  </template>

  get isHidden() {
    return !this.args.isExpanded;
  }
}

export default AccordionContent;


---

import { hash } from "@ember/helper";

import { getDataState } from "./item.gts";
import Trigger from "./trigger.gts";

import type { AccordionHeaderExternalSignature } from "./public.ts";
import type { TOC } from "@ember/component/template-only";

interface Signature extends AccordionHeaderExternalSignature {
  Args: {
    value: string;
    isExpanded: boolean;
    disabled?: boolean;
    toggleItem: () => void;
  };
}

export const AccordionHeader: TOC<Signature> = <template>
  <div
    role="heading"
    aria-level="3"
    data-state={{getDataState @isExpanded}}
    data-disabled={{@disabled}}
    ...attributes
  >
    {{yield
      (hash
        Trigger=(component
          Trigger value=@value isExpanded=@isExpanded disabled=@disabled toggleItem=@toggleItem
        )
      )
    }}
  </div>
</template>;

export default AccordionHeader;


---

import Component from "@glimmer/component";
import { hash } from "@ember/helper";

import Content from "./content.gts";
import Header from "./header.gts";

import type { AccordionItemExternalSignature } from "./public.ts";

export function getDataState(isExpanded: boolean): string {
  return isExpanded ? "open" : "closed";
}

interface Signature extends AccordionItemExternalSignature {
  Args: AccordionItemExternalSignature["Args"] & {
    selectedValue?: string | string[];
    disabled?: boolean;
    toggleItem: (value: string) => void;
  };
}

export class AccordionItem extends Component<Signature> {
  <template>
    <div data-state={{getDataState this.isExpanded}} data-disabled={{@disabled}} ...attributes>
      {{yield
        (hash
          isExpanded=this.isExpanded
          Header=(component
            Header
            value=@value
            isExpanded=this.isExpanded
            disabled=@disabled
            toggleItem=this.toggleItem
          )
          Content=(component Content value=@value isExpanded=this.isExpanded disabled=@disabled)
        )
      }}
    </div>
  </template>

  get isExpanded(): boolean {
    if (Array.isArray(this.args.selectedValue)) {
      return this.args.selectedValue.includes(this.args.value);
    }

    return this.args.selectedValue === this.args.value;
  }

  toggleItem = (): void => {
    if (this.args.disabled) return;

    this.args.toggleItem(this.args.value);
  };
}

export default AccordionItem;


---

import type Content from './content.gts';
import type Header from './header.gts';
import type Trigger from './trigger.gts';
import type { WithBoundArgs } from '@glint/template';

export interface AccordionTriggerExternalSignature {
  Element: HTMLButtonElement;
  Blocks: {
    default: [];
  };
}

export interface AccordionContentExternalSignature {
  Element: HTMLDivElement;
  Blocks: {
    default: [];
  };
}

export interface AccordionHeaderExternalSignature {
  /**
   * Add aria-level according to the heading level where the accordion is used (default: 3).
   * See https://www.w3.org/WAI/ARIA/apg/patterns/accordion/ for more information.
   */
  Element: HTMLDivElement;
  Blocks: {
    default: [
      {
        /**
         * The AccordionTrigger component.
         */
        Trigger: WithBoundArgs<typeof Trigger, 'value' | 'isExpanded' | 'disabled' | 'toggleItem'>;
      },
    ];
  };
}

export interface AccordionItemExternalSignature {
  Element: HTMLDivElement;
  Blocks: {
    default: [
      {
        /**
         * Whether the accordion item is expanded.
         */
        isExpanded: boolean;
        /**
         * The AccordionHeader component.
         */
        Header: WithBoundArgs<typeof Header, 'value' | 'isExpanded' | 'disabled' | 'toggleItem'>;
        /**
         * The AccordionContent component.
         */
        Content: WithBoundArgs<typeof Content, 'value' | 'isExpanded' | 'disabled'>;
      },
    ];
  };
  Args: {
    /**
     * The value of the accordion item.
     */
    value: string;
  };
}


---

import { on } from "@ember/modifier";

import { getDataState } from "./item.gts";

import type { AccordionTriggerExternalSignature } from "./public.ts";
import type { TOC } from "@ember/component/template-only";

interface Signature extends AccordionTriggerExternalSignature {
  Args: {
    isExpanded: boolean;
    value: string;
    disabled?: boolean;
    toggleItem: () => void;
  };
}

export const AccordionTrigger: TOC<Signature> = <template>
  <button
    type="button"
    aria-controls={{@value}}
    aria-expanded={{@isExpanded}}
    data-state={{getDataState @isExpanded}}
    data-disabled={{@disabled}}
    aria-disabled={{if @disabled "true" "false"}}
    {{on "click" @toggleItem}}
    ...attributes
  >
    {{yield}}
  </button>
</template>;

export default AccordionTrigger;


---

.ember-primitives__hero__wrapper {
  width: 100dvw;
  height: 100dvh;
  position: relative;
}


---

import "./hero.css";

import type { TOC } from "@ember/component/template-only";

export const Hero: TOC<{
  /**
   * The wrapper element of the whole layout.
   */
  Element: HTMLDivElement;
  Blocks: {
    default: [];
  };
}> = <template>
  <div class="ember-primitives__hero__wrapper" ...attributes>
    {{yield}}
  </div>
</template>;


---

.ember-primitives__sticky-footer__wrapper {
  height: 100%;
  overflow: auto;
}
.ember-primitives__sticky-footer__container {
  min-height: 100%;
  display: grid;
  grid-template-rows: 1fr auto;
}


---

import "./sticky-footer.css";

import type { TOC } from "@ember/component/template-only";

export const StickyFooter: TOC<{
  /**
   * The wrapper element of the whole layout.
   * Valid parents for this element must have either a set height,
   * or a set max-height.
   */
  Element: HTMLDivElement;
  Blocks: {
    /**
     * This is the scrollable content, contained within a `<div>` element for positioning.
     * If this component is used as the main layout on a page,
     * the `<main>` element would be appropriate within here.
     */
    content: [];
    /**
     * This is the footer content, contained within a `<div>` element for positioning.
     * A `<footer>` element would be appropriate within here.
     *
     * This element will be at the bottom of the page if the content does not overflow the containing element and this element will be at the bottom of the content if there is overflow.
     */
    footer: [];
  };
}> = <template>
  <div class="ember-primitives__sticky-footer__wrapper" ...attributes>
    <div class="ember-primitives__sticky-footer__container">
      <div class="ember-primitives__sticky-footer__content">
        {{yield to="content"}}
      </div>
      <div class="ember-primitives__sticky-footer__footer">
        {{yield to="footer"}}
      </div>
    </div>
  </div>
</template>;

export default StickyFooter;


---

import { assert } from "@ember/debug";
import { on } from "@ember/modifier";

import type { TOC } from "@ember/component/template-only";

const reset = (event: Event) => {
  assert("[BUG]: reset called without an event.target", event.target instanceof HTMLElement);

  const form = event.target.closest("form");

  assert(
    "Form is missing. Cannot use <Reset> without being contained within a <form>",
    form instanceof HTMLFormElement,
  );

  form.reset();
};

export const Submit: TOC<{
  Element: HTMLButtonElement;
  Blocks: { default: [] };
}> = <template>
  <button type="submit" ...attributes>Submit</button>
</template>;

export const Reset: TOC<{
  Element: HTMLButtonElement;
  Blocks: { default: [] };
}> = <template>
  <button type="button" {{on "click" reset}} ...attributes>{{yield}}</button>
</template>;


---

import Component from "@glimmer/component";
import { warn } from "@ember/debug";
import { isDestroyed, isDestroying } from "@ember/destroyable";
import { on } from "@ember/modifier";
import { buildWaiter } from "@ember/test-waiters";

import {
  autoAdvance,
  getCollectiveValue,
  handleNavigation,
  handlePaste,
  selectAll,
} from "./utils.ts";

import type { TOC } from "@ember/component/template-only";
import type { WithBoundArgs } from "@glint/template";

const DEFAULT_LENGTH = 6;

function labelFor(inputIndex: number, labelFn: undefined | ((index: number) => string)) {
  if (labelFn) {
    return labelFn(inputIndex);
  }

  return `Please enter OTP character ${inputIndex + 1}`;
}

const waiter = buildWaiter("ember-primitives:OTPInput:handleChange");

const Fields: TOC<{
  /**
   * Any attributes passed to this component will be applied to each input.
   */
  Element: HTMLInputElement;
  Args: {
    fields: unknown[];
    labelFn: (index: number) => string;
    handleChange: (event: Event) => void;
  };
}> = <template>
  {{#each @fields as |_field i|}}
    <label>
      <span class="ember-primitives__sr-only">{{labelFor i @labelFn}}</span>
      <input
        name="code{{i}}"
        type="text"
        inputmode="numeric"
        autocomplete="off"
        ...attributes
        {{on "click" selectAll}}
        {{on "paste" handlePaste}}
        {{on "input" autoAdvance}}
        {{on "input" @handleChange}}
        {{on "keydown" handleNavigation}}
      />
    </label>
  {{/each}}
</template>;

export class OTPInput extends Component<{
  /**
   * The collection of individual OTP inputs are contained by a fieldset.
   * Applying the `disabled` attribute to this fieldset will disable
   * all of the inputs, if that's desired.
   */
  Element: HTMLFieldSetElement;
  Args: {
    /**
     * How many characters the one-time-password field should be
     * Defaults to 6
     */
    length?: number;

    /**
     * To Customize the label of the input fields, you may pass a function.
     * By default, this is `Please enter OTP character ${index + 1}`.
     */
    labelFn?: (index: number) => string;

    /**
     * If passed, this function will be called when the <Input> changes.
     * All fields are considered one input.
     */
    onChange?: (
      data: {
        /**
         * The text from the collective `<Input>`
         *
         * `code` _may_ be shorter than `length`
         * if the user has not finished typing / pasting their code
         */
        code: string;
        /**
         * will be `true` if `code`'s length matches the passed `@length` or the default of 6
         */
        complete: boolean;
      },
      /**
       * The last input event received
       */
      event: Event,
    ) => void;
  };
  Blocks: {
    /**
     * Optionally, you may control how the Fields are rendered, with proceeding text,
     * additional attributes added, etc.
     *
     * This is how you can add custom validation to each input field.
     */
    default?: [fields: WithBoundArgs<typeof Fields, "fields" | "handleChange" | "labelFn">];
  };
}> {
  /**
   * This is debounced, because we bind to each input,
   * but only want to emit one change event if someone pastes
   * multiple characters
   */
  handleChange = (event: Event) => {
    if (!this.args.onChange) return;

    if (!this.#token) {
      this.#token = waiter.beginAsync();
    }

    if (this.#frame) {
      cancelAnimationFrame(this.#frame);
    }

    // We  use requestAnimationFrame to be friendly to rendering.
    // We don't know if onChange is going to want to cause paints
    // (it's also how we debounce, under the assumption that "paste" behavior
    //  would be fast enough to be quicker than individual frames
    //   (see logic in autoAdvance)
    //  )
    this.#frame = requestAnimationFrame(() => {
      waiter.endAsync(this.#token);

      if (isDestroyed(this) || isDestroying(this)) return;
      if (!this.args.onChange) return;

      const value = getCollectiveValue(event.target, this.length);

      if (value === undefined) {
        warn(`Value could not be determined for the OTP field. was it removed from the DOM?`, {
          id: "ember-primitives.OTPInput.missing-value",
        });

        return;
      }

      this.args.onChange({ code: value, complete: value.length === this.length }, event);
    });
  };

  #token: unknown;
  #frame: number | undefined;

  get length() {
    return this.args.length ?? DEFAULT_LENGTH;
  }

  get fields() {
    // We only need to iterate a number of times,
    // so we don't care about the actual value or
    // referential integrity here
    return new Array<undefined>(this.length);
  }

  <template>
    <fieldset ...attributes>
      {{#let
        (component Fields fields=this.fields handleChange=this.handleChange labelFn=@labelFn)
        as |CurriedFields|
      }}
        {{#if (has-block)}}
          {{yield CurriedFields}}
        {{else}}
          <CurriedFields />
        {{/if}}
      {{/let}}

      <style>
        .ember-primitives__sr-only {
          position: absolute;
          width: 1px;
          height: 1px;
          padding: 0;
          margin: -1px;
          overflow: hidden;
          clip: rect(0, 0, 0, 0);
          white-space: nowrap;
          border-width: 0;
        }
      </style>
    </fieldset>
  </template>
}


---

import { assert } from "@ember/debug";
import { fn, hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { buildWaiter } from "@ember/test-waiters";

import { Reset, Submit } from "./buttons.gts";
import { OTPInput } from "./input.gts";

import type { TOC } from "@ember/component/template-only";
import type { WithBoundArgs } from "@glint/template";

const waiter = buildWaiter("ember-primitives:OTP:handleAutoSubmitAttempt");

const handleFormSubmit = (submit: (data: { code: string }) => void, event: SubmitEvent) => {
  event.preventDefault();

  assert(
    "[BUG]: handleFormSubmit was not attached to a form. Please open an issue.",
    event.currentTarget instanceof HTMLFormElement,
  );

  const formData = new FormData(event.currentTarget);

  let code = "";

  for (const [key, value] of formData.entries()) {
    if (key.startsWith("code")) {
      // eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-base-to-string
      code += value;
    }
  }

  submit({
    code,
  });
};

function handleChange(
  autoSubmit: boolean | undefined,
  data: { code: string; complete: boolean },
  event: Event,
) {
  if (!autoSubmit) return;
  if (!data.complete) return;

  assert(
    "[BUG]: event target is not a known element type",
    event.target instanceof HTMLElement || event.target instanceof SVGElement,
  );

  const form = event.target.closest("form");

  assert("[BUG]: Cannot handle event when <OTP> Inputs are not rendered within their <form>", form);

  const token = waiter.beginAsync();
  const finished = () => {
    waiter.endAsync(token);
    form.removeEventListener("submit", finished);
  };

  form.addEventListener("submit", finished);

  // NOTE: when calling .submit() the submit event handlers are not run
  form.requestSubmit();
}

export const OTP: TOC<{
  /**
   * The overall OTP Input is in its own form.
   * Modern UI/UX Patterns usually have this sort of field
   * as its own page, thus within its own form.
   *
   * By default, only the 'submit' event is bound, and is
   * what calls the `@onSubmit` argument.
   */
  Element: HTMLFormElement;
  Args: {
    /**
     * How many characters the one-time-password field should be
     * Defaults to 6
     */
    length?: number;

    /**
     * The on submit callback will give you the entered
     * one-time-password code.
     *
     * It will be called when the user manually clicks the 'submit'
     * button or when the full code is pasted and meats the validation
     * criteria.
     */
    onSubmit: (data: { code: string }) => void;

    /**
     * Whether or not to auto-submit after the code has been pasted
     * in to the collective "field".  Default is true
     */
    autoSubmit?: boolean;
  };
  Blocks: {
    default: [
      {
        /**
         * The collective input field that the OTP code will be typed/pasted in to
         */
        Input: WithBoundArgs<typeof OTPInput, "length" | "onChange">;
        /**
         * Button with `type="submit"` to submit the form
         */
        Submit: typeof Submit;
        /**
         * Pre-wired button to reset the form
         */
        Reset: typeof Reset;
      },
    ];
  };
}> = <template>
  <form {{on "submit" (fn handleFormSubmit @onSubmit)}} ...attributes>
    {{yield
      (hash
        Input=(component
          OTPInput length=@length onChange=(if @autoSubmit (fn handleChange @autoSubmit))
        )
        Submit=Submit
        Reset=Reset
      )
    }}
  </form>
</template>;


---

import { assert } from '@ember/debug';

function getInputs(current: HTMLInputElement) {
  const fieldset = current.closest('fieldset');

  assert('[BUG]: fieldset went missing', fieldset);

  return [...fieldset.querySelectorAll('input')];
}

function nextInput(current: HTMLInputElement) {
  const inputs = getInputs(current);
  const currentIndex = inputs.indexOf(current);

  return inputs[currentIndex + 1];
}

export function selectAll(event: Event) {
  const target = event.target;

  assert(`selectAll is only meant for use with input elements`, target instanceof HTMLInputElement);

  target.select();
}

export function handlePaste(event: Event) {
  const target = event.target;

  assert(
    `handlePaste is only meant for use with input elements`,
    target instanceof HTMLInputElement
  );

  const clipboardData = (event as ClipboardEvent).clipboardData;

  assert(
    `Could not get clipboardData while handling the paste event on OTP. Please report this issue on the ember-primitives repo with a reproduction. Thanks!`,
    clipboardData
  );

  // This is typically not good to prevent paste.
  // But because of the UX we're implementing,
  // we want to split the pasted value across
  // multiple text fields
  event.preventDefault();

  const value = clipboardData.getData('Text');
  const digits = value;
  let i = 0;
  let currElement: HTMLInputElement | null = target;

  while (currElement) {
    currElement.value = digits[i++] || '';

    const next = nextInput(currElement);

    if (next instanceof HTMLInputElement) {
      currElement = next;
    } else {
      break;
    }
  }

  // We want to select the first field again
  // so that if someone holds paste, or
  // pastes again, they get the same result.
  target.select();
}

export function handleNavigation(event: KeyboardEvent) {
  switch (event.key) {
    case 'Backspace':
      return handleBackspace(event);
    case 'ArrowLeft':
      return focusLeft(event);
    case 'ArrowRight':
      return focusRight(event);
  }
}

function focusLeft(event: Pick<Event, 'target'>) {
  const target = event.target;

  assert(`only allowed on input elements`, target instanceof HTMLInputElement);

  const input = previousInput(target);

  input?.focus();
  requestAnimationFrame(() => {
    input?.select();
  });
}

function focusRight(event: Pick<Event, 'target'>) {
  const target = event.target;

  assert(`only allowed on input elements`, target instanceof HTMLInputElement);

  const input = nextInput(target);

  input?.focus();
  requestAnimationFrame(() => {
    input?.select();
  });
}

const syntheticEvent = new InputEvent('input');

function handleBackspace(event: KeyboardEvent) {
  if (event.key !== 'Backspace') return;

  /**
   * We have to prevent default because we
   * - want to clear the whole field
   * - have the focus behavior keep up with the key-repeat
   *   speed of the user's computer
   */
  event.preventDefault();

  const target = event.target;

  if (target && 'value' in target) {
    if (target.value === '') {
      focusLeft({ target });
    } else {
      target.value = '';
    }
  }

  target?.dispatchEvent(syntheticEvent);
}

function previousInput(current: HTMLInputElement) {
  const inputs = getInputs(current);
  const currentIndex = inputs.indexOf(current);

  return inputs[currentIndex - 1];
}

export const autoAdvance = (event: Event) => {
  assert(
    '[BUG]: autoAdvance called on non-input element',
    event.target instanceof HTMLInputElement
  );

  const value = event.target.value;

  if (value.length === 0) return;

  if (value.length > 0) {
    if ('data' in event && event.data && typeof event.data === 'string') {
      event.target.value = event.data;
    }

    return focusRight(event);
  }
};

export function getCollectiveValue(elementTarget: EventTarget | null, length: number) {
  if (!elementTarget) return;

  assert(
    `[BUG]: somehow the element target is not HTMLElement`,
    elementTarget instanceof HTMLElement
  );

  let parent: null | HTMLElement | ShadowRoot;

  // TODO: should this logic be extracted?
  //       why is getting the target element within a shadow root hard?
  if (!(elementTarget instanceof HTMLInputElement)) {
    if (elementTarget.shadowRoot) {
      parent = elementTarget.shadowRoot;
    } else {
      parent = elementTarget.closest('fieldset');
    }
  } else {
    parent = elementTarget.closest('fieldset');
  }

  assert(`[BUG]: somehow the input fields were rendered without a parent element`, parent);

  const elements = parent.querySelectorAll('input');

  let value = '';

  assert(
    `found elements (${elements.length}) do not match length (${length}). Was the same OTP input rendered more than once?`,
    elements.length === length
  );

  for (const element of elements) {
    assert(
      '[BUG]: how did the queried elements become a non-input element?',
      element instanceof HTMLInputElement
    );
    value += element.value;
  }

  return value;
}


---

import type { ComponentLike } from '@glint/template';

/**
 * @public
 */
export interface ComponentIcons {
  /**
   * It's possible to completely manage the state of an individual Icon yourself
   * by passing a component that has ...attributes on its outer element and receives
   * a @isSelected argument which is true for selected and false for unselected.
   *
   * There is also argument passed which is the percent-amount of selection if you want fractional ratings, @selectedPercent
   */
  icon: ComponentLike<{
    Element: HTMLElement;
    Args: {
      /**
       * Is this item selected?
       */
      isSelected: boolean;
      /**
       * Which number of item is this item within the overall rating group.
       */
      value: number;
      /**
       * Should this be marked as readonly
       */
      readonly: boolean;
    };
  }>;
}

/**
 * @public
 */
export interface StringIcons {
  /**
   * The symbol to use for an unselected variant of the icon
   *
   * Defaults to "★";
   *  Can change color when selected.
   */
  icon?: string;
}


---

import { on } from "@ember/modifier";

import type { TOC } from "@ember/component/template-only";

export const RatingRange: TOC<{
  Element: HTMLInputElement;
  Args: {
    name: string;
    max: number;
    value: number;
    handleChange: (event: Event) => void;
  };
}> = <template>
  <input
    ...attributes
    name={{@name}}
    type="range"
    max={{@max}}
    value={{@value}}
    {{on "change" @handleChange}}
  />
</template>;


---

import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";

import { uniqueId } from "../../utils.ts";
import { RatingRange } from "./range.gts";
import { Stars } from "./stars.gts";
import { RatingState } from "./state.gts";

import type { ComponentIcons, StringIcons } from "./public-types.ts";
import type { WithBoundArgs } from "@glint/template";

export interface Signature {
  /*
   * The element all passed attributes / modifiers are applied to.
   *
   * This is a `<fieldset>`, becaues the rating elements are
   * powered by a group of radio buttons.
   */
  Element: HTMLFieldSetElement;
  Args: (ComponentIcons | StringIcons) & {
    /**
     * The number of stars/whichever-icon to show
     *
     * Defaults to 5
     */
    max?: number;

    /**
     * The current number of stars/whichever-icon to show as selected
     *
     * Defaults to 0
     */
    value?: number;

    /**
     * When generating the radio inputs, this changes what value of rating each radio
     * input will be incremented by.
     *
     * e.g.: Set to 0.5 for half-star ratings.
     *
     * Defaults to 1
     */
    step?: number;

    /**
     * Prevents click events on the icons and sets aria-readonly.
     *
     * Also sets data-readonly=true on the wrapping element
     */
    readonly?: boolean;

    /**
     * Toggles the ability to interact with the rating component.
     * When `true` (the default), the Rating component can be as a form input
     * to gather user feedback.
     *
     * When false, only the `@value` will be shown, and it cannot be changed.
     */
    interactive?: boolean;

    /**
     * Callback when the selected rating changes.
     * Can include half-ratings if the iconHalf argument is passed.
     */
    onChange?: (value: number) => void;
  };

  Blocks: {
    default: [
      rating: {
        /**
         * The maximum rating
         */
        max: number;
        /**
         * The maxium rating
         */
        total: number;
        /**
         * The current rating
         */
        value: number;
        /**
         * The name shared by the field group
         */
        name: string;
        /**
         * If the rating can be changed
         */
        isReadonly: boolean;
        /**
         * If the rating can be changed
         */
        isChangeable: boolean;
        /**
         * The stars / items radio group
         */
        Stars: WithBoundArgs<
          typeof Stars,
          "stars" | "icon" | "isReadonly" | "name" | "total" | "currentValue"
        >;
        /**
         * Input range for adjusting the rating via fractional means
         */
        Range: WithBoundArgs<typeof RatingRange, "max" | "value" | "name" | "handleChange">;
      },
    ];
    label: [
      state: {
        /**
         * The current rating
         */
        value: number;

        /**
         * The maximum rating
         */
        total: number;
      },
    ];
  };
}

export class Rating extends Component<Signature> {
  name = `rating-${uniqueId()}`;

  get icon() {
    return this.args.icon ?? "★";
  }

  get isInteractive() {
    return this.args.interactive ?? true;
  }

  get isChangeable() {
    const readonly = this.args.readonly ?? false;

    return !readonly && this.isInteractive;
  }

  get isReadonly() {
    return !this.isChangeable;
  }

  get needsDescription() {
    return !this.isInteractive;
  }

  <template>
    <RatingState
      @max={{@max}}
      @step={{@step}}
      @value={{@value}}
      @name={{this.name}}
      @readonly={{this.isReadonly}}
      @onChange={{@onChange}}
      as |r publicState|
    >
      <fieldset
        class="ember-primitives__rating"
        data-total={{r.total}}
        data-value={{r.value}}
        data-readonly={{this.isReadonly}}
        {{! We use event delegation, this isn't a primary interactive -- we're capturing events from inputs }}
        {{! template-lint-disable no-invalid-interactive }}
        {{on "click" r.handleClick}}
        ...attributes
      >
        {{#let
          (component
            Stars
            stars=r.stars
            icon=this.icon
            isReadonly=this.isReadonly
            name=this.name
            total=r.total
            currentValue=r.value
          )
          as |RatingStars|
        }}

          {{#if (has-block)}}
            {{yield
              (hash
                max=r.total
                total=r.total
                value=r.value
                name=this.name
                isReadonly=this.isReadonly
                isChangeable=this.isChangeable
                Stars=RatingStars
                Range=(component
                  RatingRange
                  step=r.step
                  max=r.total
                  value=r.value
                  name=this.name
                  handleChange=r.handleChange
                )
              )
            }}
          {{else}}
            {{#if this.needsDescription}}
              {{#if (has-block "label")}}
                {{yield publicState to="label"}}
              {{else}}
                <span visually-hidden class="ember-primitives__rating__label">Rated
                  {{r.value}}
                  out of
                  {{r.total}}</span>
              {{/if}}
            {{else}}
              {{#if (has-block "label")}}
                <legend>
                  {{yield publicState to="label"}}
                </legend>
              {{/if}}
            {{/if}}

            <RatingStars />
          {{/if}}
        {{/let}}

      </fieldset>
    </RatingState>
  </template>
}


---

import { uniqueId } from "../../utils.ts";
import { isString, lte } from "./utils.ts";

import type { ComponentIcons, StringIcons } from "./public-types.ts";
import type { TOC } from "@ember/component/template-only";

export const Stars: TOC<{
  Args: {
    // Configuration
    stars: number[];
    icon: StringIcons["icon"] | ComponentIcons["icon"];
    isReadonly: boolean;

    // HTML Boilerplate
    name: string;

    // State
    currentValue: number;
    total: number;
  };
}> = <template>
  <div class="ember-primitives__rating__items">
    {{#each @stars as |star|}}
      {{#let (uniqueId) as |id|}}
        <span
          class="ember-primitives__rating__item"
          data-number={{star}}
          data-selected={{lte star @currentValue}}
          data-readonly={{@isReadonly}}
        >
          <label for="input-{{id}}">
            <span visually-hidden>{{star}} star</span>
            {{#if @icon}}
              <span aria-hidden="true">
                {{#if (isString @icon)}}
                  {{@icon}}
                {{else}}
                  <@icon
                    @value={{star}}
                    @isSelected={{lte star @currentValue}}
                    @readonly={{@isReadonly}}
                  />
                {{/if}}
              </span>
            {{/if}}
          </label>

          <input
            id="input-{{id}}"
            type="radio"
            name={{@name}}
            value={{star}}
            readonly={{@isReadonly}}
            checked={{Object.is star @currentValue}}
          />
        </span>
      {{/let}}
    {{/each}}
  </div>
</template>;


---

import Component from "@glimmer/component";
import { cached } from "@glimmer/tracking";
import { assert } from "@ember/debug";
import { hash } from "@ember/helper";

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
import { localCopy } from "tracked-toolbox";

export class RatingState extends Component<{
  Args: {
    max: number | undefined;
    value: number | undefined;
    step: number | undefined;
    readonly: boolean | undefined;
    name: string;
    onChange?: (value: number) => void;
  };
  Blocks: {
    default: [
      internalApi: {
        stars: number[];
        step: number;
        value: number;
        total: number;
        handleClick: (event: Event) => void;
        handleChange: (event: Event) => void;
        setRating: (num: number) => void;
      },
      publicApi: {
        value: number;
        total: number;
      },
    ];
  };
}> {
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call
  @localCopy("args.value") declare _value: number;

  get value() {
    return this._value ?? 0;
  }

  get step() {
    return this.args.step ?? 1;
  }

  get max() {
    return this.args.max ?? 5;
  }

  @cached
  get stars() {
    const result = [];

    // 0 is "none selected"
    let current = 0;

    current += this.step;

    while (current <= this.max) {
      result.push(current);
      current += this.step;
    }

    return result;
  }

  setRating = (value: number) => {
    if (this.args.readonly) {
      return;
    }

    if (value === this._value) {
      this._value = 0;
    } else {
      this._value = value;
    }

    this.args.onChange?.(value);
  };

  setFromString = (value: unknown) => {
    assert("[BUG]: value from input must be a string.", typeof value === "string");

    const num = parseFloat(value);

    if (isNaN(num)) {
      // something went wrong.
      // Since we're using event delegation,
      // this could be from an unrelated input
      return;
    }

    this.setRating(num);
  };

  /**
   * Click events are captured by
   * - radio changes (mouse and keyboard)
   *   - but only range clicks
   */
  handleClick = (event: Event) => {
    // Since we're doing event delegation on a click, we want to make sure
    // we don't do anything on other elements
    const isValid =
      event.target instanceof HTMLInputElement &&
      event.target.name === this.args.name &&
      event.target.type === "radio";

    if (!isValid) return;

    const selected = event.target?.value;

    this.setFromString(selected);
  };

  /**
   * Only attached to a range element, if present.
   * Range elements don't fire click events on keyboard usage, like radios do
   */
  handleChange = (event: Event) => {
    const isValid = event.target !== null && "value" in event.target;

    if (!isValid) return;

    this.setFromString(event.target.value);
  };

  <template>
    {{yield
      (hash
        stars=this.stars
        total=this.stars.length
        handleClick=this.handleClick
        handleChange=this.handleChange
        setRating=this.setRating
        value=this.value
        step=this.step
      )
      (hash total=this.stars.length value=this.value)
    }}
  </template>
}


---

export function isString(x: unknown) {
  return typeof x === 'string';
}

export function lte(a: number, b: number) {
  return a <= b;
}


---

import "./styles.css";

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { buildWaiter, waitForPromise } from "@ember/test-waiters";
import { isTesting, macroCondition } from "@embroider/macros";

import { modifier } from "ember-modifier";

import type { ScrollBehavior, Signature } from "./types.ts";

const testWaiter = buildWaiter("ember-primitive:zoetrope-waiter");
const DEFAULT_GAP = 8;
const DEFAULT_OFFSET = 0;

export class Zoetrope extends Component<Signature> {
  @tracked scrollerElement: HTMLElement | null = null;
  @tracked currentlyScrolled = 0;
  @tracked scrollWidth = 0;
  @tracked offsetWidth = 0;

  private setCSSVariables = modifier(
    (element: HTMLElement, _: unknown, { gap, offset }: { gap: number; offset: number }) => {
      if (gap) element.style.setProperty("--zoetrope-gap", `${gap}px`);
      if (offset) element.style.setProperty("--zoetrope-offset", `${offset}px`);
    },
  );

  scrollerWaiter = testWaiter.beginAsync();
  noScrollWaiter = () => {
    testWaiter.endAsync(this.scrollerWaiter);
  };

  private configureScroller = modifier((element: HTMLElement) => {
    this.scrollerElement = element;
    this.currentlyScrolled = element.scrollLeft;

    const zoetropeResizeObserver = new ResizeObserver(() => {
      this.scrollWidth = element.scrollWidth;
      this.offsetWidth = element.offsetWidth;
    });

    zoetropeResizeObserver.observe(element);

    element.addEventListener("scroll", this.scrollListener, { passive: true });
    element.addEventListener("keydown", this.tabListener);

    requestAnimationFrame(() => {
      testWaiter.endAsync(this.scrollerWaiter);
    });

    return () => {
      element.removeEventListener("scroll", this.scrollListener);
      element.removeEventListener("keydown", this.tabListener);

      zoetropeResizeObserver.unobserve(element);
    };
  });

  private tabListener = (event: KeyboardEvent) => {
    const target = event.target as HTMLElement;
    const { key, shiftKey } = event;

    if (!this.scrollerElement || this.scrollerElement === target) {
      return;
    }

    if (key !== "Tab") {
      return;
    }

    const nextElement = target.nextElementSibling;
    const previousElement = target.previousElementSibling;

    if ((!shiftKey && !nextElement) || (shiftKey && !previousElement)) {
      return;
    }

    event.preventDefault();

    let newTarget: HTMLElement | null = null;

    if (shiftKey) {
      newTarget = previousElement as HTMLElement;
    } else {
      newTarget = nextElement as HTMLElement;
    }

    if (!newTarget) {
      return;
    }

    newTarget?.focus({ preventScroll: true });

    const rect = getRelativeBoundingClientRect(newTarget, this.scrollerElement);

    this.scrollerElement?.scrollBy({
      left: rect.left,
      behavior: this.scrollBehavior,
    });
  };

  private scrollListener = () => {
    this.currentlyScrolled = this.scrollerElement?.scrollLeft || 0;
  };

  get offset() {
    return this.args.offset ?? DEFAULT_OFFSET;
  }

  get gap() {
    return this.args.gap ?? DEFAULT_GAP;
  }

  get canScroll() {
    return this.scrollWidth > this.offsetWidth + this.offset;
  }

  get cannotScrollLeft() {
    return this.currentlyScrolled <= this.offset;
  }

  get cannotScrollRight() {
    return this.scrollWidth - this.offsetWidth - this.offset < this.currentlyScrolled;
  }

  get scrollBehavior(): ScrollBehavior {
    if (macroCondition(isTesting())) {
      return "instant";
    }

    return this.args.scrollBehavior || "smooth";
  }

  scrollLeft = () => {
    if (!(this.scrollerElement instanceof HTMLElement)) {
      return;
    }

    const { firstChild } = this.findOverflowingElement();

    if (!firstChild) {
      return;
    }

    const children = [...this.scrollerElement.children];

    const firstChildIndex = children.indexOf(firstChild);

    let targetElement = firstChild;
    let accumalatedWidth = 0;

    for (let i = firstChildIndex; i >= 0; i--) {
      const child = children[i];

      if (!(child instanceof HTMLElement)) {
        continue;
      }

      accumalatedWidth += child.offsetWidth + this.gap;

      if (accumalatedWidth >= this.offsetWidth) {
        break;
      }

      targetElement = child;
    }

    const rect = getRelativeBoundingClientRect(targetElement, this.scrollerElement);

    this.scrollerElement.scrollBy({
      left: rect.left,
      behavior: this.scrollBehavior,
    });

    void waitForPromise(new Promise(requestAnimationFrame));
  };

  scrollRight = () => {
    if (!(this.scrollerElement instanceof HTMLElement)) {
      return;
    }

    const { activeSlide, lastChild } = this.findOverflowingElement();

    if (!lastChild) {
      return;
    }

    let rect = getRelativeBoundingClientRect(lastChild, this.scrollerElement);

    // If the card is larger than the container then skip to the next card
    if (rect.width > this.offsetWidth && activeSlide === lastChild) {
      const children = [...this.scrollerElement.children];
      const lastChildIndex = children.indexOf(lastChild);
      const targetElement = children[lastChildIndex + 1];

      if (!targetElement) {
        return;
      }

      rect = getRelativeBoundingClientRect(targetElement, this.scrollerElement);
    }

    this.scrollerElement?.scrollBy({
      left: rect.left,
      behavior: this.scrollBehavior,
    });

    void waitForPromise(new Promise(requestAnimationFrame));
  };

  private findOverflowingElement() {
    const returnObj: {
      activeSlide?: Element;
      firstChild?: Element;
      lastChild?: Element;
    } = {
      firstChild: undefined,
      lastChild: undefined,
      activeSlide: undefined,
    };

    if (!this.scrollerElement) {
      return returnObj;
    }

    const parentElement = this.scrollerElement.parentElement;

    if (!parentElement) {
      return returnObj;
    }

    const containerRect = getRelativeBoundingClientRect(this.scrollerElement, parentElement);

    const children = [...this.scrollerElement.children];

    // Find the first child that is overflowing the left edge of the container
    // and the last child that is overflowing the right edge of the container
    for (const child of children) {
      const rect = getRelativeBoundingClientRect(child, this.scrollerElement);

      if (rect.right + this.gap >= containerRect.left && !returnObj.firstChild) {
        returnObj.firstChild = child;
      }

      if (rect.left >= this.offset && !returnObj.activeSlide) {
        returnObj.activeSlide = child;
      }

      if (rect.right >= containerRect.width && !returnObj.lastChild) {
        returnObj.lastChild = child;

        break;
      }
    }

    if (!returnObj.firstChild) {
      returnObj.firstChild = children[0];
    }

    if (!returnObj.lastChild) {
      returnObj.lastChild = children[children.length - 1];
    }

    return returnObj;
  }

  <template>
    <section
      class="ember-primitives__zoetrope"
      {{this.setCSSVariables gap=this.gap offset=this.offset}}
      ...attributes
    >
      {{#if (has-block "header")}}
        <div class="ember-primitives__zoetrope__header">
          {{yield to="header"}}
        </div>
      {{/if}}

      {{#if (has-block "controls")}}
        {{yield
          (hash
            cannotScrollLeft=this.cannotScrollLeft
            cannotScrollRight=this.cannotScrollRight
            canScroll=this.canScroll
            scrollLeft=this.scrollLeft
            scrollRight=this.scrollRight
          )
          to="controls"
        }}
      {{else}}
        {{#if this.canScroll}}
          <div class="ember-primitives__zoetrope__controls">
            <button
              type="button"
              {{on "click" this.scrollLeft}}
              disabled={{this.cannotScrollLeft}}
            >Left</button>

            <button
              type="button"
              {{on "click" this.scrollRight}}
              disabled={{this.cannotScrollRight}}
            >Right</button>
          </div>
        {{/if}}
      {{/if}}
      {{#if (has-block "content")}}
        <div class="ember-primitives__zoetrope__scroller" {{this.configureScroller}}>
          {{yield to="content"}}
        </div>
      {{else}}
        {{(this.noScrollWaiter)}}
      {{/if}}
    </section>
  </template>
}

export default Zoetrope;

function getRelativeBoundingClientRect(childElement: Element, parentElement: Element) {
  if (!childElement || !parentElement) {
    throw new Error("Both childElement and parentElement must be provided");
  }

  // Get the bounding rect of the child and parent elements
  const childRect = childElement.getBoundingClientRect();
  const parentRect = parentElement.getBoundingClientRect();

  // Get computed styles of the parent element
  const parentStyles = window.getComputedStyle(parentElement);

  // Extract and parse parent's padding, and border, for all sides
  const parentPaddingTop = parseFloat(parentStyles.paddingTop);
  const parentPaddingLeft = parseFloat(parentStyles.paddingLeft);

  const parentBorderTopWidth = parseFloat(parentStyles.borderTopWidth);
  const parentBorderLeftWidth = parseFloat(parentStyles.borderLeftWidth);

  // Calculate child's position relative to parent's content area (including padding and borders)
  return {
    width: childRect.width,
    height: childRect.height,
    top: childRect.top - parentRect.top - parentBorderTopWidth - parentPaddingTop,
    left: childRect.left - parentRect.left - parentBorderLeftWidth - parentPaddingLeft,
    bottom:
      childRect.top - parentRect.top - parentBorderTopWidth - parentPaddingTop + childRect.height,
    right:
      childRect.left -
      parentRect.left -
      parentBorderLeftWidth -
      parentPaddingLeft +
      childRect.width,
  };
}


---

.ember-primitives__zoetrope {
  display: flex;
  flex-wrap: wrap;
  position: relative;
  width: 100%;
}

.ember-primitives__zoetrope__header {
  align-items: center;
  display: flex;
  flex: 1;
  justify-content: space-between;
  padding-left: var(--zoetrope-offset, 0);
}

.ember-primitives__zoetrope__controls {
  align-items: center;
  display: flex;
  padding-right: var(--zoetrope-offset, 0);
  gap: 4px;
}

.ember-primitives__zoetrope__scroller {
  display: flex;
  flex-flow: row nowrap;
  gap: var(--zoetrope-gap, 8px);
  overflow: scroll visible;
  padding: 8px var(--zoetrope-offset, 0);
  scroll-behavior: smooth;
  scroll-padding-left: var(--zoetrope-offset, 0);
  scroll-snap-type: x mandatory;
  scrollbar-color: transparent transparent;
  scrollbar-width: none;
  width: 100%;

  & > * {
    flex-shrink: 0;
    scroll-snap-align: start;
  }
}


---

export type ScrollBehavior = 'auto' | 'smooth' | 'instant';

export interface Signature {
  Args: {
    /**
     * The distance in pixels between each item in the slider.
     */
    gap?: number;

    /**
     * The distance from the edge of the container to the first and last item, this allows
     * the contents to visually overflow the container
     */
    offset?: number;

    /**
     * The scroll behavior to use when scrolling the slider. Defaults to smooth.
     */
    scrollBehavior?: ScrollBehavior;
  };
  Blocks: {
    /**
     * The header block is where the header content is placed.
     */
    header: [];

    /**
     * The content block is where the items that will be scrolled are placed.
     */
    content: [];

    /**
     * The controls block is where the left and right buttons are placed.
     */
    controls: [
      {
        /**
         * Whether the slider can scroll.
         */
        canScroll: boolean;

        /**
         * Whether the slider cannot scroll left.
         */
        cannotScrollLeft: boolean;

        /**
         * Whether the slider cannot scroll right.
         */
        cannotScrollRight: boolean;

        /**
         * The function to scroll the slider left.
         */
        scrollLeft: () => void;

        /**
         * The function to scroll the slider right.
         */
        scrollRight: () => void;
      },
    ];
  };
  Element: HTMLElement;
}


---

