Gave the docs steroids. sorry for the big pr. (#4892)

* Scaffold Astro + Starlight docs site

Bootstraps a new docs-astro project to replace the Hugo-based docs,
using Astro's Starlight framework with the content collection schema
and sidebar navigation configured for WinUtil's docs structure.

* Add WinUtil-branded Starlight theme

Restyles Starlight's default look with a dark-by-default grayscale
palette and WinUtil's brand blue (#0567ff, from the app logo) as the
single accent, Geist for UI text, and JetBrains Mono for code. Also
overrides the default theme provider so first-time visitors land on
dark mode instead of following OS preference.

* Add custom Hero and CornerCard components

Hero overrides Starlight's default hero with a full-bleed grid/glow
background, a browser-chrome-framed screenshot, and a badge row driven
by frontmatter data. CornerCard is a bordered feature card with corner
brackets, used for the landing page's feature grid.

* Migrate docs content from Hugo to Astro/Starlight

Ports the landing page, user guide sections, FAQ, known issues,
contributing guide, and a sample generated tweak reference page from
the Hugo site, converting Hugo shortcodes and GFM alert syntax to
their Starlight/MDX equivalents.

* Use Windows-style caption buttons in hero window chrome

Swap the macOS traffic-light dots for a minimize/maximize/close
button group, since WinUtil is a Windows tool.

* Use Windows-style caption glyph for terminal code blocks

Replace Expressive Code's default macOS dots on terminal-framed code
blocks with a right-aligned Windows minimize/maximize/close icon,
matching the hero window chrome.

* Archive the Hugo docs site as docs-old

* Promote Astro/Starlight docs from docs-astro to docs

* Show the launch command as a copyable code block on the docs homepage

* Add docs codeowner for seanh1995

* Register custom Header component for Starlight docs site

* Add custom navbar links to Astro docs, matching the old Hugo site's top nav

* Point dev docs generator at the Astro/Starlight docs site

Output moves from docs/content/dev (Hugo) to
docs/src/content/docs/code-reference (Astro/Starlight): .mdx instead
of .md, Starlight-style title="..." code fence labels instead of
Hugo's filename/linenos shortcode, and a ":::note" aside linking back
to each entry's source file. Frontmatter description is now pulled
from the JSON Description field. Also fixes a pre-existing bug where
the embedded JSON snippets were always missing their own closing
brace.

* Add seanh1995 as codeowner for the dev docs generator

* Wire up Code Reference section in docs sidebar

Adds an Architecture & Design page plus autogenerated Tweaks/Features
Reference groups pointing at docs/src/content/docs/code-reference, and
fixes the editLink base URL to the promoted docs/ path.

* Port architecture doc to code-reference and drop stale hyperv sample

Moves the Hugo-era architecture doc into
docs/src/content/docs/code-reference/architecture.mdx: drops the
Hugo-only weight/toc frontmatter, converts the embedded code fences to
Starlight's title="..." syntax, and repoints the "Related
Documentation" links at this site's actual slugs. Also removes the
hand-written reference/tweaks/hyperv.mdx placeholder now that the
generator produces the real page under code-reference/features.

* Keep pre-conversion backup of devdocs-generator.ps1

Snapshot of the script before it was pointed at the Astro/Starlight
docs site, for reference.

* updated workflow

* Update CODEOWNERS

* Hide edit-page link on the docs landing page

The splash-template landing page isn't a source doc meant to be edited
via GitHub like the rest of the guides, so skip showing the link.

* Add site footer with copyright line, matching the old Hugo docs

The Hugo site rendered "© {year} Chris Titus Tech. All rights
reserved." in its footer; Starlight's default footer had no
equivalent. Override it to append the same copyright line below the
existing edit-link/pagination row, and collapse that row entirely
when it has nothing in it (e.g. pages with editUrl disabled and no
prev/next) instead of leaving an empty gap.

* Fix vertical alignment and size of the arrow icon in hero/CTA buttons

The right-arrow icon read as floating above the button label's
baseline. Root cause was partly a genuine optical mismatch (fixed with
a small position nudge scoped to just the arrow icon, so it doesn't
also shift the unaffected GitHub icon) and partly the final CTA's copy
getting wrapped in a <p> by MDX's markdown parser, which behaved
slightly differently under the flex layout than the Hero component's
plain text node. Switching the CTA button's label to a JS string
expression avoids the wrapper and keeps both buttons' markup, and
rendering, identical.

* Use the dark fork-button screenshot in the contributing guide

Drop the unused light-mode variant and point the guide at
Fork-Button-Dark.png instead.

* Remove old Hugo docs site and pre-conversion backup files

The docs have moved to the Astro/Starlight site; the Hugo site
(docs-old/), its workflow backup, and the devdocs-generator.ps1
pre-conversion snapshot are no longer needed.

* keeping ai happy

* Bump sharp to 0.35.3 in docs site
This commit is contained in:
Sean (ANGRYxScotsman)
2026-07-31 15:29:00 -05:00
committed by GitHub
parent 5f37ef4a80
commit 6f0629207a
178 changed files with 9165 additions and 4396 deletions
+123
View File
@@ -0,0 +1,123 @@
---
// Feature card with small corner brackets instead of a shadow/colored border.
import { Icon } from '@astrojs/starlight/components';
import type { ComponentProps } from 'astro/types';
interface Props {
icon: ComponentProps<typeof Icon>['name'];
title: string;
}
const { icon, title } = Astro.props;
---
<div class="wu-corner-card">
<span class="wu-corner-marks" aria-hidden="true">
<span class="mark top-left" /><span class="mark top-right" /><span class="mark bottom-left" /><span
class="mark bottom-right"
/>
</span>
<div class="wu-corner-card-icon">
<Icon name={icon} size="1.25rem" />
</div>
<h3><slot name="title">{title}</slot></h3>
<div class="wu-corner-card-desc"><slot /></div>
</div>
<style>
@layer starlight.core {
.wu-corner-card {
position: relative;
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1.5rem;
border: 1px solid var(--sl-color-hairline-light);
background-color: var(--sl-color-bg);
}
.wu-corner-marks {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.5;
}
.mark {
position: absolute;
background: var(--sl-color-gray-4);
}
.mark.top-left,
.mark.top-right {
top: -1px;
height: 1px;
width: 0.9rem;
}
.mark.bottom-left,
.mark.bottom-right {
bottom: -1px;
height: 1px;
width: 0.9rem;
}
.mark.top-left,
.mark.bottom-left {
left: -1px;
}
.mark.top-right,
.mark.bottom-right {
right: -1px;
}
.mark.top-left::after,
.mark.top-right::after,
.mark.bottom-left::after,
.mark.bottom-right::after {
content: '';
position: absolute;
background: var(--sl-color-gray-4);
width: 1px;
height: 0.9rem;
}
.mark.top-left::after,
.mark.top-right::after {
top: 0;
}
.mark.bottom-left::after,
.mark.bottom-right::after {
bottom: 0;
}
.mark.top-left::after,
.mark.bottom-left::after {
left: 0;
}
.mark.top-right::after,
.mark.bottom-right::after {
right: 0;
}
.wu-corner-card-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: var(--wu-radius-sm);
background-color: var(--sl-color-gray-6);
color: var(--sl-color-accent);
}
.wu-corner-card h3 {
margin: 0;
font-size: var(--sl-text-lg);
font-weight: 600;
color: var(--sl-color-white);
}
.wu-corner-card-desc {
font-size: var(--sl-text-sm);
line-height: var(--sl-line-height);
color: var(--sl-color-gray-3);
}
.wu-corner-card-desc :global(p) {
margin: 0;
}
}
</style>
+87
View File
@@ -0,0 +1,87 @@
---
import EditLink from 'virtual:starlight/components/EditLink';
import LastUpdated from 'virtual:starlight/components/LastUpdated';
import Pagination from 'virtual:starlight/components/Pagination';
import config from 'virtual:starlight/user-config';
import { Icon } from '@astrojs/starlight/components';
const year = new Date().getFullYear();
---
<footer class="sl-flex">
<div class="meta sl-flex">
<EditLink />
<LastUpdated />
</div>
<Pagination />
{
config.credits && (
<a class="kudos sl-flex" href="https://starlight.astro.build">
<Icon name={'starlight'} /> {Astro.locals.t('builtWithStarlight.label')}
</a>
)
}
<div class="site-footer-copyright">© {year} <a href="https://christitus.com">Chris Titus Tech</a>. All rights reserved.</div>
</footer>
<style>
@layer starlight.core {
footer {
flex-direction: column;
gap: 1.5rem;
}
.meta {
gap: 0.75rem 3rem;
justify-content: space-between;
flex-wrap: wrap;
margin-top: 3rem;
font-size: var(--sl-text-sm);
color: var(--sl-color-gray-3);
}
.meta > :global(p:only-child) {
margin-inline-start: auto;
}
.kudos {
align-items: center;
gap: 0.5em;
margin: 1.5rem auto;
font-size: var(--sl-text-xs);
text-decoration: none;
color: var(--sl-color-gray-3);
}
.kudos:hover {
color: var(--sl-color-white);
}
}
@layer starlight.components {
.kudos :global(svg) {
color: var(--sl-color-orange);
}
}
/* Collapse the meta/pagination rows entirely when they have nothing in them
(e.g. editUrl: false and no prev/next), instead of leaving an empty gap. */
footer .meta:empty,
footer :global(.pagination-links):empty {
display: none;
margin: 0;
}
/* Site-wide copyright line, matching the previous Hugo docs site's footer.
The divider above it comes from Starlight's own .content-panel border. */
.site-footer-copyright {
font-size: var(--sl-text-xs);
color: var(--sl-color-gray-3);
}
.site-footer-copyright a {
color: inherit;
text-decoration: underline;
}
.site-footer-copyright a:hover {
color: var(--sl-color-white);
}
</style>
+178
View File
@@ -0,0 +1,178 @@
---
import config from 'virtual:starlight/user-config';
import LanguageSelect from 'virtual:starlight/components/LanguageSelect';
import Search from 'virtual:starlight/components/Search';
import SiteTitle from 'virtual:starlight/components/SiteTitle';
import SocialIcons from 'virtual:starlight/components/SocialIcons';
import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
/**
* Render the `Search` component if Pagefind is enabled or the default search component has been overridden.
*/
const shouldRenderSearch =
config.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro';
---
<div class="header">
<div class="title-wrapper sl-flex">
<SiteTitle />
</div>
<div class="sl-flex print:hidden">
{shouldRenderSearch && <Search />}
</div>
<div class="sl-hidden md:sl-flex print:hidden right-group">
<nav class="site-nav" aria-label="Top">
<a class="site-nav-link" href="/guides/">User Guides</a>
<div class="site-nav-dropdown">
<button type="button" class="site-nav-link" aria-haspopup="true">Documentation</button>
<div class="site-nav-menu">
<a href="/contributing/">Contribution Guides</a>
<a href="/code-reference/architecture/">Developer Docs</a>
</div>
</div>
<div class="site-nav-dropdown">
<button type="button" class="site-nav-link" aria-haspopup="true">Help</button>
<div class="site-nav-menu">
<a href="/faq/">FAQ</a>
<a href="/known-issues/">Known Issues</a>
<a href="https://forum.christitus.com/" target="_blank" rel="noreferrer">Forums</a>
</div>
</div>
<a class="site-nav-link" href="https://christitus.com/downloads/" target="_blank" rel="noreferrer">Store</a>
</nav>
<div class="sl-flex social-icons">
<SocialIcons />
</div>
<ThemeSelect />
<LanguageSelect />
</div>
</div>
<style>
@layer starlight.core {
.header {
display: flex;
gap: var(--sl-nav-gap);
justify-content: space-between;
align-items: center;
height: 100%;
}
.title-wrapper {
/* Prevent long titles overflowing and covering the search and menu buttons on narrow viewports. */
overflow: clip;
/* Avoid clipping focus ring around title wrapper. */
padding: 0.25rem;
margin: -0.25rem;
min-width: 0;
}
.right-group,
.social-icons {
gap: 1rem;
align-items: center;
}
.social-icons::after {
content: '';
height: 2rem;
border-inline-end: 1px solid var(--sl-color-hairline-light);
}
@media (min-width: 50rem) {
:global(:root[data-has-sidebar]) {
--__sidebar-pad: calc(2 * var(--sl-nav-pad-x));
}
:global(:root:not([data-has-toc])) {
--__toc-width: 0rem;
}
.header {
--__sidebar-width: max(0rem, var(--sl-content-inline-start, 0rem) - var(--sl-nav-pad-x));
--__main-column-fr: calc(
(
100% + var(--__sidebar-pad, 0rem) - var(--__toc-width, var(--sl-sidebar-width)) -
(2 * var(--__toc-width, var(--sl-nav-pad-x))) - var(--sl-content-inline-start, 0rem) -
var(--sl-content-width)
) / 2
);
display: grid;
grid-template-columns:
/* 1 (site title): runs up until the main content columns left edge or the width of the title, whichever is the largest */
minmax(
calc(var(--__sidebar-width) + max(0rem, var(--__main-column-fr) - var(--sl-nav-gap))),
auto
)
/* 2 (search box): all free space that is available. */
1fr
/* 3 (right items): use the space that these need. */
auto;
align-content: center;
}
}
}
/* Top nav links + dropdowns, matching the previous Hugo docs site's navbar. */
.site-nav {
display: flex;
align-items: center;
gap: 1.25rem;
padding-inline-end: 1.25rem;
border-inline-end: 1px solid var(--sl-color-hairline-light);
}
.site-nav-link {
font-size: var(--sl-text-sm);
font-weight: 500;
color: var(--sl-color-gray-2);
background: none;
border: none;
padding: 0;
font-family: inherit;
cursor: pointer;
white-space: nowrap;
text-decoration: none;
}
.site-nav-link:hover {
color: var(--sl-color-white);
}
.site-nav-dropdown {
position: relative;
/* Extends the dropdown's own hoverable box down to meet the menu, so the
cursor never crosses a dead zone between the trigger and the menu. */
padding-bottom: 0.75rem;
margin-bottom: -0.75rem;
}
.site-nav-menu {
display: none;
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
min-width: 11rem;
padding: 0.5rem;
flex-direction: column;
gap: 0.125rem;
background: var(--sl-color-bg-nav);
border: 1px solid var(--sl-color-hairline-light);
border-radius: var(--wu-radius-sm, 0.5rem);
box-shadow: var(--sl-shadow-lg);
z-index: 10;
}
.site-nav-dropdown:hover .site-nav-menu,
.site-nav-dropdown:focus-within .site-nav-menu {
display: flex;
}
.site-nav-menu a {
padding: 0.4rem 0.6rem;
border-radius: var(--wu-radius-sm, 0.5rem);
font-size: var(--sl-text-sm);
color: var(--sl-color-gray-2);
text-decoration: none;
white-space: nowrap;
}
.site-nav-menu a:hover {
color: var(--sl-color-white);
background: var(--sl-color-gray-6);
}
</style>
+294
View File
@@ -0,0 +1,294 @@
---
// Custom hero override: eyebrow, heading, tagline, actions, badges, screenshot.
import { Image } from 'astro:assets';
import { Icon } from '@astrojs/starlight/components';
// Matches Starlight's internal PAGE_TITLE_ID (used by the skip-to-content link).
const PAGE_TITLE_ID = '_top';
const { data } = Astro.locals.starlightRoute.entry;
const { title = data.title, tagline, image, actions = [] } = data.hero || {};
const { heroEyebrow, heroCaption, heroBadges = [] } = data;
let heroImage: ImageMetadata | undefined;
if (image && 'file' in image) heroImage = image.file;
---
<section class="wu-hero">
<div class="wu-hero-grid" aria-hidden="true"></div>
<div class="wu-hero-glow" aria-hidden="true"></div>
<div class="wu-hero-inner">
<div class="wu-hero-copy">
{heroEyebrow && <p class="wu-eyebrow">{heroEyebrow}</p>}
<h1 id={PAGE_TITLE_ID} data-page-title set:html={title} />
{tagline && <p class="wu-hero-tagline" set:html={tagline} />}
{
actions.length > 0 && (
<div class="wu-hero-actions">
{actions.map(({ icon, link: href, text, variant = 'primary' }) => (
<a href={href} class:list={['wu-btn', variant === 'primary' ? 'wu-btn-primary' : 'wu-btn-secondary']}>
{text}
{icon?.name && (
<Icon
name={icon.name}
size="1.5em"
class:list={[(icon.name === 'right-arrow' || icon.name === 'left-arrow') && 'wu-icon-arrow']}
/>
)}
</a>
))}
</div>
)
}
{heroCaption && <p class="wu-hero-caption">{heroCaption}</p>}
</div>
{
heroImage && (
<div class="wu-hero-media">
<div class="wu-window">
<div class="wu-window-bar">
<span class="wu-window-title">winutil.ps1</span>
<div class="wu-caption-buttons">
<span class="wu-cap-btn wu-cap-min" aria-hidden="true">
<svg viewBox="0 0 10 10"><path d="M0 5h10" /></svg>
</span>
<span class="wu-cap-btn wu-cap-max" aria-hidden="true">
<svg viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" /></svg>
</span>
<span class="wu-cap-btn wu-cap-close" aria-hidden="true">
<svg viewBox="0 0 10 10"><path d="M0 0l10 10M10 0L0 10" /></svg>
</span>
</div>
</div>
<Image src={heroImage} alt={image?.alt || ''} width={1200} loading="eager" decoding="async" />
</div>
</div>
)
}
</div>
{
heroBadges.length > 0 && (
<div class="wu-hero-bottom">
<div class="wu-badges">
{heroBadges.map(({ src, alt, href }) =>
href ? (
<a href={href}>
<img src={src} alt={alt} />
</a>
) : (
<img src={src} alt={alt} />
)
)}
</div>
</div>
)
}
</section>
<style>
@layer starlight.core {
/* Outer hero shell; pulled up to close Starlight's fixed 1.5rem ContentPanel padding. */
.wu-hero {
position: relative;
border-bottom: 1px solid var(--sl-color-hairline-light);
margin-inline: calc(-1 * var(--sl-content-pad-x));
padding-inline: var(--sl-content-pad-x);
margin-top: -1.5rem;
padding-top: 1.5rem;
}
/* Full-bleed grid/glow background, forced behind all hero content via negative z-index. */
.wu-hero-grid,
.wu-hero-glow {
position: absolute;
z-index: -1;
top: 0;
bottom: 0;
left: 50%;
width: 100vw;
margin-left: -50vw;
pointer-events: none;
}
.wu-hero-grid {
background-image:
linear-gradient(90deg, var(--sl-color-hairline-light) 1px, transparent 1px),
linear-gradient(var(--sl-color-hairline-light) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: radial-gradient(at top, #000 60%, transparent 100%);
-webkit-mask-image: radial-gradient(at top, #000 60%, transparent 100%);
opacity: 0.6;
}
.wu-hero-glow {
background: radial-gradient(
ellipse at center,
hsl(var(--wu-accent-h) 100% 61% / 0.14) 0%,
hsl(var(--wu-accent-h) 100% 61% / 0.05) 28%,
transparent 68%
);
}
/* Two-column layout: copy left, screenshot right (stacked on mobile). */
.wu-hero-inner {
position: relative;
z-index: 1;
display: grid;
gap: 3rem;
padding-block: 3rem 2rem;
}
.wu-hero-copy {
text-align: center;
}
.wu-eyebrow {
margin: 0 0 1rem;
font-size: var(--sl-text-sm);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--sl-color-accent);
}
.wu-hero-copy h1 {
margin: 0;
font-size: clamp(var(--sl-text-4xl), calc(0.25rem + 5vw), var(--sl-text-6xl));
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.02em;
color: var(--sl-color-white);
}
.wu-hero-tagline {
margin: 1.5rem auto 0;
max-width: 40rem;
font-size: var(--sl-text-lg);
line-height: var(--sl-line-height);
color: var(--sl-color-gray-2);
}
.wu-hero-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.75rem;
margin-top: 2.5rem;
}
.wu-hero-caption {
margin: 1.5rem 0 0;
font-size: var(--sl-text-sm);
color: var(--sl-color-gray-3);
}
/* Screenshot framed in a fake browser window (traffic-light dots + title bar). */
.wu-hero-media {
display: flex;
justify-content: center;
}
.wu-window {
width: 100%;
max-width: 56rem;
overflow: hidden;
border-radius: var(--wu-radius-lg);
border: 1px solid var(--sl-color-hairline-light);
background: var(--sl-color-gray-6);
box-shadow: 0 25px 50px -12px hsl(0 0% 0% / 0.5);
}
.wu-window-bar {
display: flex;
align-items: center;
gap: 0.4rem;
padding-inline-start: 0.85rem;
border-bottom: 1px solid var(--sl-color-hairline-light);
background: var(--sl-color-black);
}
.wu-window-title {
font-size: var(--sl-text-xs);
color: var(--sl-color-gray-3);
flex: 1;
}
.wu-caption-buttons {
display: flex;
height: 2.1rem;
}
.wu-cap-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 100%;
color: var(--sl-color-gray-3);
}
.wu-cap-btn svg {
width: 0.65rem;
height: 0.65rem;
fill: none;
stroke: currentColor;
stroke-width: 1;
}
.wu-cap-max svg {
fill: none;
}
.wu-cap-close svg {
fill: none;
}
.wu-cap-btn:hover {
background: var(--sl-color-gray-5);
}
.wu-cap-close:hover {
background: #e81123;
color: white;
}
.wu-window :global(img) {
display: block;
width: 100%;
height: auto;
}
/* Badge row below the two-column layout, centered full-width. */
.wu-hero-bottom {
position: relative;
z-index: 1;
display: flex;
justify-content: center;
padding-bottom: 2.5rem;
}
.wu-hero-bottom .wu-badges {
margin: 0;
}
/* Desktop breakpoint: side-by-side columns, left-aligned copy. */
@media (min-width: 50rem) {
.wu-hero-inner {
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
align-items: center;
gap: 2rem;
padding-block: 5rem 3rem;
}
.wu-hero-copy {
text-align: left;
}
.wu-hero-tagline {
margin-inline: 0;
}
.wu-hero-actions {
justify-content: flex-start;
}
.wu-hero-bottom {
padding-bottom: 3rem;
}
}
}
</style>
+36
View File
@@ -0,0 +1,36 @@
---
// Overrides Starlight's default ThemeProvider: dark by default regardless of OS preference, unless the visitor has explicitly picked a theme before.
import { Icon } from '@astrojs/starlight/components';
---
<script is:inline>
window.StarlightThemeProvider = (() => {
const storedTheme =
typeof localStorage !== 'undefined' && localStorage.getItem('starlight-theme');
const theme = storedTheme || 'dark';
document.documentElement.dataset.theme = theme === 'light' ? 'light' : 'dark';
return {
updatePickers(theme = storedTheme || 'auto') {
document.querySelectorAll('starlight-theme-select').forEach((picker) => {
const select = picker.querySelector('select');
if (select) select.value = theme;
/** @type {HTMLTemplateElement | null} */
const tmpl = document.querySelector(`#theme-icons`);
const newIcon = tmpl && tmpl.content.querySelector('.' + theme);
if (newIcon) {
const oldIcon = picker.querySelector('svg.label-icon');
if (oldIcon) {
oldIcon.replaceChildren(...newIcon.cloneNode(true).childNodes);
}
}
});
},
};
})();
</script>
<template id="theme-icons">
<Icon name="sun" class="light" />
<Icon name="moon" class="dark" />
<Icon name="laptop" class="auto" />
</template>