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
+
+```
+
+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
+
` 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.