[Bench] Extend bench app to have realistic client chunk counts (#95814)

Follow-up to https://github.com/vercel/next.js/pull/95807.

Makes payload size match prod sizes closer and aligns chunk count by
adding a lot of client modules.

I originally thought to unify them into `client.js` or such but maybe
it's good to see how compilation behaves with many small files? Which is
how real projects do that.

```
=== dashboard.html (fixture)  vs  p-overview.html (real)
  total KB                            665          637
  rows (model/I/T/other)      258/111/3/1 240/125/5/27
  byte share model/I/T %          43/57/0      36/62/2
  median row size B                   906          106
  max depth                            43           53
  mean depth                          8.9         10.5
  elements per KB(model)              7.7          5.2
  pure-data byte share %               18           21
  client-ref elements %                35           32
  objects:elements ratio             1.18         1.23
  avg props per element               2.6          3.1
  median children fanout                4            4
  string bytes %                       50           54
  median string len B                  12           15
  row refs per KB                     0.4          0.4
  encoded scalars per KB              0.7          1.2
  undefined markers                   247          740

=== docs.html (fixture)  vs  nextjs-docs.html (real)
  total KB                            487          487
  rows (model/I/T/other)        72/40/7/1   55/43/2/20
  byte share model/I/T %           89/8/3       94/6/0
  median row size B                   199          340
  max depth                            27           32
  mean depth                         22.3         12.3
  elements per KB(model)              0.6          0.8
  pure-data byte share %                7            9
  client-ref elements %                29           33
  objects:elements ratio             8.67         4.92
  avg props per element               2.0          2.8
  median children fanout                3            4
  string bytes %                       47           64
  median string len B                  17           35
  row refs per KB                     0.2          0.1
  encoded scalars per KB              7.4          2.9
  undefined markers                  3586         1377

=== blog.html (fixture)  vs  vercel-blog.html (real)
  total KB                            778          797
  rows (model/I/T/other)        72/39/0/1  102/53/7/14
  byte share model/I/T %           93/7/0       87/8/5
  median row size B                   145          505
  max depth                            34           44
  mean depth                          9.6         12.2
  elements per KB(model)              0.7          1.1
  pure-data byte share %               92           85
  client-ref elements %                38           33
  objects:elements ratio            31.19        16.39
  avg props per element               1.9          2.7
  median children fanout                3            4
  string bytes %                       46           48
  median string len B                   6            9
  row refs per KB                     0.1          0.1
  encoded scalars per KB              0.4          0.6
  undefined markers                   242          426
```

Co-authored-by: Claude Fable 5 <[email protected]>
This commit is contained in:
dan
2026-07-15 15:13:35 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 7ffacec8ef
commit 0156307f55
132 changed files with 3843 additions and 248 deletions
+2
View File
@@ -0,0 +1,2 @@
app/ui/vendor/
app/g/
+336
View File
@@ -1048,3 +1048,339 @@ button {
.m-0 {
margin: 0;
}
/* --- docs chrome --- */
.announcement {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 6px 12px;
font-size: 12px;
background: color-mix(in oklab, var(--accent, #6366f1) 14%, var(--bg, #fff));
border-bottom: 1px solid var(--border, #e5e5e5);
}
.announcement a {
display: inline-flex;
align-items: center;
gap: 4px;
font-weight: 500;
}
.announcement button {
background: none;
border: 0;
cursor: pointer;
font-size: 14px;
color: inherit;
}
.mobile-nav-toggle {
display: inline-flex;
align-items: center;
padding: 6px;
background: none;
border: 1px solid var(--border, #e5e5e5);
border-radius: 6px;
cursor: pointer;
color: inherit;
}
.docs-search {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 200px;
cursor: pointer;
}
.version-picker select,
.locale-picker select {
padding: 4px 8px;
border: 1px solid var(--border, #e5e5e5);
border-radius: 6px;
background: var(--bg, #fff);
color: inherit;
font-size: 12px;
}
.page-actions {
margin-bottom: 12px;
}
.page-actions a,
.copy-page {
display: inline-flex;
align-items: center;
gap: 4px;
}
.copy-page {
background: none;
border: 1px solid var(--border, #e5e5e5);
border-radius: 6px;
padding: 4px 8px;
cursor: pointer;
color: inherit;
}
.callout-label {
display: inline-flex;
align-items: center;
gap: 5px;
}
.callout.tip {
border-left-color: #059669;
}
.prose h2 a svg,
.prose h3 a svg {
margin-left: 6px;
opacity: 0.35;
vertical-align: baseline;
}
.toc h4 {
display: flex;
align-items: center;
gap: 6px;
}
.toc a.active {
color: var(--accent, #6366f1);
font-weight: 500;
}
.scroll-top {
margin-top: 16px;
background: none;
border: 0;
cursor: pointer;
padding: 0;
}
.docs-pager {
display: flex;
justify-content: space-between;
gap: 12px;
margin: 28px 0 16px;
}
.docs-pager a {
display: flex;
align-items: center;
gap: 10px;
border: 1px solid var(--border, #e5e5e5);
border-radius: 8px;
padding: 10px 14px;
flex: 1;
}
.docs-pager a + a {
justify-content: flex-end;
text-align: right;
}
.docs-pager small {
display: block;
}
.docs-footer a {
display: inline-flex;
align-items: center;
gap: 5px;
}
/* --- blog chrome --- */
.reading-progress {
position: sticky;
top: 0;
height: 2px;
background: var(--accent, #6366f1);
z-index: 10;
}
.category-filter {
margin-bottom: 18px;
}
.category-filter button {
border: 1px solid var(--border, #e5e5e5);
border-radius: 999px;
background: none;
color: inherit;
padding: 4px 12px;
font-size: 12px;
cursor: pointer;
}
.category-filter button[aria-pressed='true'] {
background: var(--fg, #111);
color: var(--bg, #fff);
border-color: var(--fg, #111);
}
.section-head {
margin-top: 8px;
}
.carousel-controls {
display: inline-flex;
align-items: center;
gap: 8px;
}
.carousel-controls button {
border: 1px solid var(--border, #e5e5e5);
border-radius: 6px;
background: none;
color: inherit;
padding: 2px 9px;
cursor: pointer;
}
.carousel-controls button:disabled {
opacity: 0.4;
}
.trending-pill {
display: inline-flex;
align-items: center;
gap: 3px;
color: #d97706;
font-weight: 500;
}
.like-button,
.share-menu > button {
display: inline-flex;
align-items: center;
gap: 4px;
background: none;
border: 0;
cursor: pointer;
color: inherit;
}
.like-button[aria-pressed='true'] {
color: #dc2626;
}
.share-menu {
position: relative;
}
.share-items {
position: absolute;
right: 0;
top: 100%;
display: flex;
flex-direction: column;
min-width: 160px;
background: var(--bg, #fff);
border: 1px solid var(--border, #e5e5e5);
border-radius: 8px;
padding: 4px;
z-index: 5;
}
.share-items button {
display: flex;
align-items: center;
gap: 6px;
background: none;
border: 0;
padding: 6px 8px;
font-size: 12px;
cursor: pointer;
color: inherit;
border-radius: 5px;
}
.share-items button:hover {
background: var(--hover, #f4f4f5);
}
.media-row {
display: grid;
grid-template-columns: 1.4fr 1fr 1fr;
gap: 14px;
margin: 26px 0;
}
.video-embed {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-end;
gap: 4px;
min-height: 150px;
border: 0;
border-radius: 10px;
padding: 14px;
color: #fff;
cursor: pointer;
text-align: left;
}
.video-embed .video-title {
font-weight: 500;
}
.podcast-teaser {
display: flex;
align-items: center;
gap: 10px;
border: 1px solid var(--border, #e5e5e5);
border-radius: 10px;
padding: 14px;
background: none;
color: inherit;
cursor: pointer;
text-align: left;
}
.podcast-teaser small {
display: block;
}
.newsletter-box {
border: 1px solid var(--border, #e5e5e5);
border-radius: 10px;
padding: 14px;
}
.newsletter-box h3 {
display: flex;
align-items: center;
gap: 6px;
margin: 0 0 4px;
font-size: 14px;
}
.newsletter {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.newsletter input {
flex: 1;
min-width: 0;
padding: 6px 10px;
border: 1px solid var(--border, #e5e5e5);
border-radius: 6px;
background: var(--bg, #fff);
color: inherit;
font-size: 12px;
}
.newsletter button {
padding: 6px 12px;
border: 0;
border-radius: 6px;
background: var(--fg, #111);
color: var(--bg, #fff);
font-size: 12px;
cursor: pointer;
}
.load-more {
display: block;
margin: 20px auto 0;
padding: 8px 18px;
border: 1px solid var(--border, #e5e5e5);
border-radius: 8px;
background: none;
color: inherit;
cursor: pointer;
}
.blog-footer {
margin-top: 34px;
padding-top: 16px;
border-top: 1px solid var(--border, #e5e5e5);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.trace {
margin: 10px 0 0;
}
.trace-span {
border-left: 1px solid var(--border, #e5e5e5);
margin-top: 2px;
}
.trace-label {
display: inline-flex;
gap: 6px;
padding: 1px 0 1px 6px;
}
+99 -3
View File
@@ -8,9 +8,33 @@ import TagPill from '../ui/tag-pill'
import PostGrid from '../ui/post-grid'
import SavePost from '../ui/save-post'
import MegaNav from '../ui/mega-nav'
import SearchInput from '../ui/search-input'
import BlogSearch from '../ui/blog-search'
import ThemeToggle from '../ui/theme-toggle'
import { posts } from '../lib/data'
import ReadingProgress from '../ui/reading-progress'
import CategoryFilter from '../ui/category-filter'
import CarouselControls from '../ui/carousel-controls'
import LikeButton from '../ui/like-button'
import ShareMenu from '../ui/share-menu'
import NewsletterForm from '../ui/newsletter-form'
import LoadMore from '../ui/load-more'
import VideoEmbed from '../ui/video-embed'
import PodcastTeaser from '../ui/podcast-teaser'
import LocalePicker from '../ui/locale-picker'
import IconXSocial from '../ui/icons/x-social'
import IconLinkedin from '../ui/icons/linkedin'
import IconYoutube from '../ui/icons/youtube'
import IconRss from '../ui/icons/rss'
import IconHeart from '../ui/icons/heart'
import IconMail from '../ui/icons/mail'
import IconCalendar from '../ui/icons/calendar'
import IconLink from '../ui/icons/link'
import IconShare from '../ui/icons/share'
import IconPlay from '../ui/icons/play'
import IconSparkles from '../ui/icons/sparkles'
import IconTrendingUp from '../ui/icons/trending-up'
import IconHeadphones from '../ui/icons/headphones'
import IconFilter from '../ui/icons/filter'
import { posts, categories } from '../lib/data'
export const dynamic = 'force-dynamic'
@@ -51,10 +75,30 @@ function ServerPostCard({ post }) {
/>
<span className="truncate">{post.author.name}</span>
<span>·</span>
<IconCalendar size={12} />
<time dateTime={post.publishedAt}>{post.publishedAt}</time>
<span>·</span>
<span>{post.readingMinutes} min</span>
{post.stats.views > 60000 ? (
<span className="trending-pill">
<IconTrendingUp size={11} /> Trending
</span>
) : null}
<span className="header-spacer" />
<LikeButton
slug={post.slug}
count={post.stats.likes}
icon={<IconHeart size={13} />}
/>
<ShareMenu
slug={post.slug}
trigger={<IconShare size={13} />}
items={[
{ label: 'Share on X', icon: <IconXSocial size={12} /> },
{ label: 'Share on LinkedIn', icon: <IconLinkedin size={12} /> },
{ label: 'Copy link', icon: <IconLink size={12} /> },
]}
/>
<SavePost slug={post.slug} />
</div>
</div>
@@ -79,6 +123,7 @@ export default function BlogPage() {
const [featured, ...rest] = posts
return (
<>
<ReadingProgress />
<header className="app-header">
<nav className="crumbs" aria-label="Breadcrumb">
<strong>Acme</strong>
@@ -87,10 +132,14 @@ export default function BlogPage() {
</nav>
<MegaNav />
<div className="header-spacer" />
<SearchInput placeholder="Search posts…" />
<BlogSearch placeholder="Search posts…" />
<ThemeToggle />
</header>
<main className="blog-main">
<CategoryFilter
categories={Object.values(categories)}
icon={<IconFilter size={13} />}
/>
<section className="blog-hero">
<Cover cover={featured.cover} />
<div>
@@ -116,13 +165,43 @@ export default function BlogPage() {
</div>
</div>
</section>
<div className="section-head flex items-center gap-2">
<h2 className="more-posts">Latest posts</h2>
<span className="header-spacer" />
<CarouselControls pages={4} label="Browse latest posts" />
</div>
<section className="post-grid" aria-label="Latest posts">
{rest.slice(0, 12).map((post) => (
<ServerPostCard key={post.id} post={post} />
))}
</section>
<section className="media-row" aria-label="Watch and listen">
<VideoEmbed
title="Keynote: the state of the framework"
duration="42:18"
hue={262}
playIcon={<IconPlay size={22} />}
/>
<PodcastTeaser
title="Shipping the App Router migration"
episode={48}
icon={<IconHeadphones size={18} />}
/>
<div className="newsletter-box">
<h3>
<IconSparkles size={14} /> What's new, monthly
</h3>
<p className="text-sm text-muted">
One email a month with the best posts and release notes.
</p>
<NewsletterForm icon={<IconMail size={14} />} />
</div>
</section>
<h2 className="more-posts">All posts</h2>
<PostGrid posts={rest} />
<LoadMore total={rest.length} pageSize={24} />
{posts.slice(0, 6).map((post) => (
<script
key={post.id}
@@ -130,6 +209,23 @@ export default function BlogPage() {
dangerouslySetInnerHTML={{ __html: jsonLd(post) }}
/>
))}
<footer className="blog-footer flex items-center gap-2 text-xs text-muted">
<span>© Acme Inc.</span>
<a href="#" aria-label="Acme on X">
<IconXSocial size={14} />
</a>
<a href="#" aria-label="Acme on YouTube">
<IconYoutube size={14} />
</a>
<a href="#" aria-label="Acme on LinkedIn">
<IconLinkedin size={14} />
</a>
<a href="#" aria-label="RSS feed">
<IconRss size={14} />
</a>
<span className="header-spacer" />
<LocalePicker />
</footer>
</main>
</>
)
+641 -111
View File
@@ -43,6 +43,69 @@ import IconChartBar from '../ui/icons/chart-bar'
import IconRefresh from '../ui/icons/refresh'
import IconTrash from '../ui/icons/trash'
import IconMoreHorizontal from '../ui/icons/more-horizontal'
import IconArchive from '../ui/icons/archive'
import IconBarChart from '../ui/icons/bar-chart'
import IconBellOff from '../ui/icons/bell-off'
import IconBox from '../ui/icons/box'
import IconCalendarDays from '../ui/icons/calendar-days'
import IconCheck from '../ui/icons/check'
import IconChevronLeft from '../ui/icons/chevron-left'
import IconChevronUp from '../ui/icons/chevron-up'
import IconChevronsUpDown from '../ui/icons/chevrons-up-down'
import IconCircleDot from '../ui/icons/circle-dot'
import IconCloud from '../ui/icons/cloud'
import IconCode from '../ui/icons/code'
import IconCommand from '../ui/icons/command'
import IconCopy from '../ui/icons/copy'
import IconCreditCard from '../ui/icons/credit-card'
import IconDownload from '../ui/icons/download'
import IconEye from '../ui/icons/eye'
import IconFileCode from '../ui/icons/file-code'
import IconFlame from '../ui/icons/flame'
import IconGauge from '../ui/icons/gauge'
import IconGitCommit from '../ui/icons/git-commit'
import IconGitMerge from '../ui/icons/git-merge'
import IconGitPullRequest from '../ui/icons/git-pull-request'
import IconHardDrive from '../ui/icons/hard-drive'
import IconInbox from '../ui/icons/inbox'
import IconKey from '../ui/icons/key'
import IconLifeBuoy from '../ui/icons/life-buoy'
import IconListFilter from '../ui/icons/list-filter'
import IconLoader from '../ui/icons/loader'
import IconMapPin from '../ui/icons/map-pin'
import IconMonitor from '../ui/icons/monitor'
import IconPackage from '../ui/icons/package'
import IconPause from '../ui/icons/pause'
import IconPlug from '../ui/icons/plug'
import IconPlus from '../ui/icons/plus'
import IconServer from '../ui/icons/server'
import IconSliders from '../ui/icons/sliders'
import IconStar from '../ui/icons/star'
import IconTag from '../ui/icons/tag'
import IconTimer from '../ui/icons/timer'
import IconUpload from '../ui/icons/upload'
import IconWifi from '../ui/icons/wifi'
import IconWrench from '../ui/icons/wrench'
import DateRangePicker from '../ui/date-range-picker'
import SortHeader from '../ui/sort-header'
import ColumnPicker from '../ui/column-picker'
import FilterChip from '../ui/filter-chip'
import SavedViews from '../ui/saved-views'
import RegionSelect from '../ui/region-select'
import RefreshButton from '../ui/refresh-button'
import PresenceAvatars from '../ui/presence-avatars'
import UsageAlert from '../ui/usage-alert'
import BillingMeter from '../ui/billing-meter'
import InviteMember from '../ui/invite-member'
import ApiToken from '../ui/api-token'
import LogViewer from '../ui/log-viewer'
import DeployActions from '../ui/deploy-actions'
import ChartLegend from '../ui/chart-legend'
import TableDensity from '../ui/table-density'
import IncidentBanner from '../ui/incident-banner'
import FeedbackButton from '../ui/feedback-button'
import HelpMenu from '../ui/help-menu'
import KeyboardHint from '../ui/keyboard-hint'
import { Suspense } from 'react'
import {
viewer,
@@ -55,6 +118,8 @@ import {
alerts,
members,
screenshots,
people,
logLines,
NOW,
} from '../lib/data'
@@ -94,26 +159,64 @@ async function DeferredDeployments() {
await sleep(8)
return (
<>
<table className="deploy-table">
<thead>
<tr>
<th>Deployment</th>
<th>Status</th>
<th>Commit</th>
<th>By</th>
<th>Took</th>
<th>Region</th>
<th>Created</th>
<th aria-label="Actions" />
</tr>
</thead>
<tbody>
{deployments.map((d) => (
<DeploymentRow key={d.id} d={d} />
))}
</tbody>
</table>
<div
className="panel-body relative flex min-h-0 flex-col"
data-scroll-root
>
<div className="scroll-area relative flex-1 overflow-hidden">
<div
className="scroll-viewport h-full w-full overflow-auto overscroll-contain"
data-orientation="vertical"
>
<div className="table-frame relative min-w-max" data-table-root>
<div className="table-toolbar-context" data-density="comfortable">
<div className="virtualizer relative" data-virtualized="false">
<div className="size-observer relative min-w-0">
<table className="deploy-table">
<thead>
<tr>
<th>
<SortHeader
label="Deployment"
icon={<IconChevronsUpDown size={11} />}
/>
</th>
<th>Status</th>
<th>Commit</th>
<th>By</th>
<th>
<SortHeader
label="Took"
icon={<IconChevronsUpDown size={11} />}
/>
</th>
<th>Region</th>
<th>
<SortHeader
label="Created"
icon={<IconChevronsUpDown size={11} />}
/>
</th>
<th aria-label="Actions" />
</tr>
</thead>
<tbody>
{deployments.map((d) => (
<DeploymentRow key={d.id} d={d} />
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<Pagination pages={6} />
<a href="#" className="text-xs text-muted flex items-center gap-2">
<IconChevronUp size={12} /> Back to top
</a>
</>
)
}
@@ -136,49 +239,65 @@ async function DeferredUsage() {
async function DeferredDomains() {
await sleep(10)
return (
<table className="deploy-table domain-table">
<thead>
<tr>
<th>Domain</th>
<th>Project</th>
<th>SSL</th>
<th>Registrar</th>
<th>Expires</th>
<th aria-label="Enabled" />
</tr>
</thead>
<tbody>
{domains.map((dom) => (
<tr key={dom.id} data-testid={'domain-row-' + dom.id}>
<td>
<span className="flex items-center gap-2 truncate mono text-sm">
{dom.name}
{dom.verified ? null : (
<Tooltip text="Verification pending">
<IconAlertCircle size={13} />
</Tooltip>
)}
</span>
</td>
<td className="mono">{dom.project}</td>
<td>
<StatusBadge status={dom.ssl === 'active' ? 'ready' : 'queued'} />
</td>
<td>{dom.registrar}</td>
<td>
{dom.expiresAt ? (
<RelativeTime date={dom.expiresAt} now={NOW} />
) : (
<span className="text-xs text-muted">—</span>
)}
</td>
<td>
<Toggle defaultOn label={'Enable ' + dom.name} />
</td>
</tr>
))}
</tbody>
</table>
<div className="panel-body relative flex min-h-0 flex-col" data-scroll-root>
<div className="scroll-area relative flex-1 overflow-hidden">
<div className="scroll-viewport h-full w-full overflow-auto overscroll-contain">
<table className="deploy-table domain-table">
<thead>
<tr>
<th>Domain</th>
<th>Project</th>
<th>SSL</th>
<th>Registrar</th>
<th>Expires</th>
<th aria-label="Enabled" />
</tr>
</thead>
<tbody>
{domains.map((dom) => (
<tr key={dom.id} data-testid={'domain-row-' + dom.id}>
<td>
<span className="flex items-center gap-2 truncate mono text-sm">
{dom.name}
{dom.verified ? null : (
<Tooltip text="Verification pending">
<IconAlertCircle size={13} />
</Tooltip>
)}
</span>
</td>
<td className={CELL_CLASS + ' mono'} data-col="project">
<span className={CELL_INNER_CLASS} data-slot="cell">
{dom.project}
</span>
</td>
<td>
<StatusBadge
status={dom.ssl === 'active' ? 'ready' : 'queued'}
/>
</td>
<td className={CELL_CLASS} data-col="registrar">
<span className={CELL_INNER_CLASS} data-slot="cell">
{dom.registrar}
</span>
</td>
<td>
{dom.expiresAt ? (
<RelativeTime date={dom.expiresAt} now={NOW} />
) : (
<span className="text-xs text-muted">—</span>
)}
</td>
<td>
<Toggle defaultOn label={'Enable ' + dom.name} />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)
}
@@ -187,11 +306,22 @@ async function DeferredActivity() {
return (
<ul className="activity">
{activity.map((a) => (
<li key={a.id}>
<li
key={a.id}
data-event={a.id}
data-actor={a.actor.username}
className={LIST_ITEM_CLASS}
>
<Avatar name={a.actor.name} hue={a.actor.avatarHue} size={20} />
<span>
<span className="who">{a.actor.name}</span> {a.verb}{' '}
<span className="mono">{a.target}</span> {a.suffix}
<span className="min-w-0 flex-1 truncate">
<span className="who" title={'@' + a.actor.username}>
{a.actor.name}
</span>{' '}
{a.verb}{' '}
<span className="mono" title={a.target}>
{a.target}
</span>{' '}
{a.suffix}
</span>
<RelativeTime date={a.at} now={NOW} />
</li>
@@ -202,6 +332,19 @@ async function DeferredActivity() {
export const dynamic = 'force-dynamic'
// Repeated utility-class strings, the way compiled Tailwind markup ships
// long className runs on every row and cell.
const ROW_CLASS =
'group relative flex w-full items-center gap-3 border-b border-neutral-200 px-3 py-2 text-sm leading-5 transition-colors hover:bg-neutral-50 focus-visible:outline-none focus-visible:ring-2 data-[state=error]:bg-red-50 dark:border-neutral-800 dark:hover:bg-neutral-900'
const CELL_CLASS =
'relative flex min-w-0 select-none items-center gap-2 truncate whitespace-nowrap px-2 py-1.5 text-sm leading-5 tabular-nums text-neutral-600 transition-colors group-hover:text-neutral-900 group-data-[state=error]:text-red-700 dark:text-neutral-300 dark:group-hover:text-neutral-50'
const CELL_INNER_CLASS =
'cell-inner relative inline-flex min-w-0 max-w-full items-center gap-1.5 truncate align-middle'
const LIST_ITEM_CLASS =
'relative flex items-start gap-2 border-b border-neutral-100 py-1.5 text-sm leading-5 last:border-0 hover:bg-neutral-50 dark:border-neutral-900 dark:hover:bg-neutral-900'
const CARD_CLASS =
'group relative flex flex-col gap-2 rounded-lg border border-neutral-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md dark:border-neutral-800 dark:bg-neutral-950'
const NAV_SECTIONS = [
[
'Team',
@@ -214,12 +357,38 @@ const NAV_SECTIONS = [
['Edge Config', IconZap, false],
],
],
[
'Observability',
[
['Monitoring', IconMonitor, false],
['Tracing', IconGauge, false],
['Errors', IconFlame, false],
['Cron jobs', IconTimer, false],
['Queues', IconInbox, false],
['Source maps', IconFileCode, false],
],
],
[
'Infrastructure',
[
['Compute', IconServer, false],
['Blob storage', IconHardDrive, false],
['Cache', IconBox, false],
['CDN', IconCloud, false],
['Artifacts', IconPackage, false],
['Secrets', IconKey, false],
],
],
[
'Settings',
[
['Domains', IconGlobe, false],
['Environment variables', IconLock, false],
['Integrations', IconLayers, false],
['Connected apps', IconPlug, false],
['Notifications', IconBellOff, false],
['Tags & labels', IconTag, false],
['Maintenance', IconWrench, false],
['Usage & billing', IconCpu, false],
['Members', IconUsers, false],
['Security', IconShield, false],
@@ -249,7 +418,11 @@ function Sidenav() {
}
data-testid={'nav-' + label.toLowerCase().replace(/ /g, '-')}
>
<Icon size={14} /> {label}
<Icon
size={14}
className="shrink-0 text-neutral-500 group-hover:text-neutral-800"
/>{' '}
{label}
</a>
))}
</div>
@@ -260,8 +433,13 @@ function Sidenav() {
function ProjectCard({ p }) {
return (
<article className="project-card">
<header className="flex items-center gap-2 truncate">
<article
className={'project-card ' + CARD_CLASS}
data-project={p.id}
data-status={p.status}
aria-label={p.name}
>
<header className="flex items-center gap-2 truncate" title={p.name}>
<IconFolder size={16} />
<h3 className="truncate text-sm font-medium">{p.name}</h3>
<span className="header-spacer" />
@@ -274,9 +452,20 @@ function ProjectCard({ p }) {
/>
</header>
<p className="flex items-center gap-2 truncate text-sm text-muted">
<IconGlobe size={13} /> {p.domain}
<IconGlobe size={13} />{' '}
<a
href={'https://' + p.domain}
className="truncate hover:underline"
title={'https://' + p.domain}
rel="noopener"
>
{p.domain}
</a>
</p>
<p className="flex items-center gap-2 truncate text-sm text-muted">
<p
className="flex items-center gap-2 truncate text-sm text-muted"
title={p.lastCommit}
>
<IconGitBranch size={13} /> {p.lastCommit}
</p>
<footer className="flex items-center gap-2 truncate text-xs text-muted">
@@ -291,55 +480,219 @@ function ProjectCard({ p }) {
function DeploymentRow({ d }) {
return (
<tr data-testid={'deployment-row-' + d.id} data-state={d.status}>
<td>
<tr
data-testid={'deployment-row-' + d.id}
data-state={d.status}
data-url={d.url}
className={ROW_CLASS}
aria-labelledby={'commit-' + d.id}
>
<td
className={CELL_CLASS}
data-col="deployment"
data-testid="cell-deployment"
>
<div
className="commit-cell"
data-testid="deployment-commit"
aria-describedby={'status-' + d.id}
>
<span className="commit-msg">{d.commit}</span>
<span className="branch mono">
{d.project} · {d.branch}
<span className="flex min-w-0 items-center gap-2" data-slot="cell">
<span className={CELL_INNER_CLASS} data-slot="layout">
<a
id={'commit-' + d.id}
href={d.url}
title={d.commit}
className="commit-msg truncate"
rel="noopener"
>
<span className="truncate">{d.commit}</span>
</a>
</span>
</span>
<span className="branch mono" title={d.project + '/' + d.branch}>
<span className="truncate">
{d.project} · {d.branch}
</span>
</span>
</div>
</td>
<td>
<span className="flex items-center gap-2 truncate text-sm">
<StatusBadge status={d.status} note={d.statusNote} />
<td
className={CELL_CLASS}
data-col="status"
data-testid="cell-status"
id={'status-' + d.id}
>
<span className={CELL_INNER_CLASS} data-slot="cell">
<span
className="cell-layout flex items-center gap-2 truncate text-sm"
data-slot="layout"
>
<StatusBadge status={d.status} note={d.statusNote} />
{d.status === 'building' ? <IconLoader size={12} /> : null}
{d.status === 'queued' ? <IconPause size={12} /> : null}
</span>
</span>
</td>
<td className={CELL_CLASS} data-col="commit" data-testid="cell-commit">
<span className={CELL_INNER_CLASS} data-slot="cell" title={d.sha}>
<span className="row-actions mono" data-slot="layout">
<IconGitCommit size={12} />
<a href={d.inspectorUrl} className="mono" data-sha={d.sha}>
{d.sha.slice(0, 7)}
</a>
<CopyButton
value={d.sha}
label="Copy SHA"
variant={d.copyVariant}
/>
</span>
</span>
</td>
<td className={CELL_CLASS} data-col="author" data-testid="cell-author">
<span className={CELL_INNER_CLASS} data-slot="cell">
<Tooltip text={d.author.name}>
<Avatar
name={d.author.name}
hue={d.author.avatarHue}
size={22}
title={d.author.title}
/>
</Tooltip>
</span>
</td>
<td
className={CELL_CLASS}
data-col="duration"
data-testid="cell-duration"
>
<span className={CELL_INNER_CLASS} data-slot="cell">
<span
className="cell-layout flex min-w-0 items-center gap-1"
data-slot="layout"
data-align="start"
>
<span
className="cell-content truncate"
data-slot="content"
data-overflow="ellipsis"
title="Build duration"
>
<span
className="text-neutral-700 dark:text-neutral-200"
data-slot="value"
>
{d.durationSeconds}s
</span>
</span>
</span>
</span>
</td>
<td
className={CELL_CLASS + ' mono'}
data-col="region"
data-testid="cell-region"
>
<span className={CELL_INNER_CLASS} data-slot="cell">
<span
className="cell-layout flex min-w-0 items-center gap-1"
data-slot="layout"
data-align="start"
>
<span
className="cell-content truncate"
data-slot="content"
data-overflow="ellipsis"
title={d.region}
>
<span
className="text-neutral-700 dark:text-neutral-200"
data-slot="value"
>
{d.region}
</span>
</span>
</span>
</span>
</td>
<td className={CELL_CLASS} data-col="created" data-testid="cell-created">
<span className={CELL_INNER_CLASS} data-slot="cell">
<span
className="cell-layout flex min-w-0 items-center gap-1"
data-slot="layout"
data-align="start"
>
<span
className="cell-content truncate"
data-slot="content"
data-overflow="ellipsis"
>
<span
className="text-neutral-700 dark:text-neutral-200"
data-slot="value"
>
<RelativeTime date={d.createdAt} now={NOW} />
</span>
</span>
</span>
</span>
</td>
<td>
<span className="row-actions mono">
{d.sha.slice(0, 7)}
<CopyButton value={d.sha} label="Copy SHA" variant={d.copyVariant} />
</span>
</td>
<td>
<Tooltip text={d.author.name}>
<Avatar
name={d.author.name}
hue={d.author.avatarHue}
size={22}
title={d.author.title}
/>
</Tooltip>
</td>
<td>{d.durationSeconds}s</td>
<td className="mono">{d.region}</td>
<td>
<RelativeTime date={d.createdAt} now={NOW} />
</td>
<td>
<DeployActions
promoteIcon={<IconUpload size={12} />}
rollbackIcon={<IconChevronLeft size={12} />}
/>
<Dropdown
label="…"
items={[
{ label: 'Visit', icon: <IconExternalLink size={13} /> },
{ label: 'Inspect', icon: <IconTerminal size={13} /> },
{ label: 'Promote to production', icon: <IconRocket size={13} /> },
{ label: 'Redeploy', icon: <IconRefresh size={13} /> },
{ label: 'Copy URL', icon: <IconGlobe size={13} /> },
{ label: 'Delete', icon: <IconTrash size={13} /> },
{
label: 'Visit',
icon: (
<IconExternalLink size={13} className="menu-icon shrink-0" />
),
},
{
label: 'Inspect',
icon: <IconTerminal size={13} className="menu-icon shrink-0" />,
},
{
label: 'Promote to production',
icon: <IconRocket size={13} className="menu-icon shrink-0" />,
},
{
label: 'Redeploy',
icon: <IconRefresh size={13} className="menu-icon shrink-0" />,
},
{
label: 'Copy URL',
icon: <IconGlobe size={13} className="menu-icon shrink-0" />,
},
{
label: 'Copy SHA',
icon: <IconCopy size={13} className="menu-icon shrink-0" />,
},
{
label: 'Star',
icon: <IconStar size={13} className="menu-icon shrink-0" />,
},
{
label: 'Compare to previous',
icon: <IconGitMerge size={13} className="menu-icon shrink-0" />,
},
{
label: 'Open pull request',
icon: (
<IconGitPullRequest size={13} className="menu-icon shrink-0" />
),
},
{
label: 'Archive',
icon: <IconArchive size={13} className="menu-icon shrink-0" />,
},
{
label: 'Delete',
icon: <IconTrash size={13} className="menu-icon shrink-0" />,
},
]}
/>
</td>
@@ -347,6 +700,56 @@ function DeploymentRow({ d }) {
)
}
// A span tree from a traced request, rendered as nested markup the way
// tracing UIs draw waterfalls: each child span nests inside its parent.
const TRACE = [
['request GET /acme/overview', 184],
['middleware', 12],
['route resolution', 4],
['render app/acme/overview/page', 148],
['fetch /api/projects', 38],
['fetch /api/deployments', 61],
['dedupe cache lookup', 2],
['serialize flight payload', 19],
['stream shell', 8],
['flush deferred panels', 42],
['fetch /api/usage', 31],
['aggregate series', 6],
['stream panel html', 5],
['fetch /api/activity', 22],
['normalize events', 4],
['fetch /api/members', 18],
['authorize scopes', 3],
['render member list', 6],
['stream panel html', 4],
['finalize', 3],
]
function TraceWaterfall() {
return (
<figure className="trace" aria-label="Request trace">
<figcaption className="text-xs text-muted">
Trace · GET /acme/overview · 184ms
</figcaption>
{TRACE.reduceRight(
(child, [label, ms], i) => (
<div
className="trace-span"
data-depth={i}
style={{ paddingLeft: 8 }}
title={label + ' · ' + ms + 'ms'}
>
<span className="trace-label mono text-xs">
{label} <em className="text-muted">{ms}ms</em>
</span>
{child}
</div>
),
null
)}
</figure>
)
}
export default function DashboardPage() {
return (
<>
@@ -359,6 +762,16 @@ export default function DashboardPage() {
</nav>
<div className="header-spacer" />
<SearchInput placeholder="Search deployments…" />
<PresenceAvatars people={people.slice(0, 5)} />
<HelpMenu
icon={<IconLifeBuoy size={15} />}
items={[
'Documentation',
'Contact support',
'Changelog',
'Keyboard shortcuts',
]}
/>
<NotificationBell count={3} />
<ThemeToggle />
<Dropdown
@@ -379,6 +792,10 @@ export default function DashboardPage() {
<div className="shell">
<Sidenav />
<main className="main">
<IncidentBanner
message="Elevated build queue times in sfo1."
icon={<IconCircleDot size={13} />}
/>
{alerts.map((a) => (
<AlertBanner
key={a.id}
@@ -404,15 +821,31 @@ export default function DashboardPage() {
</Suspense>
</section>
<UsageAlert
message="Bandwidth is projected to reach 96% of the included allowance this period."
icon={<IconAlertCircle size={13} />}
/>
<UsageAlert
message="Bandwidth is projected to reach 96% of the included allowance this period."
icon={<IconAlertCircle size={13} />}
/>
<section
className="metric-grid"
aria-label="Usage metrics"
style={{ marginTop: 20 }}
>
{metrics.map((m) => (
<article key={m.id} className="metric-card">
<h3>{m.label}</h3>
<p className="metric-value">{m.value}</p>
<article
key={m.id}
className="metric-card"
data-metric={m.id}
data-trend={m.trend}
aria-label={m.label}
>
<h3 className="text-xs font-medium text-muted">{m.label}</h3>
<p className="metric-value tabular-nums" title={m.label}>
{m.value}
</p>
<p className={'metric-delta trend-' + m.trend}>
{m.trend === 'up' ? <IconArrowUpRight size={11} /> : null}
{m.delta} vs last period
@@ -446,6 +879,42 @@ export default function DashboardPage() {
items={['All branches', 'main', 'staging', 'Previews only']}
align="end"
/>
<ColumnPicker
columns={[
'Status',
'Commit',
'By',
'Took',
'Region',
'Created',
]}
icon={<IconSliders size={13} />}
/>
<TableDensity icon={<IconListFilter size={13} />} />
<RefreshButton
icon={<IconRefresh size={13} />}
label="Refresh deployments"
/>
</div>
<div
className="flex items-center gap-2"
style={{ marginBottom: 8 }}
>
<SavedViews
views={['All', 'Production', 'Previews', 'Failed']}
/>
<FilterChip
label="branch: main"
icon={<IconGitBranch size={11} />}
/>
<FilterChip
label="status: ready"
icon={<IconCheck size={11} />}
/>
<FilterChip
label="region: iad1"
icon={<IconMapPin size={11} />}
/>
</div>
<Suspense fallback={<PanelSkeleton rows={12} />}>
<DeferredDeployments />
@@ -455,8 +924,19 @@ export default function DashboardPage() {
<div>
<section className="panel" aria-label="Usage">
<div className="panel-head">
<h2>Requests · 90 days</h2>
<h2>
<IconBarChart size={14} /> Requests · 90 days
</h2>
<div className="header-spacer" />
<DateRangePicker />
</div>
<ChartLegend
series={[
{ label: 'Requests', color: 'hsl(220 70% 55%)' },
{ label: 'Cached', color: 'hsl(160 60% 45%)' },
{ label: 'Errors', color: 'hsl(0 70% 55%)' },
]}
/>
<Suspense fallback={<PanelSkeleton rows={5} />}>
<DeferredUsage />
</Suspense>
@@ -469,6 +949,23 @@ export default function DashboardPage() {
<DeferredActivity />
</Suspense>
</section>
<section className="panel" aria-label="Runtime logs">
<div className="panel-head">
<h2>
<IconCode size={14} /> Runtime logs
</h2>
<div className="header-spacer" />
<a
href="#"
className="text-xs text-muted"
aria-label="Download logs"
>
<IconDownload size={13} />
</a>
</div>
<LogViewer lines={logLines} />
<TraceWaterfall />
</section>
</div>
</div>
@@ -496,6 +993,10 @@ export default function DashboardPage() {
<div className="panel-head">
<h2>Domains</h2>
<div className="header-spacer" />
<RegionSelect
regions={['iad1', 'sfo1', 'fra1', 'hnd1', 'syd1']}
icon={<IconMapPin size={13} />}
/>
<Dropdown
label="Add domain"
items={['Add existing domain', 'Buy a domain', 'Transfer in']}
@@ -509,10 +1010,24 @@ export default function DashboardPage() {
<section className="panel" aria-label="Team members">
<div className="panel-head">
<h2>Members</h2>
<div className="header-spacer" />
<InviteMember icon={<IconPlus size={13} />} />
</div>
<p className="flex items-center gap-2 text-xs text-muted">
<IconKey size={13} /> Team token:{' '}
<ApiToken
token="acme_k3y8f02mrq11xz74"
icon={<IconEye size={12} />}
/>
</p>
<ul className="member-list">
{members.map((m, i) => (
<li key={m.person.username + i}>
<li
key={m.person.username + i}
data-member={m.person.username}
data-role={m.role}
className={LIST_ITEM_CLASS}
>
<Avatar
name={m.person.name}
hue={m.person.avatarHue}
@@ -537,6 +1052,21 @@ export default function DashboardPage() {
</div>
</main>
</div>
<footer className="status-bar flex items-center gap-2 text-xs text-muted">
<IconWifi size={12} /> All systems normal
<span className="sep">·</span>
<IconServer size={12} /> iad1
<span className="sep">·</span>
<IconCalendarDays size={12} /> Billing period ends in 12 days
<BillingMeter
spent={184}
budget={400}
icon={<IconCreditCard size={12} />}
/>
<span className="header-spacer" />
<FeedbackButton />
<KeyboardHint icon={<IconCommand size={12} />} />
</footer>
<CommandMenu
commands={[
'Go to deployments',
+301 -121
View File
@@ -5,11 +5,36 @@
// https://github.com/reactjs/react.dev).
import '../bench.css'
import ThemeToggle from '../ui/theme-toggle'
import SearchInput from '../ui/search-input'
import DocsSearch from '../ui/docs-search'
import VersionPicker from '../ui/version-picker'
import MobileNav from '../ui/mobile-nav'
import TocScrollspy from '../ui/toc-scrollspy'
import CopyPageButton from '../ui/copy-page-button'
import DocsPager from '../ui/docs-pager'
import AnnouncementBanner from '../ui/announcement-banner'
import ScrollToTop from '../ui/scroll-to-top'
import Feedback from '../ui/feedback'
import CodeBlock, { PackageManagerBlock } from '../ui/server-code-block'
import Collapsible from '../ui/collapsible'
import DocsSidebar from '../ui/docs-sidebar'
import IconBook from '../ui/icons/book'
import IconHash from '../ui/icons/hash'
import IconMenu from '../ui/icons/menu'
import IconClose from '../ui/icons/close'
import IconFileText from '../ui/icons/file-text'
import IconList from '../ui/icons/list'
import IconMessageSquare from '../ui/icons/message-square'
import IconEdit from '../ui/icons/edit'
import IconClipboard from '../ui/icons/clipboard'
import IconChevronRight from '../ui/icons/chevron-right'
import IconArrowLeft from '../ui/icons/arrow-left'
import IconArrowRight from '../ui/icons/arrow-right'
import IconGithub from '../ui/icons/github'
import IconDiscord from '../ui/icons/discord'
import IconLightbulb from '../ui/icons/lightbulb'
import IconInfo from '../ui/icons/info'
import IconAlertTriangle from '../ui/icons/alert-triangle'
import IconLanguages from '../ui/icons/languages'
import { tokenize } from '../lib/tokenize'
import { docsTree } from '../lib/data'
@@ -70,41 +95,91 @@ export default function Counter() {
function H2({ id, children }) {
return (
<h2 id={id}>
<a href={'#' + id}>{children}</a>
<a href={'#' + id}>
{children}
<IconHash size={13} />
</a>
</h2>
)
}
function H3({ id, children }) {
return (
<h3 id={id}>
<a href={'#' + id}>{children}</a>
<a href={'#' + id}>
{children}
<IconHash size={12} />
</a>
</h3>
)
}
const TOC_ITEMS = [
{ id: 'reference', label: 'Reference', level: 2 },
{ id: 'usestate', label: 'useState(initialState)', level: 3 },
{ id: 'parameters', label: 'Parameters', level: 3 },
{ id: 'returns', label: 'Returns', level: 3 },
{ id: 'usage', label: 'Usage', level: 2 },
{ id: 'adding-state', label: 'Adding state', level: 3 },
{ id: 'updater-functions', label: 'Updater functions', level: 3 },
{ id: 'objects-and-arrays', label: 'Objects and arrays', level: 3 },
{ id: 'avoiding-recreating-state', label: 'Initializer functions', level: 3 },
{ id: 'resetting-state-with-a-key', label: 'Resetting with a key', level: 3 },
{ id: 'troubleshooting', label: 'Troubleshooting', level: 2 },
]
export default function DocsPage() {
return (
<>
<AnnouncementBanner>
<span>
React Conf 2026 registration is open{' '}
<a href="#">
Get tickets <IconArrowRight size={12} />
</a>
</span>
</AnnouncementBanner>
<header className="app-header">
<MobileNav
label="Toggle navigation"
openIcon={<IconMenu size={16} />}
closeIcon={<IconClose size={16} />}
/>
<nav className="crumbs" aria-label="Breadcrumb">
<IconBook size={14} />
<strong>React</strong>
<span className="sep">/</span>
<IconChevronRight size={12} />
<span>Reference</span>
<span className="sep">/</span>
<IconChevronRight size={12} />
<span className="current">useState</span>
</nav>
<div className="header-spacer" />
<SearchInput placeholder="Search docs…" />
<DocsSearch placeholder="Search docs…" />
<VersionPicker versions={['stable', 'v15', 'v14']} current="stable" />
<ThemeToggle />
</header>
<div className="docs-shell">
<DocsSidebar tree={docsTree} version="stable" />
<article className="prose">
<div className="page-actions flex items-center gap-2">
<CopyPageButton icon={<IconClipboard size={13} />} />
<a href="#" className="text-xs text-muted">
<IconEdit size={13} /> Edit this page
</a>
<a href="#" className="text-xs text-muted">
<IconFileText size={13} /> View as Markdown
</a>
<a href="#" className="text-xs text-muted">
<IconLanguages size={13} /> Translations
</a>
</div>
<h1>useState</h1>
<p className="lead">
<code>useState</code> is a React Hook that lets you add a state
variable to your component.
<code className="inline-code" dir="ltr">
useState
</code>{' '}
is a React Hook that lets you add a state variable to your
component.
</p>
<CodeBlock
lang="js"
@@ -122,11 +197,17 @@ export default function DocsPage() {
<H2 id="reference">Reference</H2>
<H3 id="usestate">useState(initialState)</H3>
<p>
Call <code>useState</code> at the top level of your component to
declare a state variable. The convention is to name state variables
like <code>[something, setSomething]</code> using array
destructuring.
<p className="prose-p">
Call{' '}
<code className="inline-code" dir="ltr">
useState
</code>{' '}
at the top level of your component to declare a state variable. The
convention is to name state variables like{' '}
<code className="inline-code" dir="ltr">
[something, setSomething]
</code>{' '}
using array destructuring.
</p>
<CodeBlock
lang="js"
@@ -145,12 +226,17 @@ export default function DocsPage() {
<tbody>
<tr>
<td>
<code>initialState</code>
<code className="inline-code" dir="ltr">
initialState
</code>
</td>
<td>
The value you want the state to be initially. It can be a
value of any type, but there is a special behavior for
functions: if you pass a function as <code>initialState</code>
functions: if you pass a function as{' '}
<code className="inline-code" dir="ltr">
initialState
</code>
, it will be treated as an initializer function. It should be
pure, should take no arguments, and should return a value of
any type.
@@ -160,37 +246,60 @@ export default function DocsPage() {
</table>
<H3 id="returns">Returns</H3>
<p>
<code>useState</code> returns an array with exactly two values:
<p className="prose-p">
<code className="inline-code" dir="ltr">
useState
</code>{' '}
returns an array with exactly two values:
</p>
<ol>
<li>
<ol className="prose-list prose-list-ordered">
<li className="prose-li">
The current state. During the first render, it will match the{' '}
<code>initialState</code> you have passed.
<code className="inline-code" dir="ltr">
initialState
</code>{' '}
you have passed.
</li>
<li>
The <code>set</code> function that lets you update the state to a
different value and trigger a re-render.
<li className="prose-li">
The{' '}
<code className="inline-code" dir="ltr">
set
</code>{' '}
function that lets you update the state to a different value and
trigger a re-render.
</li>
</ol>
<div className="callout pitfall">
<span className="callout-label">Pitfall</span>
<p>
Calling the <code>set</code> function does <em>not</em> change the
current state in the already executing code. It only affects what{' '}
<code>useState</code> will return starting from the next render.
<span className="callout-label">
<IconAlertTriangle size={13} /> Pitfall
</span>
<p className="prose-p">
Calling the{' '}
<code className="inline-code" dir="ltr">
set
</code>{' '}
function does <em>not</em> change the current state in the already
executing code. It only affects what{' '}
<code className="inline-code" dir="ltr">
useState
</code>{' '}
will return starting from the next render.
</p>
</div>
<H2 id="usage">Usage</H2>
<H3 id="adding-state">Adding state to a component</H3>
<p>
Call <code>useState</code> at the top level of your component to
declare one or more state variables.
<p className="prose-p">
Call{' '}
<code className="inline-code" dir="ltr">
useState
</code>{' '}
at the top level of your component to declare one or more state
variables.
</p>
<CodeBlock lang="js" filename="Counter.js" lines={EXAMPLES.counter} />
<p>
<p className="prose-p">
In this example, clicking the button increments the counter: React
stores the next state, renders your component again with the new
value, and updates the UI.
@@ -199,17 +308,37 @@ export default function DocsPage() {
<H3 id="updater-functions">
Updating state based on the previous state
</H3>
<p>
Suppose the age is <code>42</code>. This handler calls{' '}
<code>setAge(a =&gt; a + 1)</code> three times. Here,{' '}
<code>a =&gt; a + 1</code> is an updater function. React puts your
updater functions in a queue and, during the next render, calls them
in the same order.
<p className="prose-p">
Suppose the age is{' '}
<code className="inline-code" dir="ltr">
42
</code>
. This handler calls{' '}
<code className="inline-code" dir="ltr">
setAge(a =&gt; a + 1)
</code>{' '}
three times. Here,{' '}
<code className="inline-code" dir="ltr">
a =&gt; a + 1
</code>{' '}
is an updater function. React puts your updater functions in a queue
and, during the next render, calls them in the same order.
</p>
<CodeBlock lang="js" lines={EXAMPLES.updater} />
<div className="callout tip">
<span className="callout-label">
<IconLightbulb size={13} /> Tip
</span>
<p className="prose-p">
If you find yourself passing several updater functions in a row,
it is often simpler to compute the next state once and pass the
value directly.
</p>
</div>
<H3 id="objects-and-arrays">Updating objects and arrays in state</H3>
<p>
<p className="prose-p">
You can put objects and arrays into state. In React, state is
considered read-only, so you should <em>replace</em> it rather than
mutate your existing objects.
@@ -219,7 +348,7 @@ export default function DocsPage() {
<H3 id="avoiding-recreating-state">
Avoiding recreating the initial state
</H3>
<p>
<p className="prose-p">
React saves the initial state once and ignores it on the next
renders. If you pass the result of calling a function, it will be
re-created on every render, which can be wasteful. Pass the
@@ -228,13 +357,29 @@ export default function DocsPage() {
<CodeBlock lang="js" lines={EXAMPLES.initializer} />
<H3 id="resetting-state-with-a-key">Resetting state with a key</H3>
<p>
<p className="prose-p">
You can reset a component's state by passing a different{' '}
<code>key</code> to a component. In this example, the Reset button
changes the <code>version</code> state variable, which is passed as
a <code>key</code> to the <code>Form</code>. When the key changes,
React re-creates the <code>Form</code> component (and all of its
children) from scratch, so its state gets reset.
<code className="inline-code" dir="ltr">
key
</code>{' '}
to a component. In this example, the Reset button changes the{' '}
<code className="inline-code" dir="ltr">
version
</code>{' '}
state variable, which is passed as a{' '}
<code className="inline-code" dir="ltr">
key
</code>{' '}
to the{' '}
<code className="inline-code" dir="ltr">
Form
</code>
. When the key changes, React re-creates the{' '}
<code className="inline-code" dir="ltr">
Form
</code>{' '}
component (and all of its children) from scratch, so its state gets
reset.
</p>
<CodeBlock lang="js" filename="App.js" lines={EXAMPLES.key} />
@@ -242,14 +387,14 @@ export default function DocsPage() {
summary="Deep dive: how batching interacts with updater functions"
defaultOpen={false}
>
<p>
<p className="prose-p">
React processes state updates after event handlers have finished
running. This is called batching. If you need to update the same
state variable multiple times before the next render, you can pass
updater functions, which are queued and applied in order during
the next render.
</p>
<p>
<p className="prose-p">
Updater functions must be pure: they run during rendering, so they
should only calculate the next state and return it, without side
effects. In Strict Mode, React runs each updater function twice to
@@ -258,12 +403,17 @@ export default function DocsPage() {
</Collapsible>
<H3 id="storing-functions">Storing a function in state</H3>
<p>
<p className="prose-p">
If you want to store a function in state, you have to wrap it in an
arrow function on both sides, because functions passed to{' '}
<code>useState</code> are otherwise treated as initializers and
functions passed to <code>set</code> functions are treated as
updaters.
<code className="inline-code" dir="ltr">
useState
</code>{' '}
are otherwise treated as initializers and functions passed to{' '}
<code className="inline-code" dir="ltr">
set
</code>{' '}
functions are treated as updaters.
</p>
<CodeBlock
lang="js"
@@ -275,48 +425,73 @@ function handleClick() {
/>
<H2 id="caveats">Caveats</H2>
<ul>
<li>
<code>useState</code> is a Hook, so you can only call it at the
top level of your component or your own Hooks. You can't call it
inside loops or conditions. If you need that, extract a new
component and move the state into it.
<ul className="prose-list">
<li className="prose-li">
<code className="inline-code" dir="ltr">
useState
</code>{' '}
is a Hook, so you can only call it at the top level of your
component or your own Hooks. You can't call it inside loops or
conditions. If you need that, extract a new component and move the
state into it.
</li>
<li>
<li className="prose-li">
In Strict Mode, React will call your initializer function twice in
order to help you find accidental impurities. This is
development-only behavior and does not affect production. If your
initializer function is pure (as it should be), this should not
affect the behavior.
</li>
<li>
The <code>set</code> function only updates the state variable for
the next render. If you read the state variable after calling the{' '}
<code>set</code> function, you will still get the old value that
was on the screen before your call.
<li className="prose-li">
The{' '}
<code className="inline-code" dir="ltr">
set
</code>{' '}
function only updates the state variable for the next render. If
you read the state variable after calling the{' '}
<code className="inline-code" dir="ltr">
set
</code>{' '}
function, you will still get the old value that was on the screen
before your call.
</li>
<li>
<li className="prose-li">
If the new value you provide is identical to the current state, as
determined by an <code>Object.is</code> comparison, React will
skip re-rendering the component and its children. This is an
optimization.
determined by an{' '}
<code className="inline-code" dir="ltr">
Object.is
</code>{' '}
comparison, React will skip re-rendering the component and its
children. This is an optimization.
</li>
<li>
<li className="prose-li">
React batches state updates. It updates the screen after all the
event handlers have run and have called their <code>set</code>{' '}
event handlers have run and have called their{' '}
<code className="inline-code" dir="ltr">
set
</code>{' '}
functions. This prevents multiple re-renders during a single
event. In the rare case that you need to force React to update the
screen earlier, for example to access the DOM, you can use{' '}
<code>flushSync</code>.
<code className="inline-code" dir="ltr">
flushSync
</code>
.
</li>
</ul>
<div className="callout">
<span className="callout-label">Note</span>
<p>
React uses <code>Object.is</code> to compare state values. If the
next state is equal to the previous state, the update is skipped
and your component is not re-rendered.
<span className="callout-label">
<IconInfo size={13} /> Note
</span>
<p className="prose-p">
React uses{' '}
<code className="inline-code" dir="ltr">
Object.is
</code>{' '}
to compare state values. If the next state is equal to the
previous state, the update is skipped and your component is not
re-rendered.
</p>
</div>
@@ -324,20 +499,30 @@ function handleClick() {
<H3 id="updated-but-logging-old">
I've updated the state, but logging gives me the old value
</H3>
<p>
Calling the <code>set</code> function does not change state in the
running code, because state behaves like a snapshot. If you need to
use the next state, you can save it in a variable before passing it
to the <code>set</code> function.
<p className="prose-p">
Calling the{' '}
<code className="inline-code" dir="ltr">
set
</code>{' '}
function does not change state in the running code, because state
behaves like a snapshot. If you need to use the next state, you can
save it in a variable before passing it to the{' '}
<code className="inline-code" dir="ltr">
set
</code>{' '}
function.
</p>
<H3 id="updated-but-screen-not-updating">
I've updated the state, but the screen doesn't update
</H3>
<p>
<p className="prose-p">
React ignores your update if the next state is equal to the previous
state, as determined by an <code>Object.is</code> comparison. This
usually happens when you change an object or an array in state
directly.
state, as determined by an{' '}
<code className="inline-code" dir="ltr">
Object.is
</code>{' '}
comparison. This usually happens when you change an object or an
array in state directly.
</p>
<CodeBlock
lang="js"
@@ -353,7 +538,7 @@ setObj({
<H3 id="too-many-re-renders">
I'm getting an error: "Too many re-renders"
</H3>
<p>
<p className="prose-p">
You might get an error that says:{' '}
<em>
Too many re-renders. React limits the number of renders to prevent
@@ -378,7 +563,7 @@ return <button onClick={() => handleClick()}>Click me</button>`)}
<H3 id="initializer-runs-twice">
My initializer or updater function runs twice
</H3>
<p>
<p className="prose-p">
In Strict Mode, React will call some of your functions twice instead
of once. This is development-only behavior that helps you keep
components pure. React uses the result of one of the calls and
@@ -388,49 +573,44 @@ return <button onClick={() => handleClick()}>Click me</button>`)}
</p>
<div className="callout">
<span className="callout-label">Note</span>
<p>
Content adapted from the react.dev <code>useState</code> API
reference, licensed CC BY 4.0.
<span className="callout-label">
<IconInfo size={13} /> Note
</span>
<p className="prose-p">
Content adapted from the react.dev{' '}
<code className="inline-code" dir="ltr">
useState
</code>{' '}
API reference, licensed CC BY 4.0.
</p>
</div>
<Feedback prompt="Was this page helpful?" />
<DocsPager
prev={{ title: 'useRef', href: '#' }}
next={{ title: 'useSyncExternalStore', href: '#' }}
prevIcon={<IconArrowLeft size={14} />}
nextIcon={<IconArrowRight size={14} />}
/>
<footer className="docs-footer">
<a href="#">← useRef</a>
<a href="#">useSyncExternalStore →</a>
<a href="#">
<IconGithub size={13} /> Edit on GitHub
</a>
<a href="#">
<IconDiscord size={13} /> Join the community
</a>
<a href="#">
<IconMessageSquare size={13} /> Discuss on the forum
</a>
</footer>
</article>
<nav className="toc" aria-label="On this page">
<h4>On this page</h4>
<a href="#reference">Reference</a>
<a href="#usestate" className="toc-h3">
useState(initialState)
</a>
<a href="#parameters" className="toc-h3">
Parameters
</a>
<a href="#returns" className="toc-h3">
Returns
</a>
<a href="#usage">Usage</a>
<a href="#adding-state" className="toc-h3">
Adding state
</a>
<a href="#updater-functions" className="toc-h3">
Updater functions
</a>
<a href="#objects-and-arrays" className="toc-h3">
Objects and arrays
</a>
<a href="#avoiding-recreating-state" className="toc-h3">
Initializer functions
</a>
<a href="#resetting-state-with-a-key" className="toc-h3">
Resetting with a key
</a>
<a href="#troubleshooting">Troubleshooting</a>
<h4>
<IconList size={13} /> On this page
</h4>
<TocScrollspy items={TOC_ITEMS} />
<ScrollToTop />
</nav>
</div>
</>
+59 -12
View File
@@ -113,7 +113,7 @@ export const people = Array.from({ length: 9 }, () => {
}
})
export const deployments = Array.from({ length: 64 }, (_, i) => {
export const deployments = Array.from({ length: 22 }, (_, i) => {
return {
id:
'dpl_' +
@@ -122,11 +122,25 @@ export const deployments = Array.from({ length: 64 }, (_, i) => {
project: pick(PROJECTS),
branch: pick(BRANCHES),
commit: pick(COMMITS),
sha: Math.floor(rand() * 0xffffffff)
.toString(16)
.padStart(8, '0'),
sha: Array.from({ length: 5 }, () =>
Math.floor(rand() * 0xffffffff)
.toString(16)
.padStart(8, '0')
).join(''),
status: pick(STATUSES),
author: people[Math.floor(rand() * people.length)],
url:
'https://' +
pick(PROJECTS) +
'-' +
(100000 + Math.floor(rand() * 899999)).toString(36) +
'-acme-team.vercel.app',
inspectorUrl:
'https://vercel.com/acme/' +
pick(PROJECTS) +
'/' +
(100000 + Math.floor(rand() * 899999)).toString(36) +
(100000 + Math.floor(rand() * 899999)).toString(36),
createdAt: minutesBefore(Math.floor(rand() * 2880) + 2),
// Optional fields are sparse like real API responses.
durationSeconds: i % 4 !== 3 ? Math.floor(rand() * 340) + 18 : undefined,
@@ -212,8 +226,13 @@ function blurPlaceholder(seed, w, h) {
return 'data:image/webp;base64,UklGR' + bytes.map((b) => b64[b]).join('')
}
export function imageMeta(seed, w, h) {
const base =
'/_next/image?url=%2Fscreenshots%2Fdeploy-' + seed.toString(36) + '.webp'
return {
src: '/screenshots/deploy-' + seed.toString(36) + '.webp',
src: base + '&w=' + w * 2 + '&q=75',
srcSet: [1, 2]
.map((x) => base + '&w=' + w * x + '&q=75 ' + x + 'x')
.join(', '),
width: w,
height: h,
blurWidth: 8,
@@ -231,7 +250,7 @@ const FRAMEWORKS = [
'Astro',
'Remix',
]
export const projects = Array.from({ length: 18 }, (_, i) => ({
export const projects = Array.from({ length: 10 }, (_, i) => ({
id: 'prj_' + i,
updatedAt: minutesBefore(Math.floor(rand() * 4000) + 1),
name:
@@ -255,7 +274,7 @@ const ACTIVITY_VERBS = [
['transferred', 'to team acme'],
['enabled analytics for', ''],
]
export const activity = Array.from({ length: 36 }, (_, i) => {
export const activity = Array.from({ length: 14 }, (_, i) => {
const [verb, suffix] = pick(ACTIVITY_VERBS)
return {
id: 'evt_' + i,
@@ -440,7 +459,7 @@ function richText(seedIdx, paragraphs) {
return { nodeType: 'document', data: {}, content }
}
export const posts = Array.from({ length: 48 }, (_, i) => {
export const posts = Array.from({ length: 74 }, (_, i) => {
const [title, tag, excerpt] = TOPICS[i % TOPICS.length]
const first = pick(FIRST)
const last = pick(LAST)
@@ -520,6 +539,9 @@ export const posts = Array.from({ length: 48 }, (_, i) => {
})
const DOC_SECTIONS = [
'testing',
'authentication',
'data-fetching',
'getting-started',
'guides',
'app',
@@ -612,14 +634,14 @@ export const docsTree = Object.fromEntries(
['stable', 'v15', 'v14'].map((version) => [
version,
DOC_SECTIONS.map((section, s) =>
Array.from({ length: 4 }, (_, i) =>
Array.from({ length: 6 }, (_, i) =>
docNode(version, section, s * 5 + i, 0)
)
).flat(),
])
)
export const domains = Array.from({ length: 22 }, (_, i) => ({
export const domains = Array.from({ length: 12 }, (_, i) => ({
id: 'dom_' + i,
name:
(i % 3 === 0 ? 'www.' : '') +
@@ -654,7 +676,7 @@ export const alerts = [
},
]
export const members = Array.from({ length: 18 }, (_, i) => ({
export const members = Array.from({ length: 10 }, (_, i) => ({
person: people[i % people.length],
role: ['owner', 'member', 'member', 'developer', 'billing', 'viewer'][i % 6],
mfa: i % 4 !== 3,
@@ -663,7 +685,32 @@ export const members = Array.from({ length: 18 }, (_, i) => ({
i % 5 !== 4 ? minutesBefore(Math.floor(rand() * 10000) + 5) : undefined,
}))
export const screenshots = projects.slice(0, 9).map((p, i) => ({
export const screenshots = projects.slice(0, 4).map((p, i) => ({
project: p.name,
image: imageMeta(i * 97 + 13, 1200, 630),
}))
const LOG_TEXTS_LONG = [
'GET /api/deployments?teamId=team_kq83majnf02m&limit=20&state=READY 200 in 84ms (region: iad1, cache: MISS, requestId: pdx1::vjk2m-1751371200412-8f3ab2c19d04)',
'Cache key generated: fetch:https://api.acme.dev/v2/projects/prj_8f3ab2c19d04/deployments?state=ready&sort=created:desc [tags: deployments,projects]',
'Revalidated 4 paths for tag "deployments": /acme/overview, /acme/deployments, /acme/analytics, /api/og/deployments (took 218ms)',
]
const LOG_TEXTS = [
'GET /api/projects 200 in 24ms',
'Cache HIT for /_next/data/build-id/index.json',
'Revalidating tag: products',
'Function execution took 182ms (limit: 10s)',
'GET /dashboard 200 in 41ms',
'Edge middleware matched /acme/overview',
'ISR write completed for /blog',
'Queue consumer acked 24 messages',
'Cold start: 312ms in iad1',
'GET /api/usage 200 in 12ms',
'Cron /api/cron/cleanup completed',
'Warning: slow query in listDeployments (1.2s)',
]
export const logLines = LOG_TEXTS.concat(LOG_TEXTS_LONG).map((text, i) => ({
ts: '12:0' + (i % 10) + ':' + String(10 + ((i * 7) % 50)),
level: text.startsWith('Warning') ? 'warn' : 'info',
text,
}))
@@ -0,0 +1,20 @@
'use client'
import { describeDocsVendor } from './vendor-docs'
import { useState } from 'react'
export default function AnnouncementBanner({ children }) {
const [dismissed, setDismissed] = useState(false)
if (dismissed) return null
return (
<div className="announcement" role="status">
{children}
<button
type="button"
aria-label="Dismiss"
onClick={() => setDismissed(true)}
>
×
</button>
</div>
)
}
export const __vendor = typeof describeDocsVendor
+19
View File
@@ -0,0 +1,19 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function ApiToken({ token, icon }) {
const [revealed, setRevealed] = useState(false)
return (
<span className="api-token mono text-xs">
{revealed ? token : token.slice(0, 4) + '••••••••' + token.slice(-4)}
<button
type="button"
aria-label={revealed ? 'Hide token' : 'Reveal token'}
onClick={() => setRevealed((r) => !r)}
>
{icon}
</button>
</span>
)
}
export const __vendor = typeof describeBulkGraph
+16
View File
@@ -0,0 +1,16 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function BillingMeter({ spent, budget, icon }) {
const [showBudget, setShowBudget] = useState(false)
return (
<button
type="button"
className="billing-meter text-xs"
onClick={() => setShowBudget((s) => !s)}
>
{icon} {showBudget ? `$${spent} of $${budget}` : `$${spent} this period`}
</button>
)
}
export const __vendor = typeof describeBulkGraph
+32
View File
@@ -0,0 +1,32 @@
'use client'
import { describeBlogVendor } from './vendor-blog'
import { useState } from 'react'
export default function BlogSearch({ placeholder }) {
const [value, setValue] = useState('')
return (
<label className="search">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2" />
<path
d="m21 21-4-4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
<input
type="search"
placeholder={placeholder}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
</label>
)
}
export const __vendor = typeof describeBlogVendor
@@ -0,0 +1,30 @@
'use client'
import { describeBlogVendor } from './vendor-blog'
import { useState } from 'react'
export default function CarouselControls({ pages, label }) {
const [page, setPage] = useState(0)
return (
<span className="carousel-controls" aria-label={label}>
<button
type="button"
aria-label="Previous"
disabled={page === 0}
onClick={() => setPage((p) => Math.max(0, p - 1))}
>
←
</button>
<span className="text-xs text-muted">
{page + 1} / {pages}
</span>
<button
type="button"
aria-label="Next"
disabled={page === pages - 1}
onClick={() => setPage((p) => Math.min(pages - 1, p + 1))}
>
→
</button>
</span>
)
}
export const __vendor = typeof describeBlogVendor
+29
View File
@@ -0,0 +1,29 @@
'use client'
import { describeBlogVendor } from './vendor-blog'
import { useState } from 'react'
export default function CategoryFilter({ categories, icon }) {
const [selected, setSelected] = useState('all')
return (
<div className="category-filter flex items-center gap-2" role="group">
{icon}
<button
type="button"
aria-pressed={selected === 'all'}
onClick={() => setSelected('all')}
>
All
</button>
{categories.map((c) => (
<button
key={c.slug}
type="button"
aria-pressed={selected === c.slug}
onClick={() => setSelected(c.slug)}
>
{c.name}
</button>
))}
</div>
)
}
export const __vendor = typeof describeBlogVendor
+32
View File
@@ -0,0 +1,32 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function ChartLegend({ series }) {
const [hidden, setHidden] = useState([])
return (
<div
className="chart-legend flex items-center gap-2 text-xs"
role="group"
aria-label="Chart series"
>
{series.map((s) => (
<button
key={s.label}
type="button"
aria-pressed={!hidden.includes(s.label)}
onClick={() =>
setHidden((h) =>
h.includes(s.label)
? h.filter((x) => x !== s.label)
: [...h, s.label]
)
}
>
<span className="legend-swatch" style={{ background: s.color }} />
{s.label}
</button>
))}
</div>
)
}
export const __vendor = typeof describeBulkGraph
+38
View File
@@ -0,0 +1,38 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function ColumnPicker({ columns, icon }) {
const [open, setOpen] = useState(false)
const [hidden, setHidden] = useState([])
return (
<span className="column-picker">
<button
type="button"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
>
{icon} Columns
</button>
{open ? (
<span className="share-items" role="menu">
{columns.map((c) => (
<button
key={c}
type="button"
role="menuitemcheckbox"
aria-checked={!hidden.includes(c)}
onClick={() =>
setHidden((h) =>
h.includes(c) ? h.filter((x) => x !== c) : [...h, c]
)
}
>
{c}
</button>
))}
</span>
) : null}
</span>
)
}
export const __vendor = typeof describeBulkGraph
+3
View File
@@ -1,4 +1,5 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { describeTooling } from './vendor-tooling'
import { describeDataLayer } from './vendor-data'
import { describeAuthLayer } from './vendor-auth'
@@ -30,3 +31,5 @@ export default function CommandMenu({ commands }) {
export const __layers = [describeDataLayer, describeAuthLayer].length
export const __tooling = typeof describeTooling
export const __bulk = typeof describeBulkGraph
@@ -0,0 +1,19 @@
'use client'
import { describeDocsVendor } from './vendor-docs'
import { useState } from 'react'
export default function CopyPageButton({ icon }) {
const [copied, setCopied] = useState(false)
return (
<button
type="button"
className="copy-page text-xs"
onClick={() => {
setCopied(true)
setTimeout(() => setCopied(false), 1500)
}}
>
{icon} {copied ? 'Copied' : 'Copy page'}
</button>
)
}
export const __vendor = typeof describeDocsVendor
@@ -0,0 +1,18 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
const RANGES = ['Last 24 hours', 'Last 7 days', 'Last 30 days', 'Last 90 days']
export default function DateRangePicker({ initial }) {
const [range, setRange] = useState(initial ?? RANGES[3])
return (
<label className="date-range text-xs">
<span className="sr-only">Time range</span>
<select value={range} onChange={(e) => setRange(e.target.value)}>
{RANGES.map((r) => (
<option key={r}>{r}</option>
))}
</select>
</label>
)
}
export const __vendor = typeof describeBulkGraph
+35
View File
@@ -0,0 +1,35 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function DeployActions({ promoteIcon, rollbackIcon }) {
const [confirming, setConfirming] = useState(null)
return confirming ? (
<span className="deploy-actions text-xs">
Confirm {confirming}?
<button type="button" onClick={() => setConfirming(null)}>
Yes
</button>
<button type="button" onClick={() => setConfirming(null)}>
No
</button>
</span>
) : (
<span className="deploy-actions">
<button
type="button"
aria-label="Promote"
onClick={() => setConfirming('promote')}
>
{promoteIcon}
</button>
<button
type="button"
aria-label="Rollback"
onClick={() => setConfirming('rollback')}
>
{rollbackIcon}
</button>
</span>
)
}
export const __vendor = typeof describeBulkGraph
+25
View File
@@ -0,0 +1,25 @@
'use client'
import { describeDocsVendor } from './vendor-docs'
import { useState } from 'react'
export default function DocsPager({ prev, next, prevIcon, nextIcon }) {
const [hovered, setHovered] = useState(null)
return (
<nav className="docs-pager" aria-label="Pagination" data-hovered={hovered}>
<a href={prev.href} onMouseEnter={() => setHovered('prev')}>
{prevIcon}
<span>
<small className="text-muted">Previous</small>
{prev.title}
</span>
</a>
<a href={next.href} onMouseEnter={() => setHovered('next')}>
<span>
<small className="text-muted">Next</small>
{next.title}
</span>
{nextIcon}
</a>
</nav>
)
}
export const __vendor = typeof describeDocsVendor
+34
View File
@@ -0,0 +1,34 @@
'use client'
import { describeDocsVendor } from './vendor-docs'
import { useState } from 'react'
export default function DocsSearch({ placeholder }) {
const [open, setOpen] = useState(false)
return (
<button
type="button"
className="search docs-search"
aria-haspopup="dialog"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2" />
<path
d="m21 21-4-4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
<span className="text-muted">{placeholder}</span>
<kbd>⌘K</kbd>
</button>
)
}
export const __vendor = typeof describeDocsVendor
+32
View File
@@ -0,0 +1,32 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function FeedbackButton() {
const [open, setOpen] = useState(false)
const [text, setText] = useState('')
return open ? (
<form
className="feedback-form"
onSubmit={(e) => {
e.preventDefault()
setOpen(false)
}}
>
<textarea
placeholder="Share feedback…"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Send</button>
</form>
) : (
<button
type="button"
className="feedback-trigger text-xs"
onClick={() => setOpen(true)}
>
Feedback
</button>
)
}
export const __vendor = typeof describeBulkGraph
+20
View File
@@ -0,0 +1,20 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function FilterChip({ label, icon }) {
const [removed, setRemoved] = useState(false)
if (removed) return null
return (
<span className="filter-chip text-xs">
{icon} {label}
<button
type="button"
aria-label={'Remove filter ' + label}
onClick={() => setRemoved(true)}
>
×
</button>
</span>
)
}
export const __vendor = typeof describeBulkGraph
+29
View File
@@ -0,0 +1,29 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function HelpMenu({ icon, items }) {
const [open, setOpen] = useState(false)
return (
<span className="help-menu">
<button
type="button"
aria-haspopup="menu"
aria-expanded={open}
aria-label="Help"
onClick={() => setOpen((o) => !o)}
>
{icon}
</button>
{open ? (
<span className="share-items" role="menu">
{items.map((item) => (
<button key={item} role="menuitem" type="button">
{item}
</button>
))}
</span>
) : null}
</span>
)
}
export const __vendor = typeof describeBulkGraph
@@ -0,0 +1,15 @@
'use client'
export default function IconAlertTriangle({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m21.7 18-8-14a2 2 0 0 0-3.4 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.7-3zM12 9v4M12 17h.01"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+24
View File
@@ -0,0 +1,24 @@
'use client'
export default function IconArchive({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="2"
y="3"
width="20"
height="5"
rx="1"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8M10 12h4"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
@@ -0,0 +1,14 @@
'use client'
export default function IconArrowLeft({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M19 12H5M12 19l-7-7 7-7"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
@@ -0,0 +1,14 @@
'use client'
export default function IconArrowRight({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M5 12h14M12 5l7 7-7 7"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconBarChart({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 20V10M18 20V4M6 20v-4"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconBellOff({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M8.7 3A6 6 0 0 1 18 8c0 4.5 1.2 6.2 1.7 7M17 17H4c1-1 2-2.5 2-9M10.3 21a1.94 1.94 0 0 0 3.4 0M2 2l20 20"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconBook({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconBox({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.3 7l8.7 5 8.7-5M12 22V12"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
@@ -0,0 +1,23 @@
'use client'
export default function IconCalendarDays({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="3"
y="4"
width="18"
height="18"
rx="2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M16 2v4M8 2v4M3 10h18M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
+23
View File
@@ -0,0 +1,23 @@
'use client'
export default function IconCalendar({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="3"
y="4"
width="18"
height="18"
rx="2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M16 2v4M8 2v4M3 10h18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconCheck({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M20 6 9 17l-5-5"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
@@ -0,0 +1,14 @@
'use client'
export default function IconChevronLeft({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m15 18-6-6 6-6"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
@@ -0,0 +1,14 @@
'use client'
export default function IconChevronRight({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m9 18 6-6-6-6"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
@@ -0,0 +1,14 @@
'use client'
export default function IconChevronUp({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m18 15-6-6-6 6"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
@@ -0,0 +1,15 @@
'use client'
export default function IconChevronsUpDown({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m7 15 5 5 5-5M7 9l5-5 5 5"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
@@ -0,0 +1,16 @@
'use client'
export default function IconCircleDot({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<circle cx="12" cy="12" r="2" fill="currentColor" />
</svg>
)
}
+23
View File
@@ -0,0 +1,23 @@
'use client'
export default function IconClipboard({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<rect
x="8"
y="2"
width="8"
height="4"
rx="1"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconClose({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M18 6 6 18M6 6l12 12"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconCloud({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M17.5 19H9a7 7 0 1 1 6.7-9h1.8a4.5 4.5 0 1 1 0 9z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconCode({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m16 18 6-6-6-6M8 6l-6 6 6 6"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconCommand({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M15 6v12a3 3 0 1 0 3-3H6a3 3 0 1 0 3 3V6a3 3 0 1 0-3 3h12a3 3 0 1 0-3-3"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+23
View File
@@ -0,0 +1,23 @@
'use client'
export default function IconCopy({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="8"
y="8"
width="14"
height="14"
rx="2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
</svg>
)
}
@@ -0,0 +1,18 @@
'use client'
export default function IconCreditCard({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="2"
y="5"
width="20"
height="14"
rx="2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path d="M2 10h20" stroke="currentColor" strokeWidth="2" />
</svg>
)
}
+20
View File
@@ -0,0 +1,20 @@
'use client'
export default function IconDiscord({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M5.5 5C7.2 4.2 9 4 9 4l.4 1h5.2L15 4s1.8.2 3.5 1c2.2 3.4 2.5 8.5 2.5 8.5L19 17l-3-1 .5-1.5c-2.2.7-6.8.7-9 0L8 16l-3 1-2-3.5S3.3 8.4 5.5 5z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
<path
d="M9 11.5h.01M15 11.5h.01"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconDownload({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconEdit({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+22
View File
@@ -0,0 +1,22 @@
'use client'
export default function IconEye({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
<circle
cx="12"
cy="12"
r="3"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconFileCode({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M10 12l-2 2 2 2M14 12l2 2-2 2"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconFileText({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M16 13H8M16 17H8"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconFilter({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M22 3H2l8 9.46V19l4 2v-8.54z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconFlame({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3 2.5.5 5 2.24 5 5a4 4 0 0 1-8 0c0-1.15.44-2.2 1.5-3-.5 1 0 3.5 0 3.5zM12 2s4 3.5 4 8c2-1 3-2.5 3-2.5.67 1.6 1 3.3 1 5a8 8 0 0 1-16 0c0-4.84 4.5-8.5 8-10.5z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconGauge({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m12 14 4-4M3.34 19a10 10 0 1 1 17.32 0"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
@@ -0,0 +1,21 @@
'use client'
export default function IconGitCommit({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<circle
cx="12"
cy="12"
r="3"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M3 12h6M15 12h6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
+29
View File
@@ -0,0 +1,29 @@
'use client'
export default function IconGitMerge({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<circle
cx="18"
cy="18"
r="3"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<circle
cx="6"
cy="6"
r="3"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M6 21V9a9 9 0 0 0 9 9"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
</svg>
)
}
@@ -0,0 +1,30 @@
'use client'
export default function IconGitPullRequest({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<circle
cx="18"
cy="18"
r="3"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<circle
cx="6"
cy="6"
r="3"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M13 6h3a2 2 0 0 1 2 2v7M6 9v12"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconGithub({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4M9 18c-4.51 2-5-2-7-2"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
@@ -0,0 +1,15 @@
'use client'
export default function IconHardDrive({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M22 12H2M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11zM6 16h.01M10 16h.01"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconHash({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
@@ -0,0 +1,15 @@
'use client'
export default function IconHeadphones({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M3 14h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-7a9 9 0 0 1 18 0v7a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconHeart({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M19 14c1.5-1.4 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.1 3 5.5l7 7z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconInbox({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M22 12h-6l-2 3h-4l-2-3H2M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+21
View File
@@ -0,0 +1,21 @@
'use client'
export default function IconInfo({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M12 16v-4M12 8h.01"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconKey({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m21 2-2 2m-7.6 7.6a5.5 5.5 0 1 1-7.78 7.78 5.5 5.5 0 0 1 7.78-7.78zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconLanguages({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+28
View File
@@ -0,0 +1,28 @@
'use client'
export default function IconLifeBuoy({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<circle
cx="12"
cy="12"
r="4"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="m4.93 4.93 4.24 4.24M14.83 14.83l4.24 4.24M14.83 9.17l4.24-4.24M9.17 14.83l-4.24 4.24"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconLightbulb({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M9 18h6M10 22h4M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.4 1 2.3h6c0-.9.4-1.8 1-2.3A7 7 0 0 0 12 2z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconLink({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
+24
View File
@@ -0,0 +1,24 @@
'use client'
export default function IconLinkedin({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="2"
y="2"
width="20"
height="20"
rx="2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M8 11v5M8 8h.01M12 16v-5M16 16v-3a2 2 0 0 0-4 0"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
@@ -0,0 +1,14 @@
'use client'
export default function IconListFilter({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M3 6h18M7 12h10M11 18h2"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconList({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
+13
View File
@@ -0,0 +1,13 @@
'use client'
export default function IconLoader({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
+24
View File
@@ -0,0 +1,24 @@
'use client'
export default function IconMail({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="2"
y="4"
width="20"
height="16"
rx="2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="m22 7-10 5L2 7"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+22
View File
@@ -0,0 +1,22 @@
'use client'
export default function IconMapPin({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
<circle
cx="12"
cy="10"
r="3"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconMenu({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 6h16M4 12h16M4 18h16"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
@@ -0,0 +1,14 @@
'use client'
export default function IconMessageSquare({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+23
View File
@@ -0,0 +1,23 @@
'use client'
export default function IconMonitor({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="2"
y="3"
width="20"
height="14"
rx="2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M8 21h8M12 17v4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconPackage({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M16.5 9.4 7.55 4.24M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.3 7l8.7 5 8.7-5M12 22V12"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+27
View File
@@ -0,0 +1,27 @@
'use client'
export default function IconPause({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="6"
y="4"
width="4"
height="16"
rx="1"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<rect
x="14"
y="4"
width="4"
height="16"
rx="1"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
</svg>
)
}
+22
View File
@@ -0,0 +1,22 @@
'use client'
export default function IconPlay({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="m10 8 6 4-6 4z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconPlug({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 22v-5M9 8V2M15 8V2M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+13
View File
@@ -0,0 +1,13 @@
'use client'
export default function IconPlus({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M5 12h14M12 5v14"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconRss({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
<circle cx="5" cy="19" r="1" fill="currentColor" />
</svg>
)
}
+33
View File
@@ -0,0 +1,33 @@
'use client'
export default function IconServer({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<rect
x="2"
y="2"
width="20"
height="8"
rx="2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<rect
x="2"
y="14"
width="20"
height="8"
rx="2"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
<path
d="M6 6h.01M6 18h.01"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconShare({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8M16 6l-4-4-4 4M12 2v13"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+13
View File
@@ -0,0 +1,13 @@
'use client'
export default function IconSliders({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M2 14h4M10 8h4M18 16h4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconSparkles({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 3l1.9 5.8a2 2 0 0 0 1.3 1.3L21 12l-5.8 1.9a2 2 0 0 0-1.3 1.3L12 21l-1.9-5.8a2 2 0 0 0-1.3-1.3L3 12l5.8-1.9a2 2 0 0 0 1.3-1.3zM5 3v4M3 5h4M19 17v4M17 19h4"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconStar({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconTag({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
<circle cx="7.5" cy="7.5" r=".5" fill="currentColor" />
</svg>
)
}
+21
View File
@@ -0,0 +1,21 @@
'use client'
export default function IconTimer({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M10 2h4M12 14l3-3"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<circle
cx="12"
cy="14"
r="8"
stroke="currentColor"
strokeWidth="2"
fill="none"
/>
</svg>
)
}
@@ -0,0 +1,15 @@
'use client'
export default function IconTrendingUp({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M22 7l-8.5 8.5-5-5L2 17M16 7h6v6"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+15
View File
@@ -0,0 +1,15 @@
'use client'
export default function IconUpload({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconWifi({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M12 20h.01M2 8.82a15 15 0 0 1 20 0M5 12.86a10 10 0 0 1 14 0M8.5 16.43a5 5 0 0 1 7 0"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinecap="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconWrench({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconXSocial({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 3h4.5l4 5.5L17 3h3l-6 7.8L20.5 21H16l-4.3-6L7 21H4l6.3-8.2z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+14
View File
@@ -0,0 +1,14 @@
'use client'
export default function IconYoutube({ size = 16 }) {
return (
<svg width={size} height={size} viewBox="0 0 24 24" aria-hidden="true">
<path
d="M2.5 17a24 24 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.6 49.6 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24 24 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.6 49.6 0 0 1-16.2 0A2 2 0 0 1 2.5 17zM10 15l5-3-5-3z"
stroke="currentColor"
strokeWidth="2"
fill="none"
strokeLinejoin="round"
/>
</svg>
)
}
+21
View File
@@ -0,0 +1,21 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function IncidentBanner({ message, icon }) {
const [dismissed, setDismissed] = useState(false)
if (dismissed) return null
return (
<div className="incident-banner text-xs" role="status">
{icon} {message}
<a href="#">Status page</a>
<button
type="button"
aria-label="Dismiss"
onClick={() => setDismissed(true)}
>
×
</button>
</div>
)
}
export const __vendor = typeof describeBulkGraph
+33
View File
@@ -0,0 +1,33 @@
'use client'
import { describeBulkGraph } from './vendor-bulk'
import { useState } from 'react'
export default function InviteMember({ icon }) {
const [open, setOpen] = useState(false)
const [email, setEmail] = useState('')
return open ? (
<form
className="invite-form"
onSubmit={(e) => {
e.preventDefault()
setOpen(false)
}}
>
<input
type="email"
placeholder="[email protected]"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit">Send</button>
</form>
) : (
<button
type="button"
className="invite-button text-xs"
onClick={() => setOpen(true)}
>
{icon} Invite
</button>
)
}
export const __vendor = typeof describeBulkGraph

Some files were not shown because too many files have changed in this diff Show More