Search Shell

Responsive dialog structure for application search and command palettes. It owns visibility, dismissal, accessible dialog semantics, and the search/status/body/footer regions while applications retain query and result state.

Search commands

Open the interactive surface when needed; it starts closed in the component directory.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-button/ui-button.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-search-shell/ui-search-shell.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-input/ui-input.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-search-result-row/ui-search-result-row.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-icon/ui-icon.html"
>

<ui-button
  data-demo-trigger
>Search commands</ui-button>
<ui-search-shell
  data-demo-surface
  dismissible
  label="Search commands"
>
  <ui-input
    slot="search"
    type="search"
    aria-label="Search commands"
    placeholder="Find a command"
  ></ui-input>
  <ui-search-result-row
    slot="body"
  >
    <ui-icon
      slot="leading"
      name="file-text"
    ></ui-icon>
    <span
      slot="title"
    >Team notes</span>
  </ui-search-result-row>
</ui-search-shell>
<script setup>
  import { UiButton, UiIcon, UiInput, UiSearchResultRow, UiSearchShell } from "@threadlabs/looma";
</script>

<template>
  <UiButton
    data-demo-trigger=""
  >Search commands</UiButton>
  <UiSearchShell
    data-demo-surface=""
    :dismissible="true"
    label="Search commands"
  >
    <template #search>
      <UiInput
        type="search"
        aria-label="Search commands"
        placeholder="Find a command"
      />
    </template>
    <template #body>
      <UiSearchResultRow>
        <template #leading>
          <UiIcon
            name="file-text"
          />
        </template>
        <template #title>
          <span>Team notes</span>
        </template>
      </UiSearchResultRow>
    </template>
  </UiSearchShell>
</template>
import { UiButton, UiIcon, UiInput, UiSearchResultRow, UiSearchShell } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      data-demo-trigger=""
    >Search commands</UiButton>
    <UiSearchShell
      data-demo-surface=""
      dismissible
      label="Search commands"
      slots={{
        search: <UiInput
          type="search"
          aria-label="Search commands"
          placeholder="Find a command"
        />,
        body: <UiSearchResultRow
          slots={{
            leading: <UiIcon
              name="file-text"
            />,
            title: <span>Team notes</span>,
          }}
        />,
      }}
    />
  </>;
}
/** @jsxImportSource preact */
import { UiButton, UiIcon, UiInput, UiSearchResultRow, UiSearchShell } from "@threadlabs/looma";

export default function Example() {
  return <>
    <UiButton
      data-demo-trigger=""
    >Search commands</UiButton>
    <UiSearchShell
      data-demo-surface=""
      dismissible
      label="Search commands"
      slots={{
        search: <UiInput
          type="search"
          aria-label="Search commands"
          placeholder="Find a command"
        />,
        body: <UiSearchResultRow
          slots={{
            leading: <UiIcon
              name="file-text"
            />,
            title: <span>Team notes</span>,
          }}
        />,
      }}
    />
  </>;
}
<script>
  import { UiButton, UiIcon, UiInput, UiSearchResultRow, UiSearchShell } from "@threadlabs/looma";
</script>

