From d1bc9d9fb4cbdd8548df39b9a755f258d89cfc81 Mon Sep 17 00:00:00 2001 From: Alex Martin Date: Tue, 19 May 2026 18:01:07 -0700 Subject: [PATCH] docs(frontend): document moonshine table usage --- .agents/skills/frontend/SKILL.md | 174 +++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) diff --git a/.agents/skills/frontend/SKILL.md b/.agents/skills/frontend/SKILL.md index 0c59ef6301..0ba1aa00fe 100644 --- a/.agents/skills/frontend/SKILL.md +++ b/.agents/skills/frontend/SKILL.md @@ -70,6 +70,180 @@ export function HooksEmptyState({ Backwards-compatible callers stay ``; only the variant caller passes overrides. Avoids divergent copies of the surrounding scaffolding (provider cards, setup dialogs, etc.). +### Tables + +Use Moonshine's `Table` from `@speakeasy-api/moonshine` for dashboard tables. Do **not** add new imports from `@/components/ui/table`, do not create new shadcn table wrappers, and do not hand-roll table styling with raw `` markup when Moonshine can express the UI. Existing shadcn table usages should be migrated to Moonshine when touched. + +```tsx +import { Column, Table } from "@speakeasy-api/moonshine"; +``` + +For normal data tables, prefer the declarative `columns` / `data` / `rowKey` API. Define `Column[]` near the component so render functions stay typed, use `render` for rich cells, and use `width` for stable layouts instead of ad hoc cell class widths. + +```tsx +const columns: Column[] = [ + { + key: "name", + header: "Name", + width: "180px", + render: (role) => {role.name}, + }, + { + key: "members", + header: "Members", + width: "100px", + render: (role) => {role.memberCount}, + }, +]; + +
row.id} />; +``` + +For empty and loading states, use the Table's built-in empty surface and the shared `SkeletonTable` from `@/components/ui/skeleton`. Do not rebuild a one-off empty `` or skeleton table. + +```tsx +
row.id} + className="max-h-[500px] overflow-y-auto" + noResultsMessage={No matching API keys} +/> +``` + +Search and filter controls are siblings above the table. Keep filter state outside the table, derive filtered rows with `useMemo`, and pass the result to `data`. Use existing controls such as `SearchBar`, `MultiSelect`, `Select`, or page-specific filter pills; do not put form controls inside `Table.Header` unless they are truly column headers. If the table is paginated, reset the page index when filters change. + +```tsx +const [search, setSearch] = useState(""); +const [selectedTags, setSelectedTags] = useState([]); + +const filteredRows = useMemo(() => { + const normalizedSearch = search.trim().toLowerCase(); + + return rows.filter((row) => { + const matchesSearch = + normalizedSearch.length === 0 || + row.name.toLowerCase().includes(normalizedSearch); + const matchesTags = + selectedTags.length === 0 || + row.tags.some((tag) => selectedTags.includes(tag)); + + return matchesSearch && matchesTags; + }); +}, [rows, search, selectedTags]); + + + { + setSearch(value); + setPage(0); + }} + placeholder="Search tools" + className="w-64" + /> + { + setSelectedTags(value); + setPage(0); + }} + placeholder="Filter by tag" + autoSize + /> + + +
row.id} + noResultsMessage={No matching tools} +/>; +``` + +Footers that summarize, paginate, or load more rows should usually be sibling bars immediately below the table. Moonshine's table API does not require a special footer component for this; keep the table declarative and put pagination/load-more controls after it. + +```tsx +
row.id} />; + +{ + totalPages > 1 && ( +
+ + {pageStart}-{pageEnd} of {filteredRows.length} + +
+ + +
+
+ ); +} +``` + +Use the compound API only when the body needs custom structure that the declarative API cannot express, such as mixed rows, a full-width CTA row, or a custom no-results branch. Keep the Moonshine wrapper, header, row, and cell components as the default primitives. + +```tsx +
+ + {items.length === 0 ? ( + No results found. + ) : ( + + {items.map((item) => ( + + ))} + + )} + +
+ + Want to grant new members access? + + +
+
+
+``` + +Use grouped or expandable rows through Moonshine's table props instead of nesting unrelated cards or custom accordions around a table. Current patterns use `hideHeader` for grouped parent rows and `renderExpandedContent` for nested details. + +```tsx + row.key} + hideHeader + renderExpandedContent={(group) => ( +
row.id} + hideHeader + /> + )} +/> +``` + +Raw `` / `
` should be rare and stay inside a Moonshine `` only when native table semantics are needed and Moonshine does not expose them, such as a `colSpan` overflow row. If the row is a normal data row, use `` or the declarative `data` prop. + ### React Performance Patterns These patterns were established in the audit log (#2140) and deployment log (#2167) redesigns. Apply them whenever building search, filtering, or keyboard navigation.