mirror of
https://github.com/ChrisTitusTech/winutil.git
synced 2026-08-10 01:51:18 +10:00
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:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 column’s 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user