{#snippet search()}
  <UiInput
    type="search"
    aria-label="Search commands"
    placeholder="Find a command"
  />
{/snippet}

{#snippet body()}
  <UiSearchResultRow
    slots={{ leading, title }}
  />
{/snippet}

{#snippet leading()}
  <UiIcon
    name="file-text"
  />
{/snippet}

{#snippet title()}
  <span>Team notes</span>
{/snippet}

<UiButton
  data-demo-trigger=""
>Search commands</UiButton>
<UiSearchShell
  data-demo-surface=""
  dismissible
  label="Search commands"
  slots={{ search, body }}
/>
import { Component } from "@angular/core";
import { UiButton, UiIcon, UiInput, UiSearchResultRow, UiSearchShell } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiButton, UiSearchShell, UiInput, UiSearchResultRow, UiIcon],
  template: `
    <ng-template
      ui-button
      [hnAttributes]="{ 'data-demo-trigger': '' }"
    >Search commands</ng-template>
    <div
      ui-search-shell
      data-demo-surface=""
      [dismissible]="true"
      [label]="'Search commands'"
    >
      <ng-container ngProjectAs="[slot='search']">
        <input
          ui-input
          [type]="'search'"
          aria-label="Search commands"
          placeholder="Find a command"
        >
      </ng-container>
      <ng-container ngProjectAs="[slot='body']">
        <button
          ui-search-result-row
        >
          <ng-container ngProjectAs="[slot='leading']">
            <span
              ui-icon
              name="file-text"
            ></span>
          </ng-container>
          <ng-container ngProjectAs="[slot='title']">
            <span>Team notes</span>
          </ng-container>
        </button>
      </ng-container>
    </div>
  `,
})
export default class Example {}

open

Open reveals the labeled shell while the optional status and footer regions remain absent.

Start typing to filter commands.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-search-shell/ui-search-shell.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-input/ui-input.html"
>

<ui-search-shell
  open
  label="Search commands"
>
  <ui-input
    slot="search"
    type="search"
    aria-label="Search commands"
    placeholder="Search commands"
  > </ui-input>
  <div
    slot="body"
  >Start typing to filter commands.</div>
</ui-search-shell>
<script setup>
  import { UiInput, UiSearchShell } from "@threadlabs/looma";
</script>

<template>
  <UiSearchShell
    :open="true"
    label="Search commands"
  >
    <template #search>
      <UiInput
        type="search"
        aria-label="Search commands"
        placeholder="Search commands"
      > </UiInput>
    </template>
    <template #body>
      <div>Start typing to filter commands.</div>
    </template>
  </UiSearchShell>
</template>
import { UiInput, UiSearchShell } from "@threadlabs/looma";

export default function Example() {
  return <UiSearchShell
    open
    label="Search commands"
    slots={{
      search: <UiInput
        type="search"
        aria-label="Search commands"
        placeholder="Search commands"
      > </UiInput>,
      body: <div>Start typing to filter commands.</div>,
    }}
  >{" "}{" "}{" "}</UiSearchShell>;
}
/** @jsxImportSource preact */
import { UiInput, UiSearchShell } from "@threadlabs/looma";

export default function Example() {
  return <UiSearchShell
    open
    label="Search commands"
    slots={{
      search: <UiInput
        type="search"
        aria-label="Search commands"
        placeholder="Search commands"
      > </UiInput>,
      body: <div>Start typing to filter commands.</div>,
    }}
  >{" "}{" "}{" "}</UiSearchShell>;
}
<script>
  import { UiInput, UiSearchShell } from "@threadlabs/looma";
</script>

{#snippet search()}
  <UiInput
    type="search"
    aria-label="Search commands"
    placeholder="Search commands"
  > </UiInput>
{/snippet}

{#snippet body()}
  <div>Start typing to filter commands.</div>
{/snippet}

<UiSearchShell
  open
  label="Search commands"
  slots={{ search, body }}
>   </UiSearchShell>
import { Component } from "@angular/core";
import { UiInput, UiSearchShell } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiSearchShell, UiInput],
  template: `
    <div
      ui-search-shell
      [open]="true"
      [label]="'Search commands'"
    >
      <ng-container ngProjectAs="[slot='search']">
        <input
          ui-input
          [type]="'search'"
          aria-label="Search commands"
          placeholder="Search commands"
        >
      </ng-container>
      <ng-container ngProjectAs="[slot='body']">
        <div>Start typing to filter commands.</div>
      </ng-container>
    </div>
  `,
})
export default class Example {}

label, dismissible, status, and footer

Label names the dialog, dismissible enables backdrop dismissal, and the status, body, and footer slots host application-owned search state. Escape always closes it.

3 results

Code

<script
  type="module"
>
  import "@threadlabs/looma";

  const input = document.querySelector("#docs-search-input");
  const status = document.querySelector("#docs-search-status");
  const empty = document.querySelector("#docs-search-empty");
  const results = [
    document.querySelector("#docs-result-design-tokens"),
    document.querySelector("#docs-result-token-overrides"),
    document.querySelector("#docs-result-button-variants")
  ];

  function updateResults() {
    const query = input.value.trim().toLowerCase();
    let visible = 0;
    for (const result of results) {
      result.hidden = !result.textContent.toLowerCase().includes(query);
      if (!result.hidden) visible += 1;
    }
    status.textContent = visible + " result" + (visible === 1 ? "" : "s");
    empty.hidden = visible !== 0;
  }

  input.addEventListener("input", updateResults);
</script>

<ui-search-shell
  id="docs-search-shell"
  open
  dismissible
  label="Search documentation"
>
  <ui-input
    id="docs-search-input"
    slot="search"
    type="search"
    aria-label="Search documentation"
    placeholder="Search documentation"
  ></ui-input>
  <div
    id="docs-search-status"
    slot="status"
    aria-live="polite"
  >3 results</div>
  <div
    id="docs-search-results"
    slot="body"
  >
    <ui-search-result-row
      id="docs-result-design-tokens"
    >
      <strong
        slot="title"
      >Design tokens</strong>
      <span
        slot="excerpt"
      >Color, type, spacing, and motion.</span>
    </ui-search-result-row>
    <ui-search-result-row
      id="docs-result-token-overrides"
    >
      <strong
        slot="title"
      >Token overrides</strong>
      <span
        slot="excerpt"
      >Customize shared and component tokens.</span>
    </ui-search-result-row>
    <ui-search-result-row
      id="docs-result-button-variants"
    >
      <strong
        slot="title"
      >Button variants</strong>
      <span
        slot="excerpt"
      >Choose solid, outline, ghost, or danger.</span>
    </ui-search-result-row>
    <p
      id="docs-search-empty"
      hidden
    >No matching documentation.</p>
  </div>
  <form
    slot="footer"
    method="dialog"
  >
    <ui-button
      type="submit"
      size="sm"
    >Close</ui-button>
  </form>
</ui-search-shell>
<script setup lang="ts">
import { computed, ref } from "vue";
import { UiButton as Button, UiInput as Input, UiSearchResultRow as SearchResultRow, UiSearchShell as SearchShell } from "@threadlabs/looma";

const query = ref("");
const results = [
  ["Design tokens", "Color, type, spacing, and motion."],
  ["Token overrides", "Customize shared and component tokens."],
  ["Button variants", "Choose solid, outline, ghost, or danger."]
];
const filtered = computed(() => results.filter((result) =>
  result.join(" ").toLowerCase().includes(query.value.toLowerCase())
));
</script>

