-
-
-
-
-
-
{{ category.name }}
-
-
-
-
-
-
-
{{ item.name }} - {{ item.description }}
+
-
-
+
+
+
+
+
+
+
+
+
+
+ {{ chapter.name }}
+
+
+ {{ category.name }}
+
+
-
-
-
-
+
+
+
-
-
diff --git a/docs/.vitepress/theme/Icon.vue b/docs/.vitepress/theme/Icon.vue
new file mode 100644
index 00000000..0059e78e
--- /dev/null
+++ b/docs/.vitepress/theme/Icon.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/Layout.vue b/docs/.vitepress/theme/Layout.vue
index d3b9c038..adcbd712 100644
--- a/docs/.vitepress/theme/Layout.vue
+++ b/docs/.vitepress/theme/Layout.vue
@@ -1,20 +1,20 @@
+
+
-
-
-
-
-
+
-
-
\ No newline at end of file
diff --git a/docs/.vitepress/theme/LayoutFooter.vue b/docs/.vitepress/theme/LayoutFooter.vue
index 2721fc8e..60479b8a 100644
--- a/docs/.vitepress/theme/LayoutFooter.vue
+++ b/docs/.vitepress/theme/LayoutFooter.vue
@@ -60,7 +60,7 @@ const columns: FooterColumn[] = [
}
];
-const year = 2026;
+const year = new Date().getFullYear();
@@ -72,11 +72,7 @@ const year = 2026;
@@ -84,15 +80,16 @@ const year = 2026;
-
© {{ year }} QuestPDF by Marcin Ziąbek CodeFlint
+
© {{ year }} QuestPDF by Marcin Ziabek CodeFlint
-
diff --git a/docs/.vitepress/theme/PaddleIntegration.vue b/docs/.vitepress/theme/PaddleIntegration.vue
deleted file mode 100644
index e0b9663d..00000000
--- a/docs/.vitepress/theme/PaddleIntegration.vue
+++ /dev/null
@@ -1,11 +0,0 @@
-
diff --git a/docs/.vitepress/theme/VitepressAdjustments.css b/docs/.vitepress/theme/VitepressAdjustments.css
index b63979f1..713e4984 100644
--- a/docs/.vitepress/theme/VitepressAdjustments.css
+++ b/docs/.vitepress/theme/VitepressAdjustments.css
@@ -3,51 +3,50 @@
--vp-c-brand-2: #039BE5;
--vp-c-brand-3: #29B6F6;
--vp-c-brand-soft: #0091EA24;
+
+ --vp-c-bg: #ffffff;
+ --vp-c-bg-alt: #f6f6f8;
}
-:root:is(.dark) {
+:root.dark {
--vp-c-brand-1: #4FC3F7;
--vp-c-brand-2: #03A9F4;
--vp-c-brand-3: #0288D1;
--vp-c-brand-soft: #0091EA24;
+
+ --vp-c-bg: #101010;
+ --vp-c-bg-alt: #161618;
}
:root {
- --vp-home-hero-name-color: transparent;
-
- --vp-home-hero-image-background-image: conic-gradient(from 100deg, #2979FF55 70deg, #88888855 70deg);
- --vp-home-hero-image-filter: blur(48px);
+ font-size: 16px;
--vp-c-gutter: var(--vp-c-divider);
+ --vp-nav-logo-height: 32px;
--elevation: 0 13px 27px -5px rgb(0 0 0 / 5%), 0 8px 16px -8px rgb(0 0 0 / 8%), 0 -6px 16px -6px rgb(0 0 0 / 3%);
--elevation-hover: 0 13px 27px -5px rgb(0 0 0 / 15%), 0 8px 16px -8px rgb(0 0 0 / 24%), 0 -6px 16px -6px rgb(0 0 0 / 9%);
}
-html {
- scroll-behavior: smooth;
-}
-
-:root {
- font-size: 16px;
-}
-
body {
line-height: 24px;
}
-.VPNavBar .title {
- font-size: 24px !important;
+.VPNavBar .VPNavBarTitle .logo {
+ margin-right: 0;
}
-.logo {
- height: 32px !important;
- margin-right: 16px !important;
+@media (min-width: 48rem) {
+ .VPNavBar:not(.has-sidebar) .VPNavBarSearch {
+ padding-inline-start: 2.5rem;
+ }
}
-.VPHero img.VPImage {
- box-shadow: 0 8px 8px rgba(0, 0, 0, 0.25);
+@media (min-width: 60rem) {
+ .VPNavBar:not(.has-sidebar) .VPNavBarSearch {
+ padding-inline-start: 3.5rem;
+ }
}
.VPLink.link.active,
@@ -61,63 +60,6 @@ body {
margin-bottom: 0 !important;
}
-.VPHero {
- padding: 198px 0 96px 0 !important;
- margin: 0 32px;
-}
-
-@media (min-width: 425px) {
- .VPHero h1 span.name {
- font-size: 4rem !important;
- line-height: 4rem !important;
- margin-bottom: 2rem !important;
- }
-
- .VPHero h1 span.text {
- font-size: 1.5rem !important;
- line-height: 2rem !important;
- margin-bottom: 1rem !important;
- }
-}
-
-@media (min-width: 960px) {
- .VPHero h1 span.name {
- font-size: 6rem !important;
- line-height: 6rem !important;
- margin-bottom: 1rem !important;
- }
-
- .VPHero h1 span.text {
- font-size: 2.5rem !important;
- line-height: 3rem !important;
- margin-bottom: 1rem !important;
- }
-}
-
-@media (max-width: 960px) {
- .VPHero {
- padding: 128px 32px 64px 32px !important;
- }
-}
-
-@media (max-width: 425px) {
- .VPHero {
- padding: 96px 24px 48px !important;
- }
-
- .VPHero h1 span.name {
- font-size: 3rem !important;
- line-height: 3rem !important;
- margin-bottom: 2rem !important;
- }
-
- .VPHero h1 span.text {
- font-size: 1.25rem !important;
- line-height: 2rem !important;
- margin-bottom: 1rem !important;
- }
-}
-
html:not(.dark) .VPDoc img[alt=example] {
border: 1px solid #888;
}
@@ -137,24 +79,27 @@ html.dark .VPDoc img[alt=example] {
color: var(--vp-c-tip-1);
}
-.VPHero .VPButton {
- font-size: 1rem !important;
-}
-
.custom-block-title {
font-size: 1rem;
letter-spacing: 1px;
font-weight: bold;
}
+/* an embedded PDF scrolls on its own, so it stays shorter than the window and the page can scroll past it */
.pdf-viewer {
width: 100%;
height: 625px;
+ max-height: min(75svh, 140vw);
box-shadow: 0 13px 27px -5px rgb(50 50 93 / 15%), 0 8px 16px -8px rgb(0 0 0 / 10%), 0 -6px 16px -6px rgb(0 0 0 / 5%);
border-radius: 8px;
}
+.pdf-viewer > p {
+ margin: 0;
+ padding: 16px 20px;
+}
+
html.dark .light-only {
display: none !important;
}
@@ -175,6 +120,108 @@ html:not(.dark) .dark-only {
border-radius: 8px;
}
-.VPNavBar.top.home .VPNavBarSearchButton {
- display: none !important;
+/* Dark navbar: the bg-alt token, so the bar reads as its own surface. VitePress paints the bar through this token
+ (VPNavBar::before), so the local nav and the home bar pick it up as well. */
+html.dark {
+ --vp-nav-bg-color: var(--vp-c-bg-alt);
+}
+
+/* VitePress makes the home navbar transparent at the top of the page (and always below 60rem): restore the regular
+ surface and divider. */
+.VPNavBar.home::before {
+ background-color: var(--vp-nav-bg-color) !important;
+ backdrop-filter: var(--vp-nav-backdrop-filter) !important;
+}
+
+.VPNavBar.home .divider-line {
+ background-color: var(--vp-nav-divider-color);
+}
+
+html:not(.dark) .Layout:has(.custom-page) .VPNavBar.top .divider-line {
+ background-color: color-mix(in srgb, #2196F3 28%, #E2E2E3);
+}
+
+html.dark .Layout:has(.custom-page) .VPNavBar.top .divider-line {
+ background-color: color-mix(in srgb, #2196F3 20%, #2E2E32);
+}
+
+.VPNavBar .VPNavMenuLink.VPNavBarMenuLink {
+ font-size: 1rem;
+}
+
+/* Navbar links */
+.VPNavBar .VPNavBarMenu .VPNavMenuLink.VPNavBarMenuLink {
+ min-height: 36px;
+ padding: 0 0.875rem;
+ margin: 0;
+ border-radius: 10px;
+ transition: color 0.2s, background-color 0.2s;
+}
+
+.VPNavBar .VPNavBarMenu .VPNavMenuLink.VPNavBarMenuLink:hover {
+ color: var(--vp-c-text-1);
+ background-color: var(--vp-c-default-soft);
+}
+
+.VPNavBar .VPNavBarMenu .VPNavMenuLink.VPNavBarMenuLink.active {
+ color: #ffffff;
+ background-color: #1976D2;
+}
+
+html.dark .VPNavBar .VPNavBarMenu .VPNavMenuLink.VPNavBarMenuLink.active {
+ color: #111111;
+ background-color: #F5F5F7;
+}
+
+/* the pill is painted behind the ⋯ button, which keeps the full bar height so its dropdown opens right below it */
+.VPNavBar .VPNavBarExtra > .button {
+ position: relative;
+ isolation: isolate;
+}
+
+.VPNavBar .VPNavBarExtra > .button::before {
+ content: "";
+ position: absolute;
+ inset: calc(var(--vp-nav-height) / 2 - 18px) 0;
+ z-index: -1;
+ border-radius: 10px;
+ transition: background-color 0.2s;
+}
+
+.VPNavBar .VPNavBarExtra > .button:hover::before {
+ background-color: var(--vp-c-default-soft);
+}
+
+/* Search */
+@media (min-width: 48rem) {
+ .VPNavBar .VPNavBarSearchButton {
+ height: 40px;
+ padding: 0 8px 0 12px;
+ border: 1px solid var(--vp-c-divider);
+ border-radius: 10px;
+ transition: border-color 0.2s;
+ }
+
+ .VPNavBar .VPNavBarSearchButton:hover {
+ border-color: var(--vp-c-brand-1);
+ }
+
+ .VPNavBar .VPNavBarSearchButton .keys {
+ border-radius: 6px;
+ background-color: var(--vp-c-bg);
+ }
+}
+
+@media (min-width: 80rem) {
+ .VPNavBar .VPNavBarSearchButton {
+ min-width: 220px;
+ }
+
+ .VPNavBar .VPNavBarSearchButton .text {
+ font-size: 0.875rem;
+ }
+
+ .VPNavBar .VPNavBarSearchButton .keys {
+ margin-left: auto;
+ }
}
diff --git a/docs/.vitepress/theme/customIcons.ts b/docs/.vitepress/theme/customIcons.ts
new file mode 100644
index 00000000..2ea97c8e
--- /dev/null
+++ b/docs/.vitepress/theme/customIcons.ts
@@ -0,0 +1,27 @@
+/* Glyphs Font Awesome has no good match for, drawn on its 512 grid with its regular-style 48-unit stroke so they sit
+ next to the Pro icons unnoticed. Same definition shape as the imported ones, so Icon.vue renders them as is. */
+
+import type { IconDefinition, IconName } from "@fortawesome/fontawesome-common-types";
+
+/* a square with one large rounded corner against three sharp ones */
+export const faRoundedCorners: IconDefinition = {
+ prefix: "fak",
+ iconName: "rounded-corners" as IconName,
+ icon: [
+ 448, 512, [], "",
+ "M224 32L424 32A24 24 0 0 1 448 56L448 456A24 24 0 0 1 424 480L24 480A24 24 0 0 1 0 456L0 256A224 224 0 0 1 224 32zM224 80A176 176 0 0 0 48 256L48 432L400 432L400 80L224 80z"
+ ]
+};
+
+/* duotone: the secondary layer is the shadow cast below and right of the box, rendered at Icon.vue's 40% opacity */
+export const faDropShadow: IconDefinition = {
+ prefix: "fakd",
+ iconName: "drop-shadow" as IconName,
+ icon: [
+ 512, 512, [], "",
+ [
+ "M352 128L384 128A64 64 0 0 1 448 192L448 416A64 64 0 0 1 384 480L160 480A64 64 0 0 1 96 416L96 384L288 384A64 64 0 0 0 352 320z",
+ "M64 32L288 32A64 64 0 0 1 352 96L352 320A64 64 0 0 1 288 384L64 384A64 64 0 0 1 0 320L0 96A64 64 0 0 1 64 32zM64 80A16 16 0 0 0 48 96L48 320A16 16 0 0 0 64 336L288 336A16 16 0 0 0 304 320L304 96A16 16 0 0 0 288 80L64 80z"
+ ]
+ ]
+};
diff --git a/docs/.vitepress/theme/features.ts b/docs/.vitepress/theme/features.ts
new file mode 100644
index 00000000..377a2ad1
--- /dev/null
+++ b/docs/.vitepress/theme/features.ts
@@ -0,0 +1,898 @@
+/* The Features Overview page (FeaturesOverview.vue). Chapter and category names use the documentation's own terms;
+ feature names say what a reader gets, not what the API calls it. */
+
+import type { IconDefinition } from "@fortawesome/fontawesome-common-types";
+import {
+ faAlignJustify, faArrowRightArrowLeft, faArrowsFromLine, faArrowsMaximize, faArrowsUpDownLeftRight, faBarsStaggered,
+ faBezierCurve, faBold, faBookFont, faBookmark, faBorderAll, faBorderOuter, faBracketsCurly, faBringFront, faBrush,
+ faBug, faChartMixed, faCircle1, faCircleExclamation, faCompress, faCrop, faCrosshairs, faCubes, faDiagramSubtask,
+ faDistributeSpacingVertical, faDownLeftAndUpRightToCenter, faDroplet, faEllipsis, faExpand, faEye, faFeather, faFile,
+ faFileCode, faFileExport, faFileSvg, faFileZipper, faFillDrip, faFont, faFontCase, faForwardStep, faFunction,
+ faGlobe, faHashtag, faHighlighter, faHorizontalRule, faHyphen, faIdCard, faImage, faImages, faIndent, faInfinity,
+ faItalic, faKerning, faLanguage, faLayerGroup, faLeftRight, faLineHeight, faLink, faLinkSimple, faList, faListOl,
+ faListTree, faLocationCrosshairs, faLocationDot, faLock, faMagnifyingGlass, faMagnifyingGlassPlus, faMapLocationDot,
+ faMerge, faMountainSun, faNewspaper, faObjectGroup, faObjectsAlignCenterHorizontal, faPageBreak, faPaintbrush,
+ faPalette, faPaperclip, faParagraph, faPuzzlePiece, faQrcode, faRectangleHistory, faReflectHorizontal, faRepeat,
+ faRotate, faRuler, faRulerCombined, faRulerVertical, faScissors, faSitemap, faSliders, faSplit, faSquareDashed,
+ faSquareDashedCirclePlus, faSquareQuestion, faSquareRing, faStamp, faStop, faSubscript, faSuperscript, faSwap,
+ faSwatchbook, faTable, faTableCellsHeader, faTableCellsMerge, faTableColumns, faTableRows, faTags, faText,
+ faTextHeight, faTextSize, faTextWidth, faToggleOn, faUnderline, faUniversalAccess, faUnlock, faUpDown,
+ faUpRightAndDownLeftFromCenter, faVectorSquare, faWandMagicSparkles
+} from "@fortawesome/pro-regular-svg-icons";
+import { faDropShadow, faRoundedCorners } from "./customIcons";
+
+export interface Feature {
+ name: string;
+ description: string;
+ icon: IconDefinition;
+ link: string;
+}
+
+export interface FeatureCategory {
+ name: string;
+ features: Feature[];
+}
+
+export interface FeatureChapter {
+ name: string;
+ categories: FeatureCategory[];
+}
+
+export const featureChapters: FeatureChapter[] = [
+ {
+ name: "Development tools",
+ categories: [
+ {
+ name: "Companion App",
+ features: [
+ {
+ name: "Live preview",
+ description: "Download, install, and integrate the Companion App with your project. Enjoy real-time hot-reload preview.",
+ icon: faEye,
+ link: "/companion/usage.html"
+ },
+ {
+ name: "Magnifier",
+ description: "Zoom into any part of the page at pixel level. Inspect fine details without zooming and panning the whole preview.",
+ icon: faMagnifyingGlassPlus,
+ link: "/companion/features.html#magnifier"
+ },
+ {
+ name: "Coordinate picker",
+ description: "Read the exact position of any point or element on the page. Verify placement against your layout intent.",
+ icon: faLocationCrosshairs,
+ link: "/companion/features.html#coordinate-picker"
+ },
+ {
+ name: "Size measurement",
+ description: "Measure the width and height of visual elements, and the distance between them. Check spacing and alignment with precision.",
+ icon: faRulerCombined,
+ link: "/companion/features.html#size-measurement"
+ },
+ {
+ name: "Element inspection",
+ description: "Browse the document hierarchy and select any element. Review its configuration, position, size and every page it appears on.",
+ icon: faListTree,
+ link: "/companion/features.html#element-selection"
+ },
+ {
+ name: "Content search",
+ description: "Search the document for a phrase. Matches are highlighted in both the structure tree and the preview.",
+ icon: faMagnifyingGlass,
+ link: "/companion/features.html#content-searching"
+ },
+ {
+ name: "Jump to source code",
+ description: "Ctrl-click any area of the preview to open the code that produced it in your IDE.",
+ icon: faFileCode,
+ link: "/companion/features.html#go-to-implementation"
+ }
+ ]
+ },
+ {
+ name: "Debugging",
+ features: [
+ {
+ name: "Exception analysis",
+ description: "View detailed exception messages, stack traces, and source code context when errors occur during document generation.",
+ icon: faCircleExclamation,
+ link: "/companion/features.html#runtime-exceptions"
+ },
+ {
+ name: "Layout debugging",
+ description: "Diagnose layout problems with color-coded status indicators. Identify root causes with intelligent heuristics.",
+ icon: faBug,
+ link: "/companion/features.html#layout-issue-debugging"
+ },
+ {
+ name: "Debug outlines",
+ description: "Colored border and label overlay showing element boundaries. Visual debugging during development.",
+ icon: faVectorSquare,
+ link: "/api-reference/debug-area.html"
+ },
+ {
+ name: "Debug pointers",
+ description: "Named marker appearing in layout exception messages. Identifies problematic elements in error diagnostics.",
+ icon: faLocationDot,
+ link: "/api-reference/debug-pointer.html"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: "Code organization",
+ categories: [
+ {
+ name: "Code patterns",
+ features: [
+ {
+ name: "Execution order",
+ description: "Fluent API calls apply in the order they are chained. Learn how the order shapes the visual result.",
+ icon: faListOl,
+ link: "/concepts/code-patterns/execution-order.html"
+ },
+ {
+ name: "Document structure",
+ description: "Keep each document in one class and split its sections into well-named private methods. Modular code that stays readable as documents grow.",
+ icon: faSitemap,
+ link: "/concepts/code-patterns/document-structure.html"
+ },
+ {
+ name: "Local helpers",
+ description: "Encapsulate repeated layout logic in C# local functions, right where it is used. Clean code without single-use class members.",
+ icon: faFunction,
+ link: "/concepts/code-patterns/local-helpers.html"
+ },
+ {
+ name: "Reusable styles",
+ description: "Define shared styling once as a function and apply it to many elements. Consistent appearance with less repetition.",
+ icon: faBrush,
+ link: "/concepts/code-patterns/content-styling.html"
+ },
+ {
+ name: "Extension methods",
+ description: "Turn recurring styling and content patterns into extension methods. Reuse them across every document in your codebase.",
+ icon: faBracketsCurly,
+ link: "/concepts/code-patterns/extension-methods.html"
+ }
+ ]
+ },
+ {
+ name: "Components",
+ features: [
+ {
+ name: "Components",
+ description: "Build modular, reusable document blocks for cleaner architecture. Encapsulate layout patterns and maintain consistency across your entire codebase.",
+ icon: faPuzzlePiece,
+ link: "/concepts/code-patterns/components.html"
+ },
+ {
+ name: "Dynamic components",
+ description: "Components rendered separately on every page, with access to the page number, total page count and available space. Build progress bars or alternating layouts.",
+ icon: faCubes,
+ link: "/concepts/code-patterns/dynamic-components.html"
+ },
+ {
+ name: "Capture positions",
+ description: "Record where content lands on each page, then place other elements relative to it. Annotations, callouts and connectors.",
+ icon: faCrosshairs,
+ link: "/concepts/code-patterns/capture-content-position.html"
+ }
+ ]
+ },
+ {
+ name: "Prototyping",
+ features: [
+ {
+ name: "Placeholder text",
+ description: "Generate lorem ipsum, labels, sentences, names, emails, dates, numbers and more. Fill layouts before real data is available.",
+ icon: faWandMagicSparkles,
+ link: "/concepts/prototyping.html#text"
+ },
+ {
+ name: "Placeholder colours",
+ description: "Random colors drawn from the Material Design palette. Tell nested containers apart while prototyping.",
+ icon: faSwatchbook,
+ link: "/concepts/prototyping.html#colors"
+ },
+ {
+ name: "Placeholder images",
+ description: "Soft gradient images generated on demand. Test image placement, sizing and alignment before real assets arrive.",
+ icon: faMountainSun,
+ link: "/concepts/prototyping.html#image"
+ },
+ {
+ name: "Placeholder boxes",
+ description: "Gray rectangle with diagonal lines and optional label. Visualizes reserved space during layout prototyping.",
+ icon: faSquareDashed,
+ link: "/api-reference/placeholder.html"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: "Layout",
+ categories: [
+ {
+ name: "Page setup",
+ features: [
+ {
+ name: "Pages",
+ description: "Document foundation with size, margins, orientation, headers, footers, and watermarks. Continuous or paged content modes.",
+ icon: faFile,
+ link: "/api-reference/page/basics.html"
+ },
+ {
+ name: "Headers and footers",
+ description: "Header, content and footer slots, plus full-page background and foreground layers for watermarks and letterhead.",
+ icon: faDistributeSpacingVertical,
+ link: "/api-reference/page/slots.html"
+ },
+ {
+ name: "Page size and margins",
+ description: "Exact, continuous or flexible page sizes, standard presets with orientation, page color and per-side margins.",
+ icon: faRuler,
+ link: "/api-reference/page/settings.html"
+ }
+ ]
+ },
+ {
+ name: "Tables",
+ features: [
+ {
+ name: "Tables",
+ description: "Grid layout with rows and columns. Cell spanning, repeating headers/footers, auto-pagination, and precise cell styling.",
+ icon: faTable,
+ link: "/api-reference/table/basics.html"
+ },
+ {
+ name: "Table cell styles",
+ description: "A structured pattern for styling cells consistently. Backgrounds, padding and text styles defined once and reused.",
+ icon: faBorderAll,
+ link: "/api-reference/table/cell-style-pattern.html"
+ },
+ {
+ name: "Table headers and footers",
+ description: "Dedicated header and footer rows repeated on every page the table spans. Readers keep the context on long tables.",
+ icon: faTableCellsHeader,
+ link: "/api-reference/table/header-and-footer.html"
+ },
+ {
+ name: "Overlapping table cells",
+ description: "Place cells over one another and span them across rows and columns. Build complex structures with dynamic styling.",
+ icon: faTableCellsMerge,
+ link: "/api-reference/table/overlapping-cells.html"
+ }
+ ]
+ },
+ {
+ name: "Layout containers",
+ features: [
+ {
+ name: "Columns",
+ description: "Vertical stacking of children with configurable spacing. Automatic pagination ensures content flows seamlessly across pages.",
+ icon: faTableRows,
+ link: "/api-reference/column.html"
+ },
+ {
+ name: "Rows",
+ description: "Horizontal arrangement with flexible sizing options. Combine proportional, fixed, and auto-width items with configurable spacing.",
+ icon: faTableColumns,
+ link: "/api-reference/row.html"
+ },
+ {
+ name: "Lists",
+ description: "Ordered (numbered) and unordered (bulleted) lists. Custom markers, nested levels, and automatic numbering.",
+ icon: faList,
+ link: "/api-reference/lists.html"
+ },
+ {
+ name: "Layers",
+ description: "Stacked content planes for backgrounds, main content, and foreground overlays. Watermarks and letterhead effects.",
+ icon: faLayerGroup,
+ link: "/api-reference/layers.html"
+ },
+ {
+ name: "Stacking order",
+ description: "Stacking order for overlapping elements in Layers. Higher values render in front of lower values.",
+ icon: faBringFront,
+ link: "/api-reference/zindex.html"
+ },
+ {
+ name: "Inline wrapping",
+ description: "Inline flow layout with automatic line wrapping. Elements arranged like words in a paragraph.",
+ icon: faBarsStaggered,
+ link: "/api-reference/inlined.html"
+ },
+ {
+ name: "Multi-column text",
+ description: "Newspaper-style columns where content flows from one column to the next, continuing across pages.",
+ icon: faNewspaper,
+ link: "/api-reference/multi-column.html"
+ }
+ ]
+ },
+ {
+ name: "Size and spacing",
+ features: [
+ {
+ name: "Width",
+ description: "Horizontal size constraints: exact, minimum, and maximum values. Relative (percentage) or absolute units.",
+ icon: faLeftRight,
+ link: "/api-reference/width.html"
+ },
+ {
+ name: "Height",
+ description: "Vertical size constraints: exact, minimum, and maximum values. Relative (percentage) or absolute units.",
+ icon: faUpDown,
+ link: "/api-reference/height.html"
+ },
+ {
+ name: "Padding",
+ description: "Add breathing room around content with uniform or per-side spacing. Supports all length units for precise layout control.",
+ icon: faSquareRing,
+ link: "/api-reference/padding.html"
+ },
+ {
+ name: "Alignment",
+ description: "Positions child within available space: center, top, bottom, left, right, and corner combinations.",
+ icon: faObjectsAlignCenterHorizontal,
+ link: "/api-reference/alignment.html"
+ },
+ {
+ name: "Aspect ratio",
+ description: "Maintains width-to-height proportion. Child content scales or clips to fit the constrained area.",
+ icon: faCrop,
+ link: "/api-reference/aspect-ratio.html"
+ },
+ {
+ name: "Fill available space",
+ description: "Expands child to fill all available space horizontally, vertically, or both directions.",
+ icon: faUpRightAndDownLeftFromCenter,
+ link: "/api-reference/extend.html"
+ },
+ {
+ name: "Shrink to content",
+ description: "Reduces child to minimum required size. Prevents stretching when parent offers excess space.",
+ icon: faDownLeftAndUpRightToCenter,
+ link: "/api-reference/shrink.html"
+ },
+ {
+ name: "Unconstrained size",
+ description: "Removes parent size limits, giving children infinite available space. Reveals natural content size.",
+ icon: faInfinity,
+ link: "/api-reference/unconstrained.html"
+ },
+ {
+ name: "Length units",
+ description: "Use points (default), inches, centimeters, millimeters, meters, or feet. Specify units directly in API methods for intuitive sizing.",
+ icon: faRulerVertical,
+ link: "/concepts/length-unit-types.html"
+ }
+ ]
+ },
+ {
+ name: "Transformations",
+ features: [
+ {
+ name: "Rotation",
+ description: "Rotation by 90° increments or arbitrary angles. Content spins around its center point.",
+ icon: faRotate,
+ link: "/api-reference/rotate.html"
+ },
+ {
+ name: "Scaling",
+ description: "Proportional resizing (larger or smaller). Uniform or independent horizontal/vertical factors.",
+ icon: faExpand,
+ link: "/api-reference/scale.html"
+ },
+ {
+ name: "Scale to fit",
+ description: "Automatic scaling to fit available space. Preserves aspect ratio while maximizing size.",
+ icon: faCompress,
+ link: "/api-reference/scale-to-fit.html"
+ },
+ {
+ name: "Flip and mirror",
+ description: "Mirrors content horizontally, vertically, or both axes. Creates reflection effects.",
+ icon: faReflectHorizontal,
+ link: "/api-reference/flip.html"
+ },
+ {
+ name: "Offset",
+ description: "Shifts content position without affecting layout flow. Creates overlapping or offset effects.",
+ icon: faArrowsUpDownLeftRight,
+ link: "/api-reference/offset.html"
+ }
+ ]
+ },
+ {
+ name: "Content flow",
+ features: [
+ {
+ name: "Page breaks",
+ description: "Forces subsequent content to begin on a new page. Immediate page transition.",
+ icon: faPageBreak,
+ link: "/api-reference/page-break.html"
+ },
+ {
+ name: "Avoid page breaks",
+ description: "Keeps child content together on a single page. Moves entire block to next page if needed.",
+ icon: faLinkSimple,
+ link: "/api-reference/prevent-page-break.html"
+ },
+ {
+ name: "Reserve space",
+ description: "Requires minimum vertical space before rendering. Jumps to next page if threshold not met.",
+ icon: faArrowsFromLine,
+ link: "/api-reference/ensure-space.html"
+ },
+ {
+ name: "Keep together",
+ description: "Displays element fully on one page or skips to next page. No partial rendering or breaking.",
+ icon: faObjectGroup,
+ link: "/api-reference/show-entire.html"
+ },
+ {
+ name: "Repeating headers",
+ description: "Persistent header and footer areas that repeat on every page. Main content flows between them with pagination.",
+ icon: faRectangleHistory,
+ link: "/api-reference/decoration.html"
+ },
+ {
+ name: "Repeat on every page",
+ description: "Duplicates content on every page of a multi-page section. Repeating decorations and patterns.",
+ icon: faRepeat,
+ link: "/api-reference/repeat.html"
+ },
+ {
+ name: "Conditional content",
+ description: "Conditional rendering based on boolean logic. Content appears or disappears based on runtime conditions.",
+ icon: faToggleOn,
+ link: "/api-reference/show-if.html"
+ },
+ {
+ name: "Show on first page only",
+ description: "Renders content only on its first page occurrence. Hidden on subsequent pages of the same element.",
+ icon: faCircle1,
+ link: "/api-reference/show-once.html"
+ },
+ {
+ name: "Skip the first page",
+ description: "Hides content on first page, visible on all subsequent pages. Inverse of Show Once.",
+ icon: faForwardStep,
+ link: "/api-reference/skip-once.html"
+ },
+ {
+ name: "Truncate overflow",
+ description: "Prevents content from continuing to additional pages. Overflow content is truncated.",
+ icon: faStop,
+ link: "/api-reference/stop-paging.html"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: "Text",
+ categories: [
+ {
+ name: "Text basics",
+ features: [
+ {
+ name: "Rich text",
+ description: "Rich text rendering with multiple formatting options. Automatic wrapping and seamless pagination across pages.",
+ icon: faText,
+ link: "/api-reference/text/basics.html"
+ },
+ {
+ name: "Page numbers",
+ description: "Current page, total page count, and page numbers relative to a named section. Custom formats such as Roman numerals.",
+ icon: faHashtag,
+ link: "/api-reference/text/page-numbers.html"
+ },
+ {
+ name: "Elements inside text",
+ description: "Inject images, SVG icons or other elements into a line of text. Positioned relative to the baseline or font edges.",
+ icon: faSquareDashedCirclePlus,
+ link: "/api-reference/text/injecting-custom-content.html"
+ },
+ {
+ name: "Default text style",
+ description: "Inherited text formatting (font, size, color) applied to all descendant text elements.",
+ icon: faTextSize,
+ link: "/api-reference/default-text-style.html"
+ },
+ {
+ name: "Style inheritance",
+ description: "Styles cascade from the document down to every text element unless overridden. Consistent typography with less repeated code.",
+ icon: faDiagramSubtask,
+ link: "/api-reference/text/style-inheritance.html"
+ }
+ ]
+ },
+ {
+ name: "Text style",
+ features: [
+ {
+ name: "Font size",
+ description: "Sets how large the characters appear. Any size in points or other length units.",
+ icon: faTextHeight,
+ link: "/api-reference/text/text-style.html#font-size"
+ },
+ {
+ name: "Font family",
+ description: "Choose the typeface for any run of text. Use the bundled Lato font, your own files, or system fonts.",
+ icon: faFont,
+ link: "/api-reference/text/text-style.html#font-family"
+ },
+ {
+ name: "Font colour",
+ description: "Color of the characters and, by default, of their decorations. HEX values, alpha channel and the Material palette.",
+ icon: faDroplet,
+ link: "/api-reference/text/text-style.html#font-color"
+ },
+ {
+ name: "Highlight",
+ description: "Solid background color behind a run of text. Adds contrast and emphasis to key phrases.",
+ icon: faHighlighter,
+ link: "/api-reference/text/text-style.html#background-color"
+ },
+ {
+ name: "Font weight",
+ description: "Thickness of the characters from thin to black. Build visual hierarchy within a single font family.",
+ icon: faBold,
+ link: "/api-reference/text/text-style.html#font-weight"
+ },
+ {
+ name: "Italic",
+ description: "Slanted letterforms for emphasis or to distinguish specific words and phrases.",
+ icon: faItalic,
+ link: "/api-reference/text/text-style.html#italic"
+ },
+ {
+ name: "Underline and strikethrough",
+ description: "Decorative lines under, through or over text. Configurable color, thickness and style, such as wavy or dotted.",
+ icon: faUnderline,
+ link: "/api-reference/text/text-style.html#decorations"
+ },
+ {
+ name: "Subscript",
+ description: "Text slightly below the baseline in a smaller size. Chemical formulas and mathematical notation.",
+ icon: faSubscript,
+ link: "/api-reference/text/text-style.html#subscript"
+ },
+ {
+ name: "Superscript",
+ description: "Text slightly above the baseline in a smaller size. Exponents, footnote marks and ordinal indicators.",
+ icon: faSuperscript,
+ link: "/api-reference/text/text-style.html#superscript"
+ },
+ {
+ name: "Line height",
+ description: "Vertical spacing between lines, proportional to the text size. Tune readability and text density.",
+ icon: faLineHeight,
+ link: "/api-reference/text/text-style.html#line-height"
+ },
+ {
+ name: "Letter spacing",
+ description: "Horizontal spacing between characters, proportional to the text size. Tracking for headings and labels.",
+ icon: faKerning,
+ link: "/api-reference/text/text-style.html#letter-spacing"
+ },
+ {
+ name: "Word spacing",
+ description: "Horizontal spacing between words, proportional to the text size. Loosen or tighten a paragraph's rhythm.",
+ icon: faTextWidth,
+ link: "/api-reference/text/text-style.html#word-spacing"
+ },
+ {
+ name: "Hyphenation",
+ description: "Words break at soft hyphen characters and render a hyphen at the break. Works with any hyphenation library that inserts them.",
+ icon: faHyphen,
+ link: "/api-reference/text/text-style.html#hyphenation"
+ },
+ {
+ name: "OpenType features",
+ description: "Enable typographic features such as ligatures, kerning, small caps or old-style numerals, where the font supports them.",
+ icon: faFontCase,
+ link: "/api-reference/text/text-style.html#font-features"
+ },
+ {
+ name: "Break long words",
+ description: "Allow a line to break between any two characters. Fits long URLs, identifiers and hashes into narrow columns.",
+ icon: faSplit,
+ link: "/api-reference/text/text-style.html#break-anywhere"
+ }
+ ]
+ },
+ {
+ name: "Paragraph style",
+ features: [
+ {
+ name: "Alignment and justification",
+ description: "Left, center, right, start, end and justified text. Position each paragraph within its container.",
+ icon: faAlignJustify,
+ link: "/api-reference/text/paragraph-style.html#text-alignment"
+ },
+ {
+ name: "Paragraph spacing",
+ description: "Vertical gap between successive paragraphs. Separate blocks of text without empty lines.",
+ icon: faParagraph,
+ link: "/api-reference/text/paragraph-style.html#paragraph-spacing"
+ },
+ {
+ name: "First-line indent",
+ description: "Horizontal offset of the first line in each paragraph. The classic way to mark paragraph starts.",
+ icon: faIndent,
+ link: "/api-reference/text/paragraph-style.html#first-line-indentation"
+ },
+ {
+ name: "Line clamping",
+ description: "Limit the number of visible lines and end the text with a customizable ellipsis. Keeps cards and table cells tidy.",
+ icon: faEllipsis,
+ link: "/api-reference/text/paragraph-style.html#clamp-line-with-ellipsis"
+ }
+ ]
+ },
+ {
+ name: "Fonts and languages",
+ features: [
+ {
+ name: "Custom fonts",
+ description: "Font files deployed with your application are registered automatically. Load more from files, streams or embedded resources.",
+ icon: faBookFont,
+ link: "/api-reference/text/font-management.html"
+ },
+ {
+ name: "Font fallback",
+ description: "A list of alternative fonts used when the primary font lacks a glyph. Mixed scripts and emoji render correctly.",
+ icon: faSwap,
+ link: "/api-reference/text/text-style.html#font-fallback"
+ },
+ {
+ name: "Additional languages",
+ description: "Chinese, Japanese, Korean, Arabic, Hebrew, Thai and more by deploying the free Google Noto fonts with your application.",
+ icon: faLanguage,
+ link: "/api-reference/text/font-management.html#covering-additional-languages"
+ },
+ {
+ name: "Right-to-left text",
+ description: "Left-to-right (LTR) or right-to-left (RTL) layout direction. Full RTL language and bidirectional text support.",
+ icon: faArrowRightArrowLeft,
+ link: "/api-reference/content-direction.html"
+ },
+ {
+ name: "Missing glyph detection",
+ description: "A clear exception names the missing font families or glyphs and the fonts involved. Catch incomplete deployments before documents ship.",
+ icon: faSquareQuestion,
+ link: "/api-reference/text/font-management.html#missing-text-glyphs"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: "Visual content",
+ categories: [
+ {
+ name: "Styling",
+ features: [
+ {
+ name: "Backgrounds",
+ description: "Solid color or linear gradient behind any element. Section bands, highlighted cells and card surfaces.",
+ icon: faFillDrip,
+ link: "/api-reference/background.html"
+ },
+ {
+ name: "Borders",
+ description: "Borders on all sides or selected ones. Configurable thickness, solid or gradient color, and alignment to the edge.",
+ icon: faBorderOuter,
+ link: "/api-reference/border.html"
+ },
+ {
+ name: "Rounded corners",
+ description: "Round all corners or each one individually. Softer edges on containers, images and table cells.",
+ icon: faRoundedCorners,
+ link: "/api-reference/rounded-corners.html"
+ },
+ {
+ name: "Shadows",
+ description: "Drop shadows with configurable offset, blur, spread and color. Lift cards and callouts off the page.",
+ icon: faDropShadow,
+ link: "/api-reference/shadow.html"
+ },
+ {
+ name: "Colour palette",
+ description: "Full Google Material Design color palette included. HEX, alpha channel, and shorthand formats supported with convenient color helpers.",
+ icon: faPalette,
+ link: "/concepts/colors.html"
+ }
+ ]
+ },
+ {
+ name: "Images",
+ features: [
+ {
+ name: "Images",
+ description: "Raster images (PNG, JPEG, WebP) from files, streams or byte arrays. Automatic aspect ratio with several scaling modes.",
+ icon: faImage,
+ link: "/api-reference/image/basics.html"
+ },
+ {
+ name: "SVG",
+ description: "Scalable vector graphics placed like any image. Text inside uses the document's fonts, and preloading avoids repeated parsing.",
+ icon: faFileSvg,
+ link: "/api-reference/image/svg.html"
+ },
+ {
+ name: "Image compression",
+ description: "Control JPEG quality and target DPI per image or for the whole document. Balance file size against visual fidelity.",
+ icon: faFileZipper,
+ link: "/api-reference/image/optimization.html"
+ },
+ {
+ name: "Shared images",
+ description: "Load an image once and reuse it everywhere. Faster generation and smaller files when the same image repeats.",
+ icon: faImages,
+ link: "/api-reference/image/shared.html"
+ },
+ {
+ name: "Images sized by layout",
+ description: "Generate the image only once the available space is known. Sharp maps and charts at exactly the right resolution.",
+ icon: faArrowsMaximize,
+ link: "/api-reference/image/dynamic.html"
+ }
+ ]
+ },
+ {
+ name: "Graphics",
+ features: [
+ {
+ name: "Charts",
+ description: "Integration with charting libraries (ScottPlot, LiveCharts, Microcharts). Bar, line, pie, area charts as vector graphics.",
+ icon: faChartMixed,
+ link: "/api-reference/charts.html"
+ },
+ {
+ name: "Barcodes and QR codes",
+ description: "Generate 1D barcodes and 2D QR codes. Integration guide with popular third-party libraries.",
+ icon: faQrcode,
+ link: "/api-reference/barcodes.html"
+ },
+ {
+ name: "Maps",
+ description: "Embed static maps in your documents. Integration tutorial using Mapbox services.",
+ icon: faMapLocationDot,
+ link: "/api-reference/maps.html"
+ },
+ {
+ name: "Vector graphics",
+ description: "Canvas-style drawing API for paths, shapes, bezier curves, and custom vector graphics. Fill and stroke operations.",
+ icon: faBezierCurve,
+ link: "/api-reference/complex-graphics.html"
+ },
+ {
+ name: "SkiaSharp canvas",
+ description: "Direct SkiaSharp canvas access for shaders, gradients, image filters, blend modes, and advanced 2D graphics.",
+ icon: faPaintbrush,
+ link: "/api-reference/skiasharp-integration.html"
+ },
+ {
+ name: "Separator lines",
+ description: "Horizontal or vertical lines with configurable thickness and color. Useful as visual separators and dividers.",
+ icon: faHorizontalRule,
+ link: "/api-reference/line.html"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ name: "Document output",
+ categories: [
+ {
+ name: "Document generation",
+ features: [
+ {
+ name: "Output formats",
+ description: "Generate PDF, SVG, or raster images (PNG, JPEG, WEBP). Save to file, stream, or byte array for flexible integration.",
+ icon: faFileExport,
+ link: "/concepts/generating-output.html"
+ },
+ {
+ name: "Document metadata",
+ description: "Set PDF properties like title, author, subject, keywords, and timestamps. Improve document organization and searchability.",
+ icon: faIdCard,
+ link: "/concepts/document-metadata.html"
+ },
+ {
+ name: "Document settings",
+ description: "Configure PDF/A archival and PDF/UA accessibility compliance. Control compression, image quality, DPI, and content direction.",
+ icon: faSliders,
+ link: "/concepts/document-settings.html"
+ },
+ {
+ name: "Accessibility",
+ description: "Generate inclusive PDFs with PDF/UA compliance and tagged content. Enable screen reader support with proper semantic structure for all users.",
+ icon: faUniversalAccess,
+ link: "/concepts/accessibility.html"
+ },
+ {
+ name: "Hyperlinks",
+ description: "Create interactive clickable areas linking to URLs, email addresses, or phone numbers. Seamless integration with PDF reader capabilities.",
+ icon: faLink,
+ link: "/api-reference/hyperlink.html"
+ },
+ {
+ name: "Named sections",
+ description: "Define named regions for navigation and location-aware page numbering. Build table of contents and cross-references with ease.",
+ icon: faBookmark,
+ link: "/api-reference/section.html"
+ },
+ {
+ name: "Lazy generation",
+ description: "Optimize memory for large documents with deferred content generation. Ideal for data-intensive reports and enterprise-scale document processing.",
+ icon: faFeather,
+ link: "/api-reference/lazy.html"
+ }
+ ]
+ },
+ {
+ name: "Document operations",
+ features: [
+ {
+ name: "Merge documents",
+ description: "Combine multiple PDF files into a single document. Merge entire documents or select specific pages from each source with full control.",
+ icon: faMerge,
+ link: "/concepts/document-operations.html#merging-documents"
+ },
+ {
+ name: "Extract pages",
+ description: "Select specific pages using flexible range syntax. Extract, reorder, reverse, or filter pages with powerful selection patterns.",
+ icon: faScissors,
+ link: "/concepts/document-operations.html#page-selection"
+ },
+ {
+ name: "Overlays and watermarks",
+ description: "Apply watermarks, headers, footers, or letterhead by layering PDF content. Configure which pages receive overlays with precise control.",
+ icon: faStamp,
+ link: "/concepts/document-operations.html#overlays-and-underlays"
+ },
+ {
+ name: "Encryption",
+ description: "Secure PDFs with 40-bit, 128-bit, or 256-bit encryption. Set user/owner passwords and control permissions for printing, copying, and editing.",
+ icon: faLock,
+ link: "/concepts/document-operations.html#document-encryption"
+ },
+ {
+ name: "Decryption",
+ description: "Remove encryption and security restrictions from password-protected PDFs. Access content for further processing or distribution.",
+ icon: faUnlock,
+ link: "/concepts/document-operations.html#document-decryption"
+ },
+ {
+ name: "Linearization",
+ description: "Optimize PDFs for web delivery. Linearized files allow viewers to display content before the entire file downloads, improving user experience.",
+ icon: faGlobe,
+ link: "/concepts/document-operations.html#document-linearization"
+ },
+ {
+ name: "Attachments",
+ description: "Embed files directly within PDFs with full PDF/A compliance. Support for all embedded file relationships.",
+ icon: faPaperclip,
+ link: "/concepts/document-operations.html#file-attachments"
+ },
+ {
+ name: "XMP metadata",
+ description: "Add custom XMP metadata to PDF/A documents. Essential for industry standards like ZUGFeRD invoices and other compliance requirements.",
+ icon: faTags,
+ link: "/concepts/document-operations.html#xmp-metadata-extension"
+ }
+ ]
+ }
+ ]
+ }
+];
diff --git a/docs/.vitepress/theme/homepage/HomePage.vue b/docs/.vitepress/theme/homepage/HomePage.vue
index d36ef16c..d4df6d5b 100644
--- a/docs/.vitepress/theme/homepage/HomePage.vue
+++ b/docs/.vitepress/theme/homepage/HomePage.vue
@@ -1,117 +1,123 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
+
-
-
-
+
-
-
-
+/* flip once the video tutorial and the sample gallery are live; the footer's "Sample gallery" link goes with it */
+const plannedCardsReady = false;
-
diff --git a/docs/.vitepress/theme/homepage/HomePageAdoption.vue b/docs/.vitepress/theme/homepage/HomePageAdoption.vue
deleted file mode 100644
index 0a8cb1a9..00000000
--- a/docs/.vitepress/theme/homepage/HomePageAdoption.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-
-
-
-
-
-
-
diff --git a/docs/.vitepress/theme/homepage/HomePageCodeAnimation.vue b/docs/.vitepress/theme/homepage/HomePageCodeAnimation.vue
deleted file mode 100644
index c1030c38..00000000
--- a/docs/.vitepress/theme/homepage/HomePageCodeAnimation.vue
+++ /dev/null
@@ -1,350 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/HomePageCodeContainer.vue b/docs/.vitepress/theme/homepage/HomePageCodeContainer.vue
deleted file mode 100644
index 21a576cf..00000000
--- a/docs/.vitepress/theme/homepage/HomePageCodeContainer.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/HomePageCodeFocusedApproach.vue b/docs/.vitepress/theme/homepage/HomePageCodeFocusedApproach.vue
deleted file mode 100644
index f8956a06..00000000
--- a/docs/.vitepress/theme/homepage/HomePageCodeFocusedApproach.vue
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
-
-
-
-
-
- {{ feature.title }}
- {{ feature.description }}
-
-
-
-
-
-
-
-
-
diff --git a/docs/.vitepress/theme/homepage/HomePageCompanion.vue b/docs/.vitepress/theme/homepage/HomePageCompanion.vue
index 9c8589df..697aabe6 100644
--- a/docs/.vitepress/theme/homepage/HomePageCompanion.vue
+++ b/docs/.vitepress/theme/homepage/HomePageCompanion.vue
@@ -1,106 +1,303 @@
-
-
+
+
+
Preview, inspect and debug
+
Add one line of code and your document opens beside your IDE. With hot-reload, every change appears in the preview at once: no recompiling, no reopening PDFs.
-
-
![Companion App]()
-
![Companion App]()
-
+
+ -
+
+ {{ point.text }}
+
+
+
+
+
Get started
+
+
+
-
-
-
- {{ summary }}
-
+
+
+
-
Read more
-
+
+
-
\ No newline at end of file
+
diff --git a/docs/.vitepress/theme/homepage/HomePageCtaBuildVisual.vue b/docs/.vitepress/theme/homepage/HomePageCtaBuildVisual.vue
new file mode 100644
index 00000000..c98b8b15
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/HomePageCtaBuildVisual.vue
@@ -0,0 +1,267 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/homepage/HomePageCtaCard.vue b/docs/.vitepress/theme/homepage/HomePageCtaCard.vue
new file mode 100644
index 00000000..769e6fc4
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/HomePageCtaCard.vue
@@ -0,0 +1,298 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ title }}
+
{{ description }}
+
+
+ {{ label }}
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/homepage/HomePageCtaDeckVisual.vue b/docs/.vitepress/theme/homepage/HomePageCtaDeckVisual.vue
new file mode 100644
index 00000000..2dff7890
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/HomePageCtaDeckVisual.vue
@@ -0,0 +1,479 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/homepage/HomePageCtaFeaturesVisual.vue b/docs/.vitepress/theme/homepage/HomePageCtaFeaturesVisual.vue
new file mode 100644
index 00000000..9303ff28
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/HomePageCtaFeaturesVisual.vue
@@ -0,0 +1,316 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/homepage/HomePageCtaTutorialVisual.vue b/docs/.vitepress/theme/homepage/HomePageCtaTutorialVisual.vue
new file mode 100644
index 00000000..4921dbd2
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/HomePageCtaTutorialVisual.vue
@@ -0,0 +1,210 @@
+
+
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/homepage/HomePageFeatures.vue b/docs/.vitepress/theme/homepage/HomePageFeatures.vue
deleted file mode 100644
index bdc5fe78..00000000
--- a/docs/.vitepress/theme/homepage/HomePageFeatures.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/HomePageGettingStarted.vue b/docs/.vitepress/theme/homepage/HomePageGettingStarted.vue
deleted file mode 100644
index 7aa30eba..00000000
--- a/docs/.vitepress/theme/homepage/HomePageGettingStarted.vue
+++ /dev/null
@@ -1,215 +0,0 @@
-
-
-
-
-
-
-
-
-

-
-
-

-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/HomePageHero.vue b/docs/.vitepress/theme/homepage/HomePageHero.vue
index dfe89b46..2cc16b4b 100644
--- a/docs/.vitepress/theme/homepage/HomePageHero.vue
+++ b/docs/.vitepress/theme/homepage/HomePageHero.vue
@@ -1,30 +1,47 @@
-
-
-
-
-
+
+
- Code-first PDF generation for C#
+ Code-first PDF library for C#
-
- QuestPDF is a production-ready library that lets you design documents the way you design software with clean, maintainable C# code.
-
-
-
-
-
-
+
+
@@ -33,175 +50,164 @@ import HomePageCodeContainer from "./HomePageCodeContainer.vue";
/* Layout */
.content {
- position: relative;
-
- display: grid;
- grid-template-columns: 1fr 400px;
- grid-template-rows: auto;
- grid-gap: 64px;
+ display: flex;
+ flex-direction: column;
+ gap: 48px;
+ padding-block: 64px;
+}
- margin-top: -64px;
- padding-top: 128px;
+.content h1 {
+ margin: 0;
+ font-size: 3.5rem;
+ line-height: 1.05;
+ letter-spacing: -0.02em;
+ word-spacing: 4px;
+ text-wrap: balance;
}
-@media screen and (max-width: 1023px) {
- .content {
- grid-template-columns: 1fr auto;
+@media screen and (max-width: 820px) {
+ .content h1 {
+ font-size: 2.75rem;
}
}
-@media screen and (max-width: 600px) {
- .content {
- grid-template-columns: 1fr;
- grid-template-rows: auto auto;
- }
+h1 .highlight-foreground {
+ display: inline-block; /* so the gradient box ends at the word, not the full line */
+ padding-right: 0.05em; /* keeps the gradient clip from shaving the last letter's edge */
+ font-weight: 800;
+
+ background-image: linear-gradient(100deg, #00c6ff 0%, #0094f9 55%, #2979ff 100%);
+ background-clip: text;
+ color: transparent;
+ -webkit-text-fill-color: transparent;
}
-@media screen and (max-width: 400px) {
- h1 br {
- display: none;
- }
+html.dark h1 .highlight-foreground {
+ background-image: linear-gradient(100deg, #4ED4FF 0%, #2FAEFA 50%, #5C8DFF 100%);
}
-/* Content styles */
+/* Lists */
-h1 {
- font-size: 3.5rem !important;
- line-height: 4rem !important;
- font-weight: 700 !important;
- max-width: 500px;
- word-spacing: 4px;
+.lists {
+ isolation: isolate; /* so the dot grid's z-index puts it under the lists, not under the page canvas */
+ display: grid;
+ grid-template-columns: minmax(0, max-content) minmax(64px, 1fr) auto;
+ align-items: start;
}
-h1 .highlight-foreground {
- color: #298dff;
- font-weight: 750;
+.benefits,
+.statistics {
+ display: flex;
+ flex-direction: column;
+ gap: 1.75rem;
}
-html.dark .highlight-foreground {
- color: #32c6ff;
+.benefits li {
+ display: flex;
+ align-items: flex-start;
+ gap: 1.25rem;
+ color: var(--vp-c-text-1);
}
-.tagline {
- max-width: 600px;
- font-size: 1.5rem !important;
- line-height: 2.25rem !important;
- font-weight: 400;
- word-spacing: 2px;
- color: #000B;
+/* regular text looks thinner on white, so light mode takes half a weight step up */
+html:not(.dark) .benefits li {
+ font-weight: 450;
+ color: #142630;
}
-html.dark .tagline {
- color: #FFFB;
+.benefits li,
+.statistic {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+ text-wrap: pretty;
}
-@media screen and (max-width: 1024px) {
- h1 {
- font-size: 3rem !important;
- line-height: 3.5rem !important;
- max-width: 100%;
- }
-
- .tagline {
- font-size: 1.25rem !important;
- line-height: 1.5rem !important;
- }
+.statistic {
+ display: block;
}
-@media screen and (max-width: 821px) {
- h1 {
- font-size: 2.5rem !important;
- line-height: 3rem !important;
- }
-
- .tagline {
- font-size: 1.25rem !important;
- line-height: 1.5rem !important;
- }
+.list-icon {
+ display: inline-flex;
+ flex: 0 0 auto;
+ align-items: center;
+ justify-content: center;
+ width: 2.5rem;
+ height: 2.5rem;
+ margin-block: -0.375rem;
+ border-radius: 10px;
+ background-color: light-dark(#FFFFFF, color-mix(in srgb, var(--vp-c-brand-1) 12%, transparent));
+ box-shadow: inset 0 0 0 1.5px light-dark(rgba(0, 98, 196, 0.32), transparent);
}
-
-/* Call To Action Button */
-
-.cta-buttons {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- gap: 24px;
- margin-top: 32px;
- z-index: 10000;
- text-transform: uppercase;
-
- .action {
- word-spacing: 2px;
- letter-spacing: 0.5px;
- }
+.list-icon .icon {
+ font-size: 1.25rem;
+ color: light-dark(#0062C4, var(--vp-c-brand-1));
}
-@media screen and (min-width: 1024px) {
- .cta-buttons a.action {
- padding: 16px 24px;
- }
+/* each stat is a link whose parts set their own colours, so the global link hover never reaches them */
+.statistic .value {
+ margin-right: 0.5rem;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+ color: light-dark(#1C1C1E, var(--vp-c-text-1));
}
-html.dark .action:not(.primary) {
- background-color: #FFF2;
-
- &:hover {
- background-color: #FFF3;
- }
+.statistic .label {
+ font-weight: 400;
+ color: var(--vp-c-text-2);
}
-html:not(.dark) .action:not(.primary) {
- background-color: #00000014;
+html:not(.dark) .statistic .label {
+ font-weight: 450;
color: var(--vp-c-text-1);
- box-shadow: none;
+}
- &:hover {
- background-color: #00000022;
+@media screen and (max-width: 1023px) {
+ .lists {
+ grid-template-columns: 1fr;
+ gap: 48px;
}
-}
-.action {
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ .hero-pattern,
+ .statistics {
+ grid-area: 2 / 1;
+ }
- &:hover {
- box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+ .statistics {
+ display: grid;
+ grid-template-columns: repeat(2, max-content);
+ gap: 1.75rem 96px;
}
}
+/* the two columns and the 96px gap need about 650px with the gutters */
+@media screen and (max-width: 720px) {
+ .statistics {
+ grid-template-columns: 1fr;
+ }
+}
+/* Dot grid */
-/* Background */
-
-.background-gradient {
- position: absolute;
- inset: 0;
+.hero-pattern {
+ z-index: -1;
+ align-self: stretch;
+ margin-block: -64px;
pointer-events: none;
- background-image:
- radial-gradient(ellipse 100% 75% at 50% 0%, color-mix(in srgb, #2196F3 15%, transparent), transparent),
- radial-gradient(ellipse 1500px 600px at 100% 0%, color-mix(in srgb, #81D4FA 50%, transparent), transparent),
- radial-gradient(ellipse 1000px 200px at 20% 0%, color-mix(in srgb, #64B5F6 10%, transparent), transparent),
- radial-gradient(ellipse 800px 100px at 60% 100%, color-mix(in srgb, #FF9800 10%, transparent), transparent);
-}
+ background-image: radial-gradient(circle, color-mix(in srgb, color-mix(in srgb, #1E88E5 85%, var(--vp-c-text-1)) 36%, transparent) 1px, transparent 1.5px);
+ background-size: 24px 24px;
+ background-position: 12px 12px;
-.background-gradient-noise {
- position: absolute;
- inset: 0;
- background-image: url("/homepage/noise.svg");
- opacity: 0.25;
- pointer-events: none;
- mix-blend-mode: overlay;
+ mask-image:
+ linear-gradient(to right, transparent, #000 25%, #000 75%, transparent),
+ linear-gradient(to bottom, transparent, #000 25%, #000 75%, transparent);
+ mask-composite: intersect;
}
-html.dark .background-gradient {
- background-image:
- radial-gradient(ellipse 75% 75% at 50% 100%, color-mix(in srgb, #2196F3 20%, transparent), transparent),
- radial-gradient(ellipse 1500px 300px at 50% 100%, color-mix(in srgb, #00BCD4 5%, transparent), transparent),
- radial-gradient(ellipse 1000px 200px at 40% 100%, color-mix(in srgb, #FFEB3B 10%, transparent), transparent),
- radial-gradient(ellipse 500px 100px at 65% 100%, color-mix(in srgb, #FF9800 15%, transparent), transparent);
+html.dark .hero-pattern {
+ background-image: radial-gradient(circle, color-mix(in srgb, color-mix(in srgb, #64B5F6 50%, var(--vp-c-text-1)) 13%, transparent) 1px, transparent 1.5px);
}
-
\ No newline at end of file
+
diff --git a/docs/.vitepress/theme/homepage/HomePageHighlightedCode.vue b/docs/.vitepress/theme/homepage/HomePageHighlightedCode.vue
deleted file mode 100644
index c270bb35..00000000
--- a/docs/.vitepress/theme/homepage/HomePageHighlightedCode.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
diff --git a/docs/.vitepress/theme/homepage/HomePageLicense.vue b/docs/.vitepress/theme/homepage/HomePageLicense.vue
index 47539590..ec34e2a1 100644
--- a/docs/.vitepress/theme/homepage/HomePageLicense.vue
+++ b/docs/.vitepress/theme/homepage/HomePageLicense.vue
@@ -1,129 +1,281 @@
-w
-
+
+
+
Fair and Sustainable License
-
+
+ -
+
+ {{ item.text }}
+
+
-
-
-
-
-
-
- Free for individuals, non-profits, all FOSS projects, and organizations under $1M in annual revenue.
-
+
-
\ No newline at end of file
+
diff --git a/docs/.vitepress/theme/homepage/HomePageMultiplatform.vue b/docs/.vitepress/theme/homepage/HomePageMultiplatform.vue
deleted file mode 100644
index bf7159c6..00000000
--- a/docs/.vitepress/theme/homepage/HomePageMultiplatform.vue
+++ /dev/null
@@ -1,115 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/HomePageOverview.vue b/docs/.vitepress/theme/homepage/HomePageOverview.vue
deleted file mode 100644
index 1905022b..00000000
--- a/docs/.vitepress/theme/homepage/HomePageOverview.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-
-
-
-
-
-
-
-
- {{ feature.title }}
- {{ feature.description }}
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/HomePageQuickStart.vue b/docs/.vitepress/theme/homepage/HomePageQuickStart.vue
new file mode 100644
index 00000000..5025ef63
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/HomePageQuickStart.vue
@@ -0,0 +1,905 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ round.prompt }}
+ {{ promptText }}
+
+
+
{{ promptIndex + 1 }} / {{ session.rounds.length }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+ {{ card.title }}
+
+
+ -
+
+ {{ note.text }}
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/homepage/HomePageStandardCompliance.vue b/docs/.vitepress/theme/homepage/HomePageStandardCompliance.vue
deleted file mode 100644
index b31b8c5c..00000000
--- a/docs/.vitepress/theme/homepage/HomePageStandardCompliance.vue
+++ /dev/null
@@ -1,142 +0,0 @@
-
-
-
-
- Compliance
-
-
-
Industry-standard PDF compliance
-
-
-
-
-
-
-
-
-
{{ standard.description }}
-
-
- {{ badge }}
-
-
-
-
-
-
-
diff --git a/docs/.vitepress/theme/homepage/HomePageTrust.vue b/docs/.vitepress/theme/homepage/HomePageTrust.vue
new file mode 100644
index 00000000..84a7f0a8
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/HomePageTrust.vue
@@ -0,0 +1,311 @@
+
+
+
+
+
+
Industry-standard compliant output
+
+ Every build is automatically validated with the independent, trusted and open-source
+ veraPDF and
+ Mustang validators.
+
+
+
+
+ {{ category.title }}
+
+ {{ level }}
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/theme/homepage/HomePageWindowContainer.vue b/docs/.vitepress/theme/homepage/HomePageWindowContainer.vue
deleted file mode 100644
index e0372b69..00000000
--- a/docs/.vitepress/theme/homepage/HomePageWindowContainer.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/agentSession.ts b/docs/.vitepress/theme/homepage/agentSession.ts
new file mode 100644
index 00000000..0c4b50d7
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/agentSession.ts
@@ -0,0 +1,390 @@
+import {computed, onMounted, onUnmounted, ref, type Ref} from "vue";
+import type {Edit, QuickStartSession, Round} from "./sessionBuilder";
+import {foldLines} from "./codeFolding";
+
+/* A simulated session with a coding agent that plays by itself while the window is mostly on screen. Every pause
+ stops while the window is not mostly on screen or the tab is hidden, and goes on from where it was; under
+ prefers-reduced-motion the finished session is shown still. */
+
+export interface Line {
+ text: string;
+ kind: "normal" | "removed" | "added";
+ /* how many characters of a line being typed are in (unset: all of them) */
+ typed?: number;
+ /* the line's height is opening or closing (only while nothing else re-renders the code) */
+ state?: "opening" | "closing";
+}
+
+export interface OpenFile {
+ name: string;
+ lines: Line[];
+}
+
+type Phase = "idle" | "working" | "editing" | "built" | "done";
+
+const tick = 16;
+
+/* the session's timing, in ms: slow enough to read each request, follow the diff and look at the page */
+const timing = {
+ /* from the window coming into view to the first request typing in */
+ start: 800,
+ /* the composer empties and is clicked into (HomePageQuickStart.vue) before the next request types in */
+ beforeTyping: 750,
+ /* the typed request stays in the composer this long before it sends itself */
+ beforeSend: 1400,
+ /* from the send to the page zooming in, or the agent's first edit when it stays */
+ working: 1000,
+ /* the page's flight (1-1.3s, HomePageQuickStart.vue) ends before the agent edits, so the change happens in close-up */
+ zoom: 1350,
+ /* a file the agent opens comes to the front before it is edited */
+ switchFile: 600,
+ /* the lines an edit replaces turn red before the new ones arrive */
+ markRemoved: 800,
+ /* the room for the new lines opens */
+ openLines: 320,
+ /* typing one line, at most, and one edit, at most */
+ perLine: 170,
+ perEdit: 2600,
+ /* the finished diff, a beat before the page changes */
+ diffHold: 500,
+ /* the diff stays, with the page it made, before it is taken in */
+ resultHold: 2500,
+ /* the replaced lines close as the diff is taken in */
+ closeLines: 320,
+ /* the code, taken in, stays before the next request types in (the page stays zoomed while it does) */
+ acceptedHold: 2500,
+ /* after the last request, the code and the page's overview stay before the session fades out and starts over */
+ endHold: 7000,
+ fade: 450
+};
+
+/* how long the next key takes: a person's uneven rhythm */
+function keyDelay(typed: string) {
+ const last = typed.at(-1);
+ const delay = 26 + Math.random() * 24;
+
+ if (last === " ")
+ return delay + 40 + Math.random() * 60;
+
+ if (last === "," || last === ".")
+ return delay + 140;
+
+ return Math.random() < 0.05 ? delay + 100 + Math.random() * 120 : delay;
+}
+
+/* thrown out of a pause once the component is gone, which ends the loop */
+const unmounted = Symbol("unmounted");
+
+export function useAgentSession(root: Ref, session: QuickStartSession) {
+ const { rounds } = session;
+
+ const files = ref([]);
+ const active = ref("");
+ /* the round the code shows */
+ const phase = ref("idle");
+ const roundIndex = ref(0);
+ /* the round whose prompt is in the composer: the next one types in while the previous diff is still shown */
+ const promptIndex = ref(0);
+ const promptTyped = ref(0);
+ /* how many rounds the page shows the result of */
+ const applied = ref(0);
+ /* the part of the page the viewer is zoomed in on, from a moment after a request is sent until another request
+ zooms elsewhere (none: the page's overview) */
+ const zoom = ref();
+ /* the method the code shows unfolded (none folds until the first request that names one) */
+ const unfold = ref();
+ /* the next request is arriving in the composer: from it emptying until it is sent */
+ const asking = ref(false);
+ /* a wait the composer shows as a ring filling around the send button (the key restarts it) */
+ const countdown = ref<{ key: number, ms: number }>();
+ /* how many lines the code is scrolled by: a file longer than the window (session.lines) scrolls as an editor
+ does, to the method a request opens and to each line as it types in, only as far as it must */
+ const scrolled = ref(0);
+ /* the window is not mostly on screen, so the session and the composer's motion stand still */
+ const paused = ref(true);
+ /* the session fades out before it starts over */
+ const resetting = ref(false);
+
+ const round = computed(() => rounds[roundIndex.value]);
+ const prompt = computed(() => rounds[promptIndex.value].prompt);
+ const promptText = computed(() => prompt.value.slice(0, promptTyped.value));
+
+ const sent = computed(() => phase.value !== "idle" && promptIndex.value === roundIndex.value);
+ const typing = computed(() => !sent.value && promptTyped.value > 0 && promptTyped.value < prompt.value.length);
+ /* from the send until the page shows the result: the agent edits and QuestPDF renders */
+ const working = computed(() => phase.value === "working" || phase.value === "editing" || phase.value === "built");
+ const finished = computed(() => phase.value === "done" && sent.value);
+
+ /* the lines of the file in front, with every method but the one the request works on folded */
+ const lines = computed(() => foldLines(files.value.find(file => file.name === active.value)?.lines ?? [], unfold.value));
+
+
+ /* Edits on the files */
+
+ function resetSession() {
+ files.value = session.files.map(({ name, lines }) => ({ name, lines: lines.map(text => ({ text, kind: "normal" as const })) }));
+ active.value = session.files[0].name;
+ roundIndex.value = 0;
+ promptIndex.value = 0;
+ promptTyped.value = 0;
+ applied.value = 0;
+ zoom.value = undefined;
+ unfold.value = undefined;
+ scrolled.value = 0;
+ asking.value = false;
+ countdown.value = undefined;
+ phase.value = "idle";
+ }
+
+ function acceptDiff() {
+ for (const file of files.value)
+ file.lines = file.lines.filter(line => line.kind !== "removed").map(line => ({ text: line.text, kind: "normal" as const }));
+
+ scrolled.value = Math.min(scrolled.value, Math.max(0, lines.value.length - session.lines));
+ }
+
+ /* scrolls the shown line at `index` into the window, only as far as it must, so the code around it stays put */
+ function scrollTo(index: number) {
+ if (index >= 0)
+ scrolled.value = Math.min(Math.max(scrolled.value, index - session.lines + 1), index);
+ }
+
+ /* the file's reactive proxy, added if the edit creates it */
+ function open(name: string) {
+ if (!files.value.some(file => file.name === name))
+ files.value.push({ name, lines: [] });
+
+ return files.value.find(file => file.name === name)!;
+ }
+
+ /* marks the replaced lines and returns where the new lines go */
+ function prepare(file: OpenFile, { after, remove = 0 }: Edit) {
+ if (after === undefined)
+ return 0;
+
+ const at = file.lines.findIndex(line => line.text === after && line.kind === "normal");
+
+ for (let index = at; index < at + remove; index++)
+ file.lines[index].kind = "removed";
+
+ return at + Math.max(remove, 1);
+ }
+
+ function showFinishedSession() {
+ resetSession();
+
+ for (const r of rounds) {
+ for (const edit of r.edits) {
+ const file = open(edit.file);
+ const at = prepare(file, edit);
+ file.lines.splice(at, 0, ...edit.add.map(text => ({ text, kind: "added" as const })));
+ active.value = edit.file;
+ }
+
+ acceptDiff();
+ unfold.value = r.unfold ?? unfold.value;
+ }
+
+ roundIndex.value = rounds.length - 1;
+ promptIndex.value = roundIndex.value;
+ promptTyped.value = prompt.value.length;
+ applied.value = rounds.length;
+ phase.value = "done";
+ }
+
+
+ /* Animation engine: one async loop, paused while the window is not mostly on screen, stopped on unmount */
+
+ let running = false;
+ let inView = false;
+ let alive = true;
+ let observer: IntersectionObserver | undefined;
+ let countdowns = 0;
+
+ function sleep(ms: number) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ /* the session starts the first time the window is mostly on screen in a shown tab */
+ function updatePaused() {
+ paused.value = !inView || document.hidden;
+
+ if (!paused.value && !running)
+ void loop();
+ }
+
+ /* waits ms of time with the window in view: a pause cut by scrolling away goes on where it stopped */
+ async function pause(ms: number) {
+ let left = ms;
+
+ while (left > 0) {
+ if (paused.value) {
+ await sleep(100);
+ }
+ else {
+ const slice = Math.min(left, 100);
+ await sleep(slice);
+ left -= slice;
+ }
+
+ if (!alive)
+ throw unmounted;
+ }
+ }
+
+ /* a pause the composer shows filling up */
+ async function wait(ms: number) {
+ countdown.value = { key: ++countdowns, ms };
+ await pause(ms);
+ countdown.value = undefined;
+ }
+
+ async function askNext(index: number) {
+ promptIndex.value = index;
+ promptTyped.value = 0;
+ asking.value = true;
+ await pause(timing.beforeTyping);
+
+ while (promptTyped.value < prompt.value.length) {
+ promptTyped.value++;
+ await pause(keyDelay(promptText.value));
+ }
+
+ await wait(timing.beforeSend);
+ asking.value = false;
+ }
+
+ async function takeInDiff() {
+ const removed = files.value.flatMap(file => file.lines.filter(line => line.kind === "removed"));
+
+ if (removed.length) {
+ removed.forEach(line => line.state = "closing");
+ await pause(timing.closeLines);
+ }
+
+ acceptDiff();
+ }
+
+ async function playEdit(edit: Edit) {
+ const file = open(edit.file);
+
+ if (active.value !== edit.file) {
+ active.value = edit.file;
+ await pause(timing.switchFile);
+ }
+
+ const at = prepare(file, edit);
+
+ if (edit.remove)
+ await pause(timing.markRemoved);
+
+ const kind = edit.after === undefined ? "normal" : "added";
+ file.lines.splice(at, 0, ...edit.add.map(text => ({ text, kind, typed: 0, state: "opening" as const })));
+ await pause(timing.openLines);
+
+ for (let index = at; index < at + edit.add.length; index++)
+ file.lines[index].state = undefined;
+
+ /* the new lines the window shows (one inside a folded method takes no time), as reactive proxies, so each step
+ re-renders the window */
+ const shown = new Set(lines.value.map(({ line }) => line));
+ const added = file.lines.slice(at, at + edit.add.length).filter(line => shown.has(line));
+ const lineTicks = Math.max(1, Math.round(Math.min(timing.perLine, timing.perEdit / added.length) / tick));
+
+ for (const line of added) {
+ scrollTo(lines.value.findIndex(shown => shown.line === line));
+
+ if (!line.text)
+ await pause(tick * lineTicks);
+
+ while (line.typed! < line.text.length) {
+ line.typed = Math.min(line.text.length, line.typed! + Math.ceil(line.text.length / lineTicks));
+ await pause(tick);
+ }
+ }
+ }
+
+ async function playRound(index: number) {
+ roundIndex.value = index;
+ phase.value = "working";
+ unfold.value = round.value.unfold ?? unfold.value;
+ scrollTo(lines.value.findIndex(({ line }) => line.text === unfold.value));
+ await pause(timing.working);
+
+ /* compared by value: two requests may change the same part of the page */
+ if (String(round.value.zoom) !== String(zoom.value)) {
+ zoom.value = round.value.zoom;
+ await pause(timing.zoom);
+ }
+
+ phase.value = "editing";
+ for (const edit of round.value.edits)
+ await playEdit(edit);
+
+ phase.value = "built";
+ await pause(timing.diffHold);
+ phase.value = "done";
+ applied.value = index + 1;
+ }
+
+ async function loop() {
+ running = true;
+
+ try {
+ await pause(timing.start);
+
+ while (alive) {
+ for (let index = 0; index < rounds.length; index++) {
+ await askNext(index);
+ await playRound(index);
+ await pause(timing.resultHold);
+ await takeInDiff();
+
+ if (index === rounds.length - 1) {
+ zoom.value = undefined;
+ await wait(timing.endHold);
+ }
+ else
+ await wait(timing.acceptedHold);
+ }
+
+ resetting.value = true;
+ await pause(timing.fade);
+ resetSession();
+ resetting.value = false;
+ await pause(timing.fade);
+ }
+ }
+ catch (error) {
+ if (error !== unmounted)
+ throw error;
+ }
+ }
+
+ resetSession();
+
+ onMounted(() => {
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
+ showFinishedSession();
+ return;
+ }
+
+ /* mostly on screen: 60% of the window, or of the screen when the window is taller than it */
+ observer = new IntersectionObserver(([entry]) => {
+ const screen = entry.rootBounds?.height ?? window.innerHeight;
+ inView = entry.intersectionRect.height >= 0.6 * Math.min(entry.boundingClientRect.height, screen);
+ updatePaused();
+ }, { threshold: Array.from({ length: 21 }, (_, index) => index / 20) });
+
+ observer.observe(root.value!);
+ document.addEventListener("visibilitychange", updatePaused);
+ });
+
+ onUnmounted(() => {
+ alive = false;
+ observer?.disconnect();
+ document.removeEventListener("visibilitychange", updatePaused);
+ });
+
+ return { files, active, lines, promptIndex, promptText, typing, sent, working, finished, applied, zoom, scrolled, asking, countdown, paused, resetting, unfold };
+}
diff --git a/docs/.vitepress/theme/homepage/codeExamples/codeFirstApproach.cs b/docs/.vitepress/theme/homepage/codeExamples/codeFirstApproach.cs
deleted file mode 100644
index a08d9caa..00000000
--- a/docs/.vitepress/theme/homepage/codeExamples/codeFirstApproach.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-container.Column(column =>
-{
- column.Item().Text("Order Items").Bold();
-
- if (Model.ShowSummary)
- column.Item().Element(ComposeOrderSummary);
-
- foreach (var item in Model.Items)
- column.Item().Element(c => ComposeItem(c, item));
-});
-
-void ComposeItem(IContainer container, OrderItem item)
-{
- container
- .Border(1, Colors.Grey.Darken2)
- .Background(item.HighlightColor)
-- .Padding(12)
-+ .Padding(16)
- .Row(row =>
- {
- row.RelativeItem().Text(item.Name);
- row.AutoItem().Text($"{item.Price:F2} USD");
- });
-}
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/codeExamples/familiarProgrammingConcepts.cs b/docs/.vitepress/theme/homepage/codeExamples/familiarProgrammingConcepts.cs
deleted file mode 100644
index c7994ffb..00000000
--- a/docs/.vitepress/theme/homepage/codeExamples/familiarProgrammingConcepts.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-container
- .Column(column =>
- {
- column.Item().Text("Order Items").Bold();
-
- // use conditionals to show content based on logic
- if (Model.ShowSummary)
- column.Item().Element(ComposeOrderSummary);
-
- // use loops to iterate over collections naturally
- foreach (var item in Model.Items)
- {
- // organize your code by splitting logic into functions
- column.Item()
- .Element(container => ComposeItem(container, item));
- }
- });
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/codeExamples/helloWorld.cs b/docs/.vitepress/theme/homepage/codeExamples/helloWorld.cs
deleted file mode 100644
index c900c752..00000000
--- a/docs/.vitepress/theme/homepage/codeExamples/helloWorld.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-Document
- .Create(document =>
- {
- document.Page(page =>
- {
- page.Size(PageSizes.A4);
- page.Margin(32);
-
- page.Content()
- .Text("Hello PDF!")
- .FontSize(24);
- });
- })
- .GeneratePdfAndShow();
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/codeExamples/invoice.html b/docs/.vitepress/theme/homepage/codeExamples/invoice.html
new file mode 100644
index 00000000..f80ff282
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/codeExamples/invoice.html
@@ -0,0 +1,26 @@
+
+
+ Invoice {{number}}
+ Issued {{issued}}
+ Bill to
{{customer}}
+
+
+ {{#each items}}
+
+ | {{name}} |
+ {{quantity}} × {{money price}} |
+ {{money amount}} |
+
+ {{/each}}
+
+
+ | Subtotal | {{money subtotal}} |
+ | VAT | {{money vat}} |
+
+Total {{money total}}
diff --git a/docs/.vitepress/theme/homepage/codeFolding.ts b/docs/.vitepress/theme/homepage/codeFolding.ts
new file mode 100644
index 00000000..520dcb32
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/codeFolding.ts
@@ -0,0 +1,25 @@
+/* Folds every method but the one a request works on, as an IDE does. Shared by agentSession.ts and
+ sessionBuilder.ts. */
+
+export interface FoldableLine {
+ text: string;
+ kind: "normal" | "removed" | "added";
+}
+
+/* the lines the window shows: a folded method is its signature line alone, marked folded */
+export function foldLines(lines: T[], unfold: string | undefined) {
+ const shown: { line: T; folded: boolean }[] = [];
+ let folding = false;
+
+ for (const line of lines) {
+ if (folding) {
+ folding = line.text !== "}";
+ continue;
+ }
+
+ folding = !!unfold && line.kind === "normal" && line.text.startsWith("void ") && line.text !== unfold;
+ shown.push({ line, folded: folding });
+ }
+
+ return shown;
+}
diff --git a/docs/.vitepress/theme/homepage/createCodeHighlighter.ts b/docs/.vitepress/theme/homepage/createCodeHighlighter.ts
deleted file mode 100644
index be45d100..00000000
--- a/docs/.vitepress/theme/homepage/createCodeHighlighter.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { createHighlighterCore } from 'shiki';
-import { createOnigurumaEngine } from 'shiki/engine/oniguruma'
-import {HighlighterCore} from "@shikijs/types";
-
-let codeHighlighterInstanceCache: Promise | null = null;
-
-export default function createCodeHighlighter() {
- if (codeHighlighterInstanceCache)
- return codeHighlighterInstanceCache;
-
- codeHighlighterInstanceCache = createHighlighterCore({
- themes: [
- import('shiki/themes/dark-plus.mjs'),
- import('shiki/themes/light-plus.mjs')
- ],
- langs: [
- import('shiki/langs/csharp.mjs'),
- ],
- engine: createOnigurumaEngine(import('shiki/wasm'))
- });
-
- return codeHighlighterInstanceCache;
-}
\ No newline at end of file
diff --git a/docs/.vitepress/theme/homepage/ctaTimeline.ts b/docs/.vitepress/theme/homepage/ctaTimeline.ts
new file mode 100644
index 00000000..38240e24
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/ctaTimeline.ts
@@ -0,0 +1,33 @@
+/* Shared by the CTA visuals. A pass is a sequence of Web Animations on the document timeline, waits included, so it
+ advances only while the page renders: nothing runs ahead in a hidden tab, and one clock drives it all. */
+
+export const reducedMotion = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+
+export class Timeline {
+ private readonly running = new Set();
+
+ animate(target: Element, keyframes: Keyframe[] | null, options: KeyframeAnimationOptions): Animation {
+ const animation = target.animate(keyframes, options);
+ this.running.add(animation);
+ animation.finished.then(() => this.running.delete(animation), () => this.running.delete(animation));
+ return animation;
+ }
+
+ /* an empty animation: finishes `ms` of timeline time later */
+ wait(target: Element, ms: number): Animation {
+ return this.animate(target, null, { duration: ms });
+ }
+
+ /* true once every animation has finished, false if any was cancelled */
+ static settled(animations: Animation[]): Promise {
+ return Promise.all(animations.map(animation => animation.finished)).then(() => true, () => false);
+ }
+
+ /* for unmount */
+ cancel() {
+ for (const animation of this.running)
+ animation.cancel();
+
+ this.running.clear();
+ }
+}
diff --git a/docs/.vitepress/theme/homepage/ctaVisual.css b/docs/.vitepress/theme/homepage/ctaVisual.css
new file mode 100644
index 00000000..6b7b4123
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/ctaVisual.css
@@ -0,0 +1,86 @@
+/* Shared by HomePageCtaBuildVisual.vue and HomePageCtaDeckVisual.vue. The model has a 148 x 200 coordinate system
+ for its shapes; the pose variables place it. */
+
+.scene {
+ --tilt-x: 8deg;
+ --scale: 0.74;
+ --shift: 0px;
+ --rise: 0px;
+ --ink: #A7AEBB;
+
+ position: relative;
+ width: 100%;
+ height: 100%;
+ min-height: 196px;
+}
+
+.stage {
+ position: absolute;
+ inset: 0;
+ perspective: 640px;
+ perspective-origin: 50% 45%;
+}
+
+.model {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 148px;
+ height: 200px;
+ margin: -100px 0 0 -74px;
+ transform-style: preserve-3d;
+ transform: translate(var(--shift), var(--rise)) scale(var(--scale)) rotateX(var(--tilt-x)) rotateY(var(--tilt-y)) rotateZ(var(--tilt-z));
+ transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1);
+}
+
+.paper {
+ background-color: light-dark(#FFF, #F4F4F6);
+ box-shadow: 0 0 0 1px light-dark(rgba(0, 0, 0, 0.10), rgba(255, 255, 255, 0.06));
+}
+
+.page {
+ position: absolute;
+ inset: 0;
+ border-radius: 3px;
+ background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0) 50%, rgba(0, 0, 0, var(--page-shade, 0.025)));
+}
+
+html.dark .page {
+ background-image: none;
+}
+
+.shadow {
+ position: absolute;
+ inset: 8px -6px -10px 10px;
+ border-radius: 8px;
+ background: light-dark(rgba(20, 24, 40, 0.16), rgba(0, 0, 0, 0.45));
+ filter: blur(10px);
+ transform: translateZ(-1px);
+}
+
+/* Placeholders */
+.shape {
+ position: absolute;
+ display: block;
+ box-sizing: border-box;
+ border-radius: 2px;
+ background-color: currentColor;
+ opacity: 0.7;
+}
+
+.shape.strong {
+ opacity: 1;
+}
+
+.shape.soft {
+ opacity: 0.5;
+}
+
+
+/* Compact */
+
+@media screen and (max-width: 700px) {
+ .scene {
+ min-height: 180px;
+ }
+}
diff --git a/docs/.vitepress/theme/homepage/quickStart.data.ts b/docs/.vitepress/theme/homepage/quickStart.data.ts
new file mode 100644
index 00000000..ffc9e9d3
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/quickStart.data.ts
@@ -0,0 +1,203 @@
+import { defineLoader } from "vitepress";
+import fs from "node:fs";
+import { buildSession, type QuickStartSession, type Round } from "./sessionBuilder.ts";
+
+/* The homepage quick start's coding session, built by sessionBuilder.ts. The viewer shows
+ public/homepage/quick-start-.webp (n requests applied; 0 is the page's chrome alone): render them again, and
+ measure the rounds' zoom again, whenever a request changes. */
+
+declare const data: QuickStartSession;
+export { data };
+
+const rounds: Round[] = [
+ {
+ prompt: "Migrate our legacy HTML-to-PDF invoice to QuestPDF.",
+ unfold: "void ComposePage(PageDescriptor page)",
+ edits: [
+ {
+ file: "Invoice.cs",
+ add: [
+ "void ComposePage(PageDescriptor page)",
+ "{",
+ " page.Size(PageSizes.A4);",
+ " page.Margin(2, Unit.Centimetre);",
+ " page.DefaultTextStyle(x => x.FontSize(10));",
+ "",
+ " page.Header().Element(ComposeHeader);",
+ "",
+ " page.Content().PaddingVertical(24).Column(column =>",
+ " {",
+ " foreach (var item in Model.Items)",
+ " column.Item().Element(c => ComposeItem(c, item));",
+ "",
+ " column.Item().PaddingTop(12).Element(ComposeTotals);",
+ " });",
+ "}",
+ "",
+ "void ComposeHeader(IContainer container)",
+ "{",
+ " container.Row(row =>",
+ " {",
+ " row.RelativeItem().Column(column =>",
+ " {",
+ " column.Item()",
+ " .Text($\"Invoice {Model.Number}\")",
+ " .FontSize(20)",
+ " .Bold();",
+ "",
+ " column.Item().Text($\"Issued {Model.Issued:d MMM yyyy}\");",
+ " });",
+ "",
+ " row.AutoItem().Text($\"Bill to\\n{Model.Customer}\");",
+ " });",
+ "}",
+ "",
+ "void ComposeItem(IContainer container, InvoiceItem item)",
+ "{",
+ " container.BorderBottom(0.5f).PaddingVertical(6).Row(row =>",
+ " {",
+ " row.RelativeItem().Text(item.Name);",
+ "",
+ " row.ConstantItem(100)",
+ " .AlignRight()",
+ " .Text($\"{item.Quantity} × {item.Price:C}\");",
+ "",
+ " row.ConstantItem(80).AlignRight().Text($\"{item.Amount:C}\");",
+ " });",
+ "}",
+ "",
+ "void ComposeTotals(IContainer container)",
+ "{",
+ " container.AlignRight().Width(200).Column(column =>",
+ " {",
+ " Line(\"Subtotal\", Model.Subtotal);",
+ " Line(\"VAT\", Model.Vat);",
+ "",
+ " column.Item()",
+ " .PaddingTop(8)",
+ " .AlignRight()",
+ " .Text($\"Total {Model.Total:C}\")",
+ " .FontSize(14)",
+ " .Bold();",
+ "",
+ " void Line(string label, decimal amount) =>",
+ " column.Item().Row(row =>",
+ " {",
+ " row.RelativeItem().Text(label);",
+ " row.AutoItem().Text($\"{amount:C}\");",
+ " });",
+ " });",
+ "}"
+ ]
+ }
+ ]
+ },
+ {
+ prompt: "Add our logo and brand color.",
+ unfold: "void ComposeHeader(IContainer container)",
+ zoom: [36, 40, 300, 122],
+ edits: [
+ {
+ file: "Invoice.cs",
+ after: " {",
+ add: [
+ " column.Item()",
+ " .PaddingBottom(16)",
+ " .Height(32)",
+ " .Svg(\"logo.svg\")",
+ " .FitHeight();",
+ ""
+ ]
+ },
+ {
+ file: "Invoice.cs",
+ after: " .Bold();",
+ remove: 1,
+ add: [
+ " .Bold()",
+ " .FontColor(Colors.Teal.Darken3);"
+ ]
+ }
+ ]
+ },
+ {
+ prompt: "Show item details under each name.",
+ unfold: "void ComposeItem(IContainer container, InvoiceItem item)",
+ zoom: [40, 165, 260, 230],
+ edits: [
+ {
+ file: "Invoice.cs",
+ after: " row.RelativeItem().Text(item.Name);",
+ remove: 1,
+ add: [
+ " row.RelativeItem().Column(column =>",
+ " {",
+ " column.Item().Text(item.Name).SemiBold();",
+ "",
+ " column.Item()",
+ " .Text(item.Details)",
+ " .FontSize(8)",
+ " .FontColor(Colors.Grey.Darken1);",
+ " });"
+ ]
+ }
+ ]
+ },
+ {
+ prompt: "Break down VAT by rate.",
+ unfold: "void ComposeTotals(IContainer container)",
+ zoom: [300, 396, 260, 148],
+ edits: [
+ {
+ file: "Invoice.cs",
+ after: " Line(\"VAT\", Model.Vat);",
+ remove: 1,
+ add: [
+ " foreach (var vat in Model.Items.GroupBy(x => x.VatRate))",
+ " Line($\"VAT {vat.Key:P0}\", vat.Sum(x => x.Vat));"
+ ]
+ }
+ ]
+ },
+ {
+ prompt: "Add a Pay now button linking to checkout.",
+ unfold: "void ComposePage(PageDescriptor page)",
+ zoom: [300, 396, 260, 148],
+ edits: [
+ {
+ file: "Invoice.cs",
+ after: " column.Item().PaddingTop(12).Element(ComposeTotals);",
+ add: [
+ "",
+ " column.Item()",
+ " .PaddingTop(16)",
+ " .AlignRight()",
+ " .Width(200)",
+ " .Hyperlink(Model.PaymentLink)",
+ " .Background(Colors.Teal.Darken3)",
+ " .CornerRadius(4)",
+ " .PaddingVertical(8)",
+ " .AlignCenter()",
+ " .Text(\"Pay now\")",
+ " .FontColor(Colors.White)",
+ " .Bold();"
+ ]
+ }
+ ]
+ }
+];
+
+export default defineLoader({
+ watch: ["./codeExamples/invoice.html"],
+
+ load: () => buildSession({
+ title: "Invoice.pdf",
+ renders: "/homepage/quick-start-",
+ page: { width: 595, height: 842 },
+ overview: { width: 595, height: 340 },
+ files: [
+ { name: "invoice.html", lines: fs.readFileSync(new URL("./codeExamples/invoice.html", import.meta.url), "utf-8").trimEnd().split("\n") }
+ ],
+ rounds
+ })
+});
diff --git a/docs/.vitepress/theme/homepage/sessionBuilder.ts b/docs/.vitepress/theme/homepage/sessionBuilder.ts
new file mode 100644
index 00000000..9c18e9cd
--- /dev/null
+++ b/docs/.vitepress/theme/homepage/sessionBuilder.ts
@@ -0,0 +1,156 @@
+import { codeToHtml, stringifyTokenStyle, type ThemedToken } from "shiki";
+import { type FoldableLine, foldLines } from "./codeFolding.ts";
+
+/* Turns the quick start session (quickStart.data.ts) into what the window needs, highlighting every line it will
+ show at build time, so no highlighter ships to the browser. */
+
+/* Shiki's languages: the legacy HTML template stays plain text, so the C# it becomes is what stands out */
+export type Lang = "csharp" | "text";
+
+export interface Edit {
+ file: string;
+ /* the line the edit starts at, matched by text; omitted for a new file */
+ after?: string;
+ /* how many lines from `after` on are replaced (marked red, kept above the new lines); 0 inserts below it */
+ remove?: number;
+ add: string[];
+}
+
+export interface Round {
+ prompt: string;
+ edits: Edit[];
+ /* the method the request works on: the window shows it and folds every other one */
+ unfold?: string;
+ /* the part of the page the viewer flies to, as [x, y, width, height] in the page box's pixels, its sides in the
+ gaps between columns so the viewer's edges cut no words (none: the page's overview) */
+ zoom?: [number, number, number, number];
+}
+
+export interface SessionDefinition {
+ /* the viewer window's title, the document's file name */
+ title: string;
+ /* the URL the renders are served at, less the round's number and ".webp" (round n: `${renders}${n}.webp`) */
+ renders: string;
+ /* the page's box in the viewer's units, which zoom is measured in: 595 wide, as tall as the page's proportions
+ make it */
+ page: { width: number; height: number };
+ /* the part of the page, from its top-left corner, that the viewer shows at rest: the head and the first content */
+ overview: { width: number; height: number };
+ /* the files open as the session starts */
+ files: { name: string; lines: string[] }[];
+ rounds: Round[];
+}
+
+export interface Token {
+ content: string;
+ /* both themes' colours, as --shiki-light and --shiki-dark (VitePress's code styles pick one) */
+ style: string;
+}
+
+export interface QuickStartSession extends SessionDefinition {
+ /* per language, the tokens of every line that appears, by its text */
+ tokens: Record>;
+ /* per file, its language (a data loader exports nothing but its data, so the client cannot call langOf) */
+ langs: Record;
+ /* the most lines the window shows at once, which it keeps room for */
+ lines: number;
+ /* how many digits the window's line numbers take: those of the longest file */
+ digits: number;
+}
+
+/* the most lines the window shows at once: 26 keep it, title bar included, within a laptop screen; a longer file
+ scrolls to the lines being edited */
+const maxLines = 26;
+
+const langOf = (file: string): Lang => file.endsWith(".html") ? "text" : "csharp";
+
+/* a file after an edit: the diff (the replaced lines still in place above the new ones) and the accepted lines */
+function applyEdit(lines: string[], { after, remove = 0, add }: Edit) {
+ if (after === undefined) {
+ const added = add.map(text => ({ text, kind: "normal" as const }));
+ return { diff: [...added, ...lines.map(text => ({ text, kind: "normal" as const }))], accepted: [...add, ...lines] };
+ }
+
+ const at = lines.indexOf(after);
+
+ if (at < 0)
+ throw new Error(`[session] no line "${after.trim()}" in the code for a request to edit`);
+
+ const end = at + Math.max(remove, 1);
+ const kept = (text: string, index: number): FoldableLine => ({ text, kind: index >= at && index < at + remove ? "removed" : "normal" });
+
+ const diff = [
+ ...lines.slice(0, end).map(kept),
+ ...add.map(text => ({ text, kind: "added" as const })),
+ ...lines.slice(end).map((text, index) => kept(text, end + index))
+ ];
+
+ return { diff, accepted: [...lines.slice(0, remove ? at : end), ...add, ...lines.slice(end)] };
+}
+
+export async function buildSession(definition: SessionDefinition): Promise {
+ const { files, rounds } = definition;
+
+ /* every state a file passes through: as the session starts, then per edit the diff and the accepted lines the
+ next edit starts from; and the most lines the window shows of any of them, with the method each request
+ works on unfolded */
+ const accepted: Record = Object.fromEntries(files.map(file => [file.name, file.lines]));
+ const states = files.map(({ name, lines }) => ({ file: name, lines, diff: false }));
+ const asNormal = (lines: string[]) => lines.map(text => ({ text, kind: "normal" as const }));
+ let unfold: string | undefined;
+ let lines = Math.max(0, ...files.map(file => file.lines.length));
+
+ for (const round of rounds) {
+ unfold = round.unfold ?? unfold;
+
+ for (const file of Object.values(accepted))
+ lines = Math.max(lines, foldLines(asNormal(file), unfold).length);
+
+ for (const edit of round.edits) {
+ const { diff, accepted: next } = applyEdit(accepted[edit.file] ?? [], edit);
+ accepted[edit.file] = next;
+ states.push({ file: edit.file, lines: diff.map(line => line.text), diff: true }, { file: edit.file, lines: next, diff: false });
+ lines = Math.max(lines, foldLines(diff, unfold).length, foldLines(asNormal(next), unfold).length);
+ }
+ }
+
+ const tokens: QuickStartSession["tokens"] = { csharp: {}, text: {} };
+
+ /* a diff holds the replaced lines above their replacement, which is not valid code (two lambdas opened where
+ one closes), so a line keeps the tokens of a file as accepted and a diff only highlights the lines it alone has */
+ for (const { file, lines, diff } of [...states.filter(state => !state.diff), ...states.filter(state => state.diff)]) {
+ const lang = langOf(file);
+
+ for (const [index, line] of (await highlight(lines, lang)).entries()) {
+ const text = lines[index];
+ const lineTokens = line.map(token => ({ content: token.content, style: stringifyTokenStyle(token.htmlStyle ?? {}) }));
+
+ if (diff && text in tokens[lang])
+ continue;
+
+ if (text in tokens[lang] && JSON.stringify(tokens[lang][text]) !== JSON.stringify(lineTokens))
+ throw new Error(`[session] "${text.trim()}" is highlighted differently in two places`);
+
+ tokens[lang][text] = lineTokens;
+ }
+ }
+
+ const langs = Object.fromEntries(states.map(({ file }) => [file, langOf(file)]));
+ const digits = String(Math.max(...states.map(state => state.lines.length))).length;
+
+ return { ...definition, tokens, langs, lines: Math.min(lines, maxLines), digits };
+}
+
+/* the tokens exactly as codeToHtml renders them (whitespace merged into the token that follows it) */
+async function highlight(lines: string[], lang: Lang): Promise {
+ let result: ThemedToken[][] = [];
+
+ await codeToHtml(lines.join("\n"), {
+ lang,
+ themes: { light: "light-plus", dark: "dark-plus" },
+ defaultColor: false,
+ transformers: [{ tokens: tokens => { result = tokens; } }]
+ });
+
+ return result;
+}
diff --git a/docs/.vitepress/theme/license/LicenseIndex.vue b/docs/.vitepress/theme/license/LicenseIndex.vue
index 3e85c84b..d436b11a 100644
--- a/docs/.vitepress/theme/license/LicenseIndex.vue
+++ b/docs/.vitepress/theme/license/LicenseIndex.vue
@@ -1,4 +1,6 @@
-
-
@@ -61,43 +61,35 @@ const documents = [
.content {
display: flex;
flex-direction: column;
- gap: 24px;
+ gap: 32px;
align-items: center;
- justify-self: center;
-
- width: 100%;
}
-.card {
- background-color: var(--vp-c-bg) !important;
- padding: 0;
-
+.documents {
display: flex;
flex-direction: column;
- align-items: stretch;
-
+ width: 100%;
+ max-width: 640px;
+ padding: 0;
overflow: hidden;
}
.document-link {
display: flex;
- flex-direction: row;
align-items: center;
gap: 32px;
- padding: 16px 24px;
- text-decoration: none;
- color: inherit;
+ padding: 18px 24px 18px 32px;
transition: background-color 0.2s ease;
+}
- .link-title {
- color: var(--vp-c-text-1);
- font-weight: 500;
- }
+.document-link .link-title {
+ color: var(--vp-c-text-1);
+ font-weight: 500;
+}
- &:not(:last-child) {
- border-bottom: 1px solid var(--vp-c-divider);
- }
+.document-link:not(:last-child) {
+ border-bottom: 1px solid var(--vp-c-divider);
}
.document-link .arrow {
@@ -109,11 +101,21 @@ const documents = [
}
.document-link:hover {
- background-color: #2196F311;
+ background-color: color-mix(in srgb, #2196F3 5%, transparent);
+}
+
+.document-link:hover .link-title {
+ color: #2196F3;
+}
+
+.document-link:hover .arrow {
+ color: #2196F3;
+ transform: translateX(4px);
+}
- .arrow {
- color: #2196F3;
- transform: translateX(4px);
+@media screen and (max-width: 700px) {
+ .document-link {
+ padding: 18px 20px;
}
}
-
\ No newline at end of file
+
diff --git a/docs/.vitepress/theme/license/LicensePurchaseSuccess.vue b/docs/.vitepress/theme/license/LicensePurchaseSuccess.vue
index a6bdf111..a6981a51 100644
--- a/docs/.vitepress/theme/license/LicensePurchaseSuccess.vue
+++ b/docs/.vitepress/theme/license/LicensePurchaseSuccess.vue
@@ -1,6 +1,8 @@
-
-
-
-
-
-
-
-
-
- Please check your inbox. We have sent a confirmation email with your license details, license confirmation PDF, and developer setup instructions.
-
+
+
+
-
-
-
-
- {{ step.title }}
-
-
-
-
-
-
Forward to Development Team
-
-
-
- Billing and renewal
+
+
+ {{ card.title }}
-
-
+
+
+
+
-
{{ step.title }}
-
+
{{ item.title }}
+
-
Open Customer Portal
+
{{ card.action.label }}
-
- Do you need assistance?
+
-
- If you have any questions about your license, setup, or billing, please reach out. We typically respond within one business day.
-
+
+
+
Do you need assistance?
+
+ If you have any questions about your license, setup, or billing, please reach out. We typically respond within one business day.
+
+
- Contact Support
+ Contact Support
@@ -124,123 +121,146 @@ const orderDetails = [
\ No newline at end of file
+
diff --git a/docs/.vitepress/theme/license/documents/DocumentTemplate.vue b/docs/.vitepress/theme/license/documents/DocumentTemplate.vue
index 72563e21..ed0110a0 100644
--- a/docs/.vitepress/theme/license/documents/DocumentTemplate.vue
+++ b/docs/.vitepress/theme/license/documents/DocumentTemplate.vue
@@ -1,73 +1,165 @@
-