From c9768e25aec412766f13596abf85cb38c80e4499 Mon Sep 17 00:00:00 2001 From: Abeuty Date: Tue, 18 Aug 2026 13:59:37 -0600 Subject: [PATCH 01/11] feat(ds): tabs system, Panel rename, neutral status badges, squircles Design-system pass building on the grouped-buttons branch. - Tabs: new DS Tabs component with two styles (primary underline, secondary segmented) sharing one accessible API (role=tablist/tab/tabpanel, aria-selected, roving tabindex + arrow/home/end keys). /ds/tabs showcase with a size selector; per-size squircle corner radii. - SegmentedControl flattened to the Figma spec (equal chips, warm hover, filled squircle selection, shared styling with Tabs); its showcase merged into Tabs. - Rename Collapsible -> Panel everywhere: component, file, ~10 production consumers, and the DS page/route (/ds/collapsible -> /ds/panel). - Neutral library status badges: LibraryStatusBadge and the nav MegaMenuItem badge no longer color-code alpha/beta/RC/new, so status never competes with a library's brand color; documented in the DS Badges showcase. - Contributors page (all libraries): replace the bespoke layout toggle with the small-icon SegmentedControl. - Squircles: roll out the corner-squircle utility to card/surface components (Card, DS Card, ShowcaseCard, MaintainerCard, StatsSection, HomeSocialProof, PartnersGrid, ChartsCatalogGallery); cross-browser Squircle helper on blog cards and the homepage hero. - Blog index Linear-style refinements; hero mobile/CTA polish. - Sync the DS Navbar showcase to the live nav; emblem PageHeader on the DS overview; alphabetize the Components nav; prune dead ts-filter-details CSS. Co-Authored-By: Claude Opus 4.8 --- package.json | 1 + pnpm-lock.yaml | 8 + src/components/ApplicationStarter.tsx | 42 +-- src/components/BlogBrowseNav.tsx | 235 ++++++++++---- src/components/ButtonGroup.tsx | 88 +++++- src/components/Card.tsx | 2 +- src/components/LibraryLayout.tsx | 12 +- src/components/LibraryStatusBadge.tsx | 16 +- src/components/MaintainerCard.tsx | 6 +- src/components/MegaMenuItem.tsx | 4 +- src/components/Navbar.tsx | 10 +- src/components/{Collapsible.tsx => Panel.tsx} | 44 +-- src/components/PartnersGrid.tsx | 2 +- src/components/ShowcaseCard.tsx | 4 +- src/components/Squircle.tsx | 93 ++++++ src/components/builder/FeaturePicker.tsx | 32 +- .../charts/ChartsNotebookPage.client.tsx | 18 +- src/components/ds/DsKit.tsx | 26 +- src/components/ds/ds-nav.ts | 85 ++--- src/components/ds/ui/BlogPostCard.tsx | 51 ++- src/components/ds/ui/PageHeader.tsx | 2 +- src/components/ds/ui/StatsSection.tsx | 4 +- src/components/ds/ui/Tabs.tsx | 299 ++++++++++++++++++ src/components/ds/ui/index.tsx | 31 +- .../home/HomeSocialProofSection.tsx | 2 +- .../landing/ChartsCatalogGallery.tsx | 4 +- src/routeTree.gen.ts | 63 ++-- .../$libraryId/$version.docs.blog.tsx | 106 ++++--- .../$libraryId/$version.docs.contributors.tsx | 59 ++-- src/routes/blog.index.tsx | 183 +++++++---- src/routes/ds.badges.tsx | 30 +- src/routes/ds.buttons.tsx | 54 ++-- src/routes/ds.index.tsx | 11 +- src/routes/ds.navbar.tsx | 45 ++- .../{ds.collapsible.tsx => ds.panel.tsx} | 46 +-- src/routes/ds.tabs.tsx | 249 +++++++++++++++ src/routes/index.tsx | 16 +- .../intent/registry/$packageName.index.tsx | 18 +- src/routes/intent/registry/$packageName.tsx | 10 +- src/routes/partners.netlify.tsx | 20 +- src/routes/partners.railway.tsx | 20 +- src/styles/app.css | 111 +++++++ 42 files changed, 1659 insertions(+), 503 deletions(-) rename src/components/{Collapsible.tsx => Panel.tsx} (65%) create mode 100644 src/components/Squircle.tsx create mode 100644 src/components/ds/ui/Tabs.tsx rename src/routes/{ds.collapsible.tsx => ds.panel.tsx} (61%) create mode 100644 src/routes/ds.tabs.tsx diff --git a/package.json b/package.json index 1bba510f5..d4ca3de07 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "drizzle-orm": "^0.45.2", "encoding-sniffer": "^0.2.1", "esbuild-wasm": "0.27.4", + "figma-squircle": "^1.1.0", "gifenc": "^1.0.3", "gray-matter": "^4.0.3", "iconv-lite": "^0.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dfa084cad..3b45dc22a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,6 +210,9 @@ importers: esbuild-wasm: specifier: 0.27.4 version: 0.27.4 + figma-squircle: + specifier: ^1.1.0 + version: 1.1.0 gifenc: specifier: ^1.0.3 version: 1.0.3 @@ -5008,6 +5011,9 @@ packages: fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figma-squircle@1.1.0: + resolution: {integrity: sha512-3aa6bOurrsybsuYo1TMRuasBpXgVjgnmOUtZ40il6e1dJF2lhTRq89z7oy1uzKDspbH3630rLZD2UvJqXDiktA==} + figures@6.1.0: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} @@ -11542,6 +11548,8 @@ snapshots: fflate@0.8.2: {} + figma-squircle@1.1.0: {} + figures@6.1.0: dependencies: is-unicode-supported: 2.1.0 diff --git a/src/components/ApplicationStarter.tsx b/src/components/ApplicationStarter.tsx index c4ec2c9b7..b6a646607 100644 --- a/src/components/ApplicationStarter.tsx +++ b/src/components/ApplicationStarter.tsx @@ -19,10 +19,10 @@ import type { ApplicationStarterResult, } from '~/utils/application-starter' import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from '~/components/Collapsible' + Panel, + PanelContent, + PanelTrigger, +} from '~/components/Panel' import { GeneratedPromptPreviewBody, GeneratedPromptPreviewHeader, @@ -868,8 +868,8 @@ export function ApplicationStarter({ ) : null} - - + + {showOptionsSection ? (
@@ -946,8 +946,8 @@ export function ApplicationStarter({
) : null} -
-
+ + ) : ( <> @@ -1170,8 +1170,8 @@ export function ApplicationStarter({ ) : null} - - + ) : null} - - + + - - + ) : null} - - + + {showPromptPreview && hasGeneratedPrompt ? (
@@ -1571,9 +1571,9 @@ function StarterCustomizationSection({ title: string }) { return ( - +
- - - {open ? children : null} + + {open ? children : null}
-
+ ) } diff --git a/src/components/BlogBrowseNav.tsx b/src/components/BlogBrowseNav.tsx index 58fcc7c31..8822357a6 100644 --- a/src/components/BlogBrowseNav.tsx +++ b/src/components/BlogBrowseNav.tsx @@ -1,19 +1,23 @@ import * as React from 'react' -import { CaretDownIcon, RssIcon } from '@phosphor-icons/react' +import { CaretDownIcon } from '@phosphor-icons/react' import { twMerge } from 'tailwind-merge' -import { BlogAuthorFilter } from '~/components/BlogAuthorFilter' -import { BlogSearchFilter } from '~/components/BlogSearchFilter' import { Eyebrow } from '~/components/ds/ui' -import { categoryTextColor, libraryCategories } from '~/libraries/categories' +import { + categoryTextColor, + libraryCategories, + type LibraryCategory, +} from '~/libraries/categories' import { fallbackLibraryIcon, libraryIcons } from '~/libraries/icons' +import { findMaintainerByAuthorName } from '~/utils/authors' +import authorFallbackAvatar from '~/images/author-fallback.svg' import type { LibrarySlim } from '~/libraries' export type BlogTopic = { library: LibrarySlim; count: number } export type BlogYear = { year: string; count: number } +type FilterPanel = 'topics' | 'author' | 'archive' + type BlogBrowseNavProps = { - searchQuery: string - onSearchChange: (query: string) => void topics: Array totalCount: number selectedLibrary: string | undefined @@ -26,13 +30,10 @@ type BlogBrowseNavProps = { onYearToggle: (year: string | undefined) => void hasActiveFilters: boolean onClearAll: () => void - /** Scopes the search input's `id` so the desktop and mobile copies of this - * nav don't collide on duplicate ids. */ - idPrefix: string } const rowClassName = - 'flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-colors' + 'flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-sm transition-[color,background-color,transform] duration-150 ease-out motion-safe:active:scale-[0.98]' function rowStateClass(isActive: boolean) { return isActive @@ -40,25 +41,72 @@ function rowStateClass(isActive: boolean) { : 'text-text-secondary hover:bg-surface-state-hover' } +// A selected topic row is tinted to its library's category rather than the +// generic blue the other facets use. Written as literal classes so Tailwind's +// JIT emits each category utility. +const categoryActiveRow: Record = { + framework: 'bg-category-framework/10 font-semibold text-category-framework', + data: 'bg-category-data/10 font-semibold text-category-data', + ui: 'bg-category-ui/10 font-semibold text-category-ui', + performance: + 'bg-category-performance/10 font-semibold text-category-performance', + tooling: 'bg-category-tooling/10 font-semibold text-category-tooling', +} + // The count rides alongside the label in parens (ragged right edge) rather than // right-aligned in its own column. function Count({ value }: { value: number }) { return ({value}) } +function getAuthorAvatar(name: string): string { + return findMaintainerByAuthorName(name)?.avatar ?? authorFallbackAvatar +} + // A collapsible section header (Eyebrow + chevron) for progressive disclosure. -function DisclosureSummary({ label }: { label: string }) { +// When collapsed, the current selection rides alongside the label so the active +// filter stays visible without expanding; it hides on open, where the +// highlighted row already carries it. A non-default selection is tinted to read +// as active. +function DisclosureSummary({ + label, + value, + isActive, + icon, + activeClassName, +}: { + label: string + value: string + isActive: boolean + /** Rendered before the value when active (e.g. the selected library icon). */ + icon?: React.ReactNode + /** Color treatment for an active selection; defaults to blue. Topics passes + * its library-category color here. */ + activeClassName?: string +}) { return ( - - {label} - + + + {label} + + {isActive ? icon : null} + {value} + + + ) } export function BlogBrowseNav({ - searchQuery, - onSearchChange, topics, totalCount, selectedLibrary, @@ -71,38 +119,44 @@ export function BlogBrowseNav({ onYearToggle, hasActiveFilters, onClearAll, - idPrefix, }: BlogBrowseNavProps) { - // Progressive disclosure: Topics and Archive start collapsed, but open on - // mount if that facet already has an active selection. - const [topicsOpen, setTopicsOpen] = React.useState(Boolean(selectedLibrary)) - const [archiveOpen, setArchiveOpen] = React.useState(Boolean(selectedYear)) + // The three facet panels behave as an accordion: every facet starts collapsed, + // and opening one closes the others. Selections persist regardless — they live + // in the URL, not the panel's open state — and stay legible in the collapsed + // header (see DisclosureSummary). + const [openPanel, setOpenPanel] = React.useState(null) + const togglePanel = (panel: FilterPanel) => (open: boolean) => + setOpenPanel((current) => + open ? panel : current === panel ? null : current, + ) + + // Ignore URL values that don't correspond to a real author in the list. + const activeAuthor = + selectedAuthor && authors.includes(selectedAuthor) + ? selectedAuthor + : undefined + + // Current-selection labels shown in each collapsed section header. + const selectedTopic = topics.find( + ({ library }) => library.id === selectedLibrary, + ) + const topicLabel = selectedTopic + ? selectedTopic.library.name.replace('TanStack ', '') + : 'All topics' + const selectedTopicCategory = selectedTopic + ? (libraryCategories[selectedTopic.library.id] ?? 'tooling') + : undefined + const SelectedTopicIcon = selectedTopic + ? (libraryIcons[selectedTopic.library.id] ?? fallbackLibraryIcon) + : null return (