<template>
  <SearchShell open dismissible label="Search documentation">
    <template #search>
      <Input type="search" aria-label="Search documentation"
        :value="query" @input="query = $event.target.value" />
    </template>
    <template #status><div aria-live="polite">{{ filtered.length }} results</div></template>
    <template #body>
      <SearchResultRow v-for="result in filtered" :key="result[0]">
        <template #title><strong>{{ result[0] }}</strong></template>
        <template #excerpt><span>{{ result[1] }}</span></template>
      </SearchResultRow>
      <p v-if="filtered.length === 0">No matching documentation.</p>
    </template>
    <template #footer><form method="dialog"><Button type="submit" size="sm">Close</Button></form></template>
  </SearchShell>
</template>
import { UiButton, UiInput, UiSearchResultRow, UiSearchShell } from "@threadlabs/looma";

export default function Example() {
  return <UiSearchShell
    id="docs-search-shell"
    data-example-id="docs-search-shell"
    open
    dismissible
    label="Search documentation"
    slots={{
      search: <UiInput
        id="docs-search-input"
        data-example-id="docs-search-input"
        type="search"
        aria-label="Search documentation"
        placeholder="Search documentation"
      >{" "}</UiInput>,
      status: <div
        id="docs-search-status"
        data-example-id="docs-search-status"
        aria-live="polite"
      >3 results</div>,
      body: <div
        id="docs-search-results"
        data-example-id="docs-search-results"
      >
        <UiSearchResultRow
          id="docs-result-design-tokens"
          data-example-id="docs-result-design-tokens"
          slots={{
            title: <strong>Design tokens</strong>,
            excerpt: <span>Color, type, spacing, and motion.</span>,
          }}
        >{" "}{" "}{" "}</UiSearchResultRow>
        <UiSearchResultRow
          id="docs-result-token-overrides"
          data-example-id="docs-result-token-overrides"
          slots={{
            title: <strong>Token overrides</strong>,
            excerpt: <span>Customize shared and component tokens.</span>,
          }}
        >{" "}{" "}{" "}</UiSearchResultRow>
        <UiSearchResultRow
          id="docs-result-button-variants"
          data-example-id="docs-result-button-variants"
          slots={{
            title: <strong>Button variants</strong>,
            excerpt: <span>Choose solid, outline, ghost, or danger.</span>,
          }}
        >{" "}{" "}{" "}</UiSearchResultRow>
        <p
          id="docs-search-empty"
          data-example-id="docs-search-empty"
          hidden
        >No matching documentation.</p>
      </div>,
      footer: <form
        method="dialog"
      >
        <UiButton
          type="submit"
          size="sm"
        >Close</UiButton>
      </form>,
    }}
  >{" "}{" "}{" "}{" "}{" "}</UiSearchShell>;
}
/** @jsxImportSource preact */
import { UiButton, UiInput, UiSearchResultRow, UiSearchShell } from "@threadlabs/looma";

export default function Example() {
  return <UiSearchShell
    id="docs-search-shell"
    data-example-id="docs-search-shell"
    open
    dismissible
    label="Search documentation"
    slots={{
      search: <UiInput
        id="docs-search-input"
        data-example-id="docs-search-input"
        type="search"
        aria-label="Search documentation"
        placeholder="Search documentation"
      >{" "}</UiInput>,
      status: <div
        id="docs-search-status"
        data-example-id="docs-search-status"
        aria-live="polite"
      >3 results</div>,
      body: <div
        id="docs-search-results"
        data-example-id="docs-search-results"
      >
        <UiSearchResultRow
          id="docs-result-design-tokens"
          data-example-id="docs-result-design-tokens"
          slots={{
            title: <strong>Design tokens</strong>,
            excerpt: <span>Color, type, spacing, and motion.</span>,
          }}
        >{" "}{" "}{" "}</UiSearchResultRow>
        <UiSearchResultRow
          id="docs-result-token-overrides"
          data-example-id="docs-result-token-overrides"
          slots={{
            title: <strong>Token overrides</strong>,
            excerpt: <span>Customize shared and component tokens.</span>,
          }}
        >{" "}{" "}{" "}</UiSearchResultRow>
        <UiSearchResultRow
          id="docs-result-button-variants"
          data-example-id="docs-result-button-variants"
          slots={{
            title: <strong>Button variants</strong>,
            excerpt: <span>Choose solid, outline, ghost, or danger.</span>,
          }}
        >{" "}{" "}{" "}</UiSearchResultRow>
        <p
          id="docs-search-empty"
          data-example-id="docs-search-empty"
          hidden
        >No matching documentation.</p>
      </div>,
      footer: <form
        method="dialog"
      >
        <UiButton
          type="submit"
          size="sm"
        >Close</UiButton>
      </form>,
    }}
  >{" "}{" "}{" "}{" "}{" "}</UiSearchShell>;
}
<script lang="ts">
  import "@threadlabs/looma";
  let query = "";
  const results = [
    ["Design tokens", "Color, type, spacing, and motion."],
    ["Token overrides", "Customize shared and component tokens."],
    ["Button variants", "Choose solid, outline, ghost, or danger."]
  ];
  $: filtered = results.filter((result) =>
    result.join(" ").toLowerCase().includes(query.toLowerCase())
  );
</script>

