---
title: Store
description: A plugin store with featured picks, search, permission details, and install flows.
category: TemplateContent
status: beta
since: 0.6.0
tags: [template, store, plugins, install, permissions]
syncStatus: reviewed
verified: false
---

## Usage

### Plugin store
:::TuffDemoWrapper{demo="TemplateStoreDemo" code-lang="vue" description="Installs, updates, and grants happen only after you click; Unit Converter fails its first download to show Retry."}
---
code: |
  <script setup lang="ts">
  import { reactive } from 'vue'
  import TemplateFrame from './TemplateFrame.vue'
  import TemplateStoreAction from './TemplateStoreAction.vue'
  import TemplateStorePermissions from './TemplateStorePermissions.vue'

  // One state table keyed by plugin id: cards, carousel, details and the
  // installed list all read it, so a download shows its progress everywhere
  const installs = reactive<Record<string, InstallState>>(seedInstalls())
  const confirm = reactive({ open: false, id: '' })

  // Installing starts with the permission dialog; no timer ever presses Allow
  function onAction(plugin: Plugin) {
    if (!installs[plugin.id].version)
      Object.assign(confirm, { open: true, id: plugin.id })
  }

  function grant(choice: 'always' | 'session') {
    confirm.open = false
    runFlow(confirm.id, choice) // queued → downloading → verifying → installing → installed
  }
  </script>

  <template>
    <TemplateFrame title="Plugin store" :height="580" @enter="play">
      <TxFilterChips v-model="view" :items="views" role="tablist" aria-label="Store views" />
      <TxSearchSelect remote :options="suggestions" placeholder="Search plugins, features or triggers…" @search="filter" @select="openDetail" />

      <!-- A hand-built carousel: the pause button is always there, hidden slides are inert, stacked slides only fade -->
      <section role="region" aria-roledescription="carousel" aria-label="Editor’s picks">…</section>

      <ul class="grid">
        <li v-for="plugin in visible" :key="plugin.id">
          <TxCard size="small" :radius="14">
            <!-- The title button's ::after covers the whole card; the action sits above it -->
            <button class="open" @click="openDetail(plugin.id)">{{ plugin.name }}</button>
            <TemplateStoreAction :view="actionOf(plugin)" compact @press="onAction(plugin)" />
          </TxCard>
        </li>
      </ul>

      <!-- Details are an in-template view (a side panel once expanded), not a TxDrawer -->
      <section class="detail">
        <TxTabs v-model="tab" placement="top" borderless :content-scrollable="false" indicator-variant="pill">
          <TxTabItem name="overview"><TxImageGallery :items="shots" preview-title="Screenshot preview" /></TxTabItem>
          <TxTabItem name="permissions"><TemplateStorePermissions :rows="permissionRows" /></TxTabItem>
          <TxTabItem name="versions"><TxTimeline>…</TxTimeline></TxTabItem>
          <TxTabItem name="reviews"><TxRating v-model="draft.stars" :star-label="n => `Rate ${n} stars`" /></TxTabItem>
        </TxTabs>
      </section>

      <TxModal v-model="confirm.open" title="Plugin permission request">
        <TemplateStorePermissions :rows="requiredRows" compact />
        <template #footer>
          <TxButton variant="ghost" @click="reject">Reject install</TxButton>
          <TxButton variant="secondary" @click="grant('session')">Allow this session</TxButton>
          <TxButton variant="primary" @click="grant('always')">Always allow</TxButton>
        </template>
      </TxModal>
    </TemplateFrame>
  </template>
---
:::

## Overview

The plugin store in the Tuff client; manifests and permissions come from the repo, ratings, installs, and reviews are sample data, and nothing is for sale.

- Installing, updating, granting, and uninstalling start only from your click; playback just turns the carousel once and reports updates, and reduced motion keeps the carousel still.
- The carousel art is dark on both light and dark docs pages, so the whole carousel renders in the dark theme.
- Install opens the permission dialog at once (the client downloads and verifies first): Always allow, Allow this session, or Reject install; community plugins add an unofficial-source warning.
- A download can be cancelled; Unit Converter times out on its first download, and Retry doesn't ask for permissions again.
- In the column, details replace the list, and Back or Esc restores the scroll position; expanded, they are a side panel. They avoid TxDrawer, which would drag focus back and close two layers on one Esc.
- Hidden carousel slides are `inert`, and nothing is announced while it plays; with focus inside it, ← / → change slides.
- Risk colors and icons always come with words; read-only stars are hidden from screen readers in favor of a sentence.

## Customization

- With a real store, read plugins, channels, permissions, and reasons from the manifests and the store API, and group by the store's own `category`.
- The install table is keyed by plugin id: replace the timers with the phases the client reports, and every action slot follows.
- Put ratings and reviews through moderation: show a submission as Pending and count it only once approved.
- For suggestions with icons, replace TxSearchSelect with TxSearchInput and a hand-built list, and write the combobox keyboard and screen-reader behavior yourself.

## Related components

| Region | Components | Role |
|---|---|---|
| Header | [TxFilterChips](/docs/dev/components/filter-chips), [TxSearchSelect](/docs/dev/components/search-select) | View switch and search suggestions |
| Categories and sorting | [TxFilterChips](/docs/dev/components/filter-chips), [TxFlatRadio](/docs/dev/components/flat-radio), [TxSidebarNav](/docs/dev/components/sidebar-nav) | Category counts and sorting |
| Featured carousel | Hand-built region, [TxButton](/docs/dev/components/button) | Hand-built carousel with controls |
| Plugin cards | [TxCard](/docs/dev/components/card), [TxIconChip](/docs/dev/components/icon-chip), [TxRating](/docs/dev/components/rating), [TxTag](/docs/dev/components/tag) | Icon, summary, rating, channel |
| Action slot | [TxButton](/docs/dev/components/button), [TxProgressBar](/docs/dev/components/progress-bar) | One shared install state |
| Updates notice | [TxStatusBadge](/docs/dev/components/status-badge) | Shown after an update check |
| Details header | [TxCornerOverlay](/docs/dev/components/corner-overlay), [TxTooltip](/docs/dev/components/tooltip), [TxDropdownMenu](/docs/dev/components/dropdown-menu) | Official badge and the ⋯ menu |
| Details tabs | [TxTabs](/docs/dev/components/tabs), [TxImageGallery](/docs/dev/components/image-gallery), [TxKbd](/docs/dev/components/kbd), [TxStatusBadge](/docs/dev/components/status-badge), [TxTimeline](/docs/dev/components/timeline) | Overview, permissions, versions, reviews |
| Reviews | [TxRating](/docs/dev/components/rating), [TxProgressBar](/docs/dev/components/progress-bar), [TxAvatar](/docs/dev/components/avatar), [TxTextarea](/docs/dev/components/textarea) | Average, distribution, write a review |
| Installed | [TxCardItem](/docs/dev/components/card-item), [TxSwitch](/docs/dev/components/switch), [TxEmptyState](/docs/dev/components/empty-state) | Installed rows and enable switches |
| Confirmations | [TxModal](/docs/dev/components/modal) | Permission, update, uninstall prompts |
| Empty results and feedback | [TxSearchEmpty](/docs/dev/components/search-empty), [TxToastPanel](/docs/dev/components/toast-panel) | Clear search and result notices |
