From 2173c77ce0661fff30f5d523f29931fe21a103b2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 03:28:07 +0000 Subject: [PATCH 01/10] docs: Show useQuery sorting as a team table that sorts by any column The Sorting & Filtering demo is now a team table: pick a column header to sort by it (again to reverse), or a segment to show admins or members. One Query does both on what the store already holds, so nothing refetches. It plays itself once in view. The design system gains rule-free tables (value cells end-aligned, a row band under the pointer), a sortable header button (`sort`, with `aria-sort` driving its arrow) and a chosen segment for `buttonGroup` filters (`aria-pressed`). The Scalar demo's table drops its inline styles and align attributes for these. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y5C5ZC3WYndrqLXmu9F42e --- docs/core/api/useQuery.md | 261 +++++++++++++----- docs/rest/shared/_ScalarDemo.mdx | 32 +-- .../Playground/DesignSystem/base.css | 6 + .../Playground/DesignSystem/design-system.css | 140 ++++++++-- website/src/components/Playground/README.md | 6 +- website/src/fixtures/team.ts | 21 ++ 6 files changed, 364 insertions(+), 102 deletions(-) create mode 100644 website/src/fixtures/team.ts diff --git a/docs/core/api/useQuery.md b/docs/core/api/useQuery.md index 37928931f37f..eca86f477f89 100644 --- a/docs/core/api/useQuery.md +++ b/docs/core/api/useQuery.md @@ -10,9 +10,21 @@ import ConditionalDependencies from '../shared/\_conditional_dependencies.mdx'; import FrameworkPlayground from '@site/src/components/FrameworkPlayground'; import StackBlitz from '@site/src/components/StackBlitz'; import { RestEndpoint } from '@data-client/rest'; +import { teamFixtures } from '@site/src/fixtures/team'; import VoteDemo from '../shared/\_VoteDemo.mdx'; import VueArgs from '../shared/\_vueArgs.mdx'; +export const sortDemo = [ + // most posts first: the rows reorder, nothing refetches + { delay: 1200, click: { selector: 'button.sort', hasText: 'Posts' } }, + { wait: 1200 }, + { click: { selector: 'button.sort', hasText: 'Posts' } }, + { wait: 1800 }, + { click: { selector: '.buttonGroup button', hasText: 'Admins' } }, + { wait: 1800 }, + { click: { selector: 'button.sort', hasText: 'Joined' } }, +]; + # useQuery() Data rendering without the fetch. @@ -101,28 +113,25 @@ interface Queryable { ### Sorting & Filtering -[Query](/rest/api/Query) provides programmatic access to the Reactive Data Client store. +[Query](/rest/api/Query) sorts and filters what is already in the store, so picking another column +re-sorts the table without fetching again. - + -```ts title="UserResource" collapsed +```ts title="resources/User" collapsed import { Entity, resource } from '@data-client/rest'; export class User extends Entity { id = ''; name = ''; isAdmin = false; + posts = 0; + joined = 0; static key = 'User'; } @@ -132,40 +141,132 @@ export const UserResource = resource({ }); ``` -:::react - -```tsx title="UsersPage" {22} +```ts title="sortedUsers" import { All, Query } from '@data-client/rest'; -import { useQuery, useFetch } from '@data-client/react'; -import { UserResource, User } from './UserResource'; +import { User } from './resources/User'; -interface Args { - asc: boolean; - isAdmin?: boolean; +export type SortBy = 'name' | 'posts' | 'joined'; +export interface Sort { + sortBy: SortBy; + desc: boolean; +} +interface Args extends Sort { + /** admins only (true), members only (false), or everyone */ + isAdmin: boolean | undefined; } -const sortedUsers = new Query( + +export const sortedUsers = new Query( new All(User), - (entries, { asc, isAdmin }: Args = { asc: false }) => { - let sorted = [...entries].sort((a, b) => a.name.localeCompare(b.name)); - if (isAdmin !== undefined) - sorted = sorted.filter(user => user.isAdmin === isAdmin); - if (asc) return sorted; - return sorted.reverse(); + (entries, { sortBy, desc, isAdmin }: Args) => { + const users = entries + .filter(user => isAdmin === undefined || user.isAdmin === isAdmin) + .sort((a, b) => + sortBy === 'name' ? + a.name.localeCompare(b.name) + : a[sortBy] - b[sortBy], + ); + return desc ? users.reverse() : users; }, ); +``` + +:::react + +```tsx title="UsersPage" {15} +import React from 'react'; +import { useQuery, useSuspense } from '@data-client/react'; +import { UserResource } from './resources/User'; +import { sortedUsers, type Sort, type SortBy } from './sortedUsers'; + +const filters = { All: undefined, Admins: true, Members: false }; function UsersPage() { - useFetch(UserResource.getList); - const users = useQuery(sortedUsers, { asc: true }); - if (!users) return
No users in cache yet
; + useSuspense(UserResource.getList); + const [sort, setSort] = React.useState({ + sortBy: 'name', + desc: false, + }); + const [isAdmin, setIsAdmin] = React.useState(); + const users = useQuery(sortedUsers, { ...sort, isAdmin }) ?? []; + const onSort = (sortBy: SortBy) => + setSort(prev => ({ + sortBy, + desc: prev.sortBy === sortBy && !prev.desc, + })); + return ( -
- {users.map(user => ( -
{user.name}
- ))} -
+ <> +
+

Team

+
+ {Object.entries(filters).map(([label, value]) => ( + + ))} +
+
+ + + + + Name + + + + Posts + + + Joined + + + + + {users.map(user => ( + + + + + + + ))} + +
Role
{user.name}{user.isAdmin ? 'Admin' : 'Member'}{user.posts}{user.joined}
+ ); } + +function SortHeader({ column, sort, onSort, numeric, children }: Props) { + const sorted = sort.sortBy === column; + return ( + + + + ); +} +interface Props { + column: SortBy; + sort: Sort; + onSort: (column: SortBy) => void; + numeric?: boolean; + children: React.ReactNode; +} render(); ``` @@ -173,38 +274,70 @@ render(); :::vue -```html title="UsersPage.vue" {24} +```html title="UsersPage.vue" {12-15} ``` diff --git a/docs/rest/shared/_ScalarDemo.mdx b/docs/rest/shared/_ScalarDemo.mdx index 44c68a0eb7cb..247fe4958e34 100644 --- a/docs/rest/shared/_ScalarDemo.mdx +++ b/docs/rest/shared/_ScalarDemo.mdx @@ -50,26 +50,26 @@ import { type Company } from './api/Company'; export default function CompanyGrid({ companies }: { companies: Company[] }) { return ( - +
- - - - + + + + {companies.map(c => ( - - + - - - + + diff --git a/.agents/skills/data-client-vue/references/useQuery.md b/.agents/skills/data-client-vue/references/useQuery.md index 181677cda04a..f8771d7134b5 100644 --- a/.agents/skills/data-client-vue/references/useQuery.md +++ b/.agents/skills/data-client-vue/references/useQuery.md @@ -219,7 +219,7 @@ export interface Sort { ``` -```html title="UsersTable.vue" {34} +```html title="UsersTable.vue" {34-37} ``` @@ -340,7 +346,7 @@ export interface Sort {

Team

- + ``` diff --git a/docs/core/api/useQuery.md b/docs/core/api/useQuery.md index e1f7379bb5a2..79f6a014762e 100644 --- a/docs/core/api/useQuery.md +++ b/docs/core/api/useQuery.md @@ -20,7 +20,7 @@ export const sortDemo = [ { wait: 1200 }, { click: { selector: 'button.sort', hasText: 'Posts' } }, { wait: 1800 }, - { click: { selector: '.buttonGroup button', hasText: 'Admins' } }, + { click: { selector: '.segmented button', hasText: 'Admins' } }, { wait: 1800 }, { click: { selector: 'button.sort', hasText: 'Joined' } }, ]; @@ -155,7 +155,7 @@ const roles = { All: undefined, Admins: true, Members: false }; export default function RoleFilter({ isAdmin, onChange }: Props) { return ( -
+
{Object.entries(roles).map(([label, value]) => (
- - + + @@ -317,7 +324,7 @@ render();
NamePrice% EquitySharesNamePrice% EquityShares
{c.name}${c.price.toFixed(2)} + ${c.price.toFixed(2)} {c.pct_equity === undefined ? : formatPercent(c.pct_equity)} + {c.shares === undefined ? : c.shares.toLocaleString()} @@ -143,24 +143,24 @@ render(); ``` From e5c0d570877068f33db0fd2361c6c02ab782ea5c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 04:06:29 +0000 Subject: [PATCH 07/10] docs(useQuery): team directory table and a segmented role filter Adds design-system elements for the sort demo: a segmented filter whose pill slides to the chosen choice, role pills, and initials avatars. Tables lose the row hover (their rows aren't actionable) and value cells get tabular figures. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y5C5ZC3WYndrqLXmu9F42e --- .../data-client-react/references/useQuery.md | 13 +- .../data-client-vue/references/useQuery.md | 30 +-- docs/core/api/useQuery.md | 28 ++- .../Playground/DesignSystem/Avatar.tsx | 27 ++- .../Playground/DesignSystem/design-system.css | 198 +++++++++++++++--- website/src/components/Playground/README.md | 8 +- .../components/Playground/monaco/typeLibs.ts | 2 +- 7 files changed, 244 insertions(+), 62 deletions(-) diff --git a/.agents/skills/data-client-react/references/useQuery.md b/.agents/skills/data-client-react/references/useQuery.md index a037bfffaa01..327f12aba574 100644 --- a/.agents/skills/data-client-react/references/useQuery.md +++ b/.agents/skills/data-client-react/references/useQuery.md @@ -222,7 +222,7 @@ const roles = { All: undefined, Admins: true, Members: false }; export default function RoleFilter({ isAdmin, onChange }: Props) { return ( -
+
{Object.entries(roles).map(([label, value]) => (
{user.name}{user.isAdmin ? 'Admin' : 'Member'} + + {user.name} + + + {user.isAdmin ? 'Admin' : 'Member'} + + {user.posts} {user.joined}
{user.name}{user.isAdmin ? 'Admin' : 'Member'} + + {user.name} + + + {user.isAdmin ? 'Admin' : 'Member'} + + {user.posts} {user.joined}