<ui-search-shell open dismissible label="Search documentation">
  <ui-input slot="search" type="search" aria-label="Search documentation"
    value={query} oninput={(event) => query = event.currentTarget.value}></ui-input>
  <div slot="status" aria-live="polite">{filtered.length} results</div>
  <div slot="body">
    {#each filtered as result (result[0])}
      <ui-search-result-row>
        <strong slot="title">{result[0]}</strong><span slot="excerpt">{result[1]}</span>
      </ui-search-result-row>
    {/each}
    {#if filtered.length === 0}<p>No matching documentation.</p>{/if}
  </div>
  <form slot="footer" method="dialog"><ui-button type="submit" size="sm">Close</ui-button></form>
</ui-search-shell>
import { Component } from "@angular/core";
import { UiButton, UiInput, UiSearchResultRow, UiSearchShell } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiSearchShell, UiInput, UiSearchResultRow, UiButton],
  template: `
    <div
      ui-search-shell
      id="docs-search-shell"
      data-example-id="docs-search-shell"
      [open]="true"
      [dismissible]="true"
      [label]="'Search documentation'"
    >
      <ng-container ngProjectAs="[slot='search']">
        <input
          ui-input
          id="docs-search-input"
          data-example-id="docs-search-input"
          [type]="'search'"
          aria-label="Search documentation"
          placeholder="Search documentation"
        >
      </ng-container>
      <ng-container ngProjectAs="[slot='status']">
        <div
          id="docs-search-status"
          data-example-id="docs-search-status"
          aria-live="polite"
        >3 results</div>
      </ng-container>
      <ng-container ngProjectAs="[slot='body']">
        <div
          id="docs-search-results"
          data-example-id="docs-search-results"
        >
          <button
            ui-search-result-row
            id="docs-result-design-tokens"
            data-example-id="docs-result-design-tokens"
          >
            <ng-container ngProjectAs="[slot='title']">
              <strong>Design tokens</strong>
            </ng-container>
            <ng-container ngProjectAs="[slot='excerpt']">
              <span>Color, type, spacing, and motion.</span>
            </ng-container>
          </button>
          <button
            ui-search-result-row
            id="docs-result-token-overrides"
            data-example-id="docs-result-token-overrides"
          >
            <ng-container ngProjectAs="[slot='title']">
              <strong>Token overrides</strong>
            </ng-container>
            <ng-container ngProjectAs="[slot='excerpt']">
              <span>Customize shared and component tokens.</span>
            </ng-container>
          </button>
          <button
            ui-search-result-row
            id="docs-result-button-variants"
            data-example-id="docs-result-button-variants"
          >
            <ng-container ngProjectAs="[slot='title']">
              <strong>Button variants</strong>
            </ng-container>
            <ng-container ngProjectAs="[slot='excerpt']">
              <span>Choose solid, outline, ghost, or danger.</span>
            </ng-container>
          </button>
          <p
            id="docs-search-empty"
            data-example-id="docs-search-empty"
            hidden
          >No matching documentation.</p>
        </div>
      </ng-container>
      <ng-container ngProjectAs="[slot='footer']">
        <form
          method="dialog"
        >
          <ng-template
            ui-button
            [type]="'submit'"
            [size]="'sm'"
          >Close</ng-template>
        </form>
      </ng-container>
    </div>
  `,
})
export default class Example {}

Default

With selected and disabled omitted, Search Result Row renders its normal state.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-search-result-row/ui-search-result-row.html"
>

<ui-search-result-row> <span
    slot="leading"
  >○</span> <span
    slot="title"
  >Result title</span> <span
    slot="meta"
  >Metadata</span> </ui-search-result-row>
<script setup>
  import { UiSearchResultRow } from "@threadlabs/looma";
</script>

<template>
  <UiSearchResultRow>
    <template #leading>
      <span>○</span>
    </template>
    <template #title>
      <span>Result title</span>
    </template>
    <template #meta>
      <span>Metadata</span>
    </template>
  </UiSearchResultRow>
</template>
import { UiSearchResultRow } from "@threadlabs/looma";

export default function Example() {
  return <UiSearchResultRow
    slots={{
      leading: <span>○</span>,
      title: <span>Result title</span>,
      meta: <span>Metadata</span>,
    }}
  >{" "}{" "}{" "}{" "}</UiSearchResultRow>;
}
/** @jsxImportSource preact */
import { UiSearchResultRow } from "@threadlabs/looma";

export default function Example() {
  return <UiSearchResultRow
    slots={{
      leading: <span>○</span>,
      title: <span>Result title</span>,
      meta: <span>Metadata</span>,
    }}
  >{" "}{" "}{" "}{" "}</UiSearchResultRow>;
}
<script>
  import { UiSearchResultRow } from "@threadlabs/looma";
</script>

{#snippet leading()}
  <span>○</span>
{/snippet}

{#snippet title()}
  <span>Result title</span>
{/snippet}

{#snippet meta()}
  <span>Metadata</span>
{/snippet}

<UiSearchResultRow
  slots={{ leading, title, meta }}
>    </UiSearchResultRow>
import { Component } from "@angular/core";
import { UiSearchResultRow } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiSearchResultRow],
  template: `
    <button
      ui-search-result-row
    >
      <ng-container ngProjectAs="[slot='leading']">
        <span>○</span>
      </ng-container>
      <ng-container ngProjectAs="[slot='title']">
        <span>Result title</span>
      </ng-container>
      <ng-container ngProjectAs="[slot='meta']">
        <span>Metadata</span>
      </ng-container>
    </button>
  `,
})
export default class Example {}

