---
title: Composing Tuffex Interfaces
description: Build reviewable, screenshot-backed dashboard slices with Tuffex components
syncStatus: migrated
verified: true
---

# Composing Tuffex Interfaces

This guide targets Tuff / Dashboard / Admin pages. The goal is not to add more components; it is to structure a page as “actions → status → data” so users can understand state and act quickly.

## Component Suites & Imports

Tuffex is organised into three suites: Basics (`base`), Advanced (`pro`) and AI (`ai`). Besides the full package, you can import per suite from the category entries to keep dependencies organised:

```ts
import { TxButton, TxDataTable } from '@talex-touch/tuffex/base'
import { TxCommandPalette, TxLiquid } from '@talex-touch/tuffex/pro'
import { ChatList, TxPromptBar } from '@talex-touch/tuffex/ai'
```

The union of the three entries is identical to the main `@talex-touch/tuffex` entry; styles are still loaded from `@talex-touch/tuffex/style.css`, or per component via each `style.css`. See the [components overview](/docs/dev/components) for each component's suite.

## 1. Page Groups

| Group | Recommended components | Purpose |
|------|----------|------|
| Actions | `TxButton`, `TuffInput`, `TxSearchInput`, `TxSearchSelect`, `TuffSwitch` | Search, filter, refresh, deploy, sync, and other immediate operations |
| Status | `TxStatusBadge`, `TxTag`, `TxProgressBar` | Review, sync, release, capacity, and quota state |
| Data | `TxDataTable`, `TxPagination`, `TxEmptyState`, `TxSearchEmpty`, `TxSkeleton`, `TxLoadingState` | Records, row selection, pagination, empty state, and loading state |
| Feedback | `TxToastHost`, `TxTooltip`, `TxLoadingOverlay`, `TxSpinner` | Task results, action hints, local blocking refreshes, and inline waits |
| Trends | Dashboard chart components | Prefer chart wrappers for trends instead of one-off SVG |
| Permission orchestration | `TxTree`, `TxTreeSelect`, `TxTransfer`, `TxTimeline` | Permission scope, owner team, resource grants, and audit progress |
| Navigation config | `TxTabs`, `TxDropdownMenu`, `TxPopover`, `TxDrawer` | Use Tabs for fixed sections, menus/popovers for lightweight actions, and Drawer for dense configuration |
| Release configuration | `TxCascader`, `TxFlatSelect`, `TxSegmentedSlider`, `TxSlider`, `TxTagInput` | Choose release scope, package format, rollout mode, risk tier, traffic ramp, and short tags |

## 2. Minimal Working Slice

::TuffDemoWrapper{demo="ComponentsWorkflowPanelDemo" code-lang="vue" title="Release workspace composition" description="A screenshot-verifiable Dashboard composition slice."}
---
code: |
  <script setup lang="ts">
  import type { DataTableColumn } from '@talex-touch/tuffex'
  import { computed, ref } from 'vue'

  interface Row {
    id: number
    task: string
    owner: string
    status: string
    score: number
  }

  const query = ref('')
  const selectedKeys = ref<Array<number | string>>([1])
  const rows: Row[] = [
    { id: 1, task: 'Verify component screenshots', owner: 'Tuff', status: 'review', score: 84 },
    { id: 2, task: 'Complete API tables', owner: 'Tuffex', status: 'ready', score: 92 },
    { id: 3, task: 'Dark-mode regression', owner: 'QA', status: 'failed', score: 48 },
  ]
  const columns: DataTableColumn<Row>[] = [
    { key: 'task', title: 'Task' },
    { key: 'owner', title: 'Owner' },
    { key: 'status', title: 'Status' },
    { key: 'score', title: 'Score', sortable: true },
  ]
  const filteredRows = computed(() => rows.filter(row => row.task.includes(query.value) || row.owner.includes(query.value)))
  </script>

  <template>
    <section class="grid gap-3">
      <TuffInput v-model="query" placeholder="Search tasks" prefix-icon="i-carbon-search" clearable />
      <TxProgressBar :percentage="84" status="success" show-text />
      <TxDataTable v-model:selected-keys="selectedKeys" :columns="columns" :data="filteredRows" row-key="id" selectable />
    </section>
  </template>
---
::

## 3. Search And Filter Path

