mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
[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:
@@ -0,0 +1,2 @@
|
||||
app/ui/vendor/
|
||||
app/g/
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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
@@ -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 => a + 1)</code> three times. Here,{' '}
|
||||
<code>a => 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 => a + 1)
|
||||
</code>{' '}
|
||||
three times. Here,{' '}
|
||||
<code className="inline-code" dir="ltr">
|
||||
a => 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>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user