selected and disabled

Selected marks the current result, visibly and with aria-current; disabled keeps an unavailable result visible but inert.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-search-result-row/ui-search-result-row.html"
>

<div>
  <ui-search-result-row
    selected
  > <span
      slot="title"
    >Selected result</span> <span
      slot="meta"
    >Available</span> </ui-search-result-row>
  <ui-search-result-row
    disabled
  > <span
      slot="title"
    >Disabled result</span> <span
      slot="meta"
    >Unavailable</span> </ui-search-result-row>
</div>
<script setup>
  import { UiSearchResultRow } from "@threadlabs/looma";
</script>

<template>
  <div>
    <UiSearchResultRow
      :selected="true"
    >
      <template #title>
        <span>Selected result</span>
      </template>
      <template #meta>
        <span>Available</span>
      </template>
    </UiSearchResultRow>
    <UiSearchResultRow
      :disabled="true"
    >
      <template #title>
        <span>Disabled result</span>
      </template>
      <template #meta>
        <span>Unavailable</span>
      </template>
    </UiSearchResultRow>
  </div>
</template>
import { UiSearchResultRow } from "@threadlabs/looma";

export default function Example() {
  return <div>
    <UiSearchResultRow
      selected
      slots={{
        title: <span>Selected result</span>,
        meta: <span>Available</span>,
      }}
    >{" "}{" "}{" "}</UiSearchResultRow>
    <UiSearchResultRow
      disabled
      slots={{
        title: <span>Disabled result</span>,
        meta: <span>Unavailable</span>,
      }}
    >{" "}{" "}{" "}</UiSearchResultRow>
  </div>;
}
/** @jsxImportSource preact */
import { UiSearchResultRow } from "@threadlabs/looma";

export default function Example() {
  return <div>
    <UiSearchResultRow
      selected
      slots={{
        title: <span>Selected result</span>,
        meta: <span>Available</span>,
      }}
    >{" "}{" "}{" "}</UiSearchResultRow>
    <UiSearchResultRow
      disabled
      slots={{
        title: <span>Disabled result</span>,
        meta: <span>Unavailable</span>,
      }}
    >{" "}{" "}{" "}</UiSearchResultRow>
  </div>;
}
<script>
  import { UiSearchResultRow } from "@threadlabs/looma";
</script>

