diff --git a/.gitignore b/.gitignore index 94470435ddd8..fe9ca0bfc4d1 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ static/js/homepage.*.js static/js/marketing.*.js static/js/consent-manager.*.js static/js/marketing-homepage.*.js +static/js/header-nav.*.js data/js_manifest.json data/openapi-spec.json diff --git a/BUILD-AND-DEPLOY.md b/BUILD-AND-DEPLOY.md index 5392d7f64474..a2833c5bb64c 100644 --- a/BUILD-AND-DEPLOY.md +++ b/BUILD-AND-DEPLOY.md @@ -109,7 +109,7 @@ make clean # Remove build artifacts and dependencies │ 1. Asset Compilation (Webpack, Tailwind CSS) │ │ 2. Documentation Generation (TypeDoc, Sphinx, CLI) │ │ 3. Hugo Build (Markdown → HTML) │ -│ 4. CSS Optimization (PurgeCSS, CSSNano) │ +│ 4. CSS Minification (cssnano) │ │ 5. Search Index Generation (Algolia) │ └──────────────────┬───────────────────────────────────────────┘ │ @@ -621,10 +621,9 @@ Location: `theme/webpack.config.js` { bundle: './src/ts/main.ts', // Main site JavaScript marketing: './src/ts/marketing.ts', // Marketing pages - 'marketing-homepage': './src/ts/marketingHomepage.ts', // Marketing homepage - homepage: './src/ts/homepage.ts', // Homepage-specific JS algolia: './src/ts/algolia-entry.ts', // Search (Algolia) 'consent-manager': './src/ts/consent-manager/index.ts', // Cookie consent (vanilla TS) + 'header-nav': './src/ts/header-nav.ts', // Site header navigation } ``` @@ -636,10 +635,9 @@ Async chunks use a similar pattern: `chunk-[contenthash:8].js`. ``` static/js/bundle..js static/js/marketing..js -static/js/marketing-homepage..js -static/js/homepage..js static/js/algolia..js static/js/consent-manager..js +static/js/header-nav..js static/js/chunk-.js assets/css/bundle.css assets/css/marketing.css @@ -676,18 +674,15 @@ Multi-stage CSS optimization pipeline: - Autoprefixer for browser compatibility - Custom PostCSS plugins -3. **PurgeCSS** (Production Only) - - Scans: `public/**/*.html`, `public/js/bundle.*.js` - - Removes unused CSS classes - - Safelist patterns: - - `hs-*` (HubSpot) - - `highlight`, `code-*` (syntax highlighting) - - `carousel`, `pagination` (UI components) - - `st-*` (ShareThis) - - `icon-*`, `pulumi-*` (custom icons) - - Skips: `azure-native-v1/**/*` (prevents OOM) - -4. **CSSNano Minification** +3. **Unused-class purging** is handled by Tailwind v4 itself via `@source` + directives in `theme/src/scss/main.scss` and `_marketing.scss` — Tailwind only + emits utilities it finds in the scanned files. PurgeCSS used to run as a + second pass but was removed: it is incompatible with Tailwind v4's nested-CSS + variant output (`.foo { &:hover { ... } }`) and silently dropped every + `hover:`, `focus:`, `focus-visible:`, `space-y-*`, and `data-[...]` + arbitrary-variant rule. + +4. **CSSNano Minification** (Production Only, via `scripts/minify-css.js`) - Removes whitespace and comments - Optimizes declarations - Merges rules @@ -3500,7 +3495,7 @@ All Dependabot PRs automatically receive: **Packages:** - **Webpack Ecosystem:** `webpack*`, `*-loader`, `*-webpack-plugin*` -- **CSS Processing:** `postcss*`, `sass*`, `cssnano`, `autoprefixer`, `@fullhuman/postcss-purgecss`, `tailwindcss` +- **CSS Processing:** `postcss*`, `sass*`, `cssnano`, `autoprefixer`, `tailwindcss` - **TypeScript:** `typescript` - **Pulumi:** `@pulumi/*` - **AWS SDK:** `@aws-sdk/*` (Lambda@Edge risk) @@ -3776,12 +3771,7 @@ ls -lh public/css/ **Optimization Techniques:** -1. **PurgeCSS Configuration** - - Review safelist patterns - - Remove unused components - - Optimize Tailwind config - -2. **Scope Tailwind's content scan** +1. **Scope Tailwind's content scan** Tailwind v4 tree-shakes by default — it only emits CSS for classes it detects in scanned files. If the bundle is larger than expected, narrow the scan with @@ -3796,7 +3786,7 @@ ls -lh public/css/ Avoid broad globs like `../../**` that pull in `node_modules` or generated files — these inflate the detected class list and slow builds. -3. **Code Splitting** +2. **Code Splitting** - Separate critical CSS - Load non-critical CSS async @@ -3861,7 +3851,6 @@ find static -type f \( -name "*.png" -o -name "*.jpg" \) -size +500k /js/consent-manager.*.js: 1 year /js/algolia.*.js: 1 year /js/homepage.*.js: 1 year -/js/marketing-homepage.*.js: 1 year /js/marketing.*.js: 1 year /fonts/*: 1 year /fingerprinted/*: 1 year diff --git a/cypress/e2e/site.cy.js b/cypress/e2e/site.cy.js index 576a09f826f3..9a7657dc0e73 100644 --- a/cypress/e2e/site.cy.js +++ b/cypress/e2e/site.cy.js @@ -20,28 +20,30 @@ describe("www.pulumi.com", () => { describe("home page", () => { beforeEach(() => { + // Force a desktop viewport so the new nav's desktop affordances render + // (the nav-desktop breakpoint is 1200px). + cy.viewport(1280, 800); cy.visit("/"); }); it("loads and applies CSS", () => { // Checking the computed background-color value validates that the CSS bundle // was properly loaded and applied. - cy.get(".header-container") + cy.get("header.sticky") .invoke("css", "background-color") .should("equal", "rgb(255, 255, 255)"); }); it("loads and applies JavaScript", () => { - // Checking the carousel validates that the JS bundle was loaded and applied - // (excluding Stencil components, which are bundled separately). - cy.get(".header-container") - .should("not.have.class", "is-pinned"); - - cy.wait(6000) - cy.scrollTo(0, 250); - - cy.get(".header-container") - .should("have.class", "is-pinned"); + // Clicking a nav trigger button toggles aria-expanded via the header-nav + // bundle's click handler — proves that bundle was loaded and ran. + // force: true bypasses Cypress's pointer-events actionability check, which + // can flake in CI when the overlay's data-attribute Tailwind variant hasn't + // applied yet. The click handler is still exercised end-to-end. + cy.get("[data-nav-trigger-button]").first() + .should("have.attr", "aria-expanded", "false") + .click({ force: true }) + .should("have.attr", "aria-expanded", "true"); }); }); diff --git a/data/header_nav.yaml b/data/header_nav.yaml new file mode 100644 index 000000000000..306d7cf3ee9b --- /dev/null +++ b/data/header_nav.yaml @@ -0,0 +1,159 @@ +# A single ordered list. An item with `sections:` is rendered as a dropdown; +# an item with `href:` is rendered as a plain link. +items: + - label: Product + wide: true + sections: + - heading: Core product + columns: 2 + items: + - label: Platform overview + href: /product/ + description: Everything platform engineering teams need to build, secure, and scale cloud infrastructure + icon: cloud-arrow-up + track: header-product-platform + - label: Infrastructure as code + href: /product/infrastructure-as-code/ + description: "IaC for any cloud, in any language — Node.js, Python, Go, .NET, Java, and YAML" + icon: custom/pulumi-iac + track: header-product-iac + - heading: Key capabilities + columns: 2 + items: + - label: Agentic infrastructure + href: /product/neo/ + description: Meet Neo, our AI-powered infrastructure engineering agent + icon: custom/pulumi-neo + iconWeight: fill + track: header-product-neo + - label: Secrets & configuration + href: /product/secrets-management/ + description: Environments, secrets, and configuration management + icon: custom/pulumi-secrets + track: header-product-secrets + - label: Insights & governance + href: /product/insights-governance/ + description: Asset management, compliance remediation, and AI insights over the cloud + icon: custom/pulumi-insights + track: header-product-insights + - label: Internal developer platform + href: /product/internal-developer-platforms/ + description: The fastest, most secure way to deliver cloud infrastructure + icon: custom/pulumi-idp + track: header-product-idp + + - label: For engineers + wide: true + sections: + - columns: 2 + items: + - label: Get started + href: /docs/get-started/ + description: Follow a step-by-step guide to quickly learn Pulumi + icon: rocket-launch + track: header-engineers-get-started + - label: Documentation + href: /docs/ + description: Complete guides and API references + icon: books + track: header-engineers-docs + - label: Registry + href: /registry/ + description: Browse 170+ cloud providers and packages + icon: package + track: header-engineers-registry + - label: Templates + href: /templates/ + description: Deploy common architectures on any cloud + icon: grid-four + track: header-engineers-templates + - label: Tutorials + href: /learn/ + description: Get hands-on with Pulumi concepts + icon: graduation-cap + track: header-engineers-tutorials + - label: Events and workshops + href: /events#upcoming + description: Live sessions and workshops + icon: calendar-blank + track: header-engineers-events + - label: Community + href: /community/ + description: Join 10k+ developers on Slack + icon: users-three + track: header-engineers-community + - label: Engineers love Pulumi + href: /testimonials/ + description: Hear from engineers why they love us + icon: heart + track: header-engineers-testimonials + - label: Pulumi guides + href: /guides/ + description: Practical infrastructure patterns for cloud providers + icon: compass + track: header-engineers-guides + + - label: For enterprises + sections: + - items: + - label: Enterprise solutions + href: /enterprise/ + description: Security, compliance, and support for teams + icon: buildings + track: header-enterprise-solutions + - label: Case studies + href: /case-studies/ + description: How Snowflake, Mercedes-Benz, and others use Pulumi + icon: article + track: header-enterprise-case-studies + - label: Request a demo + href: /request-a-demo/ + description: See how Pulumi can help your team + icon: chalkboard-teacher + track: header-enterprise-demo + - label: Professional services + href: /proserv/ + description: Get expert help with your implementation + icon: briefcase + track: header-enterprise-proserv + - label: Contact sales + href: /contact/ + description: Talk to our team about your needs + icon: chat-circle + track: header-enterprise-contact + + - { label: Docs, href: /docs/, track: header-docs } + - { label: Registry, href: /registry/, track: header-registry } + - { label: Blog, href: /blog/, track: header-blog } + - { label: Pricing, href: /pricing/, track: header-pricing } + + - label: Company + sections: + - items: + - label: About us + href: /about/ + description: Our purpose and values + icon: quotes + track: header-company-about + - label: Careers + href: /careers/ + description: Come work for Pulumi + icon: briefcase + track: header-company-careers + - label: Newsroom + href: /about/newsroom/ + description: Pulumi in the news + icon: newspaper + track: header-company-newsroom + - label: Awards + href: /awards/ + description: Recognition from press and analysts + icon: trophy + track: header-company-awards + +cta: + contact: { label: Contact us, href: "/contact/", track: header-contact } + signIn: { label: Sign in, href: "https://app.pulumi.com/signin", track: header-console } + # label and href come from site.Params.cta.primary so all site-wide + # "Get Started" CTAs can be changed in one place. + getStarted: { track: header-signup } diff --git a/infrastructure/index.ts b/infrastructure/index.ts index 3984f5d8e845..70e7ddd879b1 100644 --- a/infrastructure/index.ts +++ b/infrastructure/index.ts @@ -907,12 +907,6 @@ const distributionArgs: aws.cloudfront.DistributionArgs = { cachePolicyId: oneYearCachePolicy.id, responseHeadersPolicyId: ImmutableCachePolicy.id, }, - { - ...baseCacheBehavior, - pathPattern: "/css/marketing-homepage.*.css", - cachePolicyId: oneYearCachePolicy.id, - responseHeadersPolicyId: ImmutableCachePolicy.id, - }, { ...baseCacheBehavior, pathPattern: "/js/bundle.*.js", @@ -949,12 +943,6 @@ const distributionArgs: aws.cloudfront.DistributionArgs = { cachePolicyId: oneYearCachePolicy.id, responseHeadersPolicyId: ImmutableCachePolicy.id, }, - { - ...baseCacheBehavior, - pathPattern: "/js/marketing-homepage.*.js", - cachePolicyId: oneYearCachePolicy.id, - responseHeadersPolicyId: ImmutableCachePolicy.id, - }, { ...baseCacheBehavior, pathPattern: "/js/marketing.*.js", diff --git a/layouts/partials/assets.html b/layouts/partials/assets.html index 29c374c6c248..a19ec6f48e23 100644 --- a/layouts/partials/assets.html +++ b/layouts/partials/assets.html @@ -26,22 +26,18 @@ {{ end }} -{{ $cssBundle := .Params.css_bundle | default "bundle" }} - {{ $nonMarketingSections := slice "docs" "registry" "blog" "learn" "tutorials" "providers" "collections" "templates" "ai" "authors" "tags" "legal" }} {{ $isMarketing := not (in $nonMarketingSections .Section) }} -{{ template "css-link" (dict "source" (printf "css/%s.css" $cssBundle) "name" $cssBundle) }} +{{ template "css-link" (dict "source" "css/bundle.css" "name" "bundle") }} {{ if $isMarketing }} - {{ $marketingBundle := cond (eq $cssBundle "homepage") "marketing-homepage" "marketing" }} - {{ template "css-link" (dict "source" (printf "css/%s.css" $marketingBundle) "name" $marketingBundle) }} + {{ template "css-link" (dict "source" "css/marketing.css" "name" "marketing") }} {{ end }} diff --git a/layouts/partials/head.html b/layouts/partials/head.html index 82f338763c3e..cc4f43c05d4d 100644 --- a/layouts/partials/head.html +++ b/layouts/partials/head.html @@ -23,7 +23,7 @@ {{ if not .IsHome }} - + {{ end }} diff --git a/layouts/partials/header.html b/layouts/partials/header.html index 887c05b69a9f..c92465138c48 100644 --- a/layouts/partials/header.html +++ b/layouts/partials/header.html @@ -5,527 +5,5 @@ {{ .Scratch.Set "mode" .Section }} {{ else }} - - -
- -
+{{ partial "header/nav.html" . }} {{ end }} diff --git a/layouts/partials/header/nav-desktop.html b/layouts/partials/header/nav-desktop.html new file mode 100644 index 000000000000..4aaa72d23e1b --- /dev/null +++ b/layouts/partials/header/nav-desktop.html @@ -0,0 +1,63 @@ +{{- $nav := . -}} + diff --git a/layouts/partials/header/nav-dropdown-item.html b/layouts/partials/header/nav-dropdown-item.html new file mode 100644 index 000000000000..50acde5d6cb9 --- /dev/null +++ b/layouts/partials/header/nav-dropdown-item.html @@ -0,0 +1,25 @@ +{{- /* + Shared dropdown item link with icon, label, and optional description. + Used by both the desktop nav-popup and the mobile sheet collapsible panels. + + Parameters: + item — item object: { href, label, icon, iconWeight?, description?, track? } + variant — "desktop" or "mobile" +*/ -}} +{{- $item := .item -}} +{{- $isMobile := eq .variant "mobile" -}} + + + + {{ $item.label }} + {{ with $item.description }} + {{ . }} + {{ end }} + + diff --git a/layouts/partials/header/nav-mobile-sheet.html b/layouts/partials/header/nav-mobile-sheet.html new file mode 100644 index 000000000000..1462f613cf24 --- /dev/null +++ b/layouts/partials/header/nav-mobile-sheet.html @@ -0,0 +1,104 @@ +{{- /* + Mobile navigation sheet (overlay + slide-out panel). Rendered outside +
so the header's backdrop-filter does not create a new containing + block for these fixed-position elements. + + Parameter: the site.Data.header_nav object. +*/ -}} +{{- $nav := . -}} + +
+ + diff --git a/layouts/partials/header/nav-mobile-trigger.html b/layouts/partials/header/nav-mobile-trigger.html new file mode 100644 index 000000000000..f85f41aebe0f --- /dev/null +++ b/layouts/partials/header/nav-mobile-trigger.html @@ -0,0 +1,7 @@ +{{- $nav := . -}} + diff --git a/layouts/partials/header/nav.html b/layouts/partials/header/nav.html new file mode 100644 index 000000000000..aa76cf27de35 --- /dev/null +++ b/layouts/partials/header/nav.html @@ -0,0 +1,89 @@ +{{- $nav := site.Data.header_nav -}} + +
+ +
+ +{{ partial "header/nav-mobile-sheet.html" $nav }} + + diff --git a/layouts/partials/top-nav.html b/layouts/partials/top-nav.html deleted file mode 100644 index c1520ea70820..000000000000 --- a/layouts/partials/top-nav.html +++ /dev/null @@ -1,74 +0,0 @@ - \ No newline at end of file diff --git a/package.json b/package.json index a638db63a7e3..6daf213c0e08 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.1041.0", - "@fullhuman/postcss-purgecss": "^7.0.0", "@octokit/auth-action": "^5.1.1", "@octokit/graphql": "^8.1.1", "@octokit/rest": "^21.1.1", diff --git a/scripts/minify-css.js b/scripts/minify-css.js index 20a09136290f..9a219ac48f7c 100644 --- a/scripts/minify-css.js +++ b/scripts/minify-css.js @@ -1,60 +1,24 @@ const fs = require("fs"); const glob = require("glob"); const postcss = require("postcss"); -const { purgeCSSPlugin } = require("@fullhuman/postcss-purgecss"); const cssnano = require("cssnano"); -// Shared safelist patterns for classes that are dynamically generated or injected at runtime. -const sharedSafelist = [ - /^hs-/, - /^highlight$/, - /^pagination$/, - /^code-/, - /^copy-/, - /^carousel/, - /^st-/, - /^pulumi-/, - /^swiper/, - /^llm-menu/, -]; - -// Per-bundle PurgeCSS configuration. +// Per-bundle minification configuration. +// +// Tailwind v4 emits its variants using native CSS nesting (e.g. `.hover\:bg-gray-100 +// { &:hover { ... } }`), which PurgeCSS's selector matcher does not understand — +// running PurgeCSS over Tailwind v4 output silently drops every hover, focus, +// focus-visible, group-hover, space-y/x, and many responsive utility rules. Tailwind +// v4 already content-scans @source paths and only emits classes it finds, so PurgeCSS +// is both redundant and destructive here. We keep cssnano for minification only. const bundles = [ { name: "bundle", input: "public/css/bundle.*.css", - content: ["public/**/*.html", "public/js/bundle.*.js", "public/js/algolia.*.js", "public/js/consent-manager.*.js"], - safelist: [ - ...sharedSafelist, - /^icon-/, - /^package-details/, - /^resources-properties/, - /^tabular/, - ], - // Skip azure-native-v1 because it causes out-of-memory errors during - // PurgeCSS content scanning. No unique CSS classes originate from it. - skippedContentGlobs: ["public/registry/packages/azure-native-v1/**/*"], }, { name: "marketing", input: "public/css/marketing.*.css", - content: ["public/**/*.html", "public/js/bundle.*.js"], - safelist: [...sharedSafelist], - }, - { - // Homepage-specific marketing CSS: same source as marketing but purged - // against only the homepage HTML for a smaller bundle. The webpack entry - // produces assets/css/marketing-homepage.css for Hugo dev mode. - name: "marketing-homepage", - input: "public/css/marketing.*.css", - content: ["public/index.html", "public/js/bundle.*.js", "public/js/consent-manager.*.js"], - safelist: [...sharedSafelist], - }, - { - name: "homepage", - input: "public/css/homepage.*.css", - content: ["public/index.html", "public/js/bundle.*.js", "public/js/consent-manager.*.js"], - safelist: [...sharedSafelist], }, ]; @@ -68,24 +32,7 @@ function minifyCSS(config) { const css = fs.readFileSync(bundlePath); const outputPath = `public/css/${config.name}.${cssBundleId}.css`; - // PurgeCSS removes unused CSS by analyzing the built site files. - // https://purgecss.com/ return postcss([ - purgeCSSPlugin({ - content: config.content, - skippedContentGlobs: config.skippedContentGlobs || [], - css: [bundlePath], - safelist: { deep: config.safelist }, - - // We need to extract the Tailwind screen size selectors (e.g. sm, md, lg) - // so that we do not strip them out. As long as a class name appears in the HTML - // in its entirety, PurgeCSS will not remove it. - // Ex. https://tailwindcss.com/docs/optimizing-for-production#writing-purgeable-html - defaultExtractor: content => content.match(/[\w-/.:]+(? content.match(/[\w-/:]*[\w-/:]/g) || [], - // }), ], }; diff --git a/theme/src/scss/_api-python.scss b/theme/src/scss/_api-python.scss index 9b78a3109f8d..b24523dd7ea2 100644 --- a/theme/src/scss/_api-python.scss +++ b/theme/src/scss/_api-python.scss @@ -11,8 +11,6 @@ // the two kinds of pages that Sphinx emits: the first kind is a module page, containing // information on on members within a module, and the second kind is a table of contents. -/* purgecss start ignore */ - // Use the same icon that AnchorJS uses for anchor links. $anchorjs-font: 1em/1 anchorjs-icons; $anchorjs-content: "\e9cb"; @@ -141,4 +139,3 @@ section[id="pulumi-python-sdk"] { word-break: break-word; } } -/* purgecss end ignore */ diff --git a/theme/src/scss/_api-symbol.scss b/theme/src/scss/_api-symbol.scss index a3e1486f5f24..1b55acba77c9 100644 --- a/theme/src/scss/_api-symbol.scss +++ b/theme/src/scss/_api-symbol.scss @@ -1,6 +1,3 @@ -// need to ignore PurgeCSS here because PurgeCSS runs before the API Docs files get bundled into -// pulumi/docs/public/, so it looks like these CSS classes are unused and they get pruned -/* purgecss start ignore */ $size-small: 9px; $size-medium: 16px; $size-large: 24px; @@ -66,4 +63,3 @@ $size-large: 24px; content: "R"; } } -/* purgecss end ignore */ diff --git a/theme/src/scss/_container.scss b/theme/src/scss/_container.scss index b9cda64b257b..558527160a16 100644 --- a/theme/src/scss/_container.scss +++ b/theme/src/scss/_container.scss @@ -1,4 +1,3 @@ -/* purgecss start ignore */ @mixin container-2xl { @media (min-width: 1536px) { max-width: 1536px; @@ -16,4 +15,3 @@ @include container-2xl(); } } -/* purgecss end ignore */ diff --git a/theme/src/scss/_fonts.scss b/theme/src/scss/_fonts.scss index 0a848f1a8bba..0be1b69365ca 100644 --- a/theme/src/scss/_fonts.scss +++ b/theme/src/scss/_fonts.scss @@ -1,9 +1,9 @@ @font-face { font-family: "Monaspace Neon Var"; - font-weight: 100 900; + font-weight: 400; font-style: normal; font-display: swap; - src: url("/fonts/monaspace-neon-var.woff2") format("woff2"); + src: url("/fonts/monaspace-neon-regular.woff2") format("woff2"); } @@ -12,7 +12,7 @@ font-weight: 400; font-style: normal; font-display: swap; - src: url("/fonts/inter-regular.woff2") format("woff2"), url("/fonts/inter-regular.woff") format("woff"); + src: url("/fonts/inter-regular.woff2") format("woff2"); } @font-face { @@ -20,7 +20,7 @@ font-weight: 500; font-style: normal; font-display: swap; - src: url("/fonts/inter-medium.woff2") format("woff2"), url("/fonts/inter-medium.woff") format("woff"); + src: url("/fonts/inter-medium.woff2") format("woff2"); } @font-face { @@ -28,7 +28,7 @@ font-weight: 600; font-style: normal; font-display: swap; - src: url("/fonts/inter-semibold.woff2") format("woff2"), url("/fonts/inter-semibold.woff") format("woff"); + src: url("/fonts/inter-semibold.woff2") format("woff2"); } @font-face { @@ -36,7 +36,7 @@ font-weight: 700; font-style: normal; font-display: swap; - src: url("/fonts/inter-bold.woff2") format("woff2"), url("/fonts/inter-bold.woff") format("woff"); + src: url("/fonts/inter-bold.woff2") format("woff2"); } @font-face { @@ -44,7 +44,7 @@ font-weight: 400; font-style: italic; font-display: swap; - src: url("/fonts/inter-italic.woff2") format("woff2"), url("/fonts/inter-italic.woff") format("woff"); + src: url("/fonts/inter-italic.woff2") format("woff2"); } @font-face { @@ -52,7 +52,7 @@ font-weight: 600; font-style: italic; font-display: swap; - src: url("/fonts/inter-semibold-italic.woff2") format("woff2"), url("/fonts/inter-semibold-italic.woff") format("woff"); + src: url("/fonts/inter-semibold-italic.woff2") format("woff2"); } @font-face { @@ -60,5 +60,5 @@ font-weight: 700; font-style: italic; font-display: swap; - src: url("/fonts/inter-bold-italic.woff2") format("woff2"), url("/fonts/inter-bold-italic.woff") format("woff"); + src: url("/fonts/inter-bold-italic.woff2") format("woff2"); } diff --git a/theme/src/scss/_theme.scss b/theme/src/scss/_theme.scss index 5c570df77fc5..8e1e2315325f 100644 --- a/theme/src/scss/_theme.scss +++ b/theme/src/scss/_theme.scss @@ -143,8 +143,10 @@ --breakpoint-sm: 640px; --breakpoint-md: 768px; --breakpoint-lg: 1024px; - --breakpoint-xl: 1280px; + --breakpoint-xl: 1270px; --breakpoint-xxl: 1536px; + --breakpoint-nav-desktop: 1280px; --shadow-3xl: 0 35px 70px -20px rgba(0, 0, 0, 0.5); --leading-extra-tight: 1.1; } + diff --git a/theme/src/scss/main.scss b/theme/src/scss/main.scss index 24db18690a45..61cf194ced9a 100644 --- a/theme/src/scss/main.scss +++ b/theme/src/scss/main.scss @@ -3,6 +3,7 @@ // Tell Tailwind v4 where to scan for utility class usage in templates. // Paths are relative to this file (theme/src/scss/). @source "../../../layouts/**/*.html"; +@source "../../../layouts/partials/header/*.html"; @source "../../../content/**/*.{html,md}"; @source "../../../assets/js/**/*.js"; @source "../../stencil/src/**/*.tsx"; @@ -214,6 +215,11 @@ $sitenav-offset: calc($sitenav-height + 16px); } } } + + button:not(:disabled), + [role="button"]:not(:disabled) { + cursor: pointer; + } } @layer components { diff --git a/theme/src/ts/header-nav.ts b/theme/src/ts/header-nav.ts new file mode 100644 index 000000000000..b24ed97860fb --- /dev/null +++ b/theme/src/ts/header-nav.ts @@ -0,0 +1,364 @@ +(function () { + const HOVER_OPEN_DELAY = 100; + const HOVER_CLOSE_DELAY = 150; + const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; + const DUR_MORPH = '350ms'; + const DUR_OPEN = '200ms'; + const DUR_CLOSE = '150ms'; + const SLIDE_PX = 60; + + // ---------- Desktop: single shared popup that morphs between items ---------- + const navRoot = document.querySelector('[data-nav-root]'); + const popup = document.querySelector('[data-nav-popup]'); + const viewport = document.querySelector('[data-nav-viewport]'); + const triggerBtns: HTMLElement[] = navRoot + ? Array.from(navRoot.querySelectorAll('[data-nav-trigger-button]')) + : []; + const contentPanels: HTMLElement[] = navRoot + ? Array.from(navRoot.querySelectorAll('[data-nav-content]')) + : []; + + let currentIdx = -1; + let isOpen = false; + let openTimer: number | null = null; + let closeTimer: number | null = null; + let panelSizes: { width: number; height: number }[] = []; + + // Pre-measure each panel's natural dimensions by temporarily exposing them off-screen. + function measurePanels(): void { + if (!popup || !viewport || contentPanels.length === 0) return; + const ps = popup.style; + const vs = viewport.style; + + const savedPopup = { + display: ps.display, + position: ps.position, left: ps.left, top: ps.top, + width: ps.width, height: ps.height, overflow: ps.overflow, + opacity: ps.opacity, transform: ps.transform, + pointerEvents: ps.pointerEvents, transition: ps.transition, + }; + const savedViewport = { overflow: vs.overflow, width: vs.width, height: vs.height }; + const savedContents = contentPanels.map(c => ({ + position: c.style.position, + opacity: c.style.opacity, + transform: c.style.transform, + })); + + // Expose off-screen (display:block needed when initial state is display:none) + ps.display = 'block'; + ps.position = 'fixed'; + ps.left = '-9999px'; + ps.top = '0'; + ps.width = 'auto'; + ps.height = 'auto'; + ps.overflow = 'visible'; + ps.opacity = '0'; + ps.transform = 'none'; + ps.pointerEvents = 'none'; + ps.transition = 'none'; + vs.overflow = 'visible'; + vs.width = 'auto'; + vs.height = 'auto'; + contentPanels.forEach(c => { + c.style.position = 'static'; + c.style.opacity = '1'; + c.style.transform = 'none'; + }); + + popup.offsetHeight; // force layout + + panelSizes = contentPanels.map(c => { + // Measure the inner grid div which carries the explicit w-[Npx] — not the + // wrapper, which stretches to the widest sibling when all panels are stacked. + const inner = (c.firstElementChild as HTMLElement) ?? c; + return { width: inner.offsetWidth, height: inner.offsetHeight }; + }); + + // Restore + contentPanels.forEach((c, i) => { + c.style.position = savedContents[i].position; + c.style.opacity = savedContents[i].opacity; + c.style.transform = savedContents[i].transform; + }); + vs.overflow = savedViewport.overflow; + vs.width = savedViewport.width; + vs.height = savedViewport.height; + ps.display = savedPopup.display; + ps.position = savedPopup.position; + ps.left = savedPopup.left; + ps.top = savedPopup.top; + ps.width = savedPopup.width; + ps.height = savedPopup.height; + ps.overflow = savedPopup.overflow; + ps.opacity = savedPopup.opacity; + ps.transform = savedPopup.transform; + ps.pointerEvents = savedPopup.pointerEvents; + ps.transition = savedPopup.transition; + } + + function setActiveTrigger(idx: number): void { + triggerBtns.forEach((btn, i) => { + if (i === idx) { + btn.dataset.open = 'true'; + btn.setAttribute('aria-expanded', 'true'); + } else { + delete btn.dataset.open; + btn.setAttribute('aria-expanded', 'false'); + } + }); + } + + // Slide new content in, old content out, rest hidden. + // Non-active panels get pointer-events:none so their (invisible) stacked + // children don't intercept clicks meant for the active panel. + function slideContent(nextIdx: number, prevIdx: number): void { + const dir = prevIdx >= 0 && prevIdx < nextIdx ? 1 : -1; + contentPanels.forEach((c, i) => { + if (i === nextIdx) { + c.style.transition = 'none'; + c.style.opacity = '0'; + c.style.transform = `translateX(${dir * SLIDE_PX}px)`; + c.style.pointerEvents = 'auto'; + c.offsetHeight; // reflow + c.style.transition = `opacity ${DUR_MORPH} ${EASE}, transform ${DUR_MORPH} ${EASE}`; + c.style.opacity = '1'; + c.style.transform = 'translateX(0)'; + } else if (i === prevIdx) { + c.style.transition = `opacity ${DUR_MORPH} ${EASE}, transform ${DUR_MORPH} ${EASE}`; + c.style.opacity = '0'; + c.style.transform = `translateX(${-dir * SLIDE_PX}px)`; + c.style.pointerEvents = 'none'; + } else { + c.style.transition = 'none'; + c.style.opacity = '0'; + c.style.transform = 'translateX(0)'; + c.style.pointerEvents = 'none'; + } + }); + } + + function popupLeftFor(idx: number): number { + if (!navRoot || !triggerBtns[idx]) return 0; + const navRect = navRoot.getBoundingClientRect(); + const btnRect = triggerBtns[idx].getBoundingClientRect(); + let left = btnRect.left - navRect.left; + if (panelSizes[idx]) { + const overflow = left + panelSizes[idx].width - (window.innerWidth - navRect.left) - 16; + if (overflow > 0) left = Math.max(0, left - overflow); + } + return left; + } + + function openDropdown(idx: number): void { + if (!popup || !panelSizes[idx]) return; + const prevIdx = isOpen ? currentIdx : -1; + const wasOpen = isOpen; + currentIdx = idx; + isOpen = true; + setActiveTrigger(idx); + slideContent(idx, prevIdx); + + const sz = panelSizes[idx]; + const left = popupLeftFor(idx); + const ps = popup.style; + + if (!wasOpen) { + // Initial open: snap size/position, then fade + scale in + ps.transition = 'none'; + ps.display = 'block'; + ps.left = `${left}px`; + ps.width = `${sz.width}px`; + ps.height = `${sz.height}px`; + ps.opacity = '0'; + ps.transform = 'scale(0.95)'; + ps.pointerEvents = 'auto'; + popup.offsetHeight; + ps.transition = `opacity ${DUR_OPEN} ease-out, transform ${DUR_OPEN} ${EASE}`; + ps.opacity = '1'; + ps.transform = 'scale(1)'; + } else { + // Already open: morph size and reposition; opacity/scale stay at 1 + ps.transition = [ + `left ${DUR_MORPH} ${EASE}`, + `width ${DUR_MORPH} ${EASE}`, + `height ${DUR_MORPH} ${EASE}`, + ].join(', '); + ps.left = `${left}px`; + ps.width = `${sz.width}px`; + ps.height = `${sz.height}px`; + } + } + + function closeDropdowns(immediate: boolean): void { + if (!isOpen || !popup) return; + isOpen = false; + currentIdx = -1; + setActiveTrigger(-1); + const ps = popup.style; + if (immediate) { + ps.transition = 'none'; + ps.display = 'none'; + ps.opacity = '0'; + ps.transform = 'scale(0.95)'; + ps.pointerEvents = 'none'; + } else { + ps.transition = `opacity ${DUR_CLOSE} ease-in, transform ${DUR_CLOSE} ease-in`; + ps.opacity = '0'; + ps.transform = 'scale(0.95)'; + ps.pointerEvents = 'none'; + popup.addEventListener('transitionend', () => { + if (!isOpen) { + ps.transition = 'none'; + ps.display = 'none'; + } + }, { once: true }); + } + } + + if (navRoot && popup) { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', measurePanels); + } else { + measurePanels(); + } + + let resizeTimer: number | null = null; + window.addEventListener('resize', () => { + if (resizeTimer !== null) clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + measurePanels(); + if (isOpen && popup) { + const sz = panelSizes[currentIdx]; + const ps = popup.style; + ps.transition = 'none'; + ps.left = `${popupLeftFor(currentIdx)}px`; + ps.width = `${sz.width}px`; + ps.height = `${sz.height}px`; + } + }, 150); + }); + + triggerBtns.forEach((btn, idx) => { + const li = btn.parentElement!; + + li.addEventListener('mouseenter', () => { + if (closeTimer !== null) clearTimeout(closeTimer); + if (openTimer !== null) clearTimeout(openTimer); + openTimer = setTimeout(() => openDropdown(idx), HOVER_OPEN_DELAY); + }); + + li.addEventListener('mouseleave', () => { + if (openTimer !== null) clearTimeout(openTimer); + closeTimer = setTimeout(() => closeDropdowns(false), HOVER_CLOSE_DELAY); + }); + + btn.addEventListener('click', () => { + if (openTimer !== null) clearTimeout(openTimer); + if (closeTimer !== null) clearTimeout(closeTimer); + if (isOpen && currentIdx === idx) { + closeDropdowns(false); + } else { + openDropdown(idx); + } + }); + + btn.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'Escape') { + closeDropdowns(true); + btn.focus(); + } + }); + }); + + popup.addEventListener('mouseenter', () => { + if (closeTimer !== null) clearTimeout(closeTimer); + }); + + popup.addEventListener('mouseleave', () => { + closeTimer = setTimeout(() => closeDropdowns(false), HOVER_CLOSE_DELAY); + }); + + document.addEventListener('click', (e: MouseEvent) => { + if (isOpen && navRoot && !navRoot.contains(e.target as Node)) closeDropdowns(false); + }); + + document.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) closeDropdowns(true); + }); + } + + // ---------- Mobile sheet ---------- + const sheet = document.querySelector('[data-nav-sheet]'); + const sheetTrigger = document.querySelector('[data-nav-sheet-trigger]'); + const sheetCloseEls = document.querySelectorAll('[data-nav-sheet-close]'); + const overlay = document.querySelector('[data-nav-sheet-overlay]'); + const navDesktopMql = window.matchMedia('(min-width: 1200px)'); + + function openSheet(): void { + if (!sheet) return; + sheet.dataset.state = 'open'; + if (overlay) overlay.dataset.state = 'open'; + if (sheetTrigger) sheetTrigger.setAttribute('aria-expanded', 'true'); + document.body.style.overflow = 'hidden'; + } + + function closeSheet(): void { + if (!sheet) return; + sheet.dataset.state = 'closed'; + if (overlay) overlay.dataset.state = 'closed'; + if (sheetTrigger) sheetTrigger.setAttribute('aria-expanded', 'false'); + document.body.style.overflow = ''; + } + + if (sheetTrigger) sheetTrigger.addEventListener('click', openSheet); + if (overlay) overlay.addEventListener('click', closeSheet); + sheetCloseEls.forEach(el => el.addEventListener('click', closeSheet)); + + document.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'Escape' && sheet?.dataset.state === 'open') closeSheet(); + }); + + // Close the sheet when the viewport crosses into the desktop breakpoint, + // since the trigger button disappears there and would orphan the open sheet. + navDesktopMql.addEventListener('change', e => { + if (e.matches && sheet?.dataset.state === 'open') closeSheet(); + }); + + // ---------- Mobile collapsibles ---------- + document.querySelectorAll('[data-nav-collapsible]').forEach(item => { + const trigger = item.querySelector('[data-nav-collapsible-trigger]'); + const panel = item.querySelector('[data-nav-collapsible-panel]'); + if (!trigger || !panel) return; + trigger.addEventListener('click', () => { + const open = item.dataset.open === 'true'; + if (open) { + item.removeAttribute('data-open'); + panel.dataset.state = 'closed'; + trigger.setAttribute('aria-expanded', 'false'); + } else { + item.dataset.open = 'true'; + panel.dataset.state = 'open'; + trigger.setAttribute('aria-expanded', 'true'); + } + }); + }); + + // ---------- Auth-aware sign-in / dashboard toggle ---------- + try { + const cookies: Record = {}; + document.cookie.split(';').forEach(c => { + const eq = c.indexOf('='); + if (eq > -1) cookies[c.slice(0, eq).trim()] = c.slice(eq + 1).trim(); + }); + const userCookie = cookies['pulumi_web_user_info'] ?? 'j:{}'; + const userInfo = JSON.parse(decodeURIComponent(userCookie).slice(2)); + if (userInfo?.userId) { + document.querySelectorAll('[data-nav-loggedout]').forEach(el => { + el.style.display = 'none'; + }); + document.querySelectorAll('[data-nav-dashboard]').forEach(el => { + el.style.removeProperty('display'); + }); + } + } catch (e) {} +})(); diff --git a/theme/src/ts/marketingHomepage.ts b/theme/src/ts/marketingHomepage.ts deleted file mode 100644 index e1e353833534..000000000000 --- a/theme/src/ts/marketingHomepage.ts +++ /dev/null @@ -1 +0,0 @@ -import "../scss/_marketing.scss"; diff --git a/theme/stencil/src/components/header-cta/header-cta.scss b/theme/stencil/src/components/header-cta/header-cta.scss deleted file mode 100644 index 5d4e87f30f63..000000000000 --- a/theme/stencil/src/components/header-cta/header-cta.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} diff --git a/theme/stencil/src/components/header-cta/header-cta.tsx b/theme/stencil/src/components/header-cta/header-cta.tsx deleted file mode 100644 index 55fa66e6bcb1..000000000000 --- a/theme/stencil/src/components/header-cta/header-cta.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Component, Prop, State, h } from "@stencil/core"; -import { parseCookie } from "../../util/util"; - -@Component({ - tag: "header-cta", - styleUrl: "header-cta.scss", - shadow: false, -}) -export class HeaderCta { - @Prop() - buttonClass = ""; - - @Prop() - href = "https://app.pulumi.com/signup"; - - @Prop() - label = "Sign Up"; - - @State() - loading = true; - - @State() - isLoggedIn = false; - - componentWillRender() { - try { - const cookie = parseCookie(); - const userCookie = cookie["pulumi_web_user_info"] ?? "j:{}"; - const userInfo = JSON.parse(decodeURIComponent(userCookie).slice(2)); - - if (userInfo && userInfo.userId && userInfo.userId !== "") { - this.isLoggedIn = true; - } - } catch (e) { - // Swallow the error and so the component shows the "Get Started" button. - } - - this.loading = false; - } - - render() { - if (this.loading) { - return; - } - - if (this.isLoggedIn) { - return( - Dashboard - ); - } - - return ( - {this.label} - ); - } - -} diff --git a/theme/stencil/src/components/header-cta/test/header-cta.e2e.ts b/theme/stencil/src/components/header-cta/test/header-cta.e2e.ts deleted file mode 100644 index 2b74a13250ef..000000000000 --- a/theme/stencil/src/components/header-cta/test/header-cta.e2e.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { newE2EPage } from '@stencil/core/testing'; - -describe('header-cta', () => { - it('renders', async () => { - const page = await newE2EPage(); - await page.setContent(''); - - const element = await page.find('header-cta'); - expect(element).toHaveClass('hydrated'); - }); -}); diff --git a/theme/stencil/src/components/header-cta/test/header-cta.spec.tsx b/theme/stencil/src/components/header-cta/test/header-cta.spec.tsx deleted file mode 100644 index 3a7a2201c141..000000000000 --- a/theme/stencil/src/components/header-cta/test/header-cta.spec.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { newSpecPage } from '@stencil/core/testing'; -import { HeaderCta } from '../header-cta'; - -describe('header-cta', () => { - it('renders', async () => { - const page = await newSpecPage({ - components: [HeaderCta], - html: ``, - }); - expect(page.root).toEqualHtml(` - - Sign Up - - `); - }); -}); diff --git a/theme/webpack.config.js b/theme/webpack.config.js index f28db163639e..35b4512b8972 100644 --- a/theme/webpack.config.js +++ b/theme/webpack.config.js @@ -15,9 +15,9 @@ module.exports = function (env, argv = {}) { entry: { "bundle": "./src/ts/main.ts", "marketing": "./src/ts/marketing.ts", - "marketing-homepage": "./src/ts/marketingHomepage.ts", "algolia": "./src/ts/algolia-entry.ts", "consent-manager": "./src/ts/consent-manager/index.ts", + "header-nav": "./src/ts/header-nav.ts", }, output: { filename: "[name].[contenthash:8].js", diff --git a/theme/yarn.lock b/theme/yarn.lock index de6e6418c32c..3059021d21b8 100644 --- a/theme/yarn.lock +++ b/theme/yarn.lock @@ -227,13 +227,6 @@ dependencies: tslib "^2.4.0" -"@fullhuman/postcss-purgecss@^4.0.3": - version "4.1.3" - resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-4.1.3.tgz#e4eb21fc7a49257e4081c6a3a86b338618e61fce" - integrity sha512-jqcsyfvq09VOsMXxJMPLRF6Fhg/NNltzWKnC9qtzva+QKTxerCO4esG6je7hbnmkpZtaDyPTwMBj9bzfWorsrw== - dependencies: - purgecss "^4.1.3" - "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" @@ -768,11 +761,6 @@ autoprefixer@^10.5.0: picocolors "^1.1.1" postcss-value-parser "^4.2.0" -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - baseline-browser-mapping@^2.10.12: version "2.10.24" resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.24.tgz#6dc320c7bf53859ec2bf55d54db6d2e5c078df16" @@ -783,14 +771,6 @@ boolbase@^1.0.0: resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== -brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" @@ -914,16 +894,6 @@ commander@^7.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -commander@^8.0.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - concurrently@^6.2.1: version "6.5.1" resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-6.5.1.tgz#4518c67f7ac680cf5c34d5adf399a2a2047edc8c" @@ -1223,11 +1193,6 @@ fraction.js@^5.3.4: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" @@ -1243,18 +1208,6 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.1.7: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -1303,19 +1256,6 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - interpret@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" @@ -1567,13 +1507,6 @@ mini-css-extract-plugin@^2.10.2: schema-utils "^4.0.0" tapable "^2.2.1" -minimatch@^3.1.1: - version "3.1.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.4.tgz#89d910ea3970a77ac8edfd30340ccd038b758079" - integrity sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw== - dependencies: - brace-expansion "^1.1.7" - nanoid@^3.3.11: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" @@ -1606,13 +1539,6 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -1654,11 +1580,6 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -1910,7 +1831,7 @@ postcss-reduce-transforms@^5.1.0: dependencies: postcss-value-parser "^4.2.0" -postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: +postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: version "6.1.2" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== @@ -1946,7 +1867,7 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.3.5, postcss@^8.4.40, postcss@^8.5.13, postcss@^8.5.6: +postcss@^8.4.40, postcss@^8.5.13, postcss@^8.5.6: version "8.5.13" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.13.tgz#6cfaf647f2e7ef69850208eccd849e0d3f65d420" integrity sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag== @@ -1965,16 +1886,6 @@ prettier@2.8.8: resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -purgecss@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-4.1.3.tgz#683f6a133c8c4de7aa82fe2746d1393b214918f7" - integrity sha512-99cKy4s+VZoXnPxaoM23e5ABcP851nC2y2GROkkjS8eJaJtlciGavd7iYAw2V84WeBqggZ12l8ef44G99HmTaw== - dependencies: - commander "^8.0.0" - glob "^7.1.7" - postcss "^8.3.5" - postcss-selector-parser "^6.0.6" - readdirp@^4.0.1: version "4.1.2" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" @@ -2364,11 +2275,6 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" diff --git a/yarn.lock b/yarn.lock index 0b925cd3259d..1cc32c1c33b7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -796,13 +796,6 @@ dependencies: tslib "^2.8.0" -"@fullhuman/postcss-purgecss@^7.0.0": - version "7.0.2" - resolved "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-7.0.2.tgz" - integrity sha512-U4zAXNaVztbDxO9EdcLp51F3UxxYsb/7DN89rFxFJhfk2Wua2pvw2Kf3HdspbPhW/wpHjSjsxWYoIlbTgRSjbQ== - dependencies: - purgecss "^7.0.2" - "@gerrit0/mini-shiki@^3.23.0": version "3.23.0" resolved "https://registry.yarnpkg.com/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz#d9414f3080b88303b18f3a311846e37e424d800c" @@ -2748,11 +2741,6 @@ commander@^11.1.0: resolved "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz" integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== -commander@^12.1.0: - version "12.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== - commander@^14.0.3: version "14.0.3" resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.3.tgz#425d79b48f9af82fcd9e4fc1ea8af6c5ec07bbc2" @@ -3480,7 +3468,7 @@ follow-redirects@^1.0.0, follow-redirects@^1.16.0: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== -foreground-child@^3.1.0, foreground-child@^3.3.1: +foreground-child@^3.1.0: version "3.3.1" resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz" integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== @@ -3640,18 +3628,6 @@ glob@^10.4.5: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^11.0.0: - version "11.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz" - integrity sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw== - dependencies: - foreground-child "^3.3.1" - jackspeak "^4.1.1" - minimatch "^10.1.1" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^2.0.0" - global-dirs@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz" @@ -4097,13 +4073,6 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jackspeak@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz" - integrity sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ== - dependencies: - "@isaacs/cliui" "^8.0.2" - jpeg-js@^0.4.1, jpeg-js@^0.4.4: version "0.4.4" resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" @@ -4391,11 +4360,6 @@ lru-cache@^10.2.0, lru-cache@^10.4.3: resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== -lru-cache@^11.0.0: - version "11.2.4" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz" - integrity sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== - lru-cache@^7.14.1: version "7.18.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" @@ -4755,7 +4719,7 @@ mimic-response@^4.0.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-4.0.0.tgz#35468b19e7c75d10f5165ea25e75a5ceea7cf70f" integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== -minimatch@^10.1.1, minimatch@^10.2.5: +minimatch@^10.2.5: version "10.2.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== @@ -5096,14 +5060,6 @@ path-scurry@^1.11.1: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" -path-scurry@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz" - integrity sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA== - dependencies: - lru-cache "^11.0.0" - minipass "^7.1.2" - pend@~1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" @@ -5392,14 +5348,6 @@ postcss-safe-parser@^7.0.1: resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz#36e4f7e608111a0ca940fd9712ce034718c40ec0" integrity sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A== -postcss-selector-parser@^6.1.2: - version "6.1.2" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - postcss-selector-parser@^7.0.0, postcss-selector-parser@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz" @@ -5428,7 +5376,7 @@ postcss-value-parser@^4.2.0: resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.4.47, postcss@^8.4.49, postcss@^8.5.13: +postcss@^8.4.49, postcss@^8.5.13: version "8.5.13" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.13.tgz#6cfaf647f2e7ef69850208eccd849e0d3f65d420" integrity sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag== @@ -5566,16 +5514,6 @@ puppeteer-core@^24.17.1: webdriver-bidi-protocol "0.4.1" ws "^8.19.0" -purgecss@^7.0.2: - version "7.0.2" - resolved "https://registry.npmjs.org/purgecss/-/purgecss-7.0.2.tgz" - integrity sha512-4Ku8KoxNhOWi9X1XJ73XY5fv+I+hhTRedKpGs/2gaBKU8ijUiIKF/uyyIyh7Wo713bELSICF5/NswjcuOqYouQ== - dependencies: - commander "^12.1.0" - glob "^11.0.0" - postcss "^8.4.47" - postcss-selector-parser "^6.1.2" - qs@^6.4.0, qs@~6.14.1: version "6.14.2" resolved "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz" @@ -5986,7 +5924,7 @@ string-argv@^0.3.2: resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" integrity sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6003,15 +5941,6 @@ string-width@8.1.0, string-width@^8.0.0: get-east-asian-width "^1.3.0" strip-ansi "^7.1.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" @@ -6042,7 +5971,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6063,13 +5992,6 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.2" resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz" @@ -6557,16 +6479,7 @@ win-release@^1.0.0: dependencies: semver "^5.0.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==