Do not split “enter keyword”, “choose scope”, and “no matches” into disconnected regions. Compose `TxSearchInput`, `TxSearchSelect`, and `TxSearchEmpty` inside one filter container so users can see active conditions, match count, and recovery actions together.

::TuffDemoWrapper{demo="ComponentsSearchFiltersDemo" code-lang="vue" title="Dashboard filter toolbar" description="Keyword search, scope filtering, and search-empty recovery share one path."}
---
code: |
  <script setup lang="ts">
  import { computed, ref } from 'vue'

  const query = ref('')
  const selectedScope = ref<string | number>('all')
  const scopeOptions = [
    { value: 'all', label: 'All' },
    { value: 'docs', label: 'Docs' },
    { value: 'tasks', label: 'Tasks' },
  ]
  const filteredRecords = computed(() => (query.value ? [] : scopeOptions))
  </script>

  <template>
    <section class="grid gap-3">
      <TxSearchInput v-model="query" remote placeholder="Search plugins / docs / tasks" />
      <TxSearchSelect v-model="selectedScope" :options="scopeOptions" placeholder="Filter scope" />
      <TxSearchEmpty v-if="!filteredRecords.length" surface="card" />
    </section>
  </template>
---
::

## 4. Operations Status Header

Dashboard / Admin pages should first answer “is the system healthy?”. Structure the first screen in this order:

| Layer | Components | Usage |
|------|------|----------|
| Heading | Native heading + `TxButton` | Explain the operational scope and keep one primary refresh/deploy action on the right |
| Status | `TxStatusBadge` | Express state with text, not color alone |
| Metrics | `TxStatCard` | Use default cards for numbers and `variant="progress"` for health/capacity |
| Progress | `TxProgressBar` | Use `percentage` for known progress and `loading` for unknown-duration work |

::TuffDemoWrapper{demo="ComponentsOperationsStatusDemo" code-lang="vue" title="Operations status panel" description="A dashboard status, metric, and progress composition for admin headers."}
---
code: |
  <template>
    <section class="grid gap-3">
      <TxStatusBadge text="Review done" status="success" />
      <TxStatCard
        variant="progress"
        value="99.9%"
        label="API availability"
        meta="Normal"
        :progress="99"
      />
      <TxProgressBar :percentage="86" status="success" show-text />
    </section>
  </template>
---
::

## 5. Trend Sections

Dashboard trend charts should reuse `DashboardSparklineChart` / `DashboardMetricChart` instead of page-local `svg` polylines. This keeps tooltip behavior, dark theme styling, ResizeObserver resizing, and empty states consistent.

::TuffDemoWrapper{demo="ComponentsDashboardSparklineDemo" code-lang="vue" title="Dashboard trends" description="A lightweight ECharts-backed sparkline wrapper."}
---
code: |
  <script setup lang="ts">
  import DashboardSparklineChart from '~/components/dashboard/DashboardSparklineChart.client.vue'
  </script>

  <template>
    <DashboardSparklineChart
      :values="[18, 24, 21, 36, 42, 38, 54]"
      color="var(--tx-color-primary, #409eff)"
      show-grid
    />
  </template>
---
::

## 6. Data Recovery Paths

Every data panel needs three recovery paths: loading, no data, and request failure. They should occupy the same container to avoid layout jumps and duplicated placeholders.

::TuffDemoWrapper{demo="ComponentsRecoveryStatesDemo" code-lang="vue" title="Dashboard recovery states" description="Loading, empty, and error states share one data container."}
---
code: |
  <template>
    <section class="grid gap-3">
      <TxLoadingState title="Loading rules" surface="card" />
      <TxEmptyState variant="no-data" title="No automation rules yet" surface="card" />
      <TxErrorState title="Rules failed to load" surface="card" />
    </section>
  </template>
---
::

## 7. Data Lists And Pagination

Dashboard data regions should keep “table body, selection state, pagination, and loading placeholders” inside one workspace. The table should not own ad-hoc status copy, pagination should stay near the table footer, and longer waits should preserve space with skeletons.

::TuffDemoWrapper{demo="ComponentsDataOperationsDemo" code-lang="vue" title="Data operations panel" description="Table selection, pagination, and skeleton loading share one dashboard data region."}
---
code: |
  <template>
    <section class="grid gap-3">
      <TxDataTable :columns="columns" :data="pagedRows" row-key="id" selectable />
      <TxPagination v-model:current-page="page" :total="rows.length" :page-size="4" show-info />
      <TxSkeleton :loading="true" :lines="3" />
    </section>
  </template>
