{{title}}
{{caption}}
{{verdict}}
+ {{#if summary}}{{summary}}
{{/if}} +{{aside}}
+diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9f51ac2f..45bb7af6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
# CHANGELOG.md
## v0.47.0 (unreleased)
+- Added a responsive, keyboard-accessible comparison of SQLPage and five alternative approaches to the official homepage.
- The official homepage's database sculpture now scrolls natively with its section after the opening flight, and section layouts reserve its space with CSS grid.
- For sites that store their pages in a database, checking for page updates no longer makes other pages wait when the database is busy. In a test, another page loaded in under 1 ms instead of about 250 ms.
- Redesigned the official website homepage with live SQLPage examples, an interactive database sculpture, flowing section transitions, responsive layouts, and direct links to documentation and deployment guides.
diff --git a/examples/official-site/assets/landing/js/comparison.js b/examples/official-site/assets/landing/js/comparison.js
new file mode 100644
index 00000000..507037d8
--- /dev/null
+++ b/examples/official-site/assets/landing/js/comparison.js
@@ -0,0 +1,69 @@
+/** Native scrolling remains available without enhancement, including on touch screens. */
+export function initComparison(root, signal) {
+ const section = root.querySelector(".comparison-section");
+ const track = section.querySelector(".comparison-track");
+ const cards = [...track.children];
+ const previous = section.querySelector("[data-comparison-previous]");
+ const next = section.querySelector("[data-comparison-next]");
+ const status = section.querySelector("[data-comparison-status]");
+ const motion = matchMedia("(prefers-reduced-motion: reduce)");
+
+ function update() {
+ const offsets = cards.map((card) => card.offsetLeft);
+ const first = offsets.findIndex(
+ (left, index) => left + cards[index].offsetWidth > track.scrollLeft + 2,
+ );
+ const last = offsets.findLastIndex(
+ (left) => left < track.scrollLeft + track.clientWidth - 2,
+ );
+ previous.disabled = track.scrollLeft <= 2;
+ next.disabled =
+ track.scrollLeft >= track.scrollWidth - track.clientWidth - 2;
+ const range =
+ first === last ? String(first + 1) : `${first + 1}–${last + 1}`;
+ const label = `${range} of ${cards.length} alternatives`;
+ // Avoid repeating live announcements during every frame of a smooth scroll.
+ if (status.textContent !== label) status.textContent = label;
+ }
+
+ function goTo(left) {
+ track.scrollTo({
+ left,
+ behavior: motion.matches ? "instant" : "smooth",
+ });
+ }
+
+ function move(direction) {
+ // Equal-width grid columns snap one card at a time; native scrolling clamps the ends.
+ const step = cards[1].offsetLeft;
+ const index =
+ direction > 0
+ ? Math.floor((track.scrollLeft + 2) / step) + 1
+ : Math.ceil((track.scrollLeft - 2) / step) - 1;
+ goTo(index * step);
+ }
+
+ previous.addEventListener("click", () => move(-1), { signal });
+ next.addEventListener("click", () => move(1), { signal });
+ track.addEventListener("scroll", update, { passive: true, signal });
+ const keys = {
+ ArrowLeft: () => move(-1),
+ ArrowRight: () => move(1),
+ Home: () => goTo(0),
+ End: () => goTo(track.scrollWidth),
+ };
+ track.addEventListener(
+ "keydown",
+ (event) => {
+ if (event.target !== track || !keys[event.key]) return;
+ event.preventDefault();
+ keys[event.key]();
+ },
+ { signal },
+ );
+ const observer = new ResizeObserver(update);
+ observer.observe(track);
+ section.querySelector(".comparison-controls").hidden = false;
+ update();
+ return () => observer.disconnect();
+}
diff --git a/examples/official-site/assets/landing/js/sections.js b/examples/official-site/assets/landing/js/sections.js
index d2585926..5bc24862 100644
--- a/examples/official-site/assets/landing/js/sections.js
+++ b/examples/official-site/assets/landing/js/sections.js
@@ -1,4 +1,6 @@
/** Wire section controls; motion and live-preview loading live in separate modules. */
+
+import { initComparison } from "./comparison.js";
import { initComponentDemos } from "./component-demos.js";
import { initTabs } from "./tabs.js";
@@ -6,6 +8,7 @@ export function initSections(root) {
const events = new AbortController();
const { signal } = events;
const cleanupDemos = initComponentDemos(root, signal);
+ const cleanupComparison = initComparison(root, signal);
initTabs(
root.querySelector('[aria-label="Application stack"]'),
"aria-pressed",
@@ -36,5 +39,6 @@ export function initSections(root) {
return () => {
events.abort();
cleanupDemos();
+ cleanupComparison();
};
}
diff --git a/examples/official-site/assets/landing/styles/comparison.css b/examples/official-site/assets/landing/styles/comparison.css
new file mode 100644
index 00000000..8853a8ec
--- /dev/null
+++ b/examples/official-site/assets/landing/styles/comparison.css
@@ -0,0 +1,569 @@
+/* Shared row primitive for the static illustrations and section controls. */
+.comparison-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+/* Day 100: a fixed SQLPage example beside a natively scrollable set of alternatives. */
+.comparison-section {
+ --section-color: var(--brand-paper);
+ --comparison-border: #dbe2e7;
+ --comparison-copy: #49545f;
+ --comparison-muted: #626f7c;
+ background: var(--section-color);
+ color: #0b1020;
+ color-scheme: light;
+ padding-bottom: calc(var(--wave-height) + 64px);
+}
+.comparison-section .eyebrow {
+ color: #146575;
+}
+.comparison-section h2 em {
+ color: #0e8292;
+}
+.comparison-section .section-heading {
+ grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.95fr) minmax(0, 0.27fr);
+ margin-bottom: 44px;
+}
+.comparison-section h2 {
+ font-size: clamp(64px, 7.2vw, 102px);
+}
+.comparison-intro {
+ align-self: end;
+ margin: 0 0 8px;
+ font-size: clamp(19px, 1.7vw, 24px);
+ line-height: 1.4;
+ color: var(--comparison-copy);
+}
+.comparison-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1.3fr) 36px minmax(0, 2fr);
+ gap: 18px;
+ padding-top: 34px;
+ border-top: 1px solid #cbd4da;
+}
+.comparison-card {
+ min-width: 0;
+}
+.comparison-card-heading {
+ gap: 14px;
+ min-height: 62px;
+ margin-bottom: 14px;
+}
+.comparison-card-heading > svg {
+ width: 26px;
+ height: 26px;
+ flex-shrink: 0;
+}
+.comparison-card-heading img {
+ object-fit: contain;
+ flex-shrink: 0;
+}
+.comparison-card-heading h3 {
+ margin: 0;
+ font-size: clamp(21px, 2vw, 28px);
+ line-height: 1.15;
+ font-weight: 700;
+ letter-spacing: -0.025em;
+}
+.comparison-card-heading p {
+ margin: 5px 0 0;
+ color: var(--comparison-muted);
+ font: 11px / 1.5 var(--mono);
+}
+.comparison-versus {
+ flex-direction: column;
+ gap: 20px;
+ margin: 76px 0 115px;
+ color: #8a96a2;
+ font: 500 26px var(--brand-font);
+}
+.comparison-versus::before,
+.comparison-versus::after {
+ content: "";
+ width: 1px;
+ flex: 1;
+ background: #cbd4da;
+}
+.comparison-alternatives {
+ min-width: 0;
+}
+.comparison-track {
+ position: relative;
+ display: grid;
+ grid-auto-flow: column;
+ grid-auto-columns: calc((100% - 24px) / 2);
+ gap: 24px;
+ overflow-x: auto;
+ overscroll-behavior-x: contain;
+ scroll-snap-type: x mandatory;
+ scrollbar-width: thin;
+ scrollbar-color: #9caeb5 transparent;
+ padding-bottom: 15px;
+}
+.comparison-track > .comparison-card {
+ scroll-snap-align: start;
+}
+.comparison-track:focus-visible {
+ outline-offset: 4px;
+}
+.comparison-window {
+ height: 326px;
+ border: 1px solid var(--comparison-border);
+ border-radius: 12px;
+ background: #fff;
+ color: #242b32;
+ overflow: hidden;
+ box-shadow: 0 12px 24px #18262d0a;
+}
+.comparison-window svg {
+ width: 15px;
+ height: 15px;
+ flex-shrink: 0;
+ vertical-align: middle;
+}
+.comparison-code {
+ border-color: #202739;
+ background: #080d19;
+ color: #eef1f7;
+}
+.comparison-window-bar {
+ height: 36px;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 0 14px;
+ background: #f6f8fa;
+ border-bottom: 1px solid var(--comparison-border);
+ font: 11px / 1.4 var(--mono);
+}
+.comparison-window-bar > span:last-child:not(:first-child) {
+ opacity: 0.55;
+}
+.comparison-code .comparison-window-bar {
+ background: #171d29;
+ border-color: #303746;
+}
+.comparison-window-body {
+ display: flex;
+ flex-direction: column;
+ height: calc(100% - 36px);
+ padding: 18px 16px 14px;
+}
+.comparison-window h4 {
+ margin: 0 0 12px;
+ font-size: 23px;
+ line-height: 1.2;
+ letter-spacing: -0.02em;
+}
+.comparison-diff {
+ flex-wrap: wrap;
+ margin: 0 0 12px;
+ color: #97a8c3;
+ font: 11px / 1.4 var(--mono);
+}
+.comparison-added {
+ color: #5cde93;
+}
+.comparison-removed {
+ color: #ff797e;
+}
+.comparison-diff i {
+ width: 56px;
+ height: 8px;
+ background:
+ repeating-linear-gradient(90deg, transparent 0 7px, #080d19 7px 9px),
+ linear-gradient(90deg, #48bb70 82%, #f15b62 82%);
+}
+.comparison-files {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ font: 11px / 1.5 var(--mono);
+}
+.comparison-files li {
+ align-items: normal;
+ justify-content: space-between;
+ padding: 7px 0;
+ border-top: 1px solid #ffffff1c;
+}
+.comparison-files li > span:first-child {
+ gap: 7px;
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+.comparison-files li > span:last-child {
+ white-space: nowrap;
+}
+.comparison-files svg {
+ opacity: 0.55;
+}
+.comparison-files b {
+ font-weight: 400;
+}
+.comparison-deploy {
+ gap: 10px;
+ margin: auto 0 0;
+ padding-top: 14px;
+ border-top: 1px solid #ffffff1c;
+ font: 12px / 1.4 var(--mono);
+}
+.comparison-deploy svg {
+ color: #29d6df;
+ width: 18px;
+ height: 18px;
+}
+.comparison-deploy span {
+ color: #97a8c3;
+}
+.comparison-more {
+ margin: 5px 0;
+ font: 11px / 1.5 var(--mono);
+ color: #97a8b8;
+}
+.comparison-checks {
+ margin-top: auto;
+ font: 11px / 1.5 var(--mono);
+}
+.comparison-checks p {
+ margin: 4px 0 0;
+ color: #5cde93;
+}
+.comparison-checks p:first-child {
+ color: #f5c457;
+}
+.comparison-card .comparison-verdict {
+ margin: 18px 0 8px;
+ font-size: 21px;
+ line-height: 1.2;
+ font-weight: 700;
+ letter-spacing: -0.025em;
+}
+.comparison-sqlpage .comparison-verdict {
+ font-size: clamp(23px, 2.2vw, 29px);
+}
+.comparison-description {
+ margin: 0;
+ color: var(--comparison-copy);
+ font-size: 16px;
+ line-height: 1.5;
+}
+.comparison-card .comparison-aside {
+ margin: 10px 0 0;
+ color: var(--comparison-muted);
+ font-size: 11px;
+}
+.comparison-sheet .comparison-window-body {
+ padding: 0 12px 12px;
+}
+.comparison-sheet-labels,
+.comparison-sheet-files li {
+ justify-content: space-between;
+}
+.comparison-sheet-labels {
+ padding: 12px 0 8px;
+ font: 10px var(--mono);
+ color: var(--comparison-muted);
+}
+.comparison-sheet-files {
+ list-style: none;
+ margin: 0 -12px;
+ padding: 0;
+ font-size: 13px;
+}
+.comparison-sheet-files li {
+ padding: 9px 12px;
+}
+.comparison-sheet-files li:nth-child(4) {
+ background: #eaf1ff;
+}
+.comparison-sheet-files b {
+ display: grid;
+ place-items: center;
+ width: 16px;
+ height: 18px;
+ border-radius: 3px;
+ background: #217747;
+ color: white;
+ font-size: 11px;
+}
+.comparison-sheet-files small {
+ font: 10px var(--mono);
+ color: var(--comparison-muted);
+ white-space: nowrap;
+}
+.comparison-lock {
+ margin: auto 0 0;
+ padding: 10px;
+ border: 1px solid #eed3a0;
+ border-radius: 7px;
+ background: #fbf5e9;
+ color: #795100;
+ font-size: 12px;
+}
+.comparison-terminal {
+ padding-top: 20px;
+ font: 11px / 1.75 var(--mono);
+}
+.comparison-terminal p {
+ margin: 0;
+ overflow-wrap: anywhere;
+}
+.comparison-terminal p > span:not(.comparison-more) {
+ color: #5cde93;
+}
+.comparison-cursor {
+ display: inline-block;
+ width: 7px;
+ height: 13px;
+ background: #97a8b8;
+ vertical-align: middle;
+}
+.comparison-metric {
+ align-items: normal;
+ justify-content: space-between;
+}
+.comparison-metric strong {
+ display: block;
+ font-size: 30px;
+ line-height: 1.1;
+}
+.comparison-metric small,
+.comparison-chat small {
+ display: block;
+ font-size: 13px;
+ color: var(--comparison-muted);
+ margin-top: 4px;
+}
+.comparison-metric .comparison-view-only {
+ padding: 5px 10px;
+ border: 1px solid var(--comparison-border);
+ border-radius: 5px;
+ align-self: start;
+ position: relative;
+ color: #6e7c89;
+ background: #f6f8fa;
+ font-size: 12px;
+}
+.comparison-metric .comparison-view-only small {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ white-space: nowrap;
+ border-radius: 4px;
+ padding: 6px 8px;
+ background: #242b32;
+ color: white;
+ font-size: 10px;
+}
+.comparison-bars {
+ display: flex;
+ align-items: end;
+ gap: 4px;
+ height: 105px;
+ padding-top: 15px;
+ margin-top: 20px;
+ border-bottom: 1px solid var(--comparison-border);
+}
+.comparison-bars i {
+ flex: 1;
+ height: 42%;
+ background: #adc1ff;
+ border-radius: 2px 2px 0 0;
+}
+.comparison-bars i:nth-child(3n) {
+ height: 58%;
+}
+.comparison-bars i:nth-child(3n + 1) {
+ height: 78%;
+}
+.comparison-bars i:nth-child(4n) {
+ height: 65%;
+}
+.comparison-bars i:first-child {
+ height: 40%;
+}
+.comparison-bars i:nth-child(5n) {
+ height: 88%;
+}
+.comparison-bars i:last-child {
+ height: 100%;
+ background: #326cff;
+}
+.comparison-chat {
+ align-items: start;
+ gap: 10px;
+ margin: auto 0 0;
+ font-size: 14px;
+}
+.comparison-chat b {
+ display: grid;
+ place-items: center;
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ background: #326cff;
+ color: white;
+ font-size: 12px;
+}
+.comparison-lowcode .comparison-window-body {
+ position: relative;
+ background: #e0e4e8;
+}
+.comparison-lowcode table {
+ border-collapse: collapse;
+ font-size: 12px;
+ width: 100%;
+ color: #626b74;
+}
+.comparison-lowcode th {
+ font: 10px var(--mono);
+ text-align: left;
+}
+.comparison-lowcode td,
+.comparison-lowcode th {
+ padding: 7px 0;
+ border-bottom: 1px solid #cbd4da;
+}
+.comparison-lowcode td:last-child,
+.comparison-lowcode th:last-child {
+ text-align: right;
+}
+.comparison-lowcode .comparison-window-body::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: #18262d40;
+}
+.comparison-upgrade {
+ position: absolute;
+ z-index: 1;
+ top: 50%;
+ left: 16px;
+ right: 16px;
+ transform: translateY(-50%);
+ padding: 20px 16px;
+ border-radius: 12px;
+ background: #fff;
+ box-shadow: 0 15px 30px #18262d40;
+ font-size: 16px;
+}
+.comparison-upgrade p {
+ margin: 5px 0 20px;
+ font-size: 13px;
+ color: var(--comparison-copy);
+ line-height: 1.4;
+}
+.comparison-upgrade > div {
+ justify-content: end;
+ gap: 12px;
+ font-size: 12px;
+ color: var(--comparison-muted);
+}
+.comparison-upgrade > div span:last-child {
+ border-radius: 6px;
+ padding: 8px 12px;
+ background: #6844df;
+ color: white;
+}
+.comparison-footer {
+ justify-content: space-between;
+ gap: 30px;
+ margin-top: 36px;
+}
+.comparison-footer > p {
+ margin: 0;
+ color: var(--comparison-muted);
+ font-size: 11px;
+ max-width: 720px;
+}
+.comparison-section .comparison-controls {
+ /* Focus must not move an arrow between pointer-down and click during an outro. */
+ --reveal-enter-x: 0px;
+ --reveal-exit-x: 0px;
+ --reveal-enter-y: 0px;
+ --reveal-exit-y: 0px;
+ --reveal-enter-scale: 1;
+ --reveal-exit-scale: 1;
+ gap: 10px;
+ flex-shrink: 0;
+}
+.comparison-controls p {
+ margin: 0 10px 0 0;
+ color: var(--comparison-muted);
+ font: 11px / 1.5 var(--mono);
+}
+.comparison-controls button {
+ display: grid;
+ place-items: center;
+ width: 44px;
+ height: 44px;
+ padding: 0;
+ border: 1px solid #a8b5bf;
+ border-radius: 8px;
+ background: transparent;
+ color: #0b1020;
+ transition:
+ background 200ms,
+ border-color 200ms;
+}
+.comparison-controls button:not(:disabled):hover {
+ background: #e1ecef;
+ border-color: #146575;
+}
+.comparison-controls svg {
+ width: 20px;
+ height: 20px;
+}
+@media (max-width: 1100px) {
+ .comparison-section .section-heading {
+ grid-template-columns: minmax(0, 1fr) minmax(0, 0.17fr);
+ }
+ .comparison-intro {
+ grid-column: 1 / -1;
+ order: 1;
+ margin: 0;
+ }
+ .comparison-layout {
+ grid-template-columns: minmax(0, 1fr) 28px minmax(0, 1fr);
+ }
+ .comparison-track {
+ grid-auto-columns: 100%;
+ }
+}
+@media (max-width: 700px) {
+ .comparison-layout {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 20px;
+ padding-top: 20px;
+ }
+ .comparison-versus {
+ flex-direction: row;
+ margin: 4px 0;
+ gap: 16px;
+ font-size: 20px;
+ }
+ .comparison-versus::before,
+ .comparison-versus::after {
+ height: 1px;
+ }
+ .comparison-intro {
+ font-size: 19px;
+ }
+ .comparison-footer {
+ flex-direction: column-reverse;
+ align-items: start;
+ gap: 24px;
+ margin-top: 20px;
+ }
+ .comparison-controls {
+ width: 100%;
+ }
+ .comparison-controls p {
+ margin-right: auto;
+ }
+}
+@media (max-width: 600px) {
+ .comparison-section h2 {
+ font-size: clamp(34px, 10.5vw, 63px);
+ }
+}
diff --git a/examples/official-site/assets/landing/styles/main.css b/examples/official-site/assets/landing/styles/main.css
index bd2e68c4..839bde8d 100644
--- a/examples/official-site/assets/landing/styles/main.css
+++ b/examples/official-site/assets/landing/styles/main.css
@@ -14,4 +14,5 @@
@import url("./component-demos.css");
@import url("./waves.css");
@import url("./frontend.css");
+@import url("./comparison.css");
@import url("./section-motion.css");
diff --git a/examples/official-site/sqlpage/templates/landing-page.handlebars b/examples/official-site/sqlpage/templates/landing-page.handlebars
index ef9b0ffa..c0bb37fc 100644
--- a/examples/official-site/sqlpage/templates/landing-page.handlebars
+++ b/examples/official-site/sqlpage/templates/landing-page.handlebars
@@ -28,6 +28,18 @@
{{caption}} {{summary}} {{aside}}
{{else}}{{icon_img glyph}}{{/if}}
+ {{title}}
{{verdict}}
+ {{#if summary}}
-- one refunds tool, six ways to build it, on day 100
+Building got cheap.
Owning what got built is still expensive.
3 files changed +26 −2
+{{icon_img "circle-check"}} git push → live
+Scroll horizontally to explore five illustrative alternatives. Use the left and right arrow keys when this region is focused.
+214 files +8,402 −1,317
+… and 209 more files
+{{icon_img "alert-triangle"}} 2 dependency alerts
+{{icon_img "check"}} 1 approval: LGTM
+{{icon_img "lock"}} Locked for editing by dave
+$ crontab -l
+0 6 * * * python3 ~/dave/refunds.py
+$ tail -n3 refunds.log
+05-14 06:00 ok
05-15 06:00 ok
05-16 06:00 ConnectionError
+$ git log -1 --format=%an
+dave (left in March)
+$
+SCan we add a button?Not in this tool.
+| Order | Customer | Amount |
|---|---|---|
| #1042 | Ada L. | €48.00 |
| #1043 | Grace H. | €12.50 |
| #1044 | Linus T. | €76.00 |
| #1045 | Margaret H. | €24.00 |
Upgrade to export refunds as CSV.
+