-
Notifications
You must be signed in to change notification settings - Fork 268
feat: version aware minimum version badge #2327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
OrbisK
wants to merge
26
commits into
nuxt:main
Choose a base branch
from
OrbisK:feat/version-aware-min-badge
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
82262de
docs: render minimalVersion badge from layout
Ibochkarev 92a39e7
fix(content): accept number or string for minimalVersion and normaliz…
Ibochkarev 460f5ac
Merge branch 'main' into docs/minimal-version-badge-layout
benjamincanac ad7f23c
Update app/pages/docs/[...slug].vue
Ibochkarev 8c40d1f
Merge branch 'main' into docs/minimal-version-badge-layout
danielroe 95707c8
Apply suggestions from code review
danielroe 48005ec
Update content.config.ts
Ibochkarev cb24f96
Update content.config.ts
Ibochkarev 554dfbe
Update content.config.ts
Ibochkarev 2ba6236
Update content.config.ts
Ibochkarev c8101cf
feat: only show version badge if matches major version
OrbisK 5524ae1
feat: allow gte major versions just in case
OrbisK 88089c0
Merge branch 'main' into docs/minimal-version-badge-layout
Ibochkarev 6db39c1
docs: simplify minimalVersion schema, drop z.preprocess
HugoRCD 895aff5
Merge remote-tracking branch 'origin/main' into docs/minimal-version-…
HugoRCD 0de9208
feat: extract tollerance logic into composable
OrbisK 4b46833
Merge PR nuxt/nuxt.com#2203 (docs/minimal-version-badge-layout)
OrbisK 00f1737
chore: update lockfile
OrbisK 397d073
Merge branch 'main' into feat/version-aware-min-badge
OrbisK a3d2141
feat: add nightly keyword to display unrealeased versions
OrbisK 39ad20b
fix: use unreleased as keyword and add short label
OrbisK 40dc8b5
feat: version badges shouldl link to blogs or nightly docs
OrbisK 4cb3b1c
Merge branch 'main' into feat/version-aware-min-badge
OrbisK 350f844
chore: capitalize navigation badges
OrbisK 108e5c3
Apply suggestions from code review
OrbisK 9f970e4
Apply suggestions from code review
OrbisK File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| <script setup lang="ts"> | ||
| import { NuxtLink } from '#components' | ||
| import type { BadgeProps } from '@nuxt/ui' | ||
|
|
||
| interface Props extends Omit<BadgeProps, 'label'> { | ||
| /** A version number, or one of the {@link VERSION_KEYWORDS} like `unreleased` for something without a version yet. */ | ||
| version: string | VersionKeyword | ||
| /** How far behind the latest release the requirement may be. Defaults to the whole current major. */ | ||
| tolerance?: VersionTolerance | ||
| /** Overrides the release announcement or nightly guide the badge links to. */ | ||
| to?: string | ||
| } | ||
|
|
||
| const props = withDefaults(defineProps<Props>(), { | ||
| color: 'info', | ||
| variant: 'subtle', | ||
| size: 'md', | ||
| // A page badge stays relevant for the whole major it documents | ||
| tolerance: () => ({ major: 0 }) | ||
| }) | ||
|
|
||
| const { show, label, ariaLabel, to } = useVersionBadge(() => props.version, { tolerance: () => props.tolerance }) | ||
|
|
||
| const link = computed(() => props.to ?? to.value) | ||
|
|
||
| const badgeProps = computed(() => { | ||
| const { version, tolerance, to, ...rest } = props | ||
| return rest | ||
| }) | ||
| </script> | ||
|
|
||
| <template> | ||
| <UBadge | ||
| v-if="show" | ||
| v-bind="badgeProps" | ||
| :as="link ? NuxtLink : props.as" | ||
| :to="link" | ||
| :label="label" | ||
| class="align-middle" | ||
| :class="link && 'transition-opacity hover:opacity-75'" | ||
| :aria-label="ariaLabel" | ||
| /> | ||
| </template> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import type { MaybeRefOrGetter } from 'vue' | ||
|
|
||
| export interface UseVersionBadgeOptions { | ||
| /** How far behind the latest release the requirement may be. Defaults to the whole current major. */ | ||
| tolerance?: MaybeRefOrGetter<VersionTolerance> | ||
| } | ||
|
|
||
| /** | ||
| * Decides whether a minimum Nuxt version is worth surfacing as a badge for the docs | ||
| * version currently being read — a requirement older than `tolerance` is not news anymore. | ||
| * | ||
| * `version` takes a version number or one of the {@link VERSION_KEYWORDS}, e.g. `unreleased`. | ||
| */ | ||
| export const useVersionBadge = (version: MaybeRefOrGetter<string | undefined>, options: UseVersionBadgeOptions = {}) => { | ||
| const { version: docsVersion } = useDocsVersion() | ||
| const { latest } = useDocsLatestVersion() | ||
|
|
||
| const labels = computed(() => versionBadgeLabels(toValue(version), docsVersion.value?.shortTag)) | ||
| const label = computed(() => labels.value?.label) | ||
| /** For tight spots like the sidebar — `soon` instead of `nightly v4` */ | ||
| const shortLabel = computed(() => labels.value?.shortLabel) | ||
| const ariaLabel = computed(() => labels.value?.ariaLabel) | ||
|
|
||
| const show = computed(() => satisfiesVersionTolerance(toValue(version), latest.value, toValue(options.tolerance) ?? { major: 0 })) | ||
|
|
||
| const { paths: releasePaths } = useReleaseArticlePaths() | ||
|
|
||
| /** | ||
| * Where the badge points: the nightly channel guide for a keyword, the release announcement | ||
| * for a version number. `undefined` while a release has no post yet, so the badge stays plain | ||
| * text instead of linking into a 404. | ||
| */ | ||
| const to = computed(() => { | ||
| const value = toValue(version) | ||
| if (versionKeyword(value)) return nightlyChannelPath(docsVersion.value?.path) | ||
|
|
||
| const path = versionBlogPath(value) | ||
| return path && releasePaths.value.includes(path) ? path : undefined | ||
| }) | ||
|
|
||
| return { | ||
| label, | ||
| shortLabel, | ||
| ariaLabel, | ||
| to, | ||
| show, | ||
| latest | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import { coerce, getMajor, getMinor, getPatch, isGreaterOrEqual } from 'verkit' | ||
|
|
||
| /** | ||
| * Shortcuts for a requirement that has no version number yet, mapped to how they render: | ||
| * `label` where there is room, `short` in tight spots like the sidebar. | ||
| * They stand for something unreleased, so tolerance never filters them out. | ||
| */ | ||
| export const VERSION_KEYWORDS = { | ||
| unreleased: { label: 'nightly', short: 'Soon' } | ||
| } as const | ||
|
|
||
| export type VersionKeyword = keyof typeof VERSION_KEYWORDS | ||
|
|
||
| /** The keyword `version` stands for, if it is one. */ | ||
| export function versionKeyword(version: string | undefined | null): VersionKeyword | undefined { | ||
| const value = version?.trim().toLowerCase() | ||
| return value && value in VERSION_KEYWORDS ? value as VersionKeyword : undefined | ||
| } | ||
|
|
||
| /** | ||
| * How a minimum version requirement reads as a badge — a version number as `v<version>`, | ||
| * a keyword as its label qualified by the major it applies to (`unreleased` -> `nightly v4`, | ||
| * `soon` where space is tight). The aria label always spells out the long form. | ||
| * Returns `undefined` when there is nothing to show. | ||
| */ | ||
| export function versionBadgeLabels(version: string | undefined | null, tag?: string): { label: string, shortLabel: string, ariaLabel: string } | undefined { | ||
| const value = version?.trim() | ||
| if (!value) return undefined | ||
|
|
||
| const keyword = versionKeyword(value) | ||
| const label = keyword ? [VERSION_KEYWORDS[keyword].label, tag].filter(Boolean).join(' ') : `v${value}` | ||
| const shortLabel = keyword ? VERSION_KEYWORDS[keyword].short : label | ||
|
|
||
| return { label, shortLabel, ariaLabel: `Minimum Nuxt Version: ${label}` } | ||
| } | ||
|
|
||
| /** | ||
| * Where the release announcement for a version lives, e.g. `4.5.2` -> `/blog/v4-5`. | ||
| * A major release drops the minor (`4.0.0` -> `/blog/v4`), matching how the posts are named. | ||
| * The path is derived, not verified — check it against the blog collection before linking. | ||
| */ | ||
| export function versionBlogPath(version: string | undefined | null): string | undefined { | ||
| const target = coerce(version?.trim() ?? '') | ||
| if (!target) return undefined | ||
|
|
||
| const minor = getMinor(target) | ||
|
|
||
| return `/blog/v${getMajor(target)}${minor ? `-${minor}` : ''}` | ||
| } | ||
|
|
||
| /** The nightly release channel guide for a docs version, e.g. `/docs/4.x` -> how to install nightly. */ | ||
| export function nightlyChannelPath(docsPath: string | undefined): string | undefined { | ||
| if (!docsPath?.startsWith('/docs')) return undefined | ||
|
|
||
| return `${docsPath}/guide/going-further/nightly-release-channel` | ||
| } | ||
|
|
||
| /** | ||
| * How far *behind* the latest release a version may be and still be worth surfacing. | ||
| * Tolerance only ever looks backwards — versions newer than the latest release | ||
| * (unreleased minors, the next major) are always accepted. | ||
| * | ||
| * The finest given level sets the granularity, everything below it is zeroed: | ||
| * - `{ major: 0 }` keeps the whole current major | ||
| * - `{ major: 1 }` keeps the previous major too | ||
| * - `{ minor: 1 }` keeps the current and previous minor only | ||
| * - `{ patch: 2 }` keeps the current minor from two patches back | ||
| */ | ||
| export interface VersionTolerance { | ||
| major?: number | ||
| minor?: number | ||
| patch?: number | ||
| } | ||
|
|
||
| /** | ||
| * Lowest version still within `tolerance` of `latest`, | ||
| * e.g. `4.4.0` for latest `4.5.2` with `{ minor: 1 }`. | ||
| */ | ||
| export function versionThreshold(latest: string | undefined | null, tolerance: VersionTolerance = {}): string | undefined { | ||
| const release = coerce(latest ?? '') | ||
| if (!release) return undefined | ||
|
|
||
| const { major, minor, patch } = tolerance | ||
| const back = (value: number, offset = 0) => Math.max(0, value - offset) | ||
|
|
||
| return [ | ||
| back(getMajor(release), major), | ||
| minor !== undefined ? back(getMinor(release), minor) : patch !== undefined ? getMinor(release) : 0, | ||
| patch !== undefined ? back(getPatch(release), patch) : 0 | ||
| ].join('.') | ||
| } | ||
|
|
||
| /** | ||
| * Whether `version` is at or above the tolerated threshold, which means: | ||
| * - a keyword like `unreleased` -> always `true`, it describes a version that has no number yet | ||
| * - newer than `latest` -> always `true` | ||
| * - older than `latest` -> `true` while it is within `tolerance` | ||
| * - unparseable input -> `false` | ||
| */ | ||
| export function satisfiesVersionTolerance(version: string | undefined | null, latest: string | undefined | null, tolerance: VersionTolerance = {}): boolean { | ||
| if (versionKeyword(version)) return true | ||
|
|
||
| const target = coerce(version?.trim() ?? '') | ||
| const threshold = versionThreshold(latest, tolerance) | ||
| if (!target || !threshold) return false | ||
|
|
||
| return isGreaterOrEqual(target, threshold) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@HugoRCD if you want to add some classes to the navigation badge, you can do this here