---
::


## 8. Task Feedback Path

Admin task feedback should split “action explanation, result notification, local blocking, and inline waiting” into separate roles: `TxTooltip` explains the button, `TxToastHost` shows short feedback, `TxLoadingOverlay` blocks a refreshing data block, and `TxSpinner` is only for short waits without percentages.

::TuffDemoWrapper{demo="ComponentsFeedbackTaskCenterDemo" code-lang="vue" title="Dashboard task feedback center" description="A Toast / Tooltip / LoadingOverlay / Spinner composition for admin tasks."}
---
code: |
  <script setup lang="ts">
  import { toast } from '@talex-touch/tuffex/utils'
  import { ref } from 'vue'

  const syncing = ref(true)
  </script>

  <template>
    <section class="grid gap-3">
      <TxToastHost />
      <TxTooltip content="Tooltip explains the current action only.">
        <TxButton @click="toast({ title: 'Sync task queued', duration: 0 })">Send toast</TxButton>
      </TxTooltip>
      <TxLoadingOverlay :loading="syncing" text="Refreshing task queue…">
        <TxSpinner :size="16" />
      </TxLoadingOverlay>
    </section>
  </template>
---
::

## 9. Permission Orchestration Path

Admin authorization pages should first answer “which scope is being authorized?”, then “who owns it, which resources are granted, and where is the audit flow?”. Use `TxTree` for left-side scopes, `TxTreeSelect` for owner teams, `TxTransfer` for resource grants, and `TxTimeline` for audit progress.

::TuffDemoWrapper{demo="ComponentsPermissionOrchestrationDemo" code-lang="vue" title="Permission orchestration panel" description="A Tree / TreeSelect / Transfer / Timeline composition for admin authorization."}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const selectedScope = ref<string | number>('release')
  const ownerTeam = ref<string | number | undefined>('docs')
  const assignedKeys = ref<Array<string | number>>(['read-docs'])
  </script>

  <template>
    <section class="grid gap-3">
      <TxTree v-model="selectedScope" :nodes="scopeNodes" />
      <TxTreeSelect v-model="ownerTeam" :nodes="teamNodes" placeholder="Owner team" />
      <TxTransfer v-model="assignedKeys" :data="resources" />
      <TxTimeline>
        <TxTimelineItem title="Assign resources" time="10:20" color="success" active />
      </TxTimeline>
    </section>
  </template>
---
::

## 10. Navigation And Configuration Shell

Dashboard settings pages should first answer “which section am I in?”, then answer “what can I do here?”. Keep first-level sections in `TxTabs`, one-off actions in `TxDropdownMenu`, short explanations in `TxPopover`, and dense configuration in `TxDrawer`.

::TuffDemoWrapper{demo="ComponentsNavigationShellDemo" code-lang="vue" title="Dashboard navigation shell" description="A Dashboard settings composition with Tabs / DropdownMenu / Popover / Drawer."}
---
code: |
  <template>
    <section class="grid gap-3">
      <TxDropdownMenu>
        <template #trigger>
          <TxButton>Release actions</TxButton>
        </template>
        <TxDropdownItem>Quick release</TxDropdownItem>
      </TxDropdownMenu>
      <TxPopover>
        <template #reference>
          <TxButton variant="secondary">Policy notes</TxButton>
        </template>
        Keep popovers short and action-light.
      </TxPopover>
      <TxTabs placement="left" indicator-variant="pill">
        <TxTabItem name="Overview" activation>Overview settings</TxTabItem>
      </TxTabs>
      <TxDrawer :visible="false" title="Release policy" />
    </section>
  </template>
---
::

## 11. Release Policy Configuration

Release configuration pages should not dump every field into one large form. Choose scope first with `TxCascader`, keep low-noise single choices in `TxFlatSelect`, put discrete risk into `TxSegmentedSlider`, continuous percentages into `TxSlider`, and keep `TxTagInput` limited to short metadata.

