diff --git a/plugins/cookbook-index/index.js b/plugins/cookbook-index/index.js index 693a718a1b..5abac27bc9 100644 --- a/plugins/cookbook-index/index.js +++ b/plugins/cookbook-index/index.js @@ -67,6 +67,19 @@ console.log('[cookbook-index] init with docsDir:', options.docsDir); } const item = { id, title, description, tags, permalink, source }; + + if (typeof data.last_updated !== 'undefined') { + item.last_updated = data.last_updated; + } + if (typeof data.last_updated_at !== 'undefined') { + item.last_updated_at = data.last_updated_at; + } + if (typeof data.last_updated_label === 'string') { + item.last_updated_label = data.last_updated_label; + } + if (typeof data.formatted_last_updated === 'string') { + item.formatted_last_updated = data.formatted_last_updated; + } if (typeof priority === 'number') { item.priority = priority; } diff --git a/src/components/CookbookDocItem.module.css b/src/components/CookbookDocItem.module.css index 0320e7480f..63319e0a18 100644 --- a/src/components/CookbookDocItem.module.css +++ b/src/components/CookbookDocItem.module.css @@ -6,20 +6,13 @@ } .wrapper { - display: grid; - gap: 2rem; - width: min(100%, 1080px); - grid-template-columns: minmax(0, min(90vw, 720px)); - justify-content: center; -} - -.wrapper[data-has-toc='true'] { - grid-template-columns: minmax(0, min(90vw, 720px)) 260px; + width: min(100%, 1600px); + position: relative; } .article { width: 100%; - max-width: min(90vw, 720px); + max-width: min(92vw, 936px); margin: 0 auto; } @@ -33,10 +26,14 @@ .lastUpdated { margin: 0 0 var(--ifm-spacing-sm, 0.75rem); - color: #465A78; + color: var(--ifm-color-emphasis-800); font-size: 0.95rem; } +:global(html[data-theme='dark']) .article .lastUpdated { + color: #ffffff; +} + .actionsRow { display: flex; align-items: center; @@ -95,18 +92,60 @@ margin-bottom: var(--ifm-spacing-lg, 1.5rem); } +.recipePagination { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-top: var(--ifm-spacing-xl, 2rem); + padding-top: var(--ifm-spacing-lg, 1.5rem); + border-top: 1px solid color-mix(in srgb, var(--ifm-color-emphasis-400) 45%, transparent); +} + +.recipeNavLink { + display: inline-flex; + align-items: center; + gap: 0.75rem; + min-width: 0; + color: var(--ifm-color-emphasis-800); + text-decoration: none; + padding: 0.5rem 0.25rem; +} + +.recipeNavLink:hover, +.recipeNavLink:focus-visible { + text-decoration: none; + color: var(--ifm-color-emphasis-900); +} + +.recipeNavLinkNext { + justify-self: end; + text-align: right; +} + +.recipeNavLabel { + display: block; + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--ifm-color-emphasis-600); +} + +.recipeNavTitle { + display: block; + font-weight: 600; + line-height: 1.25; +} + .toc { - position: sticky; + position: fixed; + right: clamp(1.25rem, 3vw, 3.5rem); top: calc(var(--ifm-navbar-height, 60px) + 24px); + width: 260px; max-height: calc(100vh - var(--ifm-navbar-height, 60px) - 48px); overflow: auto; } -@media (max-width: 1200px) { - .wrapper[data-has-toc='true'] { - grid-template-columns: minmax(0, min(90vw, 720px)); - } - +@media (max-width: 1400px) { .toc { display: none; } @@ -126,4 +165,13 @@ width: 100%; text-align: center; } + + .recipePagination { + grid-template-columns: 1fr; + } + + .recipeNavLinkNext { + justify-self: stretch; + text-align: left; + } } diff --git a/src/components/CookbookDocItem.tsx b/src/components/CookbookDocItem.tsx index fa48065fc2..d3a082597d 100644 --- a/src/components/CookbookDocItem.tsx +++ b/src/components/CookbookDocItem.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Head from '@docusaurus/Head'; -import { DocProvider, useDoc } from '@docusaurus/plugin-content-docs/client'; +import { DocProvider, useAllDocsData, useDoc } from '@docusaurus/plugin-content-docs/client'; import DocItemMetadata from '@theme/DocItem/Metadata'; import type { Props as DocItemProps } from '@theme/DocItem'; import { HtmlClassNameProvider } from '@docusaurus/theme-common'; @@ -18,9 +18,135 @@ type CookbookDocItemProps = DocItemProps & { tags?: string[] }; type CookbookIndexItem = { id: string; + title?: string; + description?: string; + tags?: string[]; + permalink?: string; source?: string; + priority?: number; + last_updated?: unknown; + last_updated_at?: unknown; + last_updated_label?: string; + formatted_last_updated?: string; }; +type DocMeta = { + id: string; + unversionedId?: string; + title?: string; + permalink?: string; + frontMatter?: { + title?: string; + last_updated?: unknown; + last_updated_at?: unknown; + }; + lastUpdatedAt?: number | string | null; +}; + +function normalizeCookbookId(id: string | undefined): string { + return (id ?? '').replace(/^cookbook:/, '').trim(); +} + +function normalizeTimestamp(value: unknown): number | undefined { + const normalizeNumber = (input: number): number | undefined => { + if (!Number.isFinite(input)) { + return undefined; + } + return input < 1e11 ? input * 1000 : input; + }; + + if (typeof value === 'number') { + return normalizeNumber(value); + } + if (typeof value === 'string') { + const trimmed = value.trim(); + if (!trimmed) { + return undefined; + } + const numeric = Number(trimmed); + if (!Number.isNaN(numeric)) { + return normalizeNumber(numeric); + } + const parsed = Date.parse(trimmed); + return Number.isNaN(parsed) ? undefined : normalizeNumber(parsed); + } + if (value instanceof Date) { + const time = value.getTime(); + return Number.isNaN(time) ? undefined : normalizeNumber(time); + } + return undefined; +} + +function formatTimestamp(value: number): string | undefined { + if (!Number.isFinite(value)) { + return undefined; + } + try { + const formatter = new Intl.DateTimeFormat(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + return formatter.format(new Date(value)); + } catch { + return new Date(value).toLocaleDateString(); + } +} + +function resolveDocMeta(item: CookbookIndexItem, docsById: Map) { + return ( + docsById.get(item.id) ?? + docsById.get(`cookbook:${item.id}`) ?? + docsById.get(item.id.replace(/^cookbook:/, '')) ?? + null + ); +} + +function getCookbookItemTimestamp(item: CookbookIndexItem, docsById: Map): number { + const meta = resolveDocMeta(item, docsById); + const candidates = [ + meta?.frontMatter?.last_updated, + meta?.frontMatter?.last_updated_at, + meta?.lastUpdatedAt, + item.last_updated, + item.last_updated_at, + ]; + + for (const candidate of candidates) { + const normalized = normalizeTimestamp(candidate); + if (typeof normalized === 'number') { + return normalized; + } + } + + return 0; +} + +function sortCookbookItems(items: CookbookIndexItem[], docsById: Map): CookbookIndexItem[] { + return [...items].sort((a, b) => { + const priorityA = typeof a.priority === 'number' && Number.isFinite(a.priority) ? a.priority : null; + const priorityB = typeof b.priority === 'number' && Number.isFinite(b.priority) ? b.priority : null; + + if (priorityA !== null && priorityB !== null) { + if (priorityA !== priorityB) { + return priorityB - priorityA; + } + } else if (priorityA !== null) { + return -1; + } else if (priorityB !== null) { + return 1; + } + + const updatedA = getCookbookItemTimestamp(a, docsById); + const updatedB = getCookbookItemTimestamp(b, docsById); + + if (updatedA === updatedB) { + return 0; + } + return updatedB - updatedA; + }); +} + function BackArrowIcon(props: React.SVGProps) { return (

Last updated {lastUpdatedLabel}

; + return

Updated {lastUpdatedLabel}

; }, [lastUpdatedLabel]); + const renderActions = React.useCallback( () => (
@@ -223,6 +388,39 @@ function InnerCookbookDocItem({ content, tags }: CookbookDocItemProps) { [githubHref, handleGithubClick, isGithubEnabled] ); + const renderRecipePagination = React.useCallback(() => { + if (!paginationTargets.previous && !paginationTargets.next) { + return null; + } + + return ( + + ); + }, [paginationTargets.next, paginationTargets.previous]); + const components = React.useMemo(() => { const DefaultH1 = (MDXComponents?.h1 as React.ComponentType>) ?? @@ -273,6 +471,7 @@ function InnerCookbookDocItem({ content, tags }: CookbookDocItemProps) { + {renderRecipePagination()} {hasTOC && (

{description}

- {tags.length > 0 && ( -
    - {tags.map((t) => ( -
  • - {t.toUpperCase()} -
  • - ))} -
+ {(tags.length > 0 || lastUpdatedLabel) && ( +
+ {tags.length > 0 && ( +
    + {tags.map((t) => ( +
  • + {t.toUpperCase()} +
  • + ))} +
+ )} + {lastUpdatedLabel &&

Updated {lastUpdatedLabel}

} +
)}
- {href && ( -
- - Learn more - - - - -
- )} ); }