{#snippet title()}
  <span>Selected result</span>
{/snippet}

{#snippet meta()}
  <span>Available</span>
{/snippet}

{#snippet searchResultRowTitle()}
  <span>Disabled result</span>
{/snippet}

{#snippet searchResultRowMeta()}
  <span>Unavailable</span>
{/snippet}

<div>
  <UiSearchResultRow
    selected
    slots={{ title, meta }}
  >   </UiSearchResultRow>
  <UiSearchResultRow
    disabled
    slots={{ title: searchResultRowTitle, meta: searchResultRowMeta }}
  >   </UiSearchResultRow>
</div>
import { Component } from "@angular/core";
import { UiSearchResultRow } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiSearchResultRow],
  template: `
    <div>
      <button
        ui-search-result-row
        [selected]="true"
      >
        <ng-container ngProjectAs="[slot='title']">
          <span>Selected result</span>
        </ng-container>
        <ng-container ngProjectAs="[slot='meta']">
          <span>Available</span>
        </ng-container>
      </button>
      <button
        ui-search-result-row
        [disabled]="true"
      >
        <ng-container ngProjectAs="[slot='title']">
          <span>Disabled result</span>
        </ng-container>
        <ng-container ngProjectAs="[slot='meta']">
          <span>Unavailable</span>
        </ng-container>
      </button>
    </div>
  `,
})
export default class Example {}

Compact results

Three compact title-and-location rows fit in a small scroll area; the remaining results scroll.

Code

<link
  rel="component"
  href="@threadlabs/looma/components/ui-scroll-area/ui-scroll-area.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-search-result-row/ui-search-result-row.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-icon/ui-icon.html"
>
<link
  rel="component"
  href="@threadlabs/looma/components/ui-text/ui-text.html"
>

<ui-scroll-area
  size="sm"
>
  <ui-search-result-row
    density="compact"
  >
    <ui-icon
      slot="leading"
      name="file-text"
    ></ui-icon>
    <ui-text
      slot="title"
      as="strong"
      size="sm"
      weight="medium"
    >Guide</ui-text>
    <ui-text
      slot="meta"
      as="span"
      size="xs"
      tone="secondary"
    >Team / Guides</ui-text>
  </ui-search-result-row>
  <ui-search-result-row
    density="compact"
  >
    <ui-icon
      slot="leading"
      name="file-text"
    ></ui-icon>
    <ui-text
      slot="title"
      as="strong"
      size="sm"
      weight="medium"
    >Handbook</ui-text>
    <ui-text
      slot="meta"
      as="span"
      size="xs"
      tone="secondary"
    >Team / References</ui-text>
  </ui-search-result-row>
  <ui-search-result-row
    density="compact"
  >
    <ui-icon
      slot="leading"
      name="file-text"
    ></ui-icon>
    <ui-text
      slot="title"
      as="strong"
      size="sm"
      weight="medium"
    >Glossary</ui-text>
    <ui-text
      slot="meta"
      as="span"
      size="xs"
      tone="secondary"
    >Team / References</ui-text>
  </ui-search-result-row>
  <ui-search-result-row
    density="compact"
  >
    <ui-icon
      slot="leading"
      name="file-text"
    ></ui-icon>
    <ui-text
      slot="title"
      as="strong"
      size="sm"
      weight="medium"
    >Notes</ui-text>
    <ui-text
      slot="meta"
      as="span"
      size="xs"
      tone="secondary"
    >Personal / Notes</ui-text>
  </ui-search-result-row>
</ui-scroll-area>
<script setup>
  import { UiIcon, UiScrollArea, UiSearchResultRow, UiText } from "@threadlabs/looma";
</script>

<template>
  <UiScrollArea
    size="sm"
  >
    <UiSearchResultRow
      density="compact"
    >
      <template #leading>
        <UiIcon
          name="file-text"
        />
      </template>
      <template #title>
        <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Guide</UiText>
      </template>
      <template #meta>
        <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Team / Guides</UiText>
      </template>
    </UiSearchResultRow>
    <UiSearchResultRow
      density="compact"
    >
      <template #leading>
        <UiIcon
          name="file-text"
        />
      </template>
      <template #title>
        <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Handbook</UiText>
      </template>
      <template #meta>
        <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Team / References</UiText>
      </template>
    </UiSearchResultRow>
    <UiSearchResultRow
      density="compact"
    >
      <template #leading>
        <UiIcon
          name="file-text"
        />
      </template>
      <template #title>
        <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Glossary</UiText>
      </template>
      <template #meta>
        <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Team / References</UiText>
      </template>
    </UiSearchResultRow>
    <UiSearchResultRow
      density="compact"
    >
      <template #leading>
        <UiIcon
          name="file-text"
        />
      </template>
      <template #title>
        <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Notes</UiText>
      </template>
      <template #meta>
        <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Personal / Notes</UiText>
      </template>
    </UiSearchResultRow>
  </UiScrollArea>
</template>
import { UiIcon, UiScrollArea, UiSearchResultRow, UiText } from "@threadlabs/looma";

export default function Example() {
  return <UiScrollArea
    size="sm"
  >
    <UiSearchResultRow
      density="compact"
      slots={{
        leading: <UiIcon
          name="file-text"
        />,
        title: <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Guide</UiText>,
        meta: <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Team / Guides</UiText>,
      }}
    >{" "}{" "}{" "}{" "}</UiSearchResultRow>
    <UiSearchResultRow
      density="compact"
      slots={{
        leading: <UiIcon
          name="file-text"
        />,
        title: <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Handbook</UiText>,
        meta: <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Team / References</UiText>,
      }}
    >{" "}{" "}{" "}{" "}</UiSearchResultRow>
    <UiSearchResultRow
      density="compact"
      slots={{
        leading: <UiIcon
          name="file-text"
        />,
        title: <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Glossary</UiText>,
        meta: <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Team / References</UiText>,
      }}
    >{" "}{" "}{" "}{" "}</UiSearchResultRow>
    <UiSearchResultRow
      density="compact"
      slots={{
        leading: <UiIcon
          name="file-text"
        />,
        title: <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Notes</UiText>,
        meta: <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Personal / Notes</UiText>,
      }}
    >{" "}{" "}{" "}{" "}</UiSearchResultRow>
  </UiScrollArea>;
}
/** @jsxImportSource preact */
import { UiIcon, UiScrollArea, UiSearchResultRow, UiText } from "@threadlabs/looma";

export default function Example() {
  return <UiScrollArea
    size="sm"
  >
    <UiSearchResultRow
      density="compact"
      slots={{
        leading: <UiIcon
          name="file-text"
        />,
        title: <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Guide</UiText>,
        meta: <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Team / Guides</UiText>,
      }}
    >{" "}{" "}{" "}{" "}</UiSearchResultRow>
    <UiSearchResultRow
      density="compact"
      slots={{
        leading: <UiIcon
          name="file-text"
        />,
        title: <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Handbook</UiText>,
        meta: <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Team / References</UiText>,
      }}
    >{" "}{" "}{" "}{" "}</UiSearchResultRow>
    <UiSearchResultRow
      density="compact"
      slots={{
        leading: <UiIcon
          name="file-text"
        />,
        title: <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Glossary</UiText>,
        meta: <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Team / References</UiText>,
      }}
    >{" "}{" "}{" "}{" "}</UiSearchResultRow>
    <UiSearchResultRow
      density="compact"
      slots={{
        leading: <UiIcon
          name="file-text"
        />,
        title: <UiText
          as="strong"
          size="sm"
          weight="medium"
        >Notes</UiText>,
        meta: <UiText
          as="span"
          size="xs"
          tone="secondary"
        >Personal / Notes</UiText>,
      }}
    >{" "}{" "}{" "}{" "}</UiSearchResultRow>
  </UiScrollArea>;
}
<script>
  import { UiIcon, UiScrollArea, UiSearchResultRow, UiText } from "@threadlabs/looma";
</script>

{#snippet leading()}
  <UiIcon
    name="file-text"
  />
{/snippet}

{#snippet title()}
  <UiText
    as="strong"
    size="sm"
    weight="medium"
  >Guide</UiText>
{/snippet}

{#snippet meta()}
  <UiText
    as="span"
    size="xs"
    tone="secondary"
  >Team / Guides</UiText>
{/snippet}

{#snippet searchResultRowLeading()}
  <UiIcon
    name="file-text"
  />
{/snippet}

{#snippet searchResultRowTitle()}
  <UiText
    as="strong"
    size="sm"
    weight="medium"
  >Handbook</UiText>
{/snippet}

{#snippet searchResultRowMeta()}
  <UiText
    as="span"
    size="xs"
    tone="secondary"
  >Team / References</UiText>
{/snippet}

{#snippet searchResultRowLeading2()}
  <UiIcon
    name="file-text"
  />
{/snippet}

{#snippet searchResultRowTitle2()}
  <UiText
    as="strong"
    size="sm"
    weight="medium"
  >Glossary</UiText>
{/snippet}

{#snippet searchResultRowMeta2()}
  <UiText
    as="span"
    size="xs"
    tone="secondary"
  >Team / References</UiText>
{/snippet}

{#snippet searchResultRowLeading3()}
  <UiIcon
    name="file-text"
  />
{/snippet}

{#snippet searchResultRowTitle3()}
  <UiText
    as="strong"
    size="sm"
    weight="medium"
  >Notes</UiText>
{/snippet}

{#snippet searchResultRowMeta3()}
  <UiText
    as="span"
    size="xs"
    tone="secondary"
  >Personal / Notes</UiText>
{/snippet}

<UiScrollArea
  size="sm"
>
  <UiSearchResultRow
    density="compact"
    slots={{ leading, title, meta }}
  >    </UiSearchResultRow>
  <UiSearchResultRow
    density="compact"
    slots={{ leading: searchResultRowLeading, title: searchResultRowTitle, meta: searchResultRowMeta }}
  >    </UiSearchResultRow>
  <UiSearchResultRow
    density="compact"
    slots={{ leading: searchResultRowLeading2, title: searchResultRowTitle2, meta: searchResultRowMeta2 }}
  >    </UiSearchResultRow>
  <UiSearchResultRow
    density="compact"
    slots={{ leading: searchResultRowLeading3, title: searchResultRowTitle3, meta: searchResultRowMeta3 }}
  >    </UiSearchResultRow>
</UiScrollArea>
import { Component } from "@angular/core";
import { UiIcon, UiScrollArea, UiSearchResultRow, UiText } from "@threadlabs/looma";

@Component({
  selector: "example-view",
  standalone: true,
  imports: [UiScrollArea, UiSearchResultRow, UiIcon, UiText],
  template: `
    <div
      ui-scroll-area
      [size]="'sm'"
    >
      <button
        ui-search-result-row
        [density]="'compact'"
      >
        <ng-container ngProjectAs="[slot='leading']">
          <span
            ui-icon
            name="file-text"
          ></span>
        </ng-container>
        <ng-container ngProjectAs="[slot='title']">
          <ng-template
            ui-text
            [as]="'strong'"
            [size]="'sm'"
            [weight]="'medium'"
          >Guide</ng-template>
        </ng-container>
        <ng-container ngProjectAs="[slot='meta']">
          <ng-template
            ui-text
            [as]="'span'"
            [size]="'xs'"
            [tone]="'secondary'"
          >Team / Guides</ng-template>
        </ng-container>
      </button>
      <button
        ui-search-result-row
        [density]="'compact'"
      >
        <ng-container ngProjectAs="[slot='leading']">
          <span
            ui-icon
            name="file-text"
          ></span>
        </ng-container>
        <ng-container ngProjectAs="[slot='title']">
          <ng-template
            ui-text
            [as]="'strong'"
            [size]="'sm'"
            [weight]="'medium'"
          >Handbook</ng-template>
        </ng-container>
        <ng-container ngProjectAs="[slot='meta']">
          <ng-template
            ui-text
            [as]="'span'"
            [size]="'xs'"
            [tone]="'secondary'"
          >Team / References</ng-template>
        </ng-container>
      </button>
      <button
        ui-search-result-row
        [density]="'compact'"
      >
        <ng-container ngProjectAs="[slot='leading']">
          <span
            ui-icon
            name="file-text"
          ></span>
        </ng-container>
        <ng-container ngProjectAs="[slot='title']">
          <ng-template
            ui-text
            [as]="'strong'"
            [size]="'sm'"
            [weight]="'medium'"
          >Glossary</ng-template>
        </ng-container>
        <ng-container ngProjectAs="[slot='meta']">
          <ng-template
            ui-text
            [as]="'span'"
            [size]="'xs'"
            [tone]="'secondary'"
          >Team / References</ng-template>
        </ng-container>
      </button>
      <button
        ui-search-result-row
        [density]="'compact'"
      >
        <ng-container ngProjectAs="[slot='leading']">
          <span
            ui-icon
            name="file-text"
          ></span>
        </ng-container>
        <ng-container ngProjectAs="[slot='title']">
          <ng-template
            ui-text
            [as]="'strong'"
            [size]="'sm'"
            [weight]="'medium'"
          >Notes</ng-template>
        </ng-container>
        <ng-container ngProjectAs="[slot='meta']">
          <ng-template
            ui-text
            [as]="'span'"
            [size]="'xs'"
            [tone]="'secondary'"
          >Personal / Notes</ng-template>
        </ng-container>
      </button>
    </div>
  `,
})
export default class Example {}

API

Package: @threadlabs/looma · Native root: div

Attributes

NameDescriptionPropertyTypeDefaultAllowed values
dismissibleAllows backdrop activation to close the search surface. Escape always closes it.dismissiblebooleanfalse—
labelProvides the native dialog's accessible name.labelstringSearch—
modalMakes the rest of the page inert while the search surface is open.modalbooleanfalse—
openShows the search surface.openbooleanfalse—

Framework props

NameTypeDefaultAllowed values
dismissiblebooleanfalse—
labelstringSearch—
modalbooleanfalse—
openbooleanfalse—

Events

NameDetailDescription
closeobject
  • open: boolean (required)
  • reason: keyword (required)
  • trigger: keyword (required)
Emitted when the component transitions to a closed state.

Slots

NamePurpose
searchNamed content for the search region.
statusNamed content for the status region.
bodyNamed content for the body region.
footerNamed content for the footer region.

Design tokens

Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.

Component tokens

TokenDeclared valuesFallback
--ui-search-shell-border—var(--ui-overlay-border, var(--ui-border-strong))
--ui-search-shell-focus-color—var(--ui-control-focus)
--ui-search-shell-max-height—80vh
--ui-search-shell-max-width—40rem
--ui-search-shell-padding-top—10vh
--ui-search-shell-radius—var(--ui-overlay-radius, var(--ui-radius-dialog))
--ui-search-shell-shadow—var(--ui-overlay-shadow, var(--ui-elevation-dialog, var(--ui-shadow-xl)))
--ui-search-shell-surface—var(--ui-overlay-surface, var(--ui-surface-elevated))
--ui-search-shell-viewport-gap—var(--ui-space-4)
--ui-search-shell-z-index—400

Shared tokens

TokenFallback
--ui-border-strong—
--ui-border-width—
--ui-control-border—
--ui-control-focus—
--ui-control-min-block-size—
--ui-control-surface—
--ui-elevation-dialogvar(--ui-shadow-xl)
--ui-field-radiusvar(--ui-radius-md)
--ui-focus-ring—
--ui-focus-width—
--ui-font-size-sm—
--ui-overlay-backdrop—
--ui-overlay-bordervar(--ui-border-strong)
--ui-overlay-radiusvar(--ui-radius-dialog)
--ui-overlay-shadowvar(--ui-elevation-dialog, var(--ui-shadow-xl))
--ui-overlay-surfacevar(--ui-surface-elevated)
--ui-radius-dialog—
--ui-radius-md—
--ui-scroll-fade-size24px
--ui-shadow-xl—
--ui-shadow-xs—
--ui-space-2—
--ui-space-3—
--ui-space-4—
--ui-surface-elevated—
--ui-text-primary—
--ui-text-secondary—

Search Result Row API

Presentational row shell for command/search results. It owns row layout, spacing, hover/selected states, and optional leading/meta/excerpt/trailing regions while apps keep result shaping and highlighting.

Package: @threadlabs/looma · Native root: button

Attributes

NameDescriptionPropertyTypeDefaultAllowed values
densityCompact keeps a title and location together in a 3.125rem row for small destination pickers; comfortable preserves the existing rich result spacing.densitykeywordcomfortablecomfortable, compact
disabledDisables the row's button, so it cannot be chosen or focused, and gives it a flat muted treatment.disabledbooleanfalse—
selectedMarks the row as the current result: it gets the hover surface and aria-current="true", which assistive technology announces as current. Set it on one row at a time, such as the result the arrow keys have moved to.selectedbooleanfalse—

Framework props

NameTypeDefaultAllowed values
density"comfortable" | "compact"comfortablecomfortable, compact
disabledbooleanfalse—
selectedbooleanfalse—

Events

No events.

Slots

NamePurpose
leadingNamed content for the leading region.
titleNamed content for the title region.
metaNamed content for the meta region.
excerptNamed content for the excerpt region.
trailingNamed content for the trailing region.

Design tokens

Set shared theme properties on a theme boundary. Component hooks apply only to their owning component.

Component tokens

TokenDeclared valuesFallback
--ui-search-result-row-border—transparent
--ui-search-result-row-gap—var(--ui-space-3) | var(--ui-space-2)
--ui-search-result-row-hover-surface—var(--_ui-selection-context-surface, color-mix(in srgb, var(--ui-surface) 85%, black 3%))
--ui-search-result-row-min-height—56px | 3.125rem
--ui-search-result-row-padding-block—var(--ui-space-3) | var(--ui-space-1)
--ui-search-result-row-padding-inline—var(--ui-space-4) | var(--ui-space-2)
--ui-search-result-row-selected-bg—var(--_ui-selection-context-surface, var(--ui-selection-surface))
--ui-search-result-row-surface—transparent

Shared tokens

TokenFallback
--ui-border-width—
--ui-disabled-surface—
--ui-disabled-text—
--ui-focus-ring—
--ui-focus-width—
--ui-font-family-sanssans-serif
--ui-font-size-sm—
--ui-font-size-xs—
--ui-line-height—
--ui-motion-ease—
--ui-motion-fast—
--ui-selection-radius—
--ui-selection-surface—
--ui-selection-text—
--ui-space-1—
--ui-space-2—
--ui-space-3—
--ui-space-4—
--ui-surface—