::TuffDemoWrapper{demo="ComponentsReleasePolicyDemo" code-lang="vue" title="Release policy configuration" description="A Cascader / FlatSelect / SegmentedSlider / Slider / TagInput composition for admin release configuration."}
---
code: |
  <script setup lang="ts">
  import { ref } from 'vue'

  const releasePath = ref(['desktop', 'stable', 'macos'])
  const packageFormat = ref('signed')
  const rolloutMode = ref('phased')
  const riskLevel = ref(2)
  const traffic = ref(35)
  const labels = ref(['nexus', 'verified'])
  const scopeOptions = [
    {
      value: 'desktop',
      label: 'Desktop',
      children: [
        {
          value: 'stable',
          label: 'Stable',
          children: [{ value: 'macos', label: 'macOS', leaf: true }],
        },
      ],
    },
  ]
  const riskSegments = [
    { value: 1, label: 'Low' },
    { value: 2, label: 'Med' },
    { value: 3, label: 'High' },
    { value: 4, label: 'Freeze' },
  ]
  </script>

  <template>
    <section class="grid gap-3">
      <TxCascader v-model="releasePath" :options="scopeOptions" placeholder="Release scope" />
      <TxFlatSelect v-model="packageFormat" placeholder="Package format">
        <TxFlatSelectItem value="signed" label="Signed build" />
      </TxFlatSelect>
      <TxSegmentedSlider v-model="riskLevel" :segments="riskSegments" />
      <TxSlider v-model="traffic" :min="5" :max="100" :step="5" show-value />
      <TxTagInput v-model="labels" placeholder="Press Enter to add tags" />
    </section>
  </template>
---
::

## 12. Review Checklist

- **One primary action**: keep one highest-weight button per panel; use `secondary` / `ghost` for supporting actions.
- **Filter loop**: keyword, scope filtering, match count, and no-match recovery should come from the same reactive model.
- **Attached pagination**: pagination must stay near the list footer and share the same filtering and selection model.
- **Shared state**: table selection, progress, and badges should come from the same reactive model instead of duplicated constants.
- **Visible empty state**: use empty-state components when no data exists; do not leave a blank table.
- **Feedback roles**: Toast handles short feedback, Tooltip handles short hints, LoadingOverlay blocks local refreshes, and Spinner communicates short waits.
- **Authorization roles**: Tree shows hierarchy, TreeSelect chooses ownership, Transfer assigns resources, and Timeline records audit history.
- **Configuration layering**: put short actions in menus, short notes in popovers, and long configuration in drawers; do not make one Popover carry a complex form.
- **Release policy roles**: Cascader owns scope, FlatSelect owns single-choice policy, SegmentedSlider owns discrete risk, Slider owns continuous ratios, and TagInput owns short metadata.
- **Screenshot evidence**: after adding or changing demos, open the local Tuff page and capture at least one key light/dark/mobile state.

## 13. Continue Reading

- [Tuffex Components](../components/index.en.mdc)
- [SearchInput](../components/search-input.en.mdc)
- [SearchSelect](../components/search-select.en.mdc)
- [SearchEmpty](../components/search-empty.en.mdc)
- [StatCard](../components/stat-card.en.mdc)
- [DataTable](../components/data-table.en.mdc)
- [Pagination](../components/pagination.en.mdc)
- [Skeleton](../components/skeleton.en.mdc)
- [Cascader](../components/cascader.en.mdc)
- [FlatSelect](../components/flat-select.en.mdc)
- [SegmentedSlider](../components/segmented-slider.en.mdc)
- [Slider](../components/slider.en.mdc)
- [TagInput](../components/tag-input.en.mdc)
- [Tree](../components/tree.en.mdc)
- [TreeSelect](../components/tree-select.en.mdc)
- [Transfer](../components/transfer.en.mdc)
- [Timeline](../components/timeline.en.mdc)
- [LayoutSkeleton](../components/layout-skeleton.en.mdc)
- [Tabs](../components/tabs.en.mdc)
- [DropdownMenu](../components/dropdown-menu.en.mdc)
- [Popover](../components/popover.en.mdc)
- [Drawer](../components/drawer.en.mdc)
- [StatusBadge](../components/status-badge.en.mdc)
- [ProgressBar](../components/progress-bar.en.mdc)
- [EmptyState](../components/empty-state.en.mdc)
- [Toast](../components/toast.en.mdc)
- [Tooltip](../components/tooltip.en.mdc)
- [LoadingOverlay](../components/loading-overlay.en.mdc)
- [Spinner](../components/spinner.en.mdc)
