# App shell

Source: https://signy.dev/examples/app-shell/

Three contexts, provided once, used anywhere below.

The root provides a session, a theme and a notifier. The user menu, the theme switch and any page inject what they need, with no props passed through the layers between.

## Preview

[Open the page on its own](https://signy.dev/examples/app-shell/demo/index.html.md)

## Code

Save it as an `.html` file and open it in a browser. It loads Signy and Iris CSS from the CDN, so there is nothing to install or build.

**Show the full page**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="color-scheme" content="light dark">
    <title>App shell - Signy example</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/iriscss@1/dist/iris.min.css">
    <script src="https://cdn.jsdelivr.net/npm/signy@1/dist/signy.global.min.js"></script>
  </head>
  <body>
    <div id="app"></div>

    <script>
      const {
        Component, Signal, application, html, effect, For, Show,
        createContext, createRequiredContext,
      } = Signy;

      // Required: a component that injects them with no provider above it throws, so a
      // missing provider shows up at once instead of as a silent undefined
      const SessionContext = createRequiredContext("Session");
      const NotifierContext = createRequiredContext("Notifier");
      // Optional: light unless a provider says otherwise
      const ThemeContext = createContext(new Signal("light"), "Theme");

      let toastId = 1;

      function createNotifier() {
        const toasts = new Signal([]);
        const dismiss = (id) => toasts.set((list) => list.filter((toast) => toast.id !== id));
        const notify = (message, kind = "success") => {
          const id = toastId++;
          toasts.set((list) => [...list, { id, message, kind }]);
          setTimeout(() => dismiss(id), 4000);
        };
        return { toasts, notify, dismiss };
      }

      class UserMenu extends Component {
        render() {
          const session = SessionContext.inject(this);
          const { notify } = NotifierContext.inject(this);
          return html`
            ${Show({
              when: () => session.value,
              fallback: html`
                <button class="small" onClick=${() => {
                  session.value = { name: "Ada Lovelace" };
                  notify("Signed in as Ada.");
                }}>Sign in</button>
              `,
              children: (user) => html`
                <details class="dropdown">
                  <summary>${user.name}</summary>
                  <ul>
                    <li><a href="#profile">Profile</a></li>
                    <li><button onClick=${() => {
                      session.value = null;
                      notify("Signed out.", "info");
                    }}>Sign out</button></li>
                  </ul>
                </details>
              `,
            })}
          `;
        }
      }

      class ThemeSwitch extends Component {
        render() {
          const theme = ThemeContext.inject(this);
          return html`
            <label>
              <input type="checkbox" role="switch" onChange=${(e) => (theme.value = e.currentTarget.checked ? "dark" : "light")} />
              Dark
            </label>
          `;
        }
      }

      class Editor extends Component {
        render() {
          const session = SessionContext.inject(this);
          const { notify } = NotifierContext.inject(this);
          return html`
            <h1>${() => (session.value ? `Hello, ${session.value.name.split(" ")[0]}` : "Hello, guest")}</h1>
            <p>Nothing on this page passes props down. Each part injects what it needs.</p>
            <div class="cluster">
              <button class="primary" onClick=${() => notify("Draft saved.")}>Save draft</button>
              <button class="danger outlined" onClick=${() => notify("The draft was deleted.", "danger")}>Delete draft</button>
            </div>
          `;
        }
      }

      class Toasts extends Component {
        render() {
          const { toasts, dismiss } = NotifierContext.inject(this);
          return html`
            <div class="toasts" aria-live="polite">
              ${For({
                each: () => toasts.value,
                keyBy: "id",
                children: (toast) => html`
                  <div class=${`toast ${toast.kind}`} role="status">
                    <p>${toast.message}</p>
                    <button class="close" aria-label="Dismiss" onClick=${() => dismiss(toast.id)}></button>
                  </div>
                `,
              })}
            </div>
          `;
        }
      }

      class App extends Component {
        session = new Signal(null);
        theme = new Signal("light");
        notifier = createNotifier();

        setup() {
          SessionContext.provide(this, this.session);
          ThemeContext.provide(this, this.theme);
          NotifierContext.provide(this, this.notifier);
          effect(() => {
            document.documentElement.dataset.theme = this.theme.value;
          });
        }

        render() {
          return html`
            <header class="navbar">
              <nav class="container" aria-label="Main">
                <ul><li><strong>Writer</strong></li></ul>
                <ul>
                  <li><${ThemeSwitch} /></li>
                  <li><${UserMenu} /></li>
                </ul>
              </nav>
            </header>
            <main class="container"><${Editor} /></main>
            <${Toasts} />
          `;
        }
      }

      application(App).mount("#app");
    </script>
  </body>
</html>

```

## What it uses

- [createContext() and createRequiredContext()](https://signy.dev/documentation/1/components/context/index.html.md)
- [Signals](https://signy.dev/documentation/1/reactivity/signals/index.html.md) as context values
- [For](https://signy.dev/documentation/1/templates/for-and-index/index.html.md) for the toast stack
