diff --git a/.vscode/settings.json b/.vscode/settings.json index 374dabc52..29d096b6a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -269,6 +269,7 @@ "outofsync", "PABC", "pacticipant", + "PAGEDJS", "PERC", "permissioned", "pgbouncer", @@ -396,6 +397,7 @@ "ungap", "unhold", "uninstrumented", + "unlayered", "unsampled", "unscanned", "unsexy", @@ -421,6 +423,7 @@ "vmax", "VNIC", "vtbot", + "WAAPI", "WHATWG", "Wolters", "wordprocessingml", diff --git a/@types/pagedjs.d.ts b/@types/pagedjs.d.ts new file mode 100644 index 000000000..6580d1c86 --- /dev/null +++ b/@types/pagedjs.d.ts @@ -0,0 +1,22 @@ +declare module 'pagedjs' { + export interface PagedJsList { + appendList?: (_rules: unknown) => void + remove?: (_entry: unknown) => void + } + + export interface PagedJsHandlerInstance { + getMediaName?(_node: unknown): unknown + onAtMedia?(_node: unknown, _item: unknown, _list: PagedJsList): void + } + + export interface PagedJsHandlerConstructor { + new (..._args: never[]): PagedJsHandlerInstance + name: string + } + + export class Previewer { + preview(): Promise + } + + export const registeredHandlers: PagedJsHandlerConstructor[] +} \ No newline at end of file diff --git a/_TODO.md b/_TODO.md index 1c6bd61a3..afdadc2ec 100644 --- a/_TODO.md +++ b/_TODO.md @@ -1,98 +1,6 @@ # TODO -## Print - -1. Add a QR code at the bottom of printed pages so it's easier for someone to navigate to from a printed page. We have a QrCode component. - -2. Need to make sure that on print, when we have a tabbed code block with multiple languages, only the first language is printed and the other language tabs are hidden. The styling should be different for print for the code block. Maybe move other language code tabs to an appendix and add a link to them. - -[This article](https://excessivelyadequate.com/posts/print.html) shows how to control the following properties in Chrome's Print Properties dialog box from CSS: Layout, Paper size, Margins, Headers and footers, and Background graphics. Headers and footers is the checkbox that by default is enabled and adds information on printed pages. It also shows how to use Chrome from the terminal in headless mode to output a PDF file from an HTML page. - -3. For printed pages, your header should shift from a navigation tool to a document identifier. Since users cannot click links or icons on paper, these elements are "cruft" that waste space and ink. - -2. Need a layout alternative to Markup that formats for print. It should hide Table Of Contents. Need a fixed header format that adds article title, subtitle, and date. - -__Recommended Print Header Format__ - -A professional print header typically includes only these three elements: - -- Brand Identity: A high-contrast version of your logo or the site name in plain text for brand recognition. -- Document Title: The main title of the page (usually the

), ensuring the reader knows exactly what the document is. - -__Expand External Links For Print__ - -We can't (yet) directly interface with a printed page to explore links, so link URLs should be visible on the printed version of the Web page. To keep the page relatively clean, I prefer to expand only outbound links in articles, and suppress internal ones. If you've used relative URLs on your website for local links, you can easily do this through an attribute selector and `:after` pseudo classes, thus preventing internal links and links around images from being printed: - -- Break Lists across pages, the separators between columns are broken too -- Break code blocks across pages -- Callouts are breaking across pages, they shouldn't - -## PDF File Generation - -- Need a workflow to generate PDF files from Markdown for downloads. - -- Need a fixed cover page format that adds article title, subtitle, and date. - -- Table of Contents (Workarounds) - -Because the browser doesn't know which page an element (like an

) will land on until the PDF is fully rendered, you cannot generate a TOC with correct page numbers in a single pass. Common workarounds include: - -Paged.js Polyfill: Use the Paged.js library to handle sophisticated print layouts (like TOCs and cross-references) within the browser before Puppeteer "prints" the result. - -[Paged.js](https://pagedjs.org/en/documentation/) polyfills `@page` properties, and lays out an HTML document in print format where it can have page numbers generated to update in a table of contents. - -This article has different approaches to [print pagination](https://www.customjs.space/blog/html-print-pagination-footer/). One approach overlaps with PagedJS's approach. - -- Headers and Footers (Native Support) - -Puppeteer can inject dynamic data into your headers and footers using specific CSS classes. To use this, you must set `displayHeaderFooter: true` in the `page.pdf()` options. - -Dynamic Classes: Puppeteer automatically replaces these classes with actual values: -`.pageNumber`: Current page number. -`.totalPages`: Total number of pages. -`.title`: The document's `` tag. -`.date`: The date the PDF was generated. - -Requirements: You must provide sufficient margins (e.g.,` margin: { top: '50px', bottom: '50px' }`), or the headers/footers will be hidden behind the content. - -Styling: You must use inline CSS within your `headerTemplate` or `footerTemplate` strings, as they cannot access your external stylesheet. - - -Maybe something like this to check if we're on the playwright PDF generation run and set the light theme: - -```typescript -window.addEventListener('beforeprint', (event) => { - console.log('Before print dialog opens, run this script.') - // Example: change content or hide elements - document.getElementById('hide-on-print').style.display = 'none' -}) - -window.addEventListener('afterprint', (event) => { - console.log('After print dialog closes, run this script to revert changes.') - // Example: revert changes - document.getElementById('hide-on-print').style.display = '' - // You can also use this event to send an AJAX request to a server for print tracking. -}) -``` - -Or listen for changes: - -```typescript -if (window.matchMedia) { - var mediaQueryList = window.matchMedia('print'); - mediaQueryList.addListener(function(mql) { - if (mql.matches) { - // Equivalent to onbeforeprint - console.log('Entering print mode (before print dialog)'); - } else { - // Equivalent to onafterprint - console.log('Exiting print mode (after print dialog)'); - } - }); -} -``` - ## ToolTips Need a tooltip component for consistency. @@ -107,8 +15,6 @@ These have tooltips, how are they being generated? - "Report a Bug" in footer - RSS feed icon in footer -## Abbreviation Styling - - Improve `<abbr>` styling: https://codepen.io/ire/pen/NoqWpm ## HubSpot Signup Issues / Gated Content (Downloads) @@ -117,14 +23,6 @@ These have tooltips, how are they being generated? - We need a gating system, where the user gets a token to be able to download a PDF and the token is checked before downloading. If a reader has already given their email address - newsletter signup, contact form, download registration, then the download CTA on short form articles should show the PDF download button instead of trying to collect email addresses again. -- We had this copyright statement for articles, it should be in the front matter of PDFs: - -```text -Copyright © 2025 Webstack Builders, Inc. -The text, diagrams, and images in this work are licensed under CC BY-NC 4.0 -All code samples in this article are licensed under the MIT License. Feel free to use, modify, and distribute them in any project. -``` - ## Youtube video for Backstage IDP hero on Home page Discuss agentic AI integrations to add to Backstage diff --git a/astro.config.ts b/astro.config.ts index c86b9ac63..93c9f1eff 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -42,6 +42,8 @@ const viteLogger = createLogger(undefined, { allowClearScreen: false }) const sentryAuthToken = process.env['SENTRY_AUTH_TOKEN'] const shouldEnableSentryIntegration = Boolean(sentryAuthToken) const sentryReleaseName = getPackageRelease() +const astroEnvConfig = environmentalVariablesConfig as NonNullable<AstroUserConfig['env']> +const astroMarkdownConfig = markdownConfig as unknown as NonNullable<AstroUserConfig['markdown']> const shouldSuppressViteWarning = (message: string): boolean => { return ( @@ -103,7 +105,7 @@ const standardIntegrations = [ })] : []), sitemap({ serialize: createSerializeFunction({ - exclude: ['deep-dive', 'downloads', '/articles/demo', 'testing', 'hero', 'links'], + exclude: ['deep-dive', 'downloads', 'print', '/articles/demo', 'testing', 'hero', 'links'], }), }), /** @@ -123,8 +125,8 @@ export default defineConfig({ devToolbar: { enabled: false, }, - env: environmentalVariablesConfig, - markdown: markdownConfig as AstroUserConfig['markdown'], + env: astroEnvConfig, + markdown: astroMarkdownConfig, /** * Astro sets substantial Vite config internally in the framework. When you use Vitest * in an Astro project, you use Astro's getViteConfig helper to get the resolved internal @@ -204,7 +206,6 @@ export default defineConfig({ */ 'process.env.CSS_TRANSFORMER_WASM': 'false', }, - /* @ts-expect-error - tailwindcss plugin type compatibility */ plugins: [ fixContentAssetPropagation(), tailwindcss(), diff --git a/eslint.config.ts b/eslint.config.ts index f99a89024..a4cfa1d3f 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -10,6 +10,7 @@ import securityPlugin from 'eslint-plugin-security' import ymlPlugin from 'eslint-plugin-yml' import tsPlugin from 'typescript-eslint' import restrictedGlobals from 'confusing-browser-globals' +import globals from 'globals' import enforceCentralizedEventsRule from './test/eslint/enforce-centralized-events-rule' import noHtmlElementAssertionsRule from './test/eslint/no-html-element-assertions-rule' import noQuerySelectorOutsideSelectorsRule from './test/eslint/no-query-selector-outside-selectors-rule' @@ -50,7 +51,9 @@ export default [ ecmaVersion: 'latest', sourceType: 'module', globals: { - NodeJS: 'readonly', + ...globals.node, // Covers 'process', 'Buffer', etc. + NodeJS: 'readonly', // Covers TS namespaces + console: 'readonly', // Specific fix for 'console' }, }, rules: { diff --git a/package-lock.json b/package-lock.json index 2bdfce593..f81d5aefb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,8 +58,8 @@ "@types/react": "^19.2.14", "@types/sanitize-html": "^2.16.1", "@types/to-ico": "1.1.3", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", "@ungap/structured-clone": "^1.3.0", "@uppy/audio": "^3.1.0", "@uppy/box": "^4.1.0", @@ -75,11 +75,11 @@ "@vitest/coverage-v8": "^4.1.2", "@webcomponents/template-shadowroot": "^0.2.1", "alex": "^11.0.1", - "astro": "6.1.1", + "astro": "6.1.2", "astro-link-validator": "github:rodgtr1/astro-link-validator", "astro-og-canvas": "^0.10.1", "astro-vtbot": "^2.1.12", - "baseline-browser-mapping": "^2.10.11", + "baseline-browser-mapping": "^2.10.12", "canvas-confetti": "^1.9.4", "confusing-browser-globals": "1.0.11", "cross-env": "^10.1.0", @@ -99,6 +99,7 @@ "eslint-plugin-security": "4.0.0", "eslint-plugin-yml": "3.3.1", "focus-trap": "8.0.1", + "globals": "^17.4.0", "gsap": "^3.14.2", "happy-dom": "^20.8.9", "hast-util-heading-rank": "^3.0.0", @@ -110,7 +111,7 @@ "isomorphic-git": "^1.37.4", "js-cookie": "^3.0.5", "jsdom": "^29.0.1", - "libphonenumber-js": "1.12.40", + "libphonenumber-js": "1.12.41", "lighthouse": "^13.0.3", "lit": "^3.3.2", "markdownlint-cli2": "^0.22.0", @@ -119,7 +120,8 @@ "nanostores": "^1.2.0", "node-html-parser": "^7.1.0", "nodemailer": "^8.0.4", - "npm": "^11.12.0", + "npm": "^11.12.1", + "pdf-lib": "^1.17.1", "playwright-lighthouse": "^4.0.0", "postcss": "8.5.8", "postcss-html": "1.8.1", @@ -127,6 +129,7 @@ "prettier": "3.8.1", "prettier-plugin-astro": "0.14.1", "pubsub-js": "^1.9.5", + "puppeteer": "^24.40.0", "qr-code-styling": "^1.9.2", "rehype-accessible-emojis": "^0.3.2", "rehype-external-links": "^3.0.0", @@ -151,7 +154,7 @@ "remark-stringify": "^11.0.0", "remark-supersub": "^1.0.0", "remark-video": "^0.10.0", - "resend": "^6.9.4", + "resend": "^6.10.0", "retext": "^9.0.0", "retext-smartypants": "^6.2.0", "rimraf": "6.1.3", @@ -171,15 +174,15 @@ "to-ico": "1.1.5", "tslib": "2.8.1", "typescript": "6.0.2", - "typescript-eslint": "8.57.2", + "typescript-eslint": "8.58.0", "unified": "^11.0.5", "unist": "^0.0.1", "unist-util-inspect": "^8.1.0", "unist-util-is": "^6.0.1", "unist-util-visit": "^5.1.0", "uuid": "^13.0.0", - "vercel": "^50.37.1", - "vite": "^8.0.3", + "vercel": "^50.37.3", + "vite": "^7.3.1", "vitest": "4.1.2", "vitest-axe": "0.1.0", "workbox-build": "7.4.0", @@ -190,7 +193,7 @@ "npm": ">=10.0.0" }, "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "^4.60.0" + "@rollup/rollup-linux-x64-gnu": "^4.60.1" } }, "node_modules/@adobe/mdast-util-gridtables": { @@ -501,123 +504,6 @@ "preact": "^10.6.5" } }, - "node_modules/@astrojs/preact/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/@astrojs/preact/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/@astrojs/preact/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@astrojs/preact/node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/@astrojs/prism": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.1.tgz", @@ -5393,9 +5279,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", - "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "version": "0.110.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.110.0.tgz", + "integrity": "sha512-6Ct21OIlrEnFEJk5LT4e63pk3btsI6/TusD/GStLi7wYlGJNOl1GI9qvXAnRAxQU9zqA2Oz+UwhfTOU2rPZVow==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -5756,6 +5642,24 @@ "third-party-web": "latest" } }, + "node_modules/@pdf-lib/standard-fonts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz", + "integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.6" + } + }, + "node_modules/@pdf-lib/upng": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz", + "integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.10" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -6055,9 +5959,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.1.tgz", + "integrity": "sha512-He6ZoCfv5D7dlRbrhNBkuMVIHd0GDnjJwbICE1OWpG7G3S2gmJ+eXkcNLJjzjNDpeI2aRy56ou39AJM9AD8YFA==", "cpu": [ "arm64" ], @@ -6071,9 +5975,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.1.tgz", + "integrity": "sha512-YzJdn08kSOXnj85ghHauH2iHpOJ6eSmstdRTLyaziDcUxe9SyQJgGyx/5jDIhDvtOcNvMm2Ju7m19+S/Rm1jFg==", "cpu": [ "arm64" ], @@ -6087,9 +5991,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.1.tgz", + "integrity": "sha512-cIvAbqM+ZVV6lBSKSBtlNqH5iCiW933t1q8j0H66B3sjbe8AxIRetVqfGgcHcJtMzBIkIALlL9fcDrElWLJQcQ==", "cpu": [ "x64" ], @@ -6103,9 +6007,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", - "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.1.tgz", + "integrity": "sha512-rVt+B1B/qmKwCl1XD02wKfgh3vQPXRXdB/TicV2w6g7RVAM1+cZcpigwhLarqiVCxDObFZ7UgXCxPC7tpDoRog==", "cpu": [ "x64" ], @@ -6119,9 +6023,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", - "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.1.tgz", + "integrity": "sha512-69YKwJJBOFprQa1GktPgbuBOfnn+EGxu8sBJ1TjPER+zhSpYeaU4N07uqmyBiksOLGXsMegymuecLobfz03h8Q==", "cpu": [ "arm" ], @@ -6135,9 +6039,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.1.tgz", + "integrity": "sha512-9JDhHUf3WcLfnViFWm+TyorqUtnSAHaCzlSNmMOq824prVuuzDOK91K0Hl8DUcEb9M5x2O+d2/jmBMsetRIn3g==", "cpu": [ "arm64" ], @@ -6151,9 +6055,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.1.tgz", + "integrity": "sha512-UvApLEGholmxw/HIwmUnLq3CwdydbhaHHllvWiCTNbyGom7wTwOtz5OAQbAKZYyiEOeIXZNPkM7nA4Dtng7CLw==", "cpu": [ "arm64" ], @@ -6166,42 +6070,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", - "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.1.tgz", + "integrity": "sha512-uVctNgZHiGnJx5Fij7wHLhgw4uyZBVi6mykeWKOqE7bVy9Hcxn0fM/IuqdMwk6hXlaf9fFShDTFz2+YejP+x0A==", "cpu": [ "x64" ], @@ -6215,9 +6087,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", - "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.1.tgz", + "integrity": "sha512-T6Eg0xWwcxd/MzBcuv4Z37YVbUbJxy5cMNnbIt/Yr99wFwli30O4BPlY8hKeGyn6lWNtU0QioBS46lVzDN38bg==", "cpu": [ "x64" ], @@ -6231,9 +6103,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", - "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.1.tgz", + "integrity": "sha512-PuGZVS2xNJyLADeh2F04b+Cz4NwvpglbtWACgrDOa5YDTEHKwmiTDjoD5eZ9/ptXtcpeFrMqD2H4Zn33KAh1Eg==", "cpu": [ "arm64" ], @@ -6247,9 +6119,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", - "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.1.tgz", + "integrity": "sha512-2mOxY562ihHlz9lEXuaGEIDCZ1vI+zyFdtsoa3M62xsEunDXQE+DVPO4S4x5MPK9tKulG/aFcA/IH5eVN257Cw==", "cpu": [ "wasm32" ], @@ -6279,9 +6151,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.1.tgz", + "integrity": "sha512-oQVOP5cfAWZwRD0Q3nGn/cA9FW3KhMMuQ0NIndALAe6obqjLhqYVYDiGGRGrxvnjJsVbpLwR14gIUYnpIcHR1g==", "cpu": [ "arm64" ], @@ -6295,9 +6167,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", - "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.1.tgz", + "integrity": "sha512-Ydsxxx++FNOuov3wCBPaYjZrEvKOOGq3k+BF4BPridhg2pENfitSRD2TEuQ8i33bp5VptuNdC9IzxRKU031z5A==", "cpu": [ "x64" ], @@ -6311,9 +6183,9 @@ } }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", - "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.1.tgz", + "integrity": "sha512-UTBjtTxVOhodhzFVp/ayITaTETRHPUPYZPXQe0WU0wOgxghMojXxYjOiPOauKIYNWJAWS2fd7gJgGQK8GU8vDA==", "license": "MIT" }, "node_modules/@rollup/plugin-node-resolve": { @@ -6624,12 +6496,15 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -8701,19 +8576,19 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", - "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/type-utils": "8.57.2", - "@typescript-eslint/utils": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8723,50 +8598,31 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.2", + "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", - "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/utils": "8.57.2", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">= 4" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "node_modules/@typescript-eslint/parser": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.2", - "@typescript-eslint/tsconfig-utils": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8776,17 +8632,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", - "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.2", - "@typescript-eslint/types": "^8.57.2", + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "engines": { @@ -8797,35 +8654,17 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", - "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", - "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -8833,28 +8672,13 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.57.2", - "@typescript-eslint/tsconfig-utils": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -8863,35 +8687,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", - "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.2", - "@typescript-eslint/types": "^8.57.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", - "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -8900,143 +8710,39 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", - "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "node_modules/@typescript-eslint/types": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", - "debug": "^4.4.3" - }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.2", - "@typescript-eslint/tsconfig-utils": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", - "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.2", - "@typescript-eslint/types": "^8.57.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "ts-api-utils": "^2.5.0" }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", - "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9045,10 +8751,10 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/balanced-match": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", @@ -9057,7 +8763,7 @@ "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", @@ -9069,13 +8775,13 @@ "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -9084,7 +8790,7 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/parser/node_modules/semver": { + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", @@ -9096,14 +8802,16 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", - "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "node_modules/@typescript-eslint/utils": { + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9111,28 +8819,19 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", - "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", - "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -9741,9 +9440,9 @@ } }, "node_modules/@vercel/build-utils": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.11.0.tgz", - "integrity": "sha512-cPy4lgk7Gj++zOLWNHQD0mlffTF2Cmu0ga9+ckND8K9wHkN3sIbu9J8k6lhHAPFOCdWCRJ5D3Xdjv0/tCj59rA==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/@vercel/build-utils/-/build-utils-13.12.0.tgz", + "integrity": "sha512-GnavaJQPt1tm9qxFdghMsN1Sf2P+Bo3fIG3iEKJBuYTLahWm17AZrH+/Hh1A+R3nRgS3De5BJ2/fDgh0VV2QZA==", "license": "Apache-2.0", "dependencies": { "@vercel/python-analysis": "0.11.0", @@ -9773,12 +9472,12 @@ } }, "node_modules/@vercel/elysia": { - "version": "0.1.54", - "resolved": "https://registry.npmjs.org/@vercel/elysia/-/elysia-0.1.54.tgz", - "integrity": "sha512-zB505j4pwqo56ycd8yh2WMdaZ8LLb6Dr6EspfNVWguIwBfDrNNMi+zwycwwM9WywvhizSQMTCL1crBqYfRCxVQ==", + "version": "0.1.55", + "resolved": "https://registry.npmjs.org/@vercel/elysia/-/elysia-0.1.55.tgz", + "integrity": "sha512-LKXOglYMdihN4CKMFZsuf9Oa7ymO1YEWQUdey61fe4qcJzywrEJD70ZFO0bNuRtXMn7Gf0iiGXKmPIPBUklH1g==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.6.21", + "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, @@ -9789,14 +9488,14 @@ "license": "Apache-2.0" }, "node_modules/@vercel/express": { - "version": "0.1.64", - "resolved": "https://registry.npmjs.org/@vercel/express/-/express-0.1.64.tgz", - "integrity": "sha512-uLiFfCD05PdG0MVATrRa0IKPcnLVucCozp9dIsGy4Tt9vWBKvMj5c+kKwWGJ7iZ24egZ4PpbR5QqIZ4tS7W9Dg==", + "version": "0.1.65", + "resolved": "https://registry.npmjs.org/@vercel/express/-/express-0.1.65.tgz", + "integrity": "sha512-hTCTKv7XZJ+SDOpyPBrneX+Qq7tV1WiWmA2sr2/foMb36M4JYexn4OxcGyXQrLEntAVMPg+tp4dHHL/cW/XUeg==", "license": "Apache-2.0", "dependencies": { - "@vercel/cervel": "0.0.39", + "@vercel/cervel": "0.0.40", "@vercel/nft": "1.5.0", - "@vercel/node": "5.6.21", + "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", @@ -9804,252 +9503,13 @@ "zod": "3.22.4" } }, - "node_modules/@vercel/express/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@vercel/express/node_modules/@oxc-project/types": { - "version": "0.110.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.110.0.tgz", - "integrity": "sha512-6Ct21OIlrEnFEJk5LT4e63pk3btsI6/TusD/GStLi7wYlGJNOl1GI9qvXAnRAxQU9zqA2Oz+UwhfTOU2rPZVow==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.1.tgz", - "integrity": "sha512-He6ZoCfv5D7dlRbrhNBkuMVIHd0GDnjJwbICE1OWpG7G3S2gmJ+eXkcNLJjzjNDpeI2aRy56ou39AJM9AD8YFA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.1.tgz", - "integrity": "sha512-YzJdn08kSOXnj85ghHauH2iHpOJ6eSmstdRTLyaziDcUxe9SyQJgGyx/5jDIhDvtOcNvMm2Ju7m19+S/Rm1jFg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.1.tgz", - "integrity": "sha512-cIvAbqM+ZVV6lBSKSBtlNqH5iCiW933t1q8j0H66B3sjbe8AxIRetVqfGgcHcJtMzBIkIALlL9fcDrElWLJQcQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.1.tgz", - "integrity": "sha512-rVt+B1B/qmKwCl1XD02wKfgh3vQPXRXdB/TicV2w6g7RVAM1+cZcpigwhLarqiVCxDObFZ7UgXCxPC7tpDoRog==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.1.tgz", - "integrity": "sha512-69YKwJJBOFprQa1GktPgbuBOfnn+EGxu8sBJ1TjPER+zhSpYeaU4N07uqmyBiksOLGXsMegymuecLobfz03h8Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.1.tgz", - "integrity": "sha512-9JDhHUf3WcLfnViFWm+TyorqUtnSAHaCzlSNmMOq824prVuuzDOK91K0Hl8DUcEb9M5x2O+d2/jmBMsetRIn3g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.1.tgz", - "integrity": "sha512-UvApLEGholmxw/HIwmUnLq3CwdydbhaHHllvWiCTNbyGom7wTwOtz5OAQbAKZYyiEOeIXZNPkM7nA4Dtng7CLw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.1.tgz", - "integrity": "sha512-uVctNgZHiGnJx5Fij7wHLhgw4uyZBVi6mykeWKOqE7bVy9Hcxn0fM/IuqdMwk6hXlaf9fFShDTFz2+YejP+x0A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.1.tgz", - "integrity": "sha512-T6Eg0xWwcxd/MzBcuv4Z37YVbUbJxy5cMNnbIt/Yr99wFwli30O4BPlY8hKeGyn6lWNtU0QioBS46lVzDN38bg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.1.tgz", - "integrity": "sha512-PuGZVS2xNJyLADeh2F04b+Cz4NwvpglbtWACgrDOa5YDTEHKwmiTDjoD5eZ9/ptXtcpeFrMqD2H4Zn33KAh1Eg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.1.tgz", - "integrity": "sha512-2mOxY562ihHlz9lEXuaGEIDCZ1vI+zyFdtsoa3M62xsEunDXQE+DVPO4S4x5MPK9tKulG/aFcA/IH5eVN257Cw==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.1.tgz", - "integrity": "sha512-oQVOP5cfAWZwRD0Q3nGn/cA9FW3KhMMuQ0NIndALAe6obqjLhqYVYDiGGRGrxvnjJsVbpLwR14gIUYnpIcHR1g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.1.tgz", - "integrity": "sha512-Ydsxxx++FNOuov3wCBPaYjZrEvKOOGq3k+BF4BPridhg2pENfitSRD2TEuQ8i33bp5VptuNdC9IzxRKU031z5A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@vercel/express/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.1.tgz", - "integrity": "sha512-UTBjtTxVOhodhzFVp/ayITaTETRHPUPYZPXQe0WU0wOgxghMojXxYjOiPOauKIYNWJAWS2fd7gJgGQK8GU8vDA==", - "license": "MIT" - }, "node_modules/@vercel/express/node_modules/@vercel/backends": { - "version": "0.0.52", - "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.0.52.tgz", - "integrity": "sha512-dxUCxjy2jyAcq77b9A0mWSiw8SlO9U2xfSAN814lOwtqwuiqiGN3CIxrZLgnK0IDHcbQ11jg4FDuD5Le545B2A==", + "version": "0.0.53", + "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.0.53.tgz", + "integrity": "sha512-f8JK9Jatlco6kT/NsLYVR88iqS7oZxUXzXW8TofLmyTT5o+OEXSY2hzAQSjul6r0qz/9yAhtQkMhcHG8Ne01ZQ==", "license": "Apache-2.0", "dependencies": { - "@vercel/build-utils": "13.11.0", + "@vercel/build-utils": "13.12.0", "@vercel/nft": "1.5.0", "execa": "3.2.0", "fs-extra": "11.1.0", @@ -10066,12 +9526,12 @@ } }, "node_modules/@vercel/express/node_modules/@vercel/cervel": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@vercel/cervel/-/cervel-0.0.39.tgz", - "integrity": "sha512-D0cbJ+dCVxTH2NPOvQP2oiCvH6vd/TqZ3MG3PCwwhPTKyBGNM4cL95VjBW2O6ATNucOxcTni/MZPgoqxLsxeVw==", + "version": "0.0.40", + "resolved": "https://registry.npmjs.org/@vercel/cervel/-/cervel-0.0.40.tgz", + "integrity": "sha512-5eY17uPXU6Mhn6lh8HIKyOLkhVr826D7G0CeVIujzsMfiGZjWIYc9udbXLGqikgBucvH9PwkxRQuCXI10jBzIw==", "license": "Apache-2.0", "dependencies": { - "@vercel/backends": "0.0.52" + "@vercel/backends": "0.0.53" }, "bin": { "cervel": "bin/cervel.mjs" @@ -10104,37 +9564,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/@vercel/express/node_modules/rolldown": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.1.tgz", - "integrity": "sha512-M3AeZjYE6UclblEf531Hch0WfVC/NOL43Cc+WdF3J50kk5/fvouHhDumSGTh0oRjbZ8C4faaVr5r6Nx1xMqDGg==", - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.110.0", - "@rolldown/pluginutils": "1.0.0-rc.1" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.1", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.1", - "@rolldown/binding-darwin-x64": "1.0.0-rc.1", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.1", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.1", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.1", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.1", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.1", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.1", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.1", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.1", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.1", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.1" - } - }, "node_modules/@vercel/express/node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -10159,12 +9588,12 @@ } }, "node_modules/@vercel/fastify": { - "version": "0.1.57", - "resolved": "https://registry.npmjs.org/@vercel/fastify/-/fastify-0.1.57.tgz", - "integrity": "sha512-SCvqLjOPXUR+e5XZ2bCbSg2w0AzGQ/GGdap9Day8f/veQ37jpRmMQViHZyyl+R+Z4So04P6BjNkv9IiqtAgGiQ==", + "version": "0.1.58", + "resolved": "https://registry.npmjs.org/@vercel/fastify/-/fastify-0.1.58.tgz", + "integrity": "sha512-QRuhhcPneqOvHEUT5He3gGSEkH7JTqNhSIQG298mLEx2fL8OfVJHLHq/50GHVBhRLzIkFA2lfLQbRuwqRSyDvA==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.6.21", + "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, @@ -10324,13 +9753,13 @@ } }, "node_modules/@vercel/gatsby-plugin-vercel-builder": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.1.5.tgz", - "integrity": "sha512-ygLZ5kvoFQk5VtNLclG1aeIm8oxXt3CWy66mCMFdJX9amOQQc6SIzieCvmyK1OSyJSagA/duHP/ha8w4XraM+Q==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vercel/gatsby-plugin-vercel-builder/-/gatsby-plugin-vercel-builder-2.1.6.tgz", + "integrity": "sha512-Lh0wbhs7WR/h05iBPqaoxVDY5Ut4/GgLlO15H8TUIonZZcAfHD1gm5owdyLIPwOTGvtz3qoxxsgc0hSMo/zCkw==", "license": "Apache-2.0", "dependencies": { "@sinclair/typebox": "0.25.24", - "@vercel/build-utils": "13.11.0", + "@vercel/build-utils": "13.12.0", "esbuild": "0.27.0", "etag": "1.8.1", "fs-extra": "11.1.0" @@ -10814,23 +10243,23 @@ "license": "Apache-2.0" }, "node_modules/@vercel/h3": { - "version": "0.1.63", - "resolved": "https://registry.npmjs.org/@vercel/h3/-/h3-0.1.63.tgz", - "integrity": "sha512-n25ee7ac0iciiOo1OubwGOah/hvr+ML0PdDsX41gcckASZeDXe9jxgS1uDjvKMRa+bxCJ++qZL7vrRtJwa39vg==", + "version": "0.1.64", + "resolved": "https://registry.npmjs.org/@vercel/h3/-/h3-0.1.64.tgz", + "integrity": "sha512-zH57M8boAs+YKbT0gtnnq4aBYNKjpXwd1p7h8PlnH3MrQmAAapwb9SCAndRLu8STJSUCsYbT/JLk66rWLTMXNQ==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.6.21", + "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "node_modules/@vercel/hono": { - "version": "0.2.57", - "resolved": "https://registry.npmjs.org/@vercel/hono/-/hono-0.2.57.tgz", - "integrity": "sha512-kspwVshjkxI5EItV9Re6WXbP5kG3XA3WpreBbj9GvUckF3i4s8zdYU+NB2yDecZv2gwej/6n1N2BZGTJoLkYVQ==", + "version": "0.2.58", + "resolved": "https://registry.npmjs.org/@vercel/hono/-/hono-0.2.58.tgz", + "integrity": "sha512-yOxq0aGAi5SxFagN/uKgksSv1eNj3NqosZdbhzwd3fRVttGFBloaGuDjNFG1hq/J5O6gm/tRYrLt99amIkINjQ==", "license": "Apache-2.0", "dependencies": { "@vercel/nft": "1.5.0", - "@vercel/node": "5.6.21", + "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0", "fs-extra": "11.1.0", "path-to-regexp": "8.3.0", @@ -10882,22 +10311,22 @@ } }, "node_modules/@vercel/koa": { - "version": "0.1.37", - "resolved": "https://registry.npmjs.org/@vercel/koa/-/koa-0.1.37.tgz", - "integrity": "sha512-297QvFPVrrF8se7pASI+WQgsvJJOVZaU2LzpSl+YFNhYRy3La+lAv3wW7O3M4Rcts90i7u0QSV3muFAaceYn/g==", + "version": "0.1.38", + "resolved": "https://registry.npmjs.org/@vercel/koa/-/koa-0.1.38.tgz", + "integrity": "sha512-Xtnw41TPf84l/tL7eg4ktciBdAJoNZBDZ4lVCZL4/RCVxL4cTr0moC+yRiJ6nC7/eT3leml6ni1eOvG2479kBw==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.6.21", + "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, "node_modules/@vercel/nestjs": { - "version": "0.2.58", - "resolved": "https://registry.npmjs.org/@vercel/nestjs/-/nestjs-0.2.58.tgz", - "integrity": "sha512-pkPqDNdZnsjPqbwokHNfC5QffO14VptmtyA11GXoHuv4FqY3//US1aL7j3w6MNFBlnt2MBMCtBzDxMk7quzKwg==", + "version": "0.2.59", + "resolved": "https://registry.npmjs.org/@vercel/nestjs/-/nestjs-0.2.59.tgz", + "integrity": "sha512-0LbEeFqMSUPU/2nd1alZYkEpDNq+4WU1G00ROhKAXbfap/s6ChcbBKG9Xi86jUTRJ4ntoIf6PGzlhY03bjlwcA==", "license": "Apache-2.0", "dependencies": { - "@vercel/node": "5.6.21", + "@vercel/node": "5.6.22", "@vercel/static-config": "3.2.0" } }, @@ -11033,16 +10462,16 @@ } }, "node_modules/@vercel/node": { - "version": "5.6.21", - "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.6.21.tgz", - "integrity": "sha512-9urfbYDRMt7VKFh7E1zpmBM1KKpifgT9mjqETWrvRsBr2hdZIT8BUeQS1STQNmk5jRW5L+SOsdE5rAHSiTLR/g==", + "version": "5.6.22", + "resolved": "https://registry.npmjs.org/@vercel/node/-/node-5.6.22.tgz", + "integrity": "sha512-Lu6ToUYz8oMdYwN+6OR9KCh2MbkAloBu5YFKlEmIc51Je9Q21W/kqBneA3ZuWlMMpYHstz1E19GL+tqHb7yFdw==", "license": "Apache-2.0", "dependencies": { "@edge-runtime/node-utils": "2.3.0", "@edge-runtime/primitives": "4.1.0", "@edge-runtime/vm": "3.2.0", "@types/node": "20.11.0", - "@vercel/build-utils": "13.11.0", + "@vercel/build-utils": "13.12.0", "@vercel/error-utils": "2.0.3", "@vercel/nft": "1.5.0", "@vercel/static-config": "3.2.0", @@ -11795,13 +11224,13 @@ "license": "ISC" }, "node_modules/@vercel/static-build": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.9.5.tgz", - "integrity": "sha512-aEPr8vm5NUOkHA6A1ir/ri/R6P5902AkVDHA6RMWNvFs2ZIqbLxcGCnjnShh9qnoPY4rQVKEkeAO7MkN8pjRGw==", + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/@vercel/static-build/-/static-build-2.9.6.tgz", + "integrity": "sha512-XBUHUuYNfRi2JVPJF1lx+etTQhfVFVzAW3e8/5Aw/kKtfT1fDLc+uR/fadWlCC+ezXUcyV/h25O5nAk3g3vBaA==", "license": "Apache-2.0", "dependencies": { "@vercel/gatsby-plugin-vercel-analytics": "1.0.11", - "@vercel/gatsby-plugin-vercel-builder": "2.1.5", + "@vercel/gatsby-plugin-vercel-builder": "2.1.6", "@vercel/static-config": "3.2.0", "ts-morph": "12.0.0" } @@ -11861,127 +11290,6 @@ } } }, - "node_modules/@vite-pwa/astro/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/@vite-pwa/astro/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/@vite-pwa/astro/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@vite-pwa/astro/node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "license": "MIT", - "peer": true, - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/@vite-pwa/astro/node_modules/vite-plugin-pwa": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", @@ -14150,9 +13458,9 @@ } }, "node_modules/astro": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/astro/-/astro-6.1.1.tgz", - "integrity": "sha512-vq8sHpu1JsY1fWAunn+tdKNbVDmLQNiVdyuGsVT2csgITdFGXXVAyEXFWc1DzkMN0ehElPeiHnqItyQOJK+GqA==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/astro/-/astro-6.1.2.tgz", + "integrity": "sha512-r3iIvmB6JvQxsdJLvapybKKq7Bojd1iQK6CCx5P55eRnXJIyUpHx/1UB/GdMm+em/lwaCUasxHCmIO0lCLV2uA==", "license": "MIT", "dependencies": { "@astrojs/compiler": "^3.0.1", @@ -14368,37 +13676,6 @@ "node": ">=0.3.1" } }, - "node_modules/astro/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/astro/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/astro/node_modules/p-queue": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", @@ -14471,80 +13748,6 @@ } } }, - "node_modules/astro/node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/astro/node_modules/yargs-parser": { "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", @@ -14883,9 +14086,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.11", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.11.tgz", - "integrity": "sha512-DAKrHphkJyiGuau/cFieRYhcTFeK/lBuD++C7cZ6KZHbMhBrisoi+EvhQ5RZrIfV5qwsW8kgQ07JIC+MDJRAhg==", + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", + "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -19829,6 +19032,18 @@ "which": "bin/which" } }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -22051,12 +21266,6 @@ "node": ">=14.17" } }, - "node_modules/isomorphic-git/node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -22673,9 +21882,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.40", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.40.tgz", - "integrity": "sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg==", + "version": "1.12.41", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", + "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", "license": "MIT" }, "node_modules/libsql": { @@ -27152,9 +26361,9 @@ } }, "node_modules/npm": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-11.12.0.tgz", - "integrity": "sha512-xPhOap4ZbJWyd7DAOukP564WFwNSGu/2FeTRFHhiiKthcauxhH/NpkJAQm24xD+cAn8av5tQ00phi98DqtfLsg==", + "version": "11.12.1", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.12.1.tgz", + "integrity": "sha512-zcoUuF1kezGSAo0CqtvoLXX3mkRqzuqYdL6Y5tdo8g69NVV3CkjQ6ZBhBgB4d7vGkPcV6TcvLi3GRKPDFX+xTA==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -27233,7 +26442,7 @@ "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/arborist": "^9.4.2", - "@npmcli/config": "^10.8.0", + "@npmcli/config": "^10.8.1", "@npmcli/fs": "^5.0.0", "@npmcli/map-workspaces": "^5.0.3", "@npmcli/metavuln-calculator": "^9.0.3", @@ -27413,7 +26622,7 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "10.8.0", + "version": "10.8.1", "inBundle": true, "license": "ISC", "dependencies": { @@ -29491,6 +28700,12 @@ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "license": "MIT" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -29916,6 +29131,24 @@ "through": "~2.3" } }, + "node_modules/pdf-lib": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz", + "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==", + "license": "MIT", + "dependencies": { + "@pdf-lib/standard-fonts": "^1.0.0", + "@pdf-lib/upng": "^1.0.1", + "pako": "^1.0.11", + "tslib": "^1.11.1" + } + }, + "node_modules/pdf-lib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -30176,9 +29409,9 @@ } }, "node_modules/postal-mime": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz", - "integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==", + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", "license": "MIT-0" }, "node_modules/postcss": { @@ -30638,6 +29871,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/puppeteer": { + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", + "integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1581282", + "puppeteer-core": "24.40.0", + "typed-query-selector": "^2.12.1" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/puppeteer-core": { "version": "24.34.0", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.34.0.tgz", @@ -30662,6 +29916,91 @@ "integrity": "sha512-26T91cV5dbOYnXdJi5qQHoTtUoNEqwkHcAyu/IKtjIAxiEqPMrDiRkDOPWVsGfNZGmlQVHQbZRSjD8sxagWVsQ==", "license": "BSD-3-Clause" }, + "node_modules/puppeteer/node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer/node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/puppeteer/node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "license": "BSD-3-Clause" + }, + "node_modules/puppeteer/node_modules/puppeteer-core": { + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz", + "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/puppeteer/node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/puppeteer/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/qified": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/qified/-/qified-0.6.0.tgz", @@ -32920,13 +32259,13 @@ } }, "node_modules/resend": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/resend/-/resend-6.9.4.tgz", - "integrity": "sha512-/M3dsJzu5OgozqVsA4Psd/1L7EdePgOIIxClas453GOQYFG3VHc2ZyCHZFlvqsc9aZCCd2BJRRqZgWC8D9c7/g==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.10.0.tgz", + "integrity": "sha512-i7CwZpYj4Oho1RxsTpLcCUkO08+HiL4NXrm6jLJ2WzJ89UGI8eROSieLONJA3hnUrf1OYnCyfq5F6POnHUMv1Q==", "license": "MIT", "dependencies": { - "postal-mime": "2.7.3", - "svix": "1.86.0" + "postal-mime": "2.7.4", + "svix": "1.88.0" }, "engines": { "node": ">=20" @@ -33640,13 +32979,13 @@ "license": "Unlicense" }, "node_modules/rolldown": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", - "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "version": "1.0.0-rc.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.1.tgz", + "integrity": "sha512-M3AeZjYE6UclblEf531Hch0WfVC/NOL43Cc+WdF3J50kk5/fvouHhDumSGTh0oRjbZ8C4faaVr5r6Nx1xMqDGg==", "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.122.0", - "@rolldown/pluginutils": "1.0.0-rc.12" + "@oxc-project/types": "=0.110.0", + "@rolldown/pluginutils": "1.0.0-rc.1" }, "bin": { "rolldown": "bin/cli.mjs" @@ -33655,21 +32994,19 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", - "@rolldown/binding-darwin-x64": "1.0.0-rc.12", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + "@rolldown/binding-android-arm64": "1.0.0-rc.1", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.1", + "@rolldown/binding-darwin-x64": "1.0.0-rc.1", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.1", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.1", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.1", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.1", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.1", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.1", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.1", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.1", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.1", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.1" } }, "node_modules/rollup": { @@ -35664,9 +35001,9 @@ } }, "node_modules/svix": { - "version": "1.86.0", - "resolved": "https://registry.npmjs.org/svix/-/svix-1.86.0.tgz", - "integrity": "sha512-/HTvXwjLJe1l/MsLXAO1ddCYxElJk4eNR4DzOjDOEmGrPN/3BtBE8perGwMAaJ2sT5T172VkBYzmHcjUfM1JRQ==", + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.88.0.tgz", + "integrity": "sha512-vm/JrrUd3bVyBE+3L33TIyVSs8gS5fYx7lrISvKlDJXTYX1ACH4REX8P1tHxsSKoZi/rvifM1t0XRc5Vc45THw==", "license": "MIT", "dependencies": { "standardwebhooks": "1.0.0", @@ -36332,9 +35669,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "license": "MIT", "engines": { "node": ">=18.12" @@ -36557,9 +35894,9 @@ } }, "node_modules/typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", "license": "MIT" }, "node_modules/typedarray": { @@ -36618,102 +35955,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", - "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/utils": "8.57.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.57.2", - "@typescript-eslint/tsconfig-utils": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/project-service": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", - "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.2", - "@typescript-eslint/types": "^8.57.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree/node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", - "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", - "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.0.tgz", + "integrity": "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA==", "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2" + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -36724,55 +35974,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/typescript-eslint/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/typescript-eslint/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/typescript-eslint/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/typescript-eslint/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/ua-parser-js": { @@ -37889,34 +37091,34 @@ } }, "node_modules/vercel": { - "version": "50.37.1", - "resolved": "https://registry.npmjs.org/vercel/-/vercel-50.37.1.tgz", - "integrity": "sha512-ck/jb4qCc1xsOZa/7E3qrvf7BVtfnyXwyLZbhT54TnMXRsBTRHptPzQdNg2j4+AJYSMDTDmPVTiwJCpHnevYaQ==", + "version": "50.37.3", + "resolved": "https://registry.npmjs.org/vercel/-/vercel-50.37.3.tgz", + "integrity": "sha512-ARHoIjLrCsdrXx3uJ3m7CifCE7NizI2mLyQOxH4e5KM4+FruZuTmhzhiMx2rPtJkffkpeH64YimciibwX0YcOg==", "license": "Apache-2.0", "dependencies": { - "@vercel/backends": "0.0.52", + "@vercel/backends": "0.0.53", "@vercel/blob": "2.3.0", - "@vercel/build-utils": "13.11.0", + "@vercel/build-utils": "13.12.0", "@vercel/detect-agent": "1.2.1", - "@vercel/elysia": "0.1.54", - "@vercel/express": "0.1.64", - "@vercel/fastify": "0.1.57", + "@vercel/elysia": "0.1.55", + "@vercel/express": "0.1.65", + "@vercel/fastify": "0.1.58", "@vercel/fun": "1.3.0", "@vercel/go": "3.4.6", - "@vercel/h3": "0.1.63", - "@vercel/hono": "0.2.57", + "@vercel/h3": "0.1.64", + "@vercel/hono": "0.2.58", "@vercel/hydrogen": "1.3.6", - "@vercel/koa": "0.1.37", - "@vercel/nestjs": "0.2.58", + "@vercel/koa": "0.1.38", + "@vercel/nestjs": "0.2.59", "@vercel/next": "4.16.3", - "@vercel/node": "5.6.21", + "@vercel/node": "5.6.22", "@vercel/prepare-flags-definitions": "0.2.1", "@vercel/python": "6.28.0", "@vercel/redwood": "2.4.12", "@vercel/remix-builder": "5.7.2", "@vercel/ruby": "2.3.2", "@vercel/rust": "1.0.5", - "@vercel/static-build": "2.9.5", + "@vercel/static-build": "2.9.6", "chokidar": "4.0.0", "esbuild": "0.27.0", "form-data": "^4.0.0", @@ -38348,252 +37550,13 @@ "node": ">=18" } }, - "node_modules/vercel/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/vercel/node_modules/@oxc-project/types": { - "version": "0.110.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.110.0.tgz", - "integrity": "sha512-6Ct21OIlrEnFEJk5LT4e63pk3btsI6/TusD/GStLi7wYlGJNOl1GI9qvXAnRAxQU9zqA2Oz+UwhfTOU2rPZVow==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.1.tgz", - "integrity": "sha512-He6ZoCfv5D7dlRbrhNBkuMVIHd0GDnjJwbICE1OWpG7G3S2gmJ+eXkcNLJjzjNDpeI2aRy56ou39AJM9AD8YFA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.1.tgz", - "integrity": "sha512-YzJdn08kSOXnj85ghHauH2iHpOJ6eSmstdRTLyaziDcUxe9SyQJgGyx/5jDIhDvtOcNvMm2Ju7m19+S/Rm1jFg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.1.tgz", - "integrity": "sha512-cIvAbqM+ZVV6lBSKSBtlNqH5iCiW933t1q8j0H66B3sjbe8AxIRetVqfGgcHcJtMzBIkIALlL9fcDrElWLJQcQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.1.tgz", - "integrity": "sha512-rVt+B1B/qmKwCl1XD02wKfgh3vQPXRXdB/TicV2w6g7RVAM1+cZcpigwhLarqiVCxDObFZ7UgXCxPC7tpDoRog==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.1.tgz", - "integrity": "sha512-69YKwJJBOFprQa1GktPgbuBOfnn+EGxu8sBJ1TjPER+zhSpYeaU4N07uqmyBiksOLGXsMegymuecLobfz03h8Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.1.tgz", - "integrity": "sha512-9JDhHUf3WcLfnViFWm+TyorqUtnSAHaCzlSNmMOq824prVuuzDOK91K0Hl8DUcEb9M5x2O+d2/jmBMsetRIn3g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.1.tgz", - "integrity": "sha512-UvApLEGholmxw/HIwmUnLq3CwdydbhaHHllvWiCTNbyGom7wTwOtz5OAQbAKZYyiEOeIXZNPkM7nA4Dtng7CLw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.1.tgz", - "integrity": "sha512-uVctNgZHiGnJx5Fij7wHLhgw4uyZBVi6mykeWKOqE7bVy9Hcxn0fM/IuqdMwk6hXlaf9fFShDTFz2+YejP+x0A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.1.tgz", - "integrity": "sha512-T6Eg0xWwcxd/MzBcuv4Z37YVbUbJxy5cMNnbIt/Yr99wFwli30O4BPlY8hKeGyn6lWNtU0QioBS46lVzDN38bg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.1.tgz", - "integrity": "sha512-PuGZVS2xNJyLADeh2F04b+Cz4NwvpglbtWACgrDOa5YDTEHKwmiTDjoD5eZ9/ptXtcpeFrMqD2H4Zn33KAh1Eg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.1.tgz", - "integrity": "sha512-2mOxY562ihHlz9lEXuaGEIDCZ1vI+zyFdtsoa3M62xsEunDXQE+DVPO4S4x5MPK9tKulG/aFcA/IH5eVN257Cw==", - "cpu": [ - "wasm32" - ], - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.1.tgz", - "integrity": "sha512-oQVOP5cfAWZwRD0Q3nGn/cA9FW3KhMMuQ0NIndALAe6obqjLhqYVYDiGGRGrxvnjJsVbpLwR14gIUYnpIcHR1g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.1.tgz", - "integrity": "sha512-Ydsxxx++FNOuov3wCBPaYjZrEvKOOGq3k+BF4BPridhg2pENfitSRD2TEuQ8i33bp5VptuNdC9IzxRKU031z5A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/vercel/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.1.tgz", - "integrity": "sha512-UTBjtTxVOhodhzFVp/ayITaTETRHPUPYZPXQe0WU0wOgxghMojXxYjOiPOauKIYNWJAWS2fd7gJgGQK8GU8vDA==", - "license": "MIT" - }, "node_modules/vercel/node_modules/@vercel/backends": { - "version": "0.0.52", - "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.0.52.tgz", - "integrity": "sha512-dxUCxjy2jyAcq77b9A0mWSiw8SlO9U2xfSAN814lOwtqwuiqiGN3CIxrZLgnK0IDHcbQ11jg4FDuD5Le545B2A==", + "version": "0.0.53", + "resolved": "https://registry.npmjs.org/@vercel/backends/-/backends-0.0.53.tgz", + "integrity": "sha512-f8JK9Jatlco6kT/NsLYVR88iqS7oZxUXzXW8TofLmyTT5o+OEXSY2hzAQSjul6r0qz/9yAhtQkMhcHG8Ne01ZQ==", "license": "Apache-2.0", "dependencies": { - "@vercel/build-utils": "13.11.0", + "@vercel/build-utils": "13.12.0", "@vercel/nft": "1.5.0", "execa": "3.2.0", "fs-extra": "11.1.0", @@ -38739,37 +37702,6 @@ "node": ">= 14" } }, - "node_modules/vercel/node_modules/rolldown": { - "version": "1.0.0-rc.1", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.1.tgz", - "integrity": "sha512-M3AeZjYE6UclblEf531Hch0WfVC/NOL43Cc+WdF3J50kk5/fvouHhDumSGTh0oRjbZ8C4faaVr5r6Nx1xMqDGg==", - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.110.0", - "@rolldown/pluginutils": "1.0.0-rc.1" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.1", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.1", - "@rolldown/binding-darwin-x64": "1.0.0-rc.1", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.1", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.1", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.1", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.1", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.1", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.1", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.1", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.1", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.1", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.1" - } - }, "node_modules/vercel/node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -39137,15 +38069,16 @@ } }, "node_modules/vite": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", - "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.4", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.12", + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "bin": { @@ -39162,10 +38095,9 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", + "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -39178,18 +38110,15 @@ "@types/node": { "optional": true }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, "jiti": { "optional": true }, "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, @@ -39240,6 +38169,23 @@ "he": "1.2.0" } }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -40422,9 +39368,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index b5a04f579..62a5ff82d 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "lint:style": "FORCE_COLOR=1 npx stylelint \"src/**/*.{css,astro}\"", "lint:tsc:check": "npm run sync && tsc --noEmit -p tsconfig.json --pretty false", "search:reindex": "python3 scripts/search-index.py", + "pdf:generate": "node scripts/generate-pdfs/index.mjs", "search:content-length": "python3 scripts/search_content_lengths.py", "search:relevancy": "python3 scripts/search_relevancy.py", "search:relevancy:reranking": "python3 scripts/search_relevancy.py --reranking", @@ -116,8 +117,8 @@ "@types/react": "^19.2.14", "@types/sanitize-html": "^2.16.1", "@types/to-ico": "1.1.3", - "@typescript-eslint/eslint-plugin": "8.57.2", - "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/eslint-plugin": "8.58.0", + "@typescript-eslint/parser": "8.58.0", "@ungap/structured-clone": "^1.3.0", "@uppy/audio": "^3.1.0", "@uppy/box": "^4.1.0", @@ -133,11 +134,11 @@ "@vitest/coverage-v8": "^4.1.2", "@webcomponents/template-shadowroot": "^0.2.1", "alex": "^11.0.1", - "astro": "6.1.1", + "astro": "6.1.2", "astro-link-validator": "github:rodgtr1/astro-link-validator", "astro-og-canvas": "^0.10.1", "astro-vtbot": "^2.1.12", - "baseline-browser-mapping": "^2.10.11", + "baseline-browser-mapping": "^2.10.12", "canvas-confetti": "^1.9.4", "confusing-browser-globals": "1.0.11", "cross-env": "^10.1.0", @@ -157,6 +158,7 @@ "eslint-plugin-security": "4.0.0", "eslint-plugin-yml": "3.3.1", "focus-trap": "8.0.1", + "globals": "^17.4.0", "gsap": "^3.14.2", "happy-dom": "^20.8.9", "hast-util-heading-rank": "^3.0.0", @@ -168,7 +170,7 @@ "isomorphic-git": "^1.37.4", "js-cookie": "^3.0.5", "jsdom": "^29.0.1", - "libphonenumber-js": "1.12.40", + "libphonenumber-js": "1.12.41", "lighthouse": "^13.0.3", "lit": "^3.3.2", "markdownlint-cli2": "^0.22.0", @@ -177,7 +179,8 @@ "nanostores": "^1.2.0", "node-html-parser": "^7.1.0", "nodemailer": "^8.0.4", - "npm": "^11.12.0", + "npm": "^11.12.1", + "pdf-lib": "^1.17.1", "playwright-lighthouse": "^4.0.0", "postcss": "8.5.8", "postcss-html": "1.8.1", @@ -185,6 +188,7 @@ "prettier": "3.8.1", "prettier-plugin-astro": "0.14.1", "pubsub-js": "^1.9.5", + "puppeteer": "^24.40.0", "qr-code-styling": "^1.9.2", "rehype-accessible-emojis": "^0.3.2", "rehype-external-links": "^3.0.0", @@ -209,7 +213,7 @@ "remark-stringify": "^11.0.0", "remark-supersub": "^1.0.0", "remark-video": "^0.10.0", - "resend": "^6.9.4", + "resend": "^6.10.0", "retext": "^9.0.0", "retext-smartypants": "^6.2.0", "rimraf": "6.1.3", @@ -229,21 +233,24 @@ "to-ico": "1.1.5", "tslib": "2.8.1", "typescript": "6.0.2", - "typescript-eslint": "8.57.2", + "typescript-eslint": "8.58.0", "unified": "^11.0.5", "unist": "^0.0.1", "unist-util-inspect": "^8.1.0", "unist-util-is": "^6.0.1", "unist-util-visit": "^5.1.0", "uuid": "^13.0.0", - "vercel": "^50.37.1", - "vite": "^8.0.3", + "vercel": "^50.37.3", + "vite": "^7.3.1", "vitest": "4.1.2", "vitest-axe": "0.1.0", "workbox-build": "7.4.0", "zod": "4.3.6" }, "optionalDependencies": { - "@rollup/rollup-linux-x64-gnu": "^4.60.0" + "@rollup/rollup-linux-x64-gnu": "^4.60.1" + }, + "overrides": { + "vite": "^7.3.1" } } diff --git a/public/downloads/BenefitsofEndtoEndObservability.pdf b/public/downloads/BenefitsofEndtoEndObservability.pdf deleted file mode 100644 index 475493bf1..000000000 Binary files a/public/downloads/BenefitsofEndtoEndObservability.pdf and /dev/null differ diff --git a/public/downloads/Diagramming-Cloud-Architecture.pdf b/public/downloads/Diagramming-Cloud-Architecture.pdf deleted file mode 100644 index ed75c7989..000000000 Binary files a/public/downloads/Diagramming-Cloud-Architecture.pdf and /dev/null differ diff --git a/public/downloads/Identity-For-Dummies-Cisco-Duo.pdf b/public/downloads/Identity-For-Dummies-Cisco-Duo.pdf deleted file mode 100644 index 6dcabf869..000000000 Binary files a/public/downloads/Identity-For-Dummies-Cisco-Duo.pdf and /dev/null differ diff --git a/public/downloads/Kubernetes-performance-metrics.pdf b/public/downloads/Kubernetes-performance-metrics.pdf deleted file mode 100644 index e9cb4cfd6..000000000 Binary files a/public/downloads/Kubernetes-performance-metrics.pdf and /dev/null differ diff --git a/public/downloads/KubernetesMetricsToWatch.pdf b/public/downloads/KubernetesMetricsToWatch.pdf deleted file mode 100644 index e9cb4cfd6..000000000 Binary files a/public/downloads/KubernetesMetricsToWatch.pdf and /dev/null differ diff --git a/public/downloads/ansible-automation-platform-beginner's-guide.pdf b/public/downloads/ansible-automation-platform-beginner's-guide.pdf deleted file mode 100644 index facf5e08a..000000000 Binary files a/public/downloads/ansible-automation-platform-beginner's-guide.pdf and /dev/null differ diff --git a/public/downloads/ci-pipeline-caching-docker-layers-dependency-cache.pdf b/public/downloads/ci-pipeline-caching-docker-layers-dependency-cache.pdf new file mode 100644 index 000000000..31292705e Binary files /dev/null and b/public/downloads/ci-pipeline-caching-docker-layers-dependency-cache.pdf differ diff --git a/public/downloads/kubernetes-backup-recovery-dummies.pdf b/public/downloads/kubernetes-backup-recovery-dummies.pdf deleted file mode 100644 index 1f7805551..000000000 Binary files a/public/downloads/kubernetes-backup-recovery-dummies.pdf and /dev/null differ diff --git a/public/downloads/kubernetes-operators.pdf b/public/downloads/kubernetes-operators.pdf deleted file mode 100644 index eb0b377cb..000000000 Binary files a/public/downloads/kubernetes-operators.pdf and /dev/null differ diff --git a/scripts/generate-pdfs/html.mjs b/scripts/generate-pdfs/html.mjs new file mode 100644 index 000000000..cfb777f6f --- /dev/null +++ b/scripts/generate-pdfs/html.mjs @@ -0,0 +1,79 @@ + +import parsePhoneNumber from 'libphonenumber-js' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +import { buildTemplateStyles } from './styles.mjs' + +const CONTACT_JSON = resolve('src/content/contact.json') +const KEVIN_BROWN_AVATAR = resolve('src/assets/images/avatars/kevin-brown.webp') +const BRAND_LOGO_COMPONENT = resolve('src/components/Brand/Logo.astro') +const BRAND_WORDMARK_WEBSTACK_COMPONENT = resolve('src/components/Brand/WordmarkWebstack.astro') +const BRAND_WORDMARK_BUILDERS_COMPONENT = resolve('src/components/Brand/WordmarkBuilders.astro') + +const contact = JSON.parse(readFileSync(CONTACT_JSON, 'utf-8')).company +const kevinBrownAvatarDataUri = `data:image/webp;base64,${readFileSync(KEVIN_BROWN_AVATAR).toString('base64')}` + +const extractInlineSvg = (filePath, rootClassName) => { + const source = readFileSync(filePath, 'utf-8') + const withoutFrontmatter = source.replace(/^---[\s\S]*?---\s*/, '') + + return withoutFrontmatter + .replace(/class:list=\{\['[^']+', className\]\}/, `class="${rootClassName}"`) + .replace(/\n\s*/g, ' ') + .trim() +} + +const logoSvg = extractInlineSvg(BRAND_LOGO_COMPONENT, 'logo') +const wordmarkWebstackSvg = extractInlineSvg(BRAND_WORDMARK_WEBSTACK_COMPONENT, 'wordmark__svg') +const wordmarkBuildersSvg = extractInlineSvg(BRAND_WORDMARK_BUILDERS_COMPONENT, 'wordmark__svg') + +const esc = (s) => + s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') + +const formatPhoneNumber = (phoneNumber) => { + const parsedPhoneNumber = parsePhoneNumber(phoneNumber) + return parsedPhoneNumber ? parsedPhoneNumber.format('NATIONAL') : phoneNumber +} + +export const buildHeaderTemplate = (title) => + `${buildTemplateStyles()} + <header class="pdf-header" role="banner"> + <div class="pdf-header__inner"> + <div class="pdf-brand" aria-label="Webstack Builders"> + <span class="pdf-brand__logo">${logoSvg}</span> + <span class="pdf-brand__wordmarks" aria-hidden="true"> + <span class="pdf-brand__wordmark-row">${wordmarkWebstackSvg}</span> + <span class="pdf-brand__wordmark-row">${wordmarkBuildersSvg}</span> + </span> + </div> + <span class="pdf-header__title">${esc(title)}</span> + </div> + </header>` + +export const footerTemplate = +`${buildTemplateStyles()} +<footer class="pdf-footer" role="contentinfo"> + <div class="pdf-footer__inner"> + <div class="pdf-layout-footer__left"> + <div class="pdf-layout-footer__avatar"> + <img class="pdf-layout-footer__avatar-img" src="${kevinBrownAvatarDataUri}" alt="Kevin Brown"> + </div> + <address class="pdf-layout-footer__address"> + <h2 class="pdf-layout-footer__company-name">${esc(contact.name)}</h2> + <p>${esc(contact.address)}</p> + <p>${esc(contact.city)}, ${esc(contact.state)} ${esc(contact.index)}</p> + </address> + </div> + + <div class="pdf-layout-footer__center" aria-hidden="true"> + Page <span class="pageNumber"></span> of <span class="totalPages"></span> + </div> + + <address class="pdf-layout-footer__right"> + <p>${esc(contact.email)}</p> + <p>Toll Free ${esc(formatPhoneNumber(contact.telephoneTollFree))}</p> + <p>Local ${esc(formatPhoneNumber(contact.telephoneLocal))}</p> + </address> + </div> +</footer>` diff --git a/scripts/generate-pdfs/index.mjs b/scripts/generate-pdfs/index.mjs new file mode 100644 index 000000000..9ed151f80 --- /dev/null +++ b/scripts/generate-pdfs/index.mjs @@ -0,0 +1,254 @@ +#!/usr/bin/env node + +/** + * Generate PDFs from /print/ pages using Puppeteer. + * + * Usage: + * - node scripts/generate-pdfs.mjs <slug> # single article + * - node scripts/generate-pdfs.mjs # all deep dive articles + * - OR npm run pdf:generate + * + * Requires a running server (dev or preview) at localhost:4321. + * Set PDF_SERVER_URL to override the server base URL. + */ + +import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { PDFDocument } from 'pdf-lib' +import puppeteer from 'puppeteer' +import { buildHeaderTemplate, footerTemplate } from './html.mjs' + +// --- Configuration --- + +const SERVER_BASE = process.env.PDF_SERVER_URL ?? 'http://localhost:4321' +const OUTPUT_DIR = resolve('public/downloads') +const ARTICLES_DIR = resolve('src/content/articles') +const slugFilter = process.argv[2] ?? null + +// PDF page: US Letter (8.5 × 11 in) +// These values must stay in sync with `.print-document` padding in +// src/pages/print/index.css so the viewport and ToC pagination math reflect +// the actual printable content box. +const CONTENT_TOP_PADDING_CM = 3 +const CONTENT_BOTTOM_PADDING_CM = 2.5 +const CONTENT_SIDE_PADDING_CM = 1.5 +const CONTENT_PADDING_CM = { + top: CONTENT_TOP_PADDING_CM, + right: CONTENT_SIDE_PADDING_CM, + bottom: CONTENT_BOTTOM_PADDING_CM, + left: CONTENT_SIDE_PADDING_CM, +} +const PDF_MARGIN = { top: '0px', right: '0px', bottom: '0px', left: '0px' } + +// Content area in pixels at 96 DPI (for approximate ToC page-number calculation) +const CM_PER_IN = 2.54 +const PAGE_HEIGHT_IN = 11 +const CONTENT_PADDING_TOP_IN = CONTENT_PADDING_CM.top / CM_PER_IN +const CONTENT_PADDING_BOTTOM_IN = CONTENT_PADDING_CM.bottom / CM_PER_IN +const CONTENT_PADDING_LEFT_IN = CONTENT_PADDING_CM.left / CM_PER_IN +const CONTENT_PADDING_RIGHT_IN = CONTENT_PADDING_CM.right / CM_PER_IN +const CONTENT_HEIGHT_PX = (PAGE_HEIGHT_IN - CONTENT_PADDING_TOP_IN - CONTENT_PADDING_BOTTOM_IN) * 96 +const CONTENT_WIDTH_PX = (8.5 - CONTENT_PADDING_LEFT_IN - CONTENT_PADDING_RIGHT_IN) * 96 + +// --- Templates --- + + +// --- Slug collection --- + +const collectAllSlugs = () => { + if (!existsSync(ARTICLES_DIR)) return [] + return readdirSync(ARTICLES_DIR, { withFileTypes: true }) + .filter(e => e.isDirectory() && existsSync(join(ARTICLES_DIR, e.name, 'pdf.mdx'))) + .map(e => e.name) + .sort() +} + +// --- Server check --- + +const checkServer = async () => { + try { + await fetch(SERVER_BASE, { signal: AbortSignal.timeout(5000) }) + return true + } catch { + return false + } +} + +const buildPdfOptions = ({ + title, + includeHeaderFooter, + pageRanges, +}) => { + return { + format: 'Letter', + margin: PDF_MARGIN, + displayHeaderFooter: includeHeaderFooter, + ...(includeHeaderFooter + ? { + headerTemplate: buildHeaderTemplate(title ?? ''), + footerTemplate, + } + : {}), + printBackground: true, + preferCSSPageSize: false, + tagged: true, + outline: true, + ...(pageRanges ? { pageRanges } : {}), + } +} + +const writeMergedPdf = async ({ coverPdfBytes, fullPdfBytes, outputPdf }) => { + const mergedPdf = await PDFDocument.create() + const coverPdf = await PDFDocument.load(coverPdfBytes) + const fullPdf = await PDFDocument.load(fullPdfBytes) + + const [coverPage] = await mergedPdf.copyPages(coverPdf, [0]) + mergedPdf.addPage(coverPage) + + const remainingPageIndices = fullPdf.getPageIndices().slice(1) + if (remainingPageIndices.length > 0) { + const remainingPages = await mergedPdf.copyPages(fullPdf, remainingPageIndices) + for (const pdfPage of remainingPages) { + mergedPdf.addPage(pdfPage) + } + } + + writeFileSync(outputPdf, await mergedPdf.save()) +} + +// --- PDF generation --- + +const generatePdf = async (browser, slug) => { + const inputUrl = `${SERVER_BASE}/print/${slug}` + const outputPdf = join(OUTPUT_DIR, `${slug}.pdf`) + + console.log(` ${slug}`) + console.log(` input: ${inputUrl}`) + console.log(` output: ${outputPdf}`) + + const page = await browser.newPage() + + try { + // Viewport matches PDF content area for more accurate measurement + await page.setViewport({ + width: Math.round(CONTENT_WIDTH_PX), + height: Math.round(CONTENT_HEIGHT_PX), + }) + + await page.goto(inputUrl, { waitUntil: 'networkidle0', timeout: 60_000 }) + await page.emulateMediaType('print') + + // Wait for web fonts so PDF typography matches the site instead of fallback metrics. + await page.evaluate(() => { + return globalThis.document.fonts.ready + }) + + // Wait for all images to finish loading + await page.evaluate(() => { + const doc = globalThis.document + + return Promise.all( + Array.from(doc.images) + .filter(img => !img.complete) + .map(img => new Promise(r => { img.onload = r; img.onerror = r })) + ) + }) + + const title = await page.title() + + // Inject approximate page numbers into the ToC + await page.evaluate((contentH) => { + const doc = globalThis.document + const toc = doc.querySelector('.print-toc') + const article = doc.querySelector('.print-article') + if (!toc || !article) return + + // Cover always occupies 1 page (break-after: page) + const coverPages = 1 + const tocPages = Math.max(1, Math.ceil(toc.getBoundingClientRect().height / contentH)) + const pagesBeforeArticle = coverPages + tocPages + + const articleTop = article.getBoundingClientRect().top + const pageMap = new Map() + + for (const h of article.querySelectorAll('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]')) { + const offset = h.getBoundingClientRect().top - articleTop + pageMap.set(h.id, pagesBeforeArticle + Math.floor(Math.max(0, offset) / contentH) + 1) + } + + for (const span of doc.querySelectorAll('.print-toc__page')) { + const href = span.closest('a')?.getAttribute('href')?.replace('#', '') + if (href && pageMap.has(href)) { + span.textContent = String(pageMap.get(href)) + } + } + }, CONTENT_HEIGHT_PX) + + const [coverPdfBytes, fullPdfBytes] = await Promise.all([ + page.pdf(buildPdfOptions({ includeHeaderFooter: false, pageRanges: '1' })), + page.pdf(buildPdfOptions({ includeHeaderFooter: true, title })), + ]) + + await writeMergedPdf({ coverPdfBytes, fullPdfBytes, outputPdf }) + + console.log(' ✓ done\n') + return true + } catch (error) { + console.error(` ✗ FAILED: ${error.message ?? error}\n`) + return false + } finally { + await page.close() + } +} + +// --- Main --- + +mkdirSync(OUTPUT_DIR, { recursive: true }) + +if (!(await checkServer())) { + console.error(`ERROR: No server running at ${SERVER_BASE}`) + console.error('Start the dev server (npm run dev) or preview server (npm run preview) first.') + process.exit(1) +} + +if (slugFilter) { + const pdfMdx = join(ARTICLES_DIR, slugFilter, 'pdf.mdx') + if (!existsSync(pdfMdx)) { + console.error(`ERROR: No pdf.mdx found for "${slugFilter}"`) + console.error(`Expected: ${pdfMdx}`) + process.exit(1) + } +} + +const slugs = slugFilter ? [slugFilter] : collectAllSlugs() + +if (slugs.length === 0) { + console.error('ERROR: No deep dive articles with pdf.mdx found.') + process.exit(1) +} + +console.log(`Generating ${slugs.length} PDF(s) from ${SERVER_BASE}...\n`) + +const browser = await puppeteer.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox'], +}) + +let succeeded = 0 +let failed = 0 + +try { + for (const slug of slugs) { + const ok = await generatePdf(browser, slug) + if (ok) succeeded++ + else failed++ + } +} finally { + await browser.close() +} + +if (slugs.length > 1) { + console.log(`Results: ${succeeded} succeeded, ${failed} failed out of ${slugs.length} total`) +} + +process.exit(failed > 0 ? 1 : 0) diff --git a/scripts/generate-pdfs/styles.mjs b/scripts/generate-pdfs/styles.mjs new file mode 100644 index 000000000..5f381b5e6 --- /dev/null +++ b/scripts/generate-pdfs/styles.mjs @@ -0,0 +1,237 @@ +export const buildTemplateStyles = () => { + return `<style> + html, + * { + box-sizing: border-box; + } + + html, + body { + font-family: "Onest Regular", Arial, Helvetica, sans-serif; + height: 100%; + margin: 0; + overflow: hidden; + padding: 0; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + width: 100%; + } + + /** + * Do not remove, to stretch header and footer to top and bottom of page respectively + */ + #header, #footer { padding: 0 !important; } + + /** + * PDF Header Styles + */ + + .pdf-header { + box-sizing: border-box; + height: 1.5cm; + margin: 0 !important; + padding: 0 !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + width: 100% !important; + } + + .pdf-header__inner { + align-items: flex-end; + display: flex; + height: 100%; + justify-content: space-between; + padding: 0 1cm; + width: 100% !important; + } + + /** + * Brand is the company logo and wordmark elements that appear in the header + */ + + .pdf-brand { + align-items: center; + display: flex; + min-width: 0; + } + + /** + * Company logo + */ + + .pdf-brand__logo { + display: inline-flex; + flex: 0 0 auto; + height: 30px; + width: 30px; + } + + .pdf-brand__logo .logo { + display: block; + height: 100%; + width: 100%; + } + + .pdf-brand__logo .logo-block-outer { + fill: #001A39; + } + + .pdf-brand__logo .logo-block-inner { + fill: #006dca; + } + + /** + * Wordmark with the company name as SVG paths + */ + + .pdf-brand__wordmarks { + display: flex; + flex-direction: column; + gap: 6px; + margin-left: 8px; + min-width: 0; + } + + .pdf-brand__wordmark-row { + display: block; + height: 12px; + } + + .pdf-brand__wordmark-row .wordmark__svg { + display: block; + height: 100%; + width: auto; + } + + .pdf-brand__wordmark-row .wordmark__svg-path { + fill: #001A39; + } + + /** + * Article title on the right of the header + */ + + .pdf-header__title { + color: #6b7280; + font-size: 13pt; + font-weight: 700; + line-height: 1.1; + text-align: right; + } + + /** + * PDF Footer Styles + */ + + .pdf-footer { + background-color: #E5E7EB !important; + box-sizing: border-box; + color: #001A39; + height: 2cm; + margin: 0 !important; + padding: 0 !important; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + width: 100% !important; + } + + .pdf-footer__inner { + align-items: center; + display: flex; + gap: 12px; + height: 100%; + justify-content: space-between; + padding: 0 1cm; + width: 100% !important; + } + + /** + * Left column has avatar and company address + */ + + .pdf-layout-footer__left { + align-items: center; + display: flex; + gap: 4px; + width: 40%; + } + + .pdf-layout-footer__avatar { + align-items: center; + display: flex; + } + + .pdf-layout-footer__avatar-img { + border-radius: 9999px; + display: block; + height: 100%; + height: 32px; + object-fit: cover; + width: 100%; + width: 32px; + } + + .pdf-layout-footer__address { + display: flex; + flex-direction: column; + gap: 2px; + justify-content: center; + margin-left: 8px; + min-width: 0; + width: 100%; + } + + .pdf-layout-footer__company-name { + color: #001A39; + font-size: 12pt; + font-style: normal; + font-weight: 600; + line-height: 1.1; + margin: 0; + } + + + .pdf-layout-footer__address p { + color: #001A39; + font-size: 10pt; + font-style: normal; + line-height: 1.1; + margin: 0; + } + + /** + * Center column has page numbers + */ + + .pdf-layout-footer__center { + display: flex; + justify-content: center; + color: #001A39; + font-size: 10pt; + line-height: 1.1; + margin: 0; + width: 20%; + } + + /** + * Right column has phone numbers and email address + */ + + .pdf-layout-footer__right { + align-items: flex-end; + display: flex; + flex-direction: column; + justify-content: center; + min-width: 0; + width: 40%; + } + + .pdf-layout-footer__right p { + color: #001A39; + font-size: 10pt; + font-style: normal; + line-height: 1.2; + margin: 0; + } + </style>` +} diff --git a/src/components/BugReporter/index.astro b/src/components/BugReporter/index.astro index 60181e910..4041a97be 100644 --- a/src/components/BugReporter/index.astro +++ b/src/components/BugReporter/index.astro @@ -22,6 +22,7 @@ const { --- <bug-reporter-modal-element + class="print:hidden!" trigger-id={triggerId} dialog-id={dialogId} status-id={statusId} diff --git a/src/components/CallToAction/Contact/index.astro b/src/components/CallToAction/Contact/index.astro index fec3283b8..0f4ad05d6 100644 --- a/src/components/CallToAction/Contact/index.astro +++ b/src/components/CallToAction/Contact/index.astro @@ -50,7 +50,7 @@ const descriptionId = `${idBase}-description` --- <section - class="cta mx-auto max-w-4xl rounded-lg overflow-hidden relative print:hidden" + class="cta mx-auto max-w-4xl rounded-lg overflow-hidden relative print:hidden!" aria-labelledby={titleId} aria-describedby={descriptionId} > diff --git a/src/components/CallToAction/Download/index.astro b/src/components/CallToAction/Download/index.astro index 472033bb3..5e555bb53 100644 --- a/src/components/CallToAction/Download/index.astro +++ b/src/components/CallToAction/Download/index.astro @@ -58,7 +58,7 @@ const downloadUrl = `/downloads/${normalizedResource}` --- <section - class="bg-linear-to-br from-primary/5 to-accent/5 border border-primary/20 rounded-2xl p-6 md:p-8 mb-6 print:hidden" + class="bg-linear-to-br from-primary/5 to-accent/5 border border-primary/20 rounded-2xl p-6 md:p-8 mb-6 print:hidden!" aria-labelledby={titleId} aria-describedby={descriptionId} > diff --git a/src/components/CallToAction/Newsletter/client/@types/index.d.ts b/src/components/CallToAction/Newsletter/client/@types/index.d.ts index f4d2fd959..e28082fdb 100644 --- a/src/components/CallToAction/Newsletter/client/@types/index.d.ts +++ b/src/components/CallToAction/Newsletter/client/@types/index.d.ts @@ -1,4 +1,6 @@ export interface NewsletterProps { + /** Optional base id for internal aria relationships */ + id?: string /** Main heading for the newsletter signup */ title?: string /** Supporting description text */ diff --git a/src/components/CallToAction/Newsletter/client/__tests__/index.spec.ts b/src/components/CallToAction/Newsletter/client/__tests__/index.spec.ts index 4c557a62c..e80d34787 100644 --- a/src/components/CallToAction/Newsletter/client/__tests__/index.spec.ts +++ b/src/components/CallToAction/Newsletter/client/__tests__/index.spec.ts @@ -44,6 +44,8 @@ const getElements = (root: NewsletterFormElement) => { } return { + title: selectElement<HTMLElement>('[id$="-title"]'), + description: selectElement<HTMLElement>('[id$="-description"]'), form: selectElement<HTMLFormElement>('#newsletter-form'), emailLabel: selectElement<HTMLLabelElement>('#newsletter-email-label'), emailInput: selectElement<HTMLInputElement>('#newsletter-email'), @@ -100,10 +102,14 @@ describe.each(newsletterVariants)('NewsletterFormElement web component (%s)', va test('registers the custom element and hydrates DOM references', async () => { await renderNewsletter(async ({ elements }) => { expect(customElements.get('newsletter-form')).toBeDefined() + expect(elements.title.id).toBe('newsletter-cta-' + variant + '-title') + expect(elements.description.id).toBe('newsletter-cta-' + variant + '-description') expect(elements.form.id).toBe('newsletter-form') expect(elements.emailInput.id).toBe('newsletter-email') expect(elements.consentCheckbox.id).toBe('newsletter-gdpr-consent') + expect(elements.title.textContent).toContain(defaultNewsletterProps.title) + expect(elements.description.textContent).toContain(defaultNewsletterProps.description) expect(elements.emailLabel.getAttribute('for')).toBe('newsletter-email') expect(elements.emailLabel.textContent).toContain('Email') expect(elements.buttonArrow.getAttribute('aria-hidden')).toBe('true') @@ -111,6 +117,13 @@ describe.each(newsletterVariants)('NewsletterFormElement web component (%s)', va }) }) + test('builds aria relationships from the newsletter CTA id base', async () => { + await renderNewsletter(async ({ element }) => { + expect(element.getAttribute('aria-labelledby')).toBe('newsletter-cta-' + variant + '-title') + expect(element.getAttribute('aria-describedby')).toBe('newsletter-cta-' + variant + '-description') + }) + }) + test('validates missing and malformed email input before submission', async () => { await renderNewsletter(async ({ elements }) => { const submitEvent = new Event('submit', { bubbles: true, cancelable: true }) diff --git a/src/components/CallToAction/Newsletter/index.astro b/src/components/CallToAction/Newsletter/index.astro index 8b7e49831..25ed5beff 100644 --- a/src/components/CallToAction/Newsletter/index.astro +++ b/src/components/CallToAction/Newsletter/index.astro @@ -23,21 +23,32 @@ import PageNewsletter from './layouts/page.astro' export type Props = NewsletterProps const { + id, title = 'Stay Updated', description = 'Subscribe to our newsletter for the latest insights on web development and digital solutions.', placeholder = 'Enter your email address', buttonText = 'Subscribe', variant = 'article', } = Astro.props + +const idBase = id ?? `newsletter-cta-${variant}` +const titleId = `${idBase}-title` +const descriptionId = `${idBase}-description` --- -<newsletter-form class="cta block print:hidden"> +<newsletter-form + class="cta block print:hidden!" + aria-labelledby={titleId} + aria-describedby={descriptionId} +> {variant === 'article' && ( <ArticleNewsletter title={title} description={description} placeholder={placeholder} buttonText={buttonText} + titleId={titleId} + descriptionId={descriptionId} /> )} {variant === 'home' && ( @@ -46,6 +57,8 @@ const { description={description} placeholder={placeholder} buttonText={buttonText} + titleId={titleId} + descriptionId={descriptionId} /> )} {variant === 'page' && ( @@ -54,6 +67,8 @@ const { description={description} placeholder={placeholder} buttonText={buttonText} + titleId={titleId} + descriptionId={descriptionId} /> )} </newsletter-form> diff --git a/src/components/CallToAction/Newsletter/layouts/article.astro b/src/components/CallToAction/Newsletter/layouts/article.astro index cd69946e4..3bf15c4a4 100644 --- a/src/components/CallToAction/Newsletter/layouts/article.astro +++ b/src/components/CallToAction/Newsletter/layouts/article.astro @@ -3,10 +3,23 @@ import GDPRConsent from '@components/Consent/Checkbox/index.astro' import Confetti from '@components/Animations/Confetti/index.astro' import Icon from '@components/Icon/index.astro' -const title = 'Stay Updated' -const description = 'One deep dive per month on infrastructure topics, plus quick wins you can ship the same day.' -const placeholder = 'Enter your email address' -const buttonText = 'Subscribe' +interface Props { + title?: string + description?: string + placeholder?: string + buttonText?: string + titleId: string + descriptionId: string +} + +const { + title = 'Stay Updated', + description = 'One deep dive per month on infrastructure topics, plus quick wins you can ship the same day.', + placeholder = 'Enter your email address', + buttonText = 'Subscribe', + titleId, + descriptionId, +} = Astro.props --- <section class="mb-4 md:mb-6"> @@ -19,11 +32,11 @@ const buttonText = 'Subscribe' </div> <div class="space-y-2"> - <p class="text-page-base"> + <p id={titleId} class="text-page-base"> <span class="text-success">$</span> {` ${title}`} </p> - <p class="text-page-base/70 text-sm"> + <p id={descriptionId} class="text-page-base/70 text-sm"> <span class="text-page-base/50">></span> {` ${description}`} </p> diff --git a/src/components/CallToAction/Newsletter/layouts/home.astro b/src/components/CallToAction/Newsletter/layouts/home.astro index f6ed189f5..cbb4a4c63 100644 --- a/src/components/CallToAction/Newsletter/layouts/home.astro +++ b/src/components/CallToAction/Newsletter/layouts/home.astro @@ -5,10 +5,23 @@ import Confetti from '@components/Animations/Confetti/index.astro' import Icon from '@components/Icon/index.astro' import newsletterSignupImage from '@assets/images/newsletter-signup.jpg' -const title = 'Stay Updated' -const description = 'A monthly dispatch on platform engineering, DevOps, SRE, and cloud infrastructure. Real problems, real solutions.' -const placeholder = 'Enter your email address' -const buttonText = 'Subscribe' +interface Props { + title?: string + description?: string + placeholder?: string + buttonText?: string + titleId: string + descriptionId: string +} + +const { + title = 'Stay Updated', + description = 'A monthly dispatch on platform engineering, DevOps, SRE, and cloud infrastructure. Real problems, real solutions.', + placeholder = 'Enter your email address', + buttonText = 'Subscribe', + titleId, + descriptionId, +} = Astro.props --- <div class="container mx-auto md:px-4 sm:px-6 lg:px-8"> @@ -55,10 +68,10 @@ const buttonText = 'Subscribe' Monthly Dispatch </span> </div> - <h2 class="text-2xl md:text-3xl font-black text-content leading-tight drop-shadow-sm"> + <h2 id={titleId} class="text-2xl md:text-3xl font-black text-content leading-tight drop-shadow-sm"> {title} </h2> - <p class="text-base font-medium text-content-offset mt-2 max-w-lg"> + <p id={descriptionId} class="text-base font-medium text-content-offset mt-2 max-w-lg"> {description} </p> </div> diff --git a/src/components/CallToAction/Newsletter/layouts/page.astro b/src/components/CallToAction/Newsletter/layouts/page.astro index 7fee7f7a1..01d08a8da 100644 --- a/src/components/CallToAction/Newsletter/layouts/page.astro +++ b/src/components/CallToAction/Newsletter/layouts/page.astro @@ -10,10 +10,23 @@ import GDPRConsent from '@components/Consent/Checkbox/index.astro' import Confetti from '@components/Animations/Confetti/index.astro' import Icon from '@components/Icon/index.astro' -const title = 'Stay Updated' -const description = 'Platform engineering insights delivered to your inbox once a month. No spam, no sales pitches.' -const placeholder = 'Enter your email address' -const buttonText = 'Subscribe' +interface Props { + title?: string + description?: string + placeholder?: string + buttonText?: string + titleId: string + descriptionId: string +} + +const { + title = 'Stay Updated', + description = 'Platform engineering insights delivered to your inbox once a month. No spam, no sales pitches.', + placeholder = 'Enter your email address', + buttonText = 'Subscribe', + titleId, + descriptionId, +} = Astro.props --- <div class="w-full max-w-xl mx-auto"> @@ -26,10 +39,10 @@ const buttonText = 'Subscribe' Monthly Dispatch </span> </div> - <h2 class="text-2xl md:text-3xl font-black text-content leading-tight"> + <h2 id={titleId} class="text-2xl md:text-3xl font-black text-content leading-tight"> {title} </h2> - <p class="text-base text-content-offset mt-3 max-w-md mx-auto"> + <p id={descriptionId} class="text-base text-content-offset mt-3 max-w-md mx-auto"> {description} </p> </div> diff --git a/src/components/Callout/index.astro b/src/components/Callout/index.astro index 6e5c2510c..7bc255ee1 100644 --- a/src/components/Callout/index.astro +++ b/src/components/Callout/index.astro @@ -93,7 +93,7 @@ const iconColor = accentTextClasses.startsWith('text-') ? accentTextClasses.slic ]} role="note" > - <div class="flex items-center gap-2"> + <div class="callout__header flex items-center gap-2"> <div class:list={[ accentTextClasses, diff --git a/src/components/Carousel/index.astro b/src/components/Carousel/index.astro index 1da0ceeef..8be786a1a 100644 --- a/src/components/Carousel/index.astro +++ b/src/components/Carousel/index.astro @@ -91,7 +91,7 @@ const carouselRegionRole = carouselRegionLabelledBy ? 'region' : undefined <carousel-slider data-carousel data-carousel-navigation-mode={navigationMode} - class="block print:hidden" + class="block print:hidden!" > <section class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> {title && ( diff --git a/src/components/Code/CodeTabs/client/index.ts b/src/components/Code/CodeTabs/client/index.ts index 66c0d6cd3..09ec529dc 100644 --- a/src/components/Code/CodeTabs/client/index.ts +++ b/src/components/Code/CodeTabs/client/index.ts @@ -131,7 +131,7 @@ export class CodeTabsElement extends LitElement { private buildUi(args: { shouldRenderTabs: boolean; labels: string[] }): void { const header = document.createElement('div') header.className = - 'flex w-full min-w-0 items-center justify-between bg-page-offset' + 'code-tabs-print-header flex w-full min-w-0 items-center justify-between bg-page-offset' const copyButton = this.createCopyButton() diff --git a/src/components/Consent/Banner/index.astro b/src/components/Consent/Banner/index.astro index 209471e15..ca55a9b73 100644 --- a/src/components/Consent/Banner/index.astro +++ b/src/components/Consent/Banner/index.astro @@ -5,7 +5,7 @@ import Icon from '@components/Icon/index.astro' import CookieSvg from '@assets/images/site/cookie.svg' --- -<consent-banner> +<consent-banner class="print:hidden!"> <div id="consent-modal-id" class="bg-page-offset bottom-0 left-0 fixed right-0 w-full z-(--z-content-floating)" diff --git a/src/components/Content/Layout/index.astro b/src/components/Content/Layout/index.astro index ddd9b4494..a5a2d4d24 100644 --- a/src/components/Content/Layout/index.astro +++ b/src/components/Content/Layout/index.astro @@ -29,7 +29,6 @@ export type Props = { const { author, article, path, readingTime, section } = Astro.props -const isDeepDive = section === 'Deep Dive Articles' --- <MarkdownLayout @@ -79,22 +78,20 @@ const isDeepDive = section === 'Deep Dive Articles' } {/** SLOT: after-content */} - <section class="print:hidden" slot="after-content"> + <section class="print:hidden!" slot="after-content"> <WebMentions url={`https://www.webstackbuilders.com${path}`} /> - {!isDeepDive && ( - <div class="flex flex-col gap-6"> - <Shares - url={`https://www.webstackbuilders.com${path}`} - title={article.data.title} - description={`Read "${article.data.title}" on Webstack Builders`} - /> - </div> - )} + <div class="flex flex-col gap-6"> + <Shares + url={`https://www.webstackbuilders.com${path}`} + title={article.data.title} + description={`Read "${article.data.title}" on Webstack Builders`} + /> + </div> </section> {/** SLOT: related-content */} <section - class="border-t border-trim pt-8 mt-12 print:hidden" + class="border-t border-trim pt-8 mt-12 print:hidden!" aria-labelledby="article-related-title" slot="related-content" > diff --git a/src/components/Content/ProgressBar/index.astro b/src/components/Content/ProgressBar/index.astro index 138047005..36753af0e 100644 --- a/src/components/Content/ProgressBar/index.astro +++ b/src/components/Content/ProgressBar/index.astro @@ -11,7 +11,7 @@ --- <reading-progress-bar - class="fixed left-0 right-0 block h-1.5 sm:h-2" + class="print:hidden! fixed left-0 right-0 block h-1.5 sm:h-2" style="top: calc(var(--theme-picker-offset, 0px) + var(--header-current-height, 0px)); z-index: var(--z-nav);" data-progress-bar aria-hidden="true" diff --git a/src/components/Content/Switcher/index.astro b/src/components/Content/Switcher/index.astro index 7c0b75a5c..cb8163239 100644 --- a/src/components/Content/Switcher/index.astro +++ b/src/components/Content/Switcher/index.astro @@ -37,7 +37,11 @@ if (path) { } --- -<content-switcher current-variant={currentVariant} slug={slug}></content-switcher> +<content-switcher + class="print:hidden!" + current-variant={currentVariant} + slug={slug} +></content-switcher> <script> import { registerContentSwitcherComponent } from '@components/Content/Switcher/client' diff --git a/src/components/Diagram/index.astro b/src/components/Diagram/index.astro index 0b1c5501d..3a99b4798 100644 --- a/src/components/Diagram/index.astro +++ b/src/components/Diagram/index.astro @@ -47,7 +47,7 @@ const hasDefaultSlot = Astro.slots.has('default') <summary class="group list-none outline-none focus:outline-none focus-visible:outline-none [anchor-name:unset] [&::marker]:hidden [&::-webkit-details-marker]:hidden [&::before]:content-none [&::before]:border-0 [&::after]:content-none [&::after]:border-0"> <div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-base"> <span>{title}</span> - <span class="relative inline-flex shrink-0 items-center rounded-full bg-content px-2 py-1 text-xs leading-none text-content-inverse cursor-pointer select-none transition-colors duration-150 ease-linear group-hover:bg-content-offset group-focus-visible:bg-page-active after:pointer-events-none after:absolute after:content-[''] after:inset-0 after:rounded-none after:border-2 after:border-transparent after:opacity-0 after:transition-opacity after:duration-150 after:ease-out group-focus-visible:after:opacity-100 group-focus-visible:after:-inset-1 group-focus-visible:after:border-spotlight"> + <span class="relative inline-flex shrink-0 items-center rounded-full bg-content px-2 py-1 text-xs leading-none text-content-inverse cursor-pointer select-none transition-colors duration-150 ease-linear group-hover:bg-content-offset group-focus-visible:bg-page-active after:pointer-events-none after:absolute after:content-[''] after:inset-0 after:rounded-none after:border-2 after:border-transparent after:opacity-0 after:transition-opacity after:duration-150 after:ease-out group-focus-visible:after:opacity-100 group-focus-visible:after:-inset-1 group-focus-visible:after:border-spotlight print:hidden!"> description </span> </div> diff --git a/src/components/Footer/footer.module.css.d.ts b/src/components/Footer/footer.module.css.d.ts index cef1607da..270c75e84 100644 --- a/src/components/Footer/footer.module.css.d.ts +++ b/src/components/Footer/footer.module.css.d.ts @@ -1,6 +1,7 @@ declare const styles: { readonly footerGrid: string readonly footerContact: string + readonly footerPrintContact: string readonly footerSocial: string readonly footerBottomRow: string readonly footerBottomPrimary: string diff --git a/src/components/Footer/index.astro b/src/components/Footer/index.astro index 3fe59ffd9..ae53ca2fa 100644 --- a/src/components/Footer/index.astro +++ b/src/components/Footer/index.astro @@ -62,7 +62,7 @@ const footerTextLinkClasses = [ --- <site-footer transition:persist> - <footer class="w-full bg-page-offset mt-6 pt-4" role="contentinfo"> + <footer class="w-full bg-page-offset mt-6 pt-4 print:hidden" role="contentinfo"> <div id="footer-inner" class:list={[ @@ -79,11 +79,14 @@ const footerTextLinkClasses = [ <div class="mr-3 w-16"> <Avatar name="Kevin Brown" /> </div> + {/* Mobile: company name */} <h2 class="font-bold text-2xl sm:hidden">{contactData.company.name}</h2> </div> + {/* Address and Phone Numbers Block */} <address class="mt-3 text-base sm:mt-0"> - {/* Desktop: show company name here */} + {/* Desktop: company name */} <h2 class="hidden font-bold text-2xl sm:block">{contactData.company.name}</h2> + {/* Street Address */} <div class="mt-2 sm:ml-1"> <p>{contactData.company.address}</p> <p> @@ -91,6 +94,7 @@ const footerTextLinkClasses = [ {contactData.company.index} </p> </div> + {/* Email Address */} <p class="hyphens-none"> <a href={`mailto:${contactData.company.email}`} @@ -103,6 +107,7 @@ const footerTextLinkClasses = [ {contactData.company.email} </a> </p> + {/* Toll Free Telephone Number */} <p itemprop="telephone"> <a href={`tel:${contactData.company.telephoneTollFree}`} @@ -116,6 +121,7 @@ const footerTextLinkClasses = [ Toll Free {formatPhoneNumber(contactData.company.telephoneTollFree)} </a> </p> + {/* Local Telephone Number */} <p itemprop="telephone"> <a href={`tel:${contactData.company.telephoneLocal}`} @@ -133,7 +139,10 @@ const footerTextLinkClasses = [ </div> {/* "Hire Me" block with current month and year set by script */} - <div class:list={[styles.footerHireMe, 'hidden lg:block lg:self-end lg:mt-8']}> + <div class:list={[ + styles.footerHireMe, + 'hidden lg:block lg:self-end lg:mt-8', + ]}> <a href="/contact" id="page-footer__hire-me-anchor" diff --git a/src/components/Header/index.astro b/src/components/Header/index.astro index 40824e481..35b9d4b97 100644 --- a/src/components/Header/index.astro +++ b/src/components/Header/index.astro @@ -19,7 +19,7 @@ import './index.css' {/* Hidden modal shown when theme picker icon selected - moved to BaseLayout for persistence */} {/* Header for all pages */} -<div class="header-shell"> +<div class="header-shell print:hidden"> <div class="header-footprint" aria-hidden="true"></div> <div class="header-fixed"> <header diff --git a/src/components/Image/index.astro b/src/components/Image/index.astro index 075d74b27..d221e6cbc 100644 --- a/src/components/Image/index.astro +++ b/src/components/Image/index.astro @@ -131,7 +131,7 @@ const openButtonClasses = `${openButtonBaseClasses} ${openButtonPositionClasses[ {/** Intentionally not using Button component. */} <button type="button" - class={openButtonClasses} + class:list={[openButtonClasses, 'print:hidden!']} data-image-open data-image-open-position={resolvedMagnifyButtonPosition} aria-label={openLabel} diff --git a/src/components/Layout/Copyright/index.astro b/src/components/Layout/Copyright/index.astro index 29cbcadbd..add75b367 100644 --- a/src/components/Layout/Copyright/index.astro +++ b/src/components/Layout/Copyright/index.astro @@ -1,35 +1,17 @@ --- -import Icon from '@components/Icon/index.astro' +import DefaultCopyright from '@components/Layout/Copyright/layouts/default.astro' +import PrintCopyright from '@components/Layout/Copyright/layouts/print.astro' export interface Props { + link?: string publishDate: Date + variant?: 'default' | 'print' } -const { publishDate } = Astro.props +const { publishDate, variant = 'default', link } = Astro.props const publishYear = publishDate.getFullYear() --- -<div class="flex flex-col text-content-active text-xs mb-6"> - <div class="flex justify-center"> - <span class="mr-2">Copyright</span> - <Icon - icon="copyright" - color="content-active" - size={3} - classes="mt-0" - /> - <span class="ml-2 mr-1"> - <time datetime={String(publishYear)}>{publishYear}</time> - </span> - <span class="">Webstack Builders, Inc.</span> - </div> - <div class="flex justify-center"> - <span class="mr-1">See our </span> - <a - href="/terms" - class="inline-flex min-h-6 items-center align-middle underline transition-colors hover:text-primary-offset hover:decoration-primary-offset focus-visible:text-primary-offset focus-visible:decoration-primary-offset focus-visible:outline-2 focus-visible:outline-spotlight focus-visible:outline-offset-2 focus-visible:rounded-none" - > - terms of use - </a> - </div> -</div> + +{variant === 'default' && <DefaultCopyright publishYear={publishYear} />} +{variant === 'print' && <PrintCopyright publishYear={publishYear} {...(link ? { link } : {})} />} diff --git a/src/components/Layout/Copyright/layouts/default.astro b/src/components/Layout/Copyright/layouts/default.astro new file mode 100644 index 000000000..ff51caf94 --- /dev/null +++ b/src/components/Layout/Copyright/layouts/default.astro @@ -0,0 +1,34 @@ +--- +import Icon from '@components/Icon/index.astro' + +export interface Props { + publishYear: number +} + +const { publishYear } = Astro.props +--- + +<div class="flex flex-col text-content-active text-xs mb-6 print:hidden"> + <div class="flex justify-center"> + <span class="mr-2">Copyright</span> + <Icon + icon="copyright" + color="content-active" + size={3} + classes="mt-0" + /> + <span class="ml-2 mr-1"> + <time datetime={String(publishYear)}>{publishYear}</time> + </span> + <span class="">Webstack Builders, Inc.</span> + </div> + <div class="flex items-center justify-center"> + <span class="mr-1">See our </span> + <a + href="/terms" + class="inline-flex min-h-6 items-center align-middle underline transition-colors hover:text-primary-offset hover:decoration-primary-offset focus-visible:text-primary-offset focus-visible:decoration-primary-offset focus-visible:outline-2 focus-visible:outline-spotlight focus-visible:outline-offset-2 focus-visible:rounded-none" + > + terms of use + </a> + </div> +</div> diff --git a/src/components/Layout/Copyright/layouts/print.astro b/src/components/Layout/Copyright/layouts/print.astro new file mode 100644 index 000000000..f2b7f9bd5 --- /dev/null +++ b/src/components/Layout/Copyright/layouts/print.astro @@ -0,0 +1,33 @@ +--- +import Icon from '@components/Icon/index.astro' + +export interface Props { + link?: string + publishYear: number +} + +const { link, publishYear } = Astro.props + +const companyName = 'Webstack Builders, Inc.' +const creativeCommonsLicense = 'The text, diagrams, and images in this work are licensed under CC BY-NC 4.0' +const mitLicense = 'All code samples in this article are licensed under the MIT License. Feel free to use, modify, and distribute them in any project.' +--- + +<div class="flex flex-col gap-4"> + <div class="flex"> + <span>Copyright</span> + <Icon + icon="copyright" + color="content-active" + size={3} + classes="ml-2 mt-1" + /> + <span class="ml-2 mr-1"> + <time datetime={String(publishYear)}>{publishYear}</time> + </span> + <span>{companyName}</span> + </div> + <div>{creativeCommonsLicense}</div> + <div>{mitLicense}</div> + <div>{link}</div> +</div> diff --git a/src/components/Layout/Markdown/Lead/lead-on-hero-example.png b/src/components/Layout/Markdown/Lead/lead-on-hero-example.png deleted file mode 100644 index e3c603857..000000000 Binary files a/src/components/Layout/Markdown/Lead/lead-on-hero-example.png and /dev/null differ diff --git a/src/components/Layout/Print/Cover/cover.jpg b/src/components/Layout/Print/Cover/cover.jpg new file mode 100644 index 000000000..a272c9c24 Binary files /dev/null and b/src/components/Layout/Print/Cover/cover.jpg differ diff --git a/src/components/Layout/Print/Cover/index.astro b/src/components/Layout/Print/Cover/index.astro new file mode 100644 index 000000000..1d859c06b --- /dev/null +++ b/src/components/Layout/Print/Cover/index.astro @@ -0,0 +1,118 @@ +--- +import { type CollectionEntry } from 'astro:content' +import Logo from '@components/Brand/Logo.astro' +import WordmarkBuilders from '@components/Brand/WordmarkBuilders.astro' +import WordmarkWebstack from '@components/Brand/WordmarkWebstack.astro' +import Image from '@components/Image/index.astro' +import coverImage from './cover.jpg' + +export interface Props { + article: CollectionEntry<'deepDives'> +} + +const { article } = Astro.props +const publishDateIso = article.data.publishDate.toISOString() +const publishDateLabel = article.data.publishDate.toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', +}) +--- + +<section class="relative min-h-[calc(100vh-5.5cm)] break-after-page flex flex-col items-center justify-center px-8 text-center"> + <div class="cover-background absolute top-[-3cm] right-[-1.5cm] bottom-[-2.5cm] left-[-1.5cm] overflow-hidden"> + <Image + src={coverImage} + alt="" + loading="eager" + variant="full" + /> + </div> + + <h1 class="relative mx-auto mb-8 mt-24 max-w-[90%] text-[32pt] leading-[1.15] font-extrabold tracking-tight text-primary-inverse"> + {article.data.title} + </h1> + + <div class="cover-article-image relative w-full self-stretch overflow-hidden mt-8"> + <Image + src={article.data.cover} + alt={article.data.coverAlt} + loading="eager" + variant="full" + /> + </div> + + <div class="relative mt-auto pt-8"> + <p class="text-[11pt] font-medium text-content-inverse"> + Published on <time datetime={publishDateIso}>{publishDateLabel}</time> + </p> + <div class="cover-brand flex items-center justify-center mt-2 mb-4"> + <Logo class="h-12 w-12 bg-white p-0.5" /> + <span class="flex flex-col justify-between items-center ml-4"> + <WordmarkWebstack class="h-5" /> + <WordmarkBuilders class="mt-1.5 h-5" /> + </span> + </div> + </div> +</section> + +<style> +:global(.cover-background > content-image), +:global(.cover-background [data-image-figure]), +:global(.cover-background > content-image > div), +:global(.cover-background [data-image-figure] > div) { + height: 100%; + width: 100%; +} + +:global(.cover-background > content-image) { + margin: 0; +} + +:global(.cover-background [data-image-figure]) { + max-width: none; +} + +:global(.cover-background [data-image-figure] > div) { + background: transparent; + border: 0; + border-radius: 0; +} + +:global(.cover-background img) { + height: 100%; + object-fit: cover; + object-position: center; + width: 100%; +} + +:global(.cover-article-image > content-image) { + margin: 0; +} + +:global(.cover-article-image [data-image-figure]) { + max-width: none; +} + +:global(.cover-article-image [data-image-figure] > div) { + background: transparent; + border: 0; + border-radius: 0; +} + +:global(.cover-article-image img) { + border-radius: 0; +} + +:global(.cover-brand .logo-block-outer) { + fill: #001A39; +} + +:global(.cover-brand .logo-block-inner) { + fill: #006dca; +} + +:global(.cover-brand .wordmark__svg-path) { + fill: #fff; +} +</style> diff --git a/src/components/Layout/Print/Toc/index.astro b/src/components/Layout/Print/Toc/index.astro new file mode 100644 index 000000000..fe307ecbc --- /dev/null +++ b/src/components/Layout/Print/Toc/index.astro @@ -0,0 +1,44 @@ +--- +/** + * Static table of contents for PDF output. + * + * Renders heading links with empty .print-toc__page spans. + * The PDF generation script (scripts/generate-pdfs.mjs) injects + * approximate page numbers into those spans before calling page.pdf(). + */ +import type { MarkdownHeading } from 'astro' +import { buildTocTree, type TocItem } from '@components/Toc/server/index' + +export interface Props { + headings: MarkdownHeading[] +} + +const { headings } = Astro.props +const tocTree = buildTocTree(headings) +--- + +{tocTree.length > 0 && ( + <section class="print-toc break-after-page"> + <h2 class="text-page-inverse ml-8 mb-8">Table of Contents</h2> + <nav aria-label="Table of contents" class=" mx-20"> + {(() => { + const renderNodes = (nodes: TocItem[], depth = 0): unknown => + nodes.map(node => ( + <div class:list={[ + 'mb-[0.15cm]', + depth > 0 && 'pl-[1.5em]', + ]}> + <a href={`#${node.slug}`} class="flex items-baseline text-inherit no-underline"> + <span class="shrink-0">{node.text}</span> + <span class="mx-[0.3em] min-w-[2em] grow border-b border-dotted border-[#999]" aria-hidden="true"></span> + <span class="print-toc__page"></span> + </a> + {node.children.length > 0 && renderNodes(node.children, depth + 1)} + </div> + )) + + return renderNodes(tocTree) + })()} + </nav> + </section> +)} diff --git a/src/components/List/layouts/ZebraList.astro b/src/components/List/layouts/ZebraList.astro index b4f91c289..f8ddb0e45 100644 --- a/src/components/List/layouts/ZebraList.astro +++ b/src/components/List/layouts/ZebraList.astro @@ -19,8 +19,8 @@ export type Props = { const { items, classes, style }: Props = Astro.props -const ulClass = ["border border-trim rounded-lg overflow-hidden divide-y divide-trim", classes?.ul] -const liClass = ["p-4 bg-page-base flex items-center justify-between group hover:bg-page-offset transition-colors", classes?.li] +const ulClass = ["print-zebra-list border border-trim rounded-lg overflow-hidden divide-y divide-trim", classes?.ul] +const liClass = ["print-zebra-list-item p-4 bg-page-base flex items-center justify-between group hover:bg-page-offset transition-colors", classes?.li] const emClass = ["text-page-inverse font-bold not-italic block mb-0.5", classes?.em] const textClass = ["text-content"] --- diff --git a/src/components/Navigation/index.astro b/src/components/Navigation/index.astro index 3619b8f0b..5110b1683 100644 --- a/src/components/Navigation/index.astro +++ b/src/components/Navigation/index.astro @@ -10,7 +10,7 @@ export interface Props { const { path } = Astro.props --- -<site-navigation class="contents" data-testid="site-navigation"> +<site-navigation class="contents print:hidden!" data-testid="site-navigation"> <div id="mobile-nav-focus-container" class="contents"> {/* Main Navigation Menu */} <span diff --git a/src/components/Pages/About/index.astro b/src/components/Pages/About/index.astro index 05ff11e34..53ef7baa4 100644 --- a/src/components/Pages/About/index.astro +++ b/src/components/Pages/About/index.astro @@ -89,7 +89,7 @@ const expertiseIcons = ['cloud', 'document', 'gear', 'globe', 'wrench', 'chart'] <h3 class="text-page-inverse mb-2">{item.title}</h3> <p class="text-content leading-relaxed">{item.description}</p> {index === 0 && ( - <div class="mt-3 flex justify-end"> + <div class="mt-3 flex justify-end print:hidden!"> <Button text="View C.V." variant="page-inverse" diff --git a/src/components/Pages/Services/index.astro b/src/components/Pages/Services/index.astro index dbf84bc12..660172ff5 100644 --- a/src/components/Pages/Services/index.astro +++ b/src/components/Pages/Services/index.astro @@ -95,7 +95,7 @@ const splitH2Sections = (html: string): string[] => { <article data-service-card class:list={[ - 'relative flex flex-col rounded-2xl border border-trim overflow-hidden transition-shadow hover:shadow-lg focus-within:shadow-lg', + 'service-card relative flex flex-col rounded-2xl border border-trim overflow-hidden transition-shadow hover:shadow-lg focus-within:shadow-lg', i === 0 ? 'service-card--default' : '', ]} > @@ -131,7 +131,7 @@ const splitH2Sections = (html: string): string[] => { </p> </div> - <div class="flex flex-1 flex-col bg-content-inverse px-6 py-6"> + <div class="service-card-body flex flex-1 flex-col bg-content-inverse px-6 py-6"> <div class="grid gap-6 md:grid-cols-2"> <div class="space-y-6 [&_h2]:text-sm [&_h2]:font-semibold [&_h2]:uppercase [&_h2]:tracking-wider [&_h2]:text-page-inverse [&_h2]:mb-2 [&_h2]:mt-0 [&_p]:text-content [&_p]:text-sm [&_p]:leading-relaxed [&_ul]:space-y-1.5 [&_ul]:text-sm [&_ul]:text-content [&_ul]:list-none [&_ul]:pl-0 [&_li]:flex [&_li]:items-start [&_li]:gap-2 [&_li]:before:content-['✓'] [&_li]:before:text-success [&_li]:before:mt-0.5 [&_li]:before:shrink-0"> <Fragment set:html={leftSections} /> diff --git a/src/components/Pwa/TitleBar/index.astro b/src/components/Pwa/TitleBar/index.astro index aab427019..551f88858 100644 --- a/src/components/Pwa/TitleBar/index.astro +++ b/src/components/Pwa/TitleBar/index.astro @@ -6,7 +6,7 @@ --- <div - class="fixed top-0 left-0 right-0 overflow-hidden z-(--z-titlebar) bg-page-offset" + class="fixed top-0 left-0 right-0 overflow-hidden z-(--z-titlebar) bg-page-offset print:hidden!" style="height: env(titlebar-area-height, 0);" > <div diff --git a/src/components/QrCode/index.astro b/src/components/QrCode/index.astro index c1f3ec323..6a6de6b16 100644 --- a/src/components/QrCode/index.astro +++ b/src/components/QrCode/index.astro @@ -22,7 +22,7 @@ const qrSvg = await renderQrCodeSvg({ data: qrData }) aria-label={`QR code for ${qrData}`} > <Fragment set:html={qrSvg} /> - <figcaption class="mt-3 max-w-60 break-all text-center text-sm text-content"> + <figcaption class="sr-only"> {qrData} </figcaption> </figure> diff --git a/src/components/Search/SearchBar/index.astro b/src/components/Search/SearchBar/index.astro index 65aca06aa..3dbe148d4 100644 --- a/src/components/Search/SearchBar/index.astro +++ b/src/components/Search/SearchBar/index.astro @@ -23,7 +23,7 @@ const clearButtonLabel = hasInitialQuery ? 'Clear search' : 'Close search' --- <search-bar - class:list={['relative block', styles.searchBar]} + class:list={['print:hidden!', 'relative block', styles.searchBar]} data-testid="search-bar" data-search-variant={variant} > diff --git a/src/components/Social/Embed/index.astro b/src/components/Social/Embed/index.astro index e4f2bf4dd..95ba438eb 100644 --- a/src/components/Social/Embed/index.astro +++ b/src/components/Social/Embed/index.astro @@ -80,7 +80,7 @@ const maxWidthClass = platformMaxWidth[detectedPlatform] || 'max-w-[550px]' --- <social-embed - class={`my-8 mx-auto ${maxWidthClass} ${className}`} + class={`my-8 mx-auto ${maxWidthClass} ${className} print:hidden!`} url={url} platform={requestedPlatform} data-embed-width={resolvedWidth} diff --git a/src/components/Social/Highlighter/index.astro b/src/components/Social/Highlighter/index.astro index c25d45b6e..1ac395710 100644 --- a/src/components/Social/Highlighter/index.astro +++ b/src/components/Social/Highlighter/index.astro @@ -34,4 +34,8 @@ export interface Props { const { ariaLabel, class: className } = Astro.props --- -<highlighter-element share-label={ariaLabel}><span class:list={['text-content', 'bg-warning-inverse', 'rounded', 'px-2', 'py-0.5', className]}><slot /></span></highlighter-element> +<highlighter-element share-label={ariaLabel} class="print:hidden!"> + <span class:list={['text-content', 'bg-warning-inverse', 'rounded', 'px-2', 'py-0.5', className]}> + <slot /> + </span> +</highlighter-element> diff --git a/src/components/Social/Shares/index.astro b/src/components/Social/Shares/index.astro index d6abeacc9..4ddbe9c81 100644 --- a/src/components/Social/Shares/index.astro +++ b/src/components/Social/Shares/index.astro @@ -59,7 +59,7 @@ const shareHeadingId = `${iconBankId}-heading` </div> <section - class="rounded-lg bg-page-offset flex flex-col md:flex-row items-start md:items-center p-6 mx-6 print:hidden" + class="rounded-lg bg-page-offset flex flex-col md:flex-row items-start md:items-center p-6 mx-6 print:hidden!" > <div class="h-full w-full sm:border-r sm:border-trim sm:w-auto sm:shrink-0 sm:pr-12"> <h2 diff --git a/src/components/Table/index.astro b/src/components/Table/index.astro index ad9590f74..fe406ad7b 100644 --- a/src/components/Table/index.astro +++ b/src/components/Table/index.astro @@ -53,7 +53,7 @@ const { content, variant = 'default', classes, fullWidth = true } = Astro.props const classesProps = classes ? { classes } : {} const layoutProps = { ...classesProps, fullWidth } const wrapperClass = [ - 'overflow-x-auto rounded-md border border-trim bg-content-inverse', + 'print-table-shell overflow-x-auto rounded-md border border-trim bg-content-inverse', fullWidth ? 'w-full' : 'mx-auto w-fit max-w-full', ].join(' ') --- @@ -73,6 +73,6 @@ const wrapperClass = [ {variant === 'vertical-column-delineation-table' && <VerticalColumnDelineationTable content={content as VerticalColumnDelineationTableProps['content']} {...layoutProps} />} </div> {content.figure && ( - <figcaption class="italic text-base text-center mt-3" set:html={content.figure}></figcaption> + <figcaption class="screen-table-figure-caption italic text-base text-center mt-3" set:html={content.figure}></figcaption> )} </figure> diff --git a/src/components/Table/layouts/criteria-highlight.astro b/src/components/Table/layouts/criteria-highlight.astro index 3bfdc5eaa..5eef56d47 100644 --- a/src/components/Table/layouts/criteria-highlight.astro +++ b/src/components/Table/layouts/criteria-highlight.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -37,15 +38,21 @@ const headerClass = 'px-4 py-2.5 font-semibold text-content-active' const theadClass = 'bg-page-offset border-b border-trim' const rowClass = 'transition-colors duration-150 hover:bg-page-offset border-b border-trim-offset last:border-b-0' const getOverallColumnIndex = (hasRowHeader: boolean, cellIndex: number) => (hasRowHeader ? cellIndex + 1 : cellIndex) +const hasScreenCaption = Boolean(content.caption || content.subcaption) --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/Table/layouts/grid-and-accent-header.astro b/src/components/Table/layouts/grid-and-accent-header.astro index 8727e908c..869e97724 100644 --- a/src/components/Table/layouts/grid-and-accent-header.astro +++ b/src/components/Table/layouts/grid-and-accent-header.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -37,14 +38,20 @@ const headerClass = 'px-4 py-2.5' const theadClass = 'bg-page-inverse text-page-base border-b border-trim' const tbodyClass = '' const tfootClass = '' +const hasScreenCaption = Boolean(content.caption || content.subcaption) --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/Table/layouts/minimal-borders.astro b/src/components/Table/layouts/minimal-borders.astro index bb7e1f08b..b8e7b035f 100644 --- a/src/components/Table/layouts/minimal-borders.astro +++ b/src/components/Table/layouts/minimal-borders.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -38,14 +39,20 @@ const theadClass = 'border-b-2 border-trim' const tbodyClass = '' const tfootClass = '' const rowClass = 'transition-colors duration-150 hover:bg-page-offset border-b border-trim-offset' +const hasScreenCaption = Boolean(content.caption || content.subcaption) --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/Table/layouts/numbered-review.astro b/src/components/Table/layouts/numbered-review.astro index 8c7b01158..f17b879e0 100644 --- a/src/components/Table/layouts/numbered-review.astro +++ b/src/components/Table/layouts/numbered-review.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -42,14 +43,20 @@ const theadClass = 'bg-page-offset border-b border-trim' const tbodyClass = '' const tfootClass = '' const rowClass = 'transition-colors duration-150 hover:bg-page-offset border-b border-trim-offset last:border-b-0' +const hasScreenCaption = Boolean(content.caption || content.subcaption) --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/Table/layouts/report-table.astro b/src/components/Table/layouts/report-table.astro index 49d8e3f5e..974516253 100644 --- a/src/components/Table/layouts/report-table.astro +++ b/src/components/Table/layouts/report-table.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -38,14 +39,20 @@ const theadClass = 'border-y-2 border-primary bg-page-offset' const tbodyClass = '' const tfootClass = '' const rowClass = 'transition-colors duration-150 hover:bg-page-offset border-b border-trim-offset last:border-b-0' +const hasScreenCaption = Boolean(content.caption || content.subcaption) --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/Table/layouts/semantic-coloring.astro b/src/components/Table/layouts/semantic-coloring.astro index ff74ed67b..5dc885a54 100644 --- a/src/components/Table/layouts/semantic-coloring.astro +++ b/src/components/Table/layouts/semantic-coloring.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -32,11 +33,12 @@ export type Props = { const { content, classes, fullWidth = true }: Props = Astro.props const tableClass = `text-left border-collapse${fullWidth ? ' w-full' : ''}` -const captionClass = 'text-left px-5 py-3 text-content-offset' +const captionClass = 'caption-top px-5 py-3 text-left text-content-offset' const headerClass = 'px-5 py-3 font-semibold text-content' const theadClass = 'bg-note-inverse/50 border-b border-trim' const tbodyClass = '' const tfootClass = '' +const hasScreenCaption = Boolean(content.caption || content.subcaption) const stateClasses = { accent: { @@ -157,12 +159,17 @@ const getCellStateClass = (cell: string | { label: string; color?: string }) => } --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/Table/layouts/soft-header-card.astro b/src/components/Table/layouts/soft-header-card.astro index 9ed749940..b2bb5c239 100644 --- a/src/components/Table/layouts/soft-header-card.astro +++ b/src/components/Table/layouts/soft-header-card.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -31,23 +32,27 @@ export type Props = { const { content, classes, fullWidth = true }: Props = Astro.props -const introClass = 'px-5 py-4 bg-primary text-primary-inverse' +const captionClass = 'caption-top px-5 py-4 text-left bg-primary text-primary-inverse' const tableClass = `text-left${fullWidth ? ' w-full' : ''}` const headerClass = 'px-5 py-3 font-semibold text-content-active' const theadClass = 'bg-page-offset border-b border-trim' const tbodyClass = '' const tfootClass = '' const rowClass = 'transition-colors duration-150 hover:bg-page-offset border-b border-trim-offset last:border-b-0' +const hasScreenCaption = Boolean(content.caption || content.subcaption) --- - -{(content.caption || content.subcaption) && ( - <div class={introClass}> - {content.caption && <h3 class="text-base font-semibold" set:html={content.caption}></h3>} - {content.subcaption && <p class="text-sm text-content-inverse" set:html={content.subcaption}></p>} - </div> -)} - - <table class:list={[tableClass, classes?.table]}> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block text-base font-semibold" set:html={content.caption}></span>} + {content.subcaption && <span class="block text-sm text-content-inverse" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> + )} {content.thead && ( <thead class:list={[theadClass, classes?.thead]}> <tr> @@ -101,4 +106,4 @@ const rowClass = 'transition-colors duration-150 hover:bg-page-offset border-b b ))} </tfoot> )} - </table> +</table> diff --git a/src/components/Table/layouts/sticky-header.astro b/src/components/Table/layouts/sticky-header.astro index 08e84dc0f..7f32d0ffd 100644 --- a/src/components/Table/layouts/sticky-header.astro +++ b/src/components/Table/layouts/sticky-header.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -38,14 +39,20 @@ const theadClass = 'sticky top-0 bg-page-offset' const tbodyClass = '' const tfootClass = '' const rowClass = 'transition-colors duration-150 hover:bg-page-offset border-b border-trim-offset' +const hasScreenCaption = Boolean(content.caption || content.subcaption) --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/Table/layouts/striped-rows.astro b/src/components/Table/layouts/striped-rows.astro index cf82dc0d6..b78bbbd6c 100644 --- a/src/components/Table/layouts/striped-rows.astro +++ b/src/components/Table/layouts/striped-rows.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -37,14 +38,20 @@ const headerClass = 'px-5 py-3 font-semibold' const theadClass = 'bg-note-offset text-primary-inverse' const tbodyClass = '' const tfootClass = '' +const hasScreenCaption = Boolean(content.caption || content.subcaption) --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/Table/layouts/timeline-labels.astro b/src/components/Table/layouts/timeline-labels.astro index ab875b0ad..646e52465 100644 --- a/src/components/Table/layouts/timeline-labels.astro +++ b/src/components/Table/layouts/timeline-labels.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -38,6 +39,7 @@ const theadClass = 'bg-page-offset border-b border-trim' const tbodyClass = '' const tfootClass = '' const rowClass = 'transition-colors duration-150 hover:bg-page-offset border-b border-trim-offset last:border-b-0' +const hasScreenCaption = Boolean(content.caption || content.subcaption) const getMarkerColorClass = (rowHeader: string | { label: string; color?: string }) => { if (typeof rowHeader === 'string') { @@ -54,12 +56,17 @@ const getMarkerColorClass = (rowHeader: string | { label: string; color?: string } --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/Table/layouts/vertical-column-delineation.astro b/src/components/Table/layouts/vertical-column-delineation.astro index c0a309829..8cb127786 100644 --- a/src/components/Table/layouts/vertical-column-delineation.astro +++ b/src/components/Table/layouts/vertical-column-delineation.astro @@ -3,6 +3,7 @@ export type Props = { content: { caption?: string subcaption?: string + figure?: string thead?: { th: string[] } @@ -38,14 +39,20 @@ const theadClass = 'bg-page-offset border-y border-trim' const tbodyClass = '' const tfootClass = '' const rowClass = 'transition-colors duration-150 hover:bg-page-offset border-b border-trim' +const hasScreenCaption = Boolean(content.caption || content.subcaption) --- -<table class:list={[tableClass, classes?.table]}> - {content.caption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.caption}></caption> - )} - {content.subcaption && ( - <caption class:list={[captionClass, classes?.caption]} set:html={content.subcaption}></caption> +<table class:list={[tableClass, content.thead ? 'print-table-has-head' : 'print-table-no-head', classes?.table]}> + {(hasScreenCaption || content.figure) && ( + <caption class:list={[captionClass, content.figure ? 'print-table-caption' : '', !hasScreenCaption ? 'hidden' : '', classes?.caption]}> + {hasScreenCaption && ( + <span class="screen-table-caption-copy"> + {content.caption && <span class="block" set:html={content.caption}></span>} + {content.subcaption && <span class="block" set:html={content.subcaption}></span>} + </span> + )} + {content.figure && <span class="hidden print-table-figure-copy" set:html={content.figure}></span>} + </caption> )} {content.thead && ( diff --git a/src/components/ThemePicker/index.astro b/src/components/ThemePicker/index.astro index be858d015..a221b78b1 100644 --- a/src/components/ThemePicker/index.astro +++ b/src/components/ThemePicker/index.astro @@ -15,7 +15,11 @@ export interface Props { const { closeComponent = 'close' } = Astro.props --- -<theme-picker transition:name="theme-picker-island" transition:persist> +<theme-picker + class="print:hidden!" + transition:name="theme-picker-island" + transition:persist +> <div class:list={[ styles.themePicker, diff --git a/src/components/Toc/index.astro b/src/components/Toc/index.astro index d4e072229..963109275 100644 --- a/src/components/Toc/index.astro +++ b/src/components/Toc/index.astro @@ -15,7 +15,7 @@ const tocTree = buildTocTree(items) { tocTree.length > 0 && ( <> - <table-of-contents class="block group/toc"> + <table-of-contents class="print:hidden! block group/toc"> {/* Open button, partial circle on right edge and visible when drawer is closed */} <button type="button" diff --git a/src/components/WebMentions/index.astro b/src/components/WebMentions/index.astro index 1e50bcbcd..f7a3d6571 100644 --- a/src/components/WebMentions/index.astro +++ b/src/components/WebMentions/index.astro @@ -34,7 +34,7 @@ const iconBankId = `webmentions-icon-bank-${Math.random().toString(36).slice(2)} </div> <web-mentions - class="print:hidden" + class="print:hidden!" url={url} show-facepile={showFacepile} facepile-limit={facepileLimit} diff --git a/src/integrations/sitemapSerialize/__tests__/index.spec.ts b/src/integrations/sitemapSerialize/__tests__/index.spec.ts index 3ce034a21..c3cab58d2 100644 --- a/src/integrations/sitemapSerialize/__tests__/index.spec.ts +++ b/src/integrations/sitemapSerialize/__tests__/index.spec.ts @@ -109,6 +109,15 @@ describe('createSerializeFunction', () => { ).toBeUndefined() }) + it('should exclude print routes after the route rename from pdf to print', () => { + const serialize = createSerializeFunction({ + exclude: ['print'], + }) + + expect(serialize(createSitemapItem('https://example.com/print'))).toBeUndefined() + expect(serialize(createSitemapItem('https://example.com/print/my-article'))).toBeUndefined() + }) + it('should handle multiple exclusions', () => { const serialize = createSerializeFunction({ exclude: ['downloads', 'social-shares', '/articles/demo'], diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 987b7e9b9..0f7cb5728 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -159,6 +159,7 @@ applyRenderSentryContext({ id="breadcrumbs" aria-label="Breadcrumbs and page controls" class:list={[ + 'print:hidden!', 'mt-4 items-center justify-between gap-4', hideBreadcrumbsOnMobile ? 'hidden sm:flex' : 'flex', ]} diff --git a/src/layouts/MarkdownLayout.astro b/src/layouts/MarkdownLayout.astro index aa642bc03..d2cf52cb5 100644 --- a/src/layouts/MarkdownLayout.astro +++ b/src/layouts/MarkdownLayout.astro @@ -7,6 +7,7 @@ import type { MarkdownHeading } from 'astro' * Layout for Markdown/MDX content files */ import BaseLayout from '@layouts/BaseLayout.astro' +import Copyright from '@components/Layout/Copyright/index.astro' import Lead from '@components/Layout/Markdown/Lead/index.astro' import Subtitle from '@components/Layout/Markdown/Subtitle/index.astro' import Title from '@components/Layout/Markdown/Title/index.astro' @@ -137,6 +138,8 @@ const schemaType = const { Content, headings = [] } = await render(collectionItem) const tocHeadings: MarkdownHeading[] = headings.filter(heading => heading.depth > 1) const shouldRenderToc = showToc !== false && tocHeadings.length > 0 + +const link = `https://www.webstackbuilders.com${path}` --- <BaseLayout @@ -197,7 +200,15 @@ const shouldRenderToc = showToc !== false && tocHeadings.length > 0 {/** SLOT: after-content, includes social shares and web mentions */} <slot name="after-content" /> - <QrCode data={`https://www.webstackbuilders.com${path}`} /> + + <div class="hidden mx-24 mt-8 print:grid print:grid-cols-2 print:gap-4 print:items-center print:break-inside-avoid"> + <span class="print:break-inside-avoid"> + <Copyright publishDate={publishDate ?? new Date()} variant="print" link={link} /> + </span> + <span class="print:break-inside-avoid"> + <QrCode data={link} /> + </span> + </div> </div> </article> diff --git a/src/layouts/PrintLayout.astro b/src/layouts/PrintLayout.astro new file mode 100644 index 000000000..56ac11faa --- /dev/null +++ b/src/layouts/PrintLayout.astro @@ -0,0 +1,29 @@ +--- +/** + * Minimal layout for PDF generation via Puppeteer. + * + * Unlike BaseLayout this has zero interactive web components, + * no consent banner, no theme picker, no analytics, no service worker. + * It only includes the CSS pipeline so the print pages render with + * the site's styles when Puppeteer visits them. + */ +import '@styles/index.css' + +export interface Props { + pageTitle: string +} + +const { pageTitle } = Astro.props +--- + +<!doctype html> +<html lang="en" data-theme="light"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>{pageTitle} + + + + + diff --git a/src/lib/config/environmentalVariableValidation.ts b/src/lib/config/environmentalVariableValidation.ts index 6a1cbac4a..4ce2230e5 100644 --- a/src/lib/config/environmentalVariableValidation.ts +++ b/src/lib/config/environmentalVariableValidation.ts @@ -71,6 +71,14 @@ export const environmentalVariablesConfig: AstroUserConfig['env'] = { default: 4321, optional: true, }), + /** + * Build-time flag used to enable generation of prerendered /print/ pages. + */ + GENERATE_PDFS: envField.string({ + access: 'public', + context: 'server', + optional: true, + }), /** * Google Maps Platform * diff --git a/src/pages/articles/index.astro b/src/pages/articles/index.astro index 05bb0e6e6..3a9f33d89 100644 --- a/src/pages/articles/index.astro +++ b/src/pages/articles/index.astro @@ -37,7 +37,7 @@ const path = '/articles' path={path} icon="paper-and-pen" > -
+
{ tagsWithContent diff --git a/src/pages/case-studies/index.astro b/src/pages/case-studies/index.astro index 5b1db7b67..cce7480fb 100644 --- a/src/pages/case-studies/index.astro +++ b/src/pages/case-studies/index.astro @@ -35,7 +35,7 @@ const path = '/case-studies' icon="folder-magnifying-glass" >
{ diff --git a/src/pages/print/[...slug].astro b/src/pages/print/[...slug].astro new file mode 100644 index 000000000..b59a3f70e --- /dev/null +++ b/src/pages/print/[...slug].astro @@ -0,0 +1,98 @@ +--- +export const prerender = true + +import { GENERATE_PDFS } from 'astro:env/server' +import { type CollectionEntry, getCollection, render } from 'astro:content' +import type { MarkdownHeading } from 'astro' +import { Image, Picture } from 'astro:assets' +import { isDev } from '@lib/config/environmentServer' + +import Copyright from '@components/Layout/Copyright/index.astro' +import Cover from '@components/Layout/Print/Cover/index.astro' +import PrintLayout from '@layouts/PrintLayout.astro' +import QrCode from '@components/QrCode/index.astro' +import Toc from '@components/Layout/Print/Toc/index.astro' + +/** MDX components that are safe for static PDF rendering (no web components) */ +import Avatar from '@components/Avatar/index.astro' +import Button from '@components/Button/index.astro' +import Callout from '@components/Callout/index.astro' +import CodeTabs from '@components/Code/CodeTabs/index.astro' +import Copy from '@components/Copy/index.astro' +import Diagram from '@components/Diagram/index.astro' +import Icon from '@components/Icon/index.astro' +import Inset from '@components/Inset/index.astro' +import List from '@components/List/index.astro' +import ListItem from '@components/List/ListItem.astro' +import Table from '@components/Table/index.astro' +import Time from '@components/Time/index.astro' + +const PdfComponents = { + Avatar, + Button, + Callout, + Copy, + Diagram, + Icon, + Image, + Inset, + List, + ListItem, + Picture, + QrCode, + Table, + Time, +} + +export interface Props { + article: CollectionEntry<'deepDives'> +} + +/** Generate static paths for all deep-dive articles */ +export async function getStaticPaths() { + if (!isDev() && !GENERATE_PDFS) { + return [] + } + + const articleEntries = await getCollection('deepDives') + + return articleEntries.map(article => ({ + params: { slug: article.id }, + props: { article }, + })) +} + +const { article } = Astro.props + +const { Content, headings = [] } = await render(article) +const tocHeadings: MarkdownHeading[] = headings.filter(heading => heading.depth > 1) + +const link = `https://www.webstackbuilders.com/articles/${article.id}` +const publishDate = article.data.publishDate +--- + + + + + + diff --git a/src/pages/print/index.css b/src/pages/print/index.css new file mode 100644 index 000000000..9e81c529b --- /dev/null +++ b/src/pages/print/index.css @@ -0,0 +1,175 @@ +/* ========================================================================== + PDF Generation Styles + ========================================================================== + Styles for the /print/ pages consumed by Puppeteer's page.pdf(). + Puppeteer controls page sizing, margins, headers, and footers. + This CSS controls the visual layout of content within the page. + ========================================================================== */ + +/* -------------------------------------------------------------------------- + Override print.css @page margins and running elements. + Puppeteer provides its own header/footer via page.pdf() options, + so suppress the CSS-based running elements to avoid duplication. + -------------------------------------------------------------------------- */ + +@page { + margin: 0; + + @bottom-left { + content: none; + } + + @bottom-right { + content: none; + } +} + +/** +* ============================================================================= +* Colors for Print +* ============================================================================= +*/ +@media print { + .print-document { + box-sizing: border-box; + padding: 3cm 1.5cm 2.5cm; + } + + html[data-theme="light"][data-theme] { + --theme-color-primary: #001A39; + --theme-color-primary-offset: #003d86; + --theme-color-primary-inverse: #FFF; + --theme-color-secondary: #006dca; + --theme-color-secondary-offset: #1894ff; + --theme-color-secondary-inverse: #FFF; + --theme-color-accent: #7c3aed; + --theme-color-accent-offset: #d28fe0; + --theme-color-page-base: #f3f4f6; + --theme-color-page-inverse: #003d86; + --theme-color-page-offset: #e5e7eb; + --theme-color-page-active: #9ca3af; + --theme-color-content: #374151; + --theme-color-content-offset: #6b7280; + --theme-color-content-active: #001a39; + --theme-color-content-inverse: #f3f4f6; + --theme-color-content-inverse-active: #bfdbfe; + --theme-color-trim: #4b5563; + --theme-color-trim-offset: #9ca3af; + --theme-color-spotlight: #facc15; + --theme-color-spotlight-offset: #ca8a04; + --theme-color-success: #15803d; + --theme-color-success-offset: #22c55e; + --theme-color-success-inverse: #dcfce7; + --theme-color-info: #0891b2; + --theme-color-info-offset: #38bdf8; + --theme-color-info-inverse: #cffafe; + --theme-color-note: #374151; + --theme-color-note-offset: #6b7280; + --theme-color-note-inverse: #d1d5db; + --theme-color-warning: #a16207; + --theme-color-warning-offset: #ca8a04; + --theme-color-warning-inverse: #fef3c7; + --theme-color-danger: #dc2626; + --theme-color-danger-offset: #b91c1c; + --theme-color-danger-inverse: #fecaca; + --theme-color-icons: #374151; + --theme-color-icons-active: #9ca3af; + --theme-color-black: #000; + --theme-color-white: #fff; + --theme-color-blue-50: #eff6ff; + --theme-color-blue-100: #dbeafe; + --theme-color-blue-200: #bfdbfe; + --theme-color-blue-300: #93c5fd; + --theme-color-blue-400: #60a5fa; + --theme-color-blue-500: #3b82f6; + --theme-color-blue-600: #2563eb; + --theme-color-blue-800: #1e40af; + --theme-color-blue-900: #1e3a8a; + --theme-color-gray-50: #f9fafb; + --theme-color-gray-100: #f3f4f6; + --theme-color-gray-200: #e5e7eb; + --theme-color-gray-300: #d1d5db; + --theme-color-gray-400: #9ca3af; + --theme-color-gray-500: #6b7280; + --theme-color-gray-600: #4b5563; + --theme-color-gray-700: #374151; + --theme-color-gray-800: #1f2937; + --theme-color-gray-900: #111827; + --theme-color-gray-offset: #9ca3af; + --theme-color-green-100: #d1fae5; + --theme-color-green-200: #bbf7d0; + --theme-color-green-300: #86efac; + --theme-color-green-400: #4ade80; + --theme-color-green-500: #22c55e; + --theme-color-green-600: #16a34a; + --theme-color-green-700: #15803d; + --theme-color-green-900: #14532d; + --theme-color-orange-100: #ffedd5; + --theme-color-orange-200: #fed7aa; + --theme-color-orange-300: #fdba74; + --theme-color-orange-500: #f97316; + --theme-color-orange-600: #ea580c; + --theme-color-orange-700: #c2410c; + --theme-color-purple-100: #f3e8ff; + --theme-color-purple-600: #9333ea; + --theme-color-red-400: #f87171; + --theme-color-red-500: #ef4444; + --theme-color-yellow-100: #fef9c3; + --theme-color-yellow-200: #fef08a; + --theme-color-yellow-300: #fde047; + --theme-color-yellow-400: #facc15; + --theme-color-yellow-500: #eab308; + --theme-color-yellow-600: #ca8a04; + --theme-color-yellow-700: #a16207; + --theme-color-bluesky: #00A8E8; + --theme-color-bluesky-active: #0087bd; + --theme-color-codepen: #000; + --theme-color-codepen-active: #1a1a1a; + --theme-color-github: #211F1F; + --theme-color-github-active: #333131; + --theme-color-linkedin: #0a66c2; + --theme-color-linkedin-active: #084c9e; + --theme-color-mastodon: #6364FF; + --theme-color-mastodon-active: #5557e6; + --theme-color-google-red: #EA4335; + --theme-color-google-yellow: #FBBC04; + --theme-color-google-blue: #4285F4; + --theme-color-google-blue-dark: #1967D2; + --theme-color-google-green: #34A853; + --theme-color-google-green-dark: #188038; + --theme-color-reddit: #FF4500; + --theme-color-reddit-active: #e03d00; + --theme-color-x: #000; + --theme-color-x-active: #1a1a1a; + --shiki-foreground: #24292e; + --shiki-background: #fff; + --shiki-token-constant: #005cc5; + --shiki-token-string: #032f62; + --shiki-token-comment: #6a737d; + --shiki-token-keyword: #d73a49; + --shiki-token-parameter: #24292e; + --shiki-token-function: #6f42c1; + --shiki-token-string-expression: #032f62; + --shiki-token-punctuation: #24292e; + --shiki-token-link: #032f62; + --shiki-token-inserted: #22863a; + --shiki-token-deleted: #b31d28; + --shiki-token-changed: #e36209; + --shiki-ansi-black: #24292e; + --shiki-ansi-red: #d73a49; + --shiki-ansi-green: #28a745; + --shiki-ansi-yellow: #dbab09; + --shiki-ansi-blue: #0366d6; + --shiki-ansi-magenta: #5a32a3; + --shiki-ansi-cyan: #1b7c83; + --shiki-ansi-white: #6a737d; + --shiki-ansi-bright-black: #959da5; + --shiki-ansi-bright-red: #cb2431; + --shiki-ansi-bright-green: #22863a; + --shiki-ansi-bright-yellow: #b08800; + --shiki-ansi-bright-blue: #005cc5; + --shiki-ansi-bright-magenta: #5a32a3; + --shiki-ansi-bright-cyan: #3192aa; + --shiki-ansi-bright-white: #d1d5da; + } +} diff --git a/src/styles/code-highlighting/general.css b/src/styles/code-highlighting.css similarity index 100% rename from src/styles/code-highlighting/general.css rename to src/styles/code-highlighting.css diff --git a/src/styles/code-highlighting/root.css b/src/styles/code-highlighting/root.css deleted file mode 100644 index ce984f40d..000000000 --- a/src/styles/code-highlighting/root.css +++ /dev/null @@ -1,177 +0,0 @@ -:root { - --shiki-foreground: var(--theme-shiki-foreground); - --shiki-background: var(--theme-shiki-background); - --shiki-token-constant: var(--theme-shiki-token-constant); - --shiki-token-string: var(--theme-shiki-token-string); - --shiki-token-comment: var(--theme-shiki-token-comment); - --shiki-token-keyword: var(--theme-shiki-token-keyword); - --shiki-token-parameter: var(--theme-shiki-token-parameter); - --shiki-token-function: var(--theme-shiki-token-function); - --shiki-token-string-expression: var(--theme-shiki-token-string-expression); - --shiki-token-punctuation: var(--theme-shiki-token-punctuation); - --shiki-token-link: var(--theme-shiki-token-link); - --shiki-token-inserted: var(--theme-shiki-token-inserted); - --shiki-token-deleted: var(--theme-shiki-token-deleted); - --shiki-token-changed: var(--theme-shiki-token-changed); - --shiki-ansi-black: var(--theme-shiki-ansi-black); - --shiki-ansi-red: var(--theme-shiki-ansi-red); - --shiki-ansi-green: var(--theme-shiki-ansi-green); - --shiki-ansi-yellow: var(--theme-shiki-ansi-yellow); - --shiki-ansi-blue: var(--theme-shiki-ansi-blue); - --shiki-ansi-magenta: var(--theme-shiki-ansi-magenta); - --shiki-ansi-cyan: var(--theme-shiki-ansi-cyan); - --shiki-ansi-white: var(--theme-shiki-ansi-white); - --shiki-ansi-bright-black: var(--theme-shiki-ansi-bright-black); - --shiki-ansi-bright-red: var(--theme-shiki-ansi-bright-red); - --shiki-ansi-bright-green: var(--theme-shiki-ansi-bright-green); - --shiki-ansi-bright-yellow: var(--theme-shiki-ansi-bright-yellow); - --shiki-ansi-bright-blue: var(--theme-shiki-ansi-bright-blue); - --shiki-ansi-bright-magenta: var(--theme-shiki-ansi-bright-magenta); - --shiki-ansi-bright-cyan: var(--theme-shiki-ansi-bright-cyan); - --shiki-ansi-bright-white: var(--theme-shiki-ansi-bright-white); - - /** Shiki stylings for print / a11y (GitHub Light High Contrast) **/ - --a11y-theme-shiki-foreground: #0e1116; - --a11y-theme-shiki-background: #fff; - --a11y-theme-shiki-token-constant: #023b95; - --a11y-theme-shiki-token-string: #032563; - --a11y-theme-shiki-token-comment: #66707b; - --a11y-theme-shiki-token-keyword: #a0111f; - --a11y-theme-shiki-token-parameter: #0e1116; - --a11y-theme-shiki-token-function: #023b95; - --a11y-theme-shiki-token-string-expression: #032563; - --a11y-theme-shiki-token-punctuation: #0e1116; - --a11y-theme-shiki-token-link: #032563; - --a11y-theme-shiki-token-inserted: #024c1a; - --a11y-theme-shiki-token-deleted: #6e011a; - --a11y-theme-shiki-token-changed: #702c00; - --a11y-theme-shiki-ansi-black: #0e1116; - --a11y-theme-shiki-ansi-red: #a0111f; - --a11y-theme-shiki-ansi-green: #024c1a; - --a11y-theme-shiki-ansi-yellow: #3f2200; - --a11y-theme-shiki-ansi-blue: #0349b4; - --a11y-theme-shiki-ansi-magenta: #622cbc; - --a11y-theme-shiki-ansi-cyan: #1b7c83; - --a11y-theme-shiki-ansi-white: #66707b; - --a11y-theme-shiki-ansi-bright-black: #4b535d; - --a11y-theme-shiki-ansi-bright-red: #86061d; - --a11y-theme-shiki-ansi-bright-green: #055d20; - --a11y-theme-shiki-ansi-bright-yellow: #4e2c00; - --a11y-theme-shiki-ansi-bright-blue: #1168e3; - --a11y-theme-shiki-ansi-bright-magenta: #844ae7; - --a11y-theme-shiki-ansi-bright-cyan: #3192aa; - --a11y-theme-shiki-ansi-bright-white: #88929d; -} - -[data-theme="light"] { - --theme-shiki-foreground: #24292e; - --theme-shiki-background: #fff; - --theme-shiki-token-constant: #005cc5; - --theme-shiki-token-string: #032f62; - --theme-shiki-token-comment: #6a737d; - --theme-shiki-token-keyword: #d73a49; - --theme-shiki-token-parameter: #24292e; - --theme-shiki-token-function: #6f42c1; - --theme-shiki-token-string-expression: #032f62; - --theme-shiki-token-punctuation: #24292e; - --theme-shiki-token-link: #032f62; - --theme-shiki-token-inserted: #22863a; - --theme-shiki-token-deleted: #b31d28; - --theme-shiki-token-changed: #e36209; - --theme-shiki-ansi-black: #24292e; - --theme-shiki-ansi-red: #d73a49; - --theme-shiki-ansi-green: #28a745; - --theme-shiki-ansi-yellow: #dbab09; - --theme-shiki-ansi-blue: #0366d6; - --theme-shiki-ansi-magenta: #5a32a3; - --theme-shiki-ansi-cyan: #1b7c83; - --theme-shiki-ansi-white: #6a737d; - --theme-shiki-ansi-bright-black: #959da5; - --theme-shiki-ansi-bright-red: #cb2431; - --theme-shiki-ansi-bright-green: #22863a; - --theme-shiki-ansi-bright-yellow: #b08800; - --theme-shiki-ansi-bright-blue: #005cc5; - --theme-shiki-ansi-bright-magenta: #5a32a3; - --theme-shiki-ansi-bright-cyan: #3192aa; - --theme-shiki-ansi-bright-white: #d1d5da; -} - -[data-theme="dark"] { - --theme-shiki-foreground: #e1e4e8; - --theme-shiki-background: #24292e; - --theme-shiki-token-constant: #79b8ff; - --theme-shiki-token-string: #9ecbff; - --theme-shiki-token-comment: #6a737d; - --theme-shiki-token-keyword: #f97583; - --theme-shiki-token-parameter: #e1e4e8; - --theme-shiki-token-function: #b392f0; - --theme-shiki-token-string-expression: #9ecbff; - --theme-shiki-token-punctuation: #e1e4e8; - --theme-shiki-token-link: #dbedff; - --theme-shiki-token-inserted: #85e89d; - --theme-shiki-token-deleted: #fdaeb7; - --theme-shiki-token-changed: #ffab70; - --theme-shiki-ansi-black: #586069; - --theme-shiki-ansi-red: #ea4a5a; - --theme-shiki-ansi-green: #34d058; - --theme-shiki-ansi-yellow: #ffea7f; - --theme-shiki-ansi-blue: #2188ff; - --theme-shiki-ansi-magenta: #b392f0; - --theme-shiki-ansi-cyan: #39c5cf; - --theme-shiki-ansi-white: #d1d5da; - --theme-shiki-ansi-bright-black: #959da5; - --theme-shiki-ansi-bright-red: #f97583; - --theme-shiki-ansi-bright-green: #85e89d; - --theme-shiki-ansi-bright-yellow: #ffea7f; - --theme-shiki-ansi-bright-blue: #79b8ff; - --theme-shiki-ansi-bright-magenta: #b392f0; - --theme-shiki-ansi-bright-cyan: #56d4dd; - --theme-shiki-ansi-bright-white: #fafbfc; -} - -[data-theme="a11y"] { - --theme-shiki-foreground: var(--a11y-theme-shiki-foreground); - --theme-shiki-background: var(--a11y-theme-shiki-background); - --theme-shiki-token-constant: var(--a11y-theme-shiki-token-constant); - --theme-shiki-token-string: var(--a11y-theme-shiki-token-string); - --theme-shiki-token-comment: var(--a11y-theme-shiki-token-comment); - --theme-shiki-token-keyword: var(--a11y-theme-shiki-token-keyword); - --theme-shiki-token-parameter: var(--a11y-theme-shiki-token-parameter); - --theme-shiki-token-function: var(--a11y-theme-shiki-token-function); - --theme-shiki-token-string-expression: var(--a11y-theme-shiki-token-string-expression); - --theme-shiki-token-punctuation: var(--a11y-theme-shiki-token-punctuation); - --theme-shiki-token-link: var(--a11y-theme-shiki-token-link); - --theme-shiki-token-inserted: var(--a11y-theme-shiki-token-inserted); - --theme-shiki-token-deleted: var(--a11y-theme-shiki-token-deleted); - --theme-shiki-token-changed: var(--a11y-theme-shiki-token-changed); - --theme-shiki-ansi-black: var(--a11y-theme-shiki-ansi-black); - --theme-shiki-ansi-red: var(--a11y-theme-shiki-ansi-red); - --theme-shiki-ansi-green: var(--a11y-theme-shiki-ansi-green); - --theme-shiki-ansi-yellow: var(--a11y-theme-shiki-ansi-yellow); - --theme-shiki-ansi-blue: var(--a11y-theme-shiki-ansi-blue); - --theme-shiki-ansi-magenta: var(--a11y-theme-shiki-ansi-magenta); - --theme-shiki-ansi-cyan: var(--a11y-theme-shiki-ansi-cyan); - --theme-shiki-ansi-white: var(--a11y-theme-shiki-ansi-white); - --theme-shiki-ansi-bright-black: var(--a11y-theme-shiki-ansi-bright-black); - --theme-shiki-ansi-bright-red: var(--a11y-theme-shiki-ansi-bright-red); - --theme-shiki-ansi-bright-green: var(--a11y-theme-shiki-ansi-bright-green); - --theme-shiki-ansi-bright-yellow: var(--a11y-theme-shiki-ansi-bright-yellow); - --theme-shiki-ansi-bright-blue: var(--a11y-theme-shiki-ansi-bright-blue); - --theme-shiki-ansi-bright-magenta: var(--a11y-theme-shiki-ansi-bright-magenta); - --theme-shiki-ansi-bright-cyan: var(--a11y-theme-shiki-ansi-bright-cyan); - --theme-shiki-ansi-bright-white: var(--a11y-theme-shiki-ansi-bright-white); -} - -[data-theme="holiday"] { - --theme-shiki-foreground: #ddd; - --theme-shiki-background: #222; - --theme-shiki-token-constant: #09f; - --theme-shiki-token-string: #6f6; - --theme-shiki-token-comment: #777; - --theme-shiki-token-keyword: #f66; - --theme-shiki-token-parameter: #ff6; - --theme-shiki-token-function: #6cf; - --theme-shiki-token-string-expression: #f6f; - --theme-shiki-token-punctuation: #fff; - --theme-shiki-token-link: #6ff; -} diff --git a/src/styles/index.css b/src/styles/index.css index 2d927f282..ae895b026 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -6,7 +6,7 @@ @import "./themes/holiday.css"; /** Print theme override — unlayered so it beats the theme files above */ -@import "./print-theme.css"; +@import "./themes/print-black-and-white.css"; /** * Explicit cascade layer order. @@ -32,8 +32,7 @@ @import "./print.css" layer(utilities); /** Shiki code highlighting adapted to themes */ -@import "./code-highlighting/general.css" layer(components); -@import "./code-highlighting/root.css" layer(components); +@import "./code-highlighting.css" layer(components); /** Replaces "content" key in Tailwind config */ @source "../../src/**/*.{astro,ts,tsx,md,mdx}"; diff --git a/src/styles/print-theme.css b/src/styles/print-theme.css deleted file mode 100644 index dfa9f0bba..000000000 --- a/src/styles/print-theme.css +++ /dev/null @@ -1,93 +0,0 @@ -/* ========================================================================== - Print Theme Override (UNLAYERED) - Imported without a layer so it beats the unlayered theme files - ([data-theme="light"], etc.) via source order + specificity. - ========================================================================== */ - -@media print { - html[data-theme] { - --theme-color-primary: var(--a11y-theme-color-primary); - --theme-color-primary-offset: var(--a11y-theme-color-primary-offset); - --theme-color-primary-inverse: var(--a11y-theme-color-primary-inverse); - --theme-color-secondary: var(--a11y-theme-color-secondary); - --theme-color-secondary-offset: var(--a11y-theme-color-secondary-offset); - --theme-color-secondary-inverse: var(--a11y-theme-color-secondary-inverse); - --theme-color-accent: var(--a11y-theme-color-accent); - --theme-color-accent-offset: var(--a11y-theme-color-accent-offset); - --theme-color-page-base: var(--a11y-theme-color-page-base); - --theme-color-page-active: var(--a11y-theme-color-page-active); - --theme-color-page-inverse: var(--a11y-theme-color-page-inverse); - --theme-color-page-offset: var(--a11y-theme-color-page-offset); - --theme-color-content: var(--a11y-theme-color-content); - --theme-color-content-offset: var(--a11y-theme-color-content-active); - --theme-color-content-active: var(--a11y-theme-color-content-active); - --theme-color-content-inverse: var(--a11y-theme-color-content-inverse); - --theme-color-content-inverse-active: var(--a11y-theme-color-content-inverse-active); - --theme-color-trim: var(--a11y-theme-color-trim); - --theme-color-spotlight: var(--a11y-theme-color-spotlight); - --theme-color-spotlight-offset: var(--a11y-theme-color-spotlight-offset); - --theme-color-success: var(--a11y-theme-color-success); - --theme-color-success-offset: var(--a11y-theme-color-success-offset); - --theme-color-success-inverse: var(--a11y-theme-color-success-inverse); - --theme-color-info: var(--a11y-theme-color-info); - --theme-color-info-offset: var(--a11y-theme-color-info-offset); - --theme-color-info-inverse: var(--a11y-theme-color-info-inverse); - --theme-color-note: var(--a11y-theme-color-note); - --theme-color-note-offset: var(--a11y-theme-color-note-offset); - --theme-color-note-inverse: var(--a11y-theme-color-note-inverse); - --theme-color-warning: var(--a11y-theme-color-warning); - --theme-color-warning-offset: var(--a11y-theme-color-warning-offset); - --theme-color-warning-inverse: var(--a11y-theme-color-warning-inverse); - --theme-color-danger: var(--a11y-theme-color-danger); - --theme-color-danger-offset: var(--a11y-theme-color-danger-offset); - --theme-color-danger-inverse: var(--a11y-theme-color-danger-inverse); - --theme-color-icons: var(--a11y-theme-color-icons); - --theme-color-icons-active: var(--a11y-theme-color-icons-active); - --theme-color-black: var(--a11y-theme-color-black); - --theme-color-white: var(--a11y-theme-color-white); - --theme-color-blue-100: var(--a11y-theme-color-blue-100); - --theme-color-blue-600: var(--a11y-theme-color-blue-600); - --theme-color-blue-900: var(--a11y-theme-color-blue-900); - --theme-color-gray-50: var(--a11y-theme-color-gray-50); - --theme-color-gray-200: var(--a11y-theme-color-gray-200); - --theme-color-gray-300: var(--a11y-theme-color-gray-300); - --theme-color-gray-600: var(--a11y-theme-color-gray-600); - --theme-color-gray-700: var(--a11y-theme-color-gray-700); - --theme-color-gray-800: var(--a11y-theme-color-gray-800); - --theme-color-gray-offset: var(--a11y-theme-color-gray-offset); - --theme-color-green-100: var(--a11y-theme-color-green-100); - --theme-color-green-900: var(--a11y-theme-color-green-900); - - /* Shiki code highlighting colors */ - --shiki-foreground: var(--a11y-theme-shiki-foreground); - --shiki-background: var(--a11y-theme-shiki-background); - --shiki-token-constant: var(--a11y-theme-shiki-token-constant); - --shiki-token-string: var(--a11y-theme-shiki-token-string); - --shiki-token-comment: var(--a11y-theme-shiki-token-comment); - --shiki-token-keyword: var(--a11y-theme-shiki-token-keyword); - --shiki-token-parameter: var(--a11y-theme-shiki-token-parameter); - --shiki-token-function: var(--a11y-theme-shiki-token-function); - --shiki-token-string-expression: var(--a11y-theme-shiki-token-string-expression); - --shiki-token-punctuation: var(--a11y-theme-shiki-token-punctuation); - --shiki-token-link: var(--a11y-theme-shiki-token-link); - --shiki-token-inserted: var(--a11y-theme-shiki-token-inserted); - --shiki-token-deleted: var(--a11y-theme-shiki-token-deleted); - --shiki-token-changed: var(--a11y-theme-shiki-token-changed); - --shiki-ansi-black: var(--a11y-theme-shiki-ansi-black); - --shiki-ansi-red: var(--a11y-theme-shiki-ansi-red); - --shiki-ansi-green: var(--a11y-theme-shiki-ansi-green); - --shiki-ansi-yellow: var(--a11y-theme-shiki-ansi-yellow); - --shiki-ansi-blue: var(--a11y-theme-shiki-ansi-blue); - --shiki-ansi-magenta: var(--a11y-theme-shiki-ansi-magenta); - --shiki-ansi-cyan: var(--a11y-theme-shiki-ansi-cyan); - --shiki-ansi-white: var(--a11y-theme-shiki-ansi-white); - --shiki-ansi-bright-black: var(--a11y-theme-shiki-ansi-bright-black); - --shiki-ansi-bright-red: var(--a11y-theme-shiki-ansi-bright-red); - --shiki-ansi-bright-green: var(--a11y-theme-shiki-ansi-bright-green); - --shiki-ansi-bright-yellow: var(--a11y-theme-shiki-ansi-bright-yellow); - --shiki-ansi-bright-blue: var(--a11y-theme-shiki-ansi-bright-blue); - --shiki-ansi-bright-magenta: var(--a11y-theme-shiki-ansi-bright-magenta); - --shiki-ansi-bright-cyan: var(--a11y-theme-shiki-ansi-bright-cyan); - --shiki-ansi-bright-white: var(--a11y-theme-shiki-ansi-bright-white); - } -} diff --git a/src/styles/print.css b/src/styles/print.css index 3c5568210..2f3bd6f5a 100644 --- a/src/styles/print.css +++ b/src/styles/print.css @@ -1,16 +1,7 @@ /* ========================================================================== Print Stylesheet - Imported in index.css as: @import "./print.css" layer(utilities); ========================================================================== */ -/* Preserve colors on color-capable printers */ -@media print and (color) { - * { - -webkit-print-color-adjust: exact; - print-color-adjust: exact; - } -} - @media print { /* -------------------------------------------------------------------------- Page setup @@ -18,6 +9,19 @@ @page { margin: 2cm; + + @bottom-left { + color: #000; + content: "Webstack Builders, Inc."; + font-size: 0.75rem; + } + + @bottom-right { + color: #000; + content: "Page " counter(page) " of " counter(pages); + font-family: Arial, sans-serif; + font-size: 0.8333rem; + } } /* -------------------------------------------------------------------------- @@ -72,15 +76,39 @@ width: 100% !important; } + #header { + margin: 0 !important; + max-width: none !important; + width: 100% !important; + } + + /* -------------------------------------------------------------------------- Typography — widows, orphans, page breaks -------------------------------------------------------------------------- */ - p, li, blockquote, figcaption { + p, blockquote, figcaption { orphans: 3; widows: 3; } + .screen-table-figure-caption, + .print-table-caption .screen-table-caption-copy { + display: none !important; + } + + .print-table-caption { + caption-side: bottom !important; + display: table-caption !important; + font-style: italic; + padding-top: 0.75rem !important; + text-align: center !important; + } + + .print-table-caption .print-table-figure-copy { + display: block !important; + } + /* Keep headings attached to the content that follows them */ h1, h2, h3, h4, h5, h6 { break-after: avoid; @@ -112,177 +140,170 @@ Lists, figures, tables, code — avoid page breaks inside -------------------------------------------------------------------------- */ - ul, ol, figure, table, pre, [data-qr-code] { + li, [data-qr-code] { break-inside: avoid; } - /* -------------------------------------------------------------------------- - Links — expand external URLs, suppress internal ones - -------------------------------------------------------------------------- */ - - a { - text-decoration: none; + .callout { + box-decoration-break: slice; + break-inside: avoid; + display: block !important; + overflow: visible !important; } - /* Show full URL for outbound links */ - article a[href^="http"]::after, - article a[href^="https"]::after { - content: " <" attr(href) "> "; - font-size: 0.85em; - word-break: break-all; + .callout__header { + break-after: avoid; } - /* Suppress URL expansion for internal links and anchor links */ - article a[href^="/"]::after, - article a[href^="#"]::after { - content: none; + .callout__content { + break-before: avoid; } - /* Don't expand links that wrap images */ - a:has(img)::after { - content: none; + .callout__header + .callout__content { + margin-top: 0.75rem; } - /* -------------------------------------------------------------------------- - Abbreviations — print definition in parentheses - -------------------------------------------------------------------------- */ - - abbr[title]::after { - content: " (" attr(title) ")"; + .service-card { + background-clip: padding-box; + background-color: var(--color-content-inverse) !important; + border: 1px solid var(--color-trim) !important; + border-radius: 1rem !important; + box-decoration-break: clone; + overflow: visible !important; } - /* -------------------------------------------------------------------------- - Hide interactive / screen-only chrome - -------------------------------------------------------------------------- */ - - /* Site header — hide entire shell AND undo fixed positioning */ - .header-shell { - display: none !important; + .service-card-body { + background-color: var(--color-content-inverse) !important; + padding-bottom: 2rem !important; } - .header-footprint { - display: none !important; + .services-grid { + background-color: var(--color-page-base) !important; + row-gap: 3rem !important; } - .header-fixed { - display: none !important; - position: static !important; + /** + * Make sure Zebra List doesn't leave an empty container with borders + * when it breaks across lines + */ + .print-zebra-list { + border: 0 !important; + border-radius: 0 !important; + overflow: visible !important; } - /* Belt-and-suspenders: hide nav, theme icon, search icon individually */ - #header { - display: none !important; + .print-zebra-list > .print-zebra-list-item { + border: 1px solid var(--color-trim) !important; + border-radius: 0 !important; } - /* Reading progress bar (position: fixed, would repeat on every page) */ - reading-progress-bar { - display: none !important; + .print-zebra-list > .print-zebra-list-item + .print-zebra-list-item { + margin-top: -1px; } - /* Mobile navigation splash backdrop */ - #mobile-splash { - display: none !important; + /** + * Table wrappers use a shared rounded, bordered shell for screen layouts. + * In print, that shell can fragment across pages and leave an empty bordered + * area at the bottom of the page. Make the shell non-visual and let the + * table's own row/header borders carry the printed structure instead. + */ + .print-table-shell { + background: transparent !important; + border: 0 !important; + border-radius: 0 !important; + overflow: visible !important; } - /* Footer */ - site-footer { - display: none !important; + .print-table-shell tr > :first-child { + border-left: 1px solid var(--color-trim) !important; } - /* Breadcrumbs */ - #breadcrumbs { - display: none !important; + .print-table-shell tr > :last-child { + border-right: 1px solid var(--color-trim) !important; } - /* Table of Contents */ - table-of-contents { - display: none !important; + .print-table-has-head thead > tr:first-child > * { + border-top: 1px solid var(--color-trim) !important; } - /* All Call-to-Action blocks (Newsletter, Contact, Download) */ - .cta { - display: none !important; + .print-table-no-head tbody:first-child > tr:first-child > * { + border-top: 1px solid var(--color-trim) !important; } - /* Social shares and web mentions */ - social-share-element, - web-mentions { - display: none !important; + .print-table-shell tfoot:last-child > tr:last-child > * { + border-bottom: 1px solid var(--color-trim) !important; } - /* Social / media embeds (YouTube, Twitter, etc.) */ - social-embed { - display: none !important; + .print-table-shell tbody:last-child > tr:last-child > * { + border-bottom: 1px solid var(--color-trim) !important; } - /* Text highlighter elements */ - highlighter-element { - display: none !important; - } - - /* Content switcher (article ↔ deep-dive toggle) in breadcrumb row */ - content-switcher { - display: none !important; - } + /* -------------------------------------------------------------------------- + Links — expand external URLs, suppress internal ones + -------------------------------------------------------------------------- */ - /* Related content carousel */ - carousel-slider { - display: none !important; + a { + text-decoration: none; } - /* Consent banner */ - consent-banner { - display: none !important; + /* Show full URL for outbound links */ + article a[href^="http"]::after, + article a[href^="https"]::after { + content: " <" attr(href) "> "; + font-size: 0.85em; + word-break: break-all; } - /* Bug reporter */ - bug-reporter-modal-element { - display: none !important; + /* Suppress URL expansion for internal links and anchor links */ + article a[href^="/"]::after, + article a[href^="#"]::after { + content: none; } - /* Theme picker modal */ - theme-picker { - display: none !important; + /* Don't expand links that wrap images */ + a:has(img)::after { + content: none; } - /* Search bar */ - search-bar { - display: none !important; - } + /* -------------------------------------------------------------------------- + Abbreviations — print definition in parentheses + -------------------------------------------------------------------------- */ - /* PWA title bar */ - [style*="titlebar-area-height"] { - display: none !important; + abbr[title]::after { + content: " (" attr(title) ")"; } - /* Skip-to-content link */ - [href="#main"] { - display: none !important; - } + /* -------------------------------------------------------------------------- + Fix header position — remove fixed positioning and z-index + so it prints in flow with the rest of the content + -------------------------------------------------------------------------- */ - /* Mastodon sharing modal */ - mastodon-instance-modal { - display: none !important; + .header-fixed { + position: static !important; } /* -------------------------------------------------------------------------- Tabbed code blocks — print only the first language tab -------------------------------------------------------------------------- */ - /* Hide the language tab bar */ - code-tabs [role="tablist"] { - display: none !important; - } - /* Hide all tab panels, then show only the first one */ code-tabs [role="tabpanel"] { display: none !important; } + .code-tabs-print-header + pre, + .code-tabs-print-header + [role="tabpanel"] { + break-before: avoid; + } + code-tabs [role="tabpanel"]:first-of-type { display: block !important; } + .code-tabs-print-header { + break-after: avoid; + } + /* Hide copy buttons inside code blocks */ code-tabs button { display: none !important; @@ -328,8 +349,11 @@ background-color: #fff !important; } - .bg-page-offset, - [class*="bg-page"] { + /** + * Override header background to white for print, and remove the + * "offset" effect created by the pseudo-element + */ + .site-header::after { background-color: #fff !important; } @@ -339,7 +363,18 @@ .callout, blockquote { - border-left: 3px solid var(--color-content) !important; + border-left: 3px solid var(--color-trim) !important; padding-left: 1em; } } + +/* ========================================================================== + Color Print Stylesheet - use case is primarily for PDF generation + ========================================================================== */ + +@media print and (color) { + * { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} diff --git a/src/styles/theme-inline.css b/src/styles/theme-inline.css index d021d2ce6..5c1c73f3e 100644 --- a/src/styles/theme-inline.css +++ b/src/styles/theme-inline.css @@ -242,4 +242,40 @@ --color-google-green-dark: var(--theme-color-google-green-dark); /* Light theme: #188038 */ --color-x: var(--theme-color-x); /* Light theme: #000 */ --color-x-active: var(--theme-color-x-active); /* Light theme: #1a1a1a */ + + /** + * ============================================================================= + * Shiki code highlighting colors + * ============================================================================= + */ + --shiki-foreground: var(--theme-shiki-foreground); + --shiki-background: var(--theme-shiki-background); + --shiki-token-constant: var(--theme-shiki-token-constant); + --shiki-token-string: var(--theme-shiki-token-string); + --shiki-token-comment: var(--theme-shiki-token-comment); + --shiki-token-keyword: var(--theme-shiki-token-keyword); + --shiki-token-parameter: var(--theme-shiki-token-parameter); + --shiki-token-function: var(--theme-shiki-token-function); + --shiki-token-string-expression: var(--theme-shiki-token-string-expression); + --shiki-token-punctuation: var(--theme-shiki-token-punctuation); + --shiki-token-link: var(--theme-shiki-token-link); + --shiki-token-inserted: var(--theme-shiki-token-inserted); + --shiki-token-deleted: var(--theme-shiki-token-deleted); + --shiki-token-changed: var(--theme-shiki-token-changed); + --shiki-ansi-black: var(--theme-shiki-ansi-black); + --shiki-ansi-red: var(--theme-shiki-ansi-red); + --shiki-ansi-green: var(--theme-shiki-ansi-green); + --shiki-ansi-yellow: var(--theme-shiki-ansi-yellow); + --shiki-ansi-blue: var(--theme-shiki-ansi-blue); + --shiki-ansi-magenta: var(--theme-shiki-ansi-magenta); + --shiki-ansi-cyan: var(--theme-shiki-ansi-cyan); + --shiki-ansi-white: var(--theme-shiki-ansi-white); + --shiki-ansi-bright-black: var(--theme-shiki-ansi-bright-black); + --shiki-ansi-bright-red: var(--theme-shiki-ansi-bright-red); + --shiki-ansi-bright-green: var(--theme-shiki-ansi-bright-green); + --shiki-ansi-bright-yellow: var(--theme-shiki-ansi-bright-yellow); + --shiki-ansi-bright-blue: var(--theme-shiki-ansi-bright-blue); + --shiki-ansi-bright-magenta: var(--theme-shiki-ansi-bright-magenta); + --shiki-ansi-bright-cyan: var(--theme-shiki-ansi-bright-cyan); + --shiki-ansi-bright-white: var(--theme-shiki-ansi-bright-white); } diff --git a/src/styles/themes/a11y.css b/src/styles/themes/a11y.css index e9059e690..1bb7c1b32 100644 --- a/src/styles/themes/a11y.css +++ b/src/styles/themes/a11y.css @@ -1,8 +1,8 @@ /** * ============================================================================= * HIGH-CONTRAST BLACK & WHITE THEME DEFINITIONS -* WCAG 2.2 Enhanced (AAA) – all text ≥ 7:1 on its expected background. -* Palette colors are pure grayscale; mid-tones ≤ #595959 so they pass +* WCAG 2.2 Enhanced (AAA) - all text >= 7:1 on its expected background. +* Palette colors are pure grayscale; mid-tones <= #595959 so they pass * 7:1 on white when used as text. * ============================================================================= */ @@ -156,6 +156,43 @@ --a11y-theme-color-reddit-active: #333; --a11y-theme-color-x: #000; --a11y-theme-color-x-active: #333; + + /** + * ============================================================================= + * Shiki code highlighting colors + * ============================================================================= + */ + + --a11y-theme-shiki-foreground: #000; + --a11y-theme-shiki-background: #fff; + --a11y-theme-shiki-token-constant: #111; + --a11y-theme-shiki-token-string: #333; + --a11y-theme-shiki-token-comment: #666; + --a11y-theme-shiki-token-keyword: #000; + --a11y-theme-shiki-token-parameter: #222; + --a11y-theme-shiki-token-function: #222; + --a11y-theme-shiki-token-string-expression: #444; + --a11y-theme-shiki-token-punctuation: #555; + --a11y-theme-shiki-token-link: #111; + --a11y-theme-shiki-token-inserted: #222; + --a11y-theme-shiki-token-deleted: #555; + --a11y-theme-shiki-token-changed: #444; + --a11y-theme-shiki-ansi-black: #000; + --a11y-theme-shiki-ansi-red: #111; + --a11y-theme-shiki-ansi-green: #222; + --a11y-theme-shiki-ansi-yellow: #333; + --a11y-theme-shiki-ansi-blue: #444; + --a11y-theme-shiki-ansi-magenta: #555; + --a11y-theme-shiki-ansi-cyan: #666; + --a11y-theme-shiki-ansi-white: #777; + --a11y-theme-shiki-ansi-bright-black: #333; + --a11y-theme-shiki-ansi-bright-red: #444; + --a11y-theme-shiki-ansi-bright-green: #555; + --a11y-theme-shiki-ansi-bright-yellow: #666; + --a11y-theme-shiki-ansi-bright-blue: #777; + --a11y-theme-shiki-ansi-bright-magenta: #888; + --a11y-theme-shiki-ansi-bright-cyan: #999; + --a11y-theme-shiki-ansi-bright-white: #aaa; } [data-theme="a11y"] { @@ -333,4 +370,41 @@ --theme-color-reddit-active: var(--a11y-theme-color-reddit-active); --theme-color-x: var(--a11y-theme-color-x); --theme-color-x-active: var(--a11y-theme-color-x-active); + + /** + * ============================================================================= + * Shiki code highlighting colors + * ============================================================================= + */ + + --a11y-theme-shiki-foreground: #000; + --a11y-theme-shiki-background: #fff; + --a11y-theme-shiki-token-constant: #111; + --a11y-theme-shiki-token-string: #333; + --a11y-theme-shiki-token-comment: #666; + --a11y-theme-shiki-token-keyword: #000; + --a11y-theme-shiki-token-parameter: #222; + --a11y-theme-shiki-token-function: #222; + --a11y-theme-shiki-token-string-expression: #444; + --a11y-theme-shiki-token-punctuation: #555; + --a11y-theme-shiki-token-link: #111; + --a11y-theme-shiki-token-inserted: #222; + --a11y-theme-shiki-token-deleted: #555; + --a11y-theme-shiki-token-changed: #444; + --a11y-theme-shiki-ansi-black: #000; + --a11y-theme-shiki-ansi-red: #111; + --a11y-theme-shiki-ansi-green: #222; + --a11y-theme-shiki-ansi-yellow: #333; + --a11y-theme-shiki-ansi-blue: #444; + --a11y-theme-shiki-ansi-magenta: #555; + --a11y-theme-shiki-ansi-cyan: #666; + --a11y-theme-shiki-ansi-white: #777; + --a11y-theme-shiki-ansi-bright-black: #333; + --a11y-theme-shiki-ansi-bright-red: #444; + --a11y-theme-shiki-ansi-bright-green: #555; + --a11y-theme-shiki-ansi-bright-yellow: #666; + --a11y-theme-shiki-ansi-bright-blue: #777; + --a11y-theme-shiki-ansi-bright-magenta: #888; + --a11y-theme-shiki-ansi-bright-cyan: #999; + --a11y-theme-shiki-ansi-bright-white: #aaa; } diff --git a/src/styles/themes/dark.css b/src/styles/themes/dark.css index 76c4f2d7c..daeb13eff 100644 --- a/src/styles/themes/dark.css +++ b/src/styles/themes/dark.css @@ -246,4 +246,40 @@ --theme-color-reddit-active: #e03d00; --theme-color-x: #000; --theme-color-x-active: #1a1a1a; + + /** + * ============================================================================= + * Shiki code highlighting colors + * ============================================================================= + */ + --theme-shiki-foreground: #e1e4e8; + --theme-shiki-background: #24292e; + --theme-shiki-token-constant: #79b8ff; + --theme-shiki-token-string: #9ecbff; + --theme-shiki-token-comment: #6a737d; + --theme-shiki-token-keyword: #f97583; + --theme-shiki-token-parameter: #e1e4e8; + --theme-shiki-token-function: #b392f0; + --theme-shiki-token-string-expression: #9ecbff; + --theme-shiki-token-punctuation: #e1e4e8; + --theme-shiki-token-link: #dbedff; + --theme-shiki-token-inserted: #85e89d; + --theme-shiki-token-deleted: #fdaeb7; + --theme-shiki-token-changed: #ffab70; + --theme-shiki-ansi-black: #586069; + --theme-shiki-ansi-red: #ea4a5a; + --theme-shiki-ansi-green: #34d058; + --theme-shiki-ansi-yellow: #ffea7f; + --theme-shiki-ansi-blue: #2188ff; + --theme-shiki-ansi-magenta: #b392f0; + --theme-shiki-ansi-cyan: #39c5cf; + --theme-shiki-ansi-white: #d1d5da; + --theme-shiki-ansi-bright-black: #959da5; + --theme-shiki-ansi-bright-red: #f97583; + --theme-shiki-ansi-bright-green: #85e89d; + --theme-shiki-ansi-bright-yellow: #ffea7f; + --theme-shiki-ansi-bright-blue: #79b8ff; + --theme-shiki-ansi-bright-magenta: #b392f0; + --theme-shiki-ansi-bright-cyan: #56d4dd; + --theme-shiki-ansi-bright-white: #fafbfc; } diff --git a/src/styles/themes/holiday.css b/src/styles/themes/holiday.css index 01ed7efb4..18dd30db5 100644 --- a/src/styles/themes/holiday.css +++ b/src/styles/themes/holiday.css @@ -233,4 +233,40 @@ --theme-color-reddit-active: #e03d00; --theme-color-x: #000; --theme-color-x-active: #1a1a1a; + + /** + * ============================================================================= + * Shiki code highlighting colors + * ============================================================================= + */ + --theme-shiki-foreground: #ddd; + --theme-shiki-background: #222; + --theme-shiki-token-constant: #09f; + --theme-shiki-token-string: #6f6; + --theme-shiki-token-comment: #777; + --theme-shiki-token-keyword: #f66; + --theme-shiki-token-parameter: #ff6; + --theme-shiki-token-function: #6cf; + --theme-shiki-token-string-expression: #f6f; + --theme-shiki-token-punctuation: #fff; + --theme-shiki-token-link: #6ff; + --theme-shiki-token-inserted: #6f6; + --theme-shiki-token-deleted: #f66; + --theme-shiki-token-changed: #fc3; + --theme-shiki-ansi-black: #222; + --theme-shiki-ansi-red: #f66; + --theme-shiki-ansi-green: #6f6; + --theme-shiki-ansi-yellow: #ff6; + --theme-shiki-ansi-blue: #09f; + --theme-shiki-ansi-magenta: #f6f; + --theme-shiki-ansi-cyan: #6ff; + --theme-shiki-ansi-white: #ddd; + --theme-shiki-ansi-bright-black: #777; + --theme-shiki-ansi-bright-red: #f99; + --theme-shiki-ansi-bright-green: #9f9; + --theme-shiki-ansi-bright-yellow: #ffc; + --theme-shiki-ansi-bright-blue: #6cf; + --theme-shiki-ansi-bright-magenta: #f9f; + --theme-shiki-ansi-bright-cyan: #9ff; + --theme-shiki-ansi-bright-white: #fff; } diff --git a/src/styles/themes/light.css b/src/styles/themes/light.css index b291e1314..ecba80e35 100644 --- a/src/styles/themes/light.css +++ b/src/styles/themes/light.css @@ -246,4 +246,40 @@ --theme-color-reddit-active: #e03d00; --theme-color-x: #000; --theme-color-x-active: #1a1a1a; + + /** + * ============================================================================= + * Shiki code highlighting colors + * ============================================================================= + */ + --theme-shiki-foreground: #24292e; + --theme-shiki-background: #fff; + --theme-shiki-token-constant: #005cc5; + --theme-shiki-token-string: #032f62; + --theme-shiki-token-comment: #6a737d; + --theme-shiki-token-keyword: #d73a49; + --theme-shiki-token-parameter: #24292e; + --theme-shiki-token-function: #6f42c1; + --theme-shiki-token-string-expression: #032f62; + --theme-shiki-token-punctuation: #24292e; + --theme-shiki-token-link: #032f62; + --theme-shiki-token-inserted: #22863a; + --theme-shiki-token-deleted: #b31d28; + --theme-shiki-token-changed: #e36209; + --theme-shiki-ansi-black: #24292e; + --theme-shiki-ansi-red: #d73a49; + --theme-shiki-ansi-green: #28a745; + --theme-shiki-ansi-yellow: #dbab09; + --theme-shiki-ansi-blue: #0366d6; + --theme-shiki-ansi-magenta: #5a32a3; + --theme-shiki-ansi-cyan: #1b7c83; + --theme-shiki-ansi-white: #6a737d; + --theme-shiki-ansi-bright-black: #959da5; + --theme-shiki-ansi-bright-red: #cb2431; + --theme-shiki-ansi-bright-green: #22863a; + --theme-shiki-ansi-bright-yellow: #b08800; + --theme-shiki-ansi-bright-blue: #005cc5; + --theme-shiki-ansi-bright-magenta: #5a32a3; + --theme-shiki-ansi-bright-cyan: #3192aa; + --theme-shiki-ansi-bright-white: #d1d5da; } diff --git a/src/styles/themes/print-black-and-white.css b/src/styles/themes/print-black-and-white.css new file mode 100644 index 000000000..da986f54c --- /dev/null +++ b/src/styles/themes/print-black-and-white.css @@ -0,0 +1,93 @@ +/* ========================================================================== + Print Theme Override (UNLAYERED) + Imported without a layer so it beats the unlayered theme files + ([data-theme="light"], etc.) via source order + specificity. + ========================================================================== */ + +@media print { + html[data-theme] { + --theme-color-primary: #000; + --theme-color-primary-offset: #222; + --theme-color-primary-inverse: #fff; + --theme-color-secondary: #000; + --theme-color-secondary-offset: #111; + --theme-color-secondary-inverse: #fff; + --theme-color-accent: #000; + --theme-color-accent-offset: #e5e5e5; + --theme-color-page-base: #fff; + --theme-color-page-active: #222; + --theme-color-page-inverse: #000; + --theme-color-page-offset: #f5f5f5; + --theme-color-content: #000; + --theme-color-content-offset: #222; + --theme-color-content-active: #000; + --theme-color-content-inverse: #fff; + --theme-color-content-inverse-active: #e5e5e5; + --theme-color-trim: #000; + --theme-color-spotlight: #000; + --theme-color-spotlight-offset: #333; + --theme-color-success: #000; + --theme-color-success-offset: #111; + --theme-color-success-inverse: #f0f0f0; + --theme-color-info: #000; + --theme-color-info-offset: #111; + --theme-color-info-inverse: #f0f0f0; + --theme-color-note: #000; + --theme-color-note-offset: #333; + --theme-color-note-inverse: #e5e5e5; + --theme-color-warning: #000; + --theme-color-warning-offset: #111; + --theme-color-warning-inverse: #f0f0f0; + --theme-color-danger: #000; + --theme-color-danger-offset: #111; + --theme-color-danger-inverse: #f0f0f0; + --theme-color-icons: #000; + --theme-color-icons-active: #333; + --theme-color-black: #000; + --theme-color-white: #fff; + --theme-color-blue-100: #e8e8e8; + --theme-color-blue-600: #222; + --theme-color-blue-900: #000; + --theme-color-gray-50: #f9f9f9; + --theme-color-gray-200: #e0e0e0; + --theme-color-gray-300: #c8c8c8; + --theme-color-gray-600: #333; + --theme-color-gray-700: #222; + --theme-color-gray-800: #111; + --theme-color-gray-offset: #555; + --theme-color-green-100: #e8e8e8; + --theme-color-green-900: #000; + + /* Shiki code highlighting colors */ + --shiki-foreground: #000; + --shiki-background: #fff; + --shiki-token-constant: #111; + --shiki-token-string: #333; + --shiki-token-comment: #666; + --shiki-token-keyword: #000; + --shiki-token-parameter: #222; + --shiki-token-function: #222; + --shiki-token-string-expression: #444; + --shiki-token-punctuation: #555; + --shiki-token-link: #111; + --shiki-token-inserted: #222; + --shiki-token-deleted: #555; + --shiki-token-changed: #444; + --shiki-ansi-black: #000; + --shiki-ansi-red: #111; + --shiki-ansi-green: #222; + --shiki-ansi-yellow: #333; + --shiki-ansi-blue: #444; + --shiki-ansi-magenta: #555; + --shiki-ansi-cyan: #666; + --shiki-ansi-white: #777; + --shiki-ansi-bright-black: #333; + --shiki-ansi-bright-red: #444; + --shiki-ansi-bright-green: #555; + --shiki-ansi-bright-yellow: #666; + --shiki-ansi-bright-blue: #777; + --shiki-ansi-bright-magenta: #888; + --shiki-ansi-bright-cyan: #999; + --shiki-ansi-bright-white: #aaa; + } +} diff --git a/test/e2e/specs/01-smoke/dynamic-pages.spec.ts b/test/e2e/specs/01-smoke/dynamic-pages.spec.ts index 9b08fed52..3bd72def0 100644 --- a/test/e2e/specs/01-smoke/dynamic-pages.spec.ts +++ b/test/e2e/specs/01-smoke/dynamic-pages.spec.ts @@ -4,6 +4,21 @@ * Uses API to fetch actual content IDs to ensure tests work even if content changes */ import { BasePage, test, expect, setupConsoleErrorChecker, logConsoleErrors } from '@test/e2e/helpers' +import { wait } from '@test/e2e/helpers/waitTimeouts' + +const articleLinkSelector = 'a[href*="/articles/"]' +const serviceLinkSelector = 'a[href^="/services/"]:not([href="/services/"])' +const caseStudyLinkSelector = 'a[href*="/case-studies/"]' + +const waitForListingPageReady = async (page: BasePage, linkSelector: string): Promise => { + await expect(page.locator('main#main')).toBeVisible({ timeout: wait.navigation }) + await expect(page.locator(linkSelector).first()).toBeVisible({ timeout: wait.navigation }) +} + +const waitForDetailPageReady = async (page: BasePage): Promise => { + await expect(page.locator('main#main')).toBeVisible({ timeout: wait.navigation }) + await expect(page.locator('h1[id="article-title"]')).toBeVisible({ timeout: wait.navigation }) +} const getFirstHref = async (page: BasePage, selector: string): Promise => { return await page.page.locator(selector).evaluateAll((elements) => { @@ -22,20 +37,18 @@ test.describe('Dynamic Pages @smoke', () => { const page = await BasePage.init(playwrightPage) // First, visit articles list to get an actual article link await page.goto('/articles') - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForListingPageReady(page, articleLinkSelector) // Find first article link - const firstArticleLink = page.locator('a[href*="/articles/"]').first() + const firstArticleLink = page.locator(articleLinkSelector).first() await expect(firstArticleLink).toBeVisible() - const articleUrl = await getFirstHref(page, 'a[href*="/articles/"]') + const articleUrl = await getFirstHref(page, articleLinkSelector) expect(articleUrl).toBeTruthy() // Navigate to the article await page.goto(articleUrl!) - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForDetailPageReady(page) // Verify article page loaded with expected elements await expect(page.locator('main#main')).toBeVisible() @@ -49,21 +62,21 @@ test.describe('Dynamic Pages @smoke', () => { const page = await BasePage.init(playwrightPage) // Visit services list to get an actual service link await page.goto('/services') - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await expect(page.locator('main#main')).toBeVisible({ timeout: wait.navigation }) // Find first service link - const serviceUrl = await getFirstHref(page, 'a[href^="/services/"]:not([href="/services/"])') + const serviceUrl = await getFirstHref(page, serviceLinkSelector) if (!serviceUrl) { test.skip() return } + await waitForListingPageReady(page, serviceLinkSelector) + // Navigate to the service await page.goto(serviceUrl) - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForDetailPageReady(page) // Verify service page loaded await expect(page.locator('main#main')).toBeVisible() @@ -77,21 +90,21 @@ test.describe('Dynamic Pages @smoke', () => { const page = await BasePage.init(playwrightPage) // Visit case studies list to get an actual case study link await page.goto('/case-studies') - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await expect(page.locator('main#main')).toBeVisible({ timeout: wait.navigation }) // Find first case study link - const caseStudyUrl = await getFirstHref(page, 'a[href*="/case-studies/"]') + const caseStudyUrl = await getFirstHref(page, caseStudyLinkSelector) if (!caseStudyUrl) { test.skip() return } + await waitForListingPageReady(page, caseStudyLinkSelector) + // Navigate to the case study await page.goto(caseStudyUrl) - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForDetailPageReady(page) // Verify case study page loaded await expect(page.locator('main#main')).toBeVisible() @@ -152,16 +165,14 @@ test.describe('Dynamic Pages @smoke', () => { // Test article page await page.goto('/articles') - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForListingPageReady(page, articleLinkSelector) - const articleUrl = await getFirstHref(page, 'a[href*="/articles/"]') + const articleUrl = await getFirstHref(page, articleLinkSelector) if (articleUrl) { resetChecker() await page.goto(articleUrl) - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForDetailPageReady(page) const filtered404s = errorChecker.getFiltered404s() if (filtered404s.length > 0) { @@ -171,16 +182,15 @@ test.describe('Dynamic Pages @smoke', () => { // Test service page await page.goto('/services') - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await expect(page.locator('main#main')).toBeVisible({ timeout: wait.navigation }) - const serviceUrl = await getFirstHref(page, 'a[href^="/services/"]:not([href="/services/"])') + const serviceUrl = await getFirstHref(page, serviceLinkSelector) if (serviceUrl) { + await waitForListingPageReady(page, serviceLinkSelector) resetChecker() await page.goto(serviceUrl) - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForDetailPageReady(page) const filtered404s = errorChecker.getFiltered404s() if (filtered404s.length > 0) { @@ -190,16 +200,15 @@ test.describe('Dynamic Pages @smoke', () => { // Test case study page await page.goto('/case-studies') - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await expect(page.locator('main#main')).toBeVisible({ timeout: wait.navigation }) - const caseStudyUrl = await getFirstHref(page, 'a[href*="/case-studies/"]') + const caseStudyUrl = await getFirstHref(page, caseStudyLinkSelector) if (caseStudyUrl) { + await waitForListingPageReady(page, caseStudyLinkSelector) resetChecker() await page.goto(caseStudyUrl) - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForDetailPageReady(page) const filtered404s = errorChecker.getFiltered404s() if (filtered404s.length > 0) { @@ -224,10 +233,9 @@ test.describe('Dynamic Pages @smoke', () => { const page = await BasePage.init(playwrightPage) // First, get actual article URL await page.goto('/articles') - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForListingPageReady(page, articleLinkSelector) - const articleUrl = await getFirstHref(page, 'a[href*="/articles/"]') + const articleUrl = await getFirstHref(page, articleLinkSelector) if (!articleUrl) { test.skip() @@ -238,8 +246,7 @@ test.describe('Dynamic Pages @smoke', () => { const errorChecker = setupConsoleErrorChecker(page.page) await page.goto(articleUrl) - // NOTE: Avoid strict 'networkidle' gating on WebKit/mobile-safari (can hang on long-lived requests). - await page.waitForNetworkIdleBestEffort() + await waitForDetailPageReady(page) logConsoleErrors(errorChecker)