diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91629566..53bb921d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
# CHANGELOG.md
## v0.47.0 (unreleased)
+- 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.
- Loading embedded card content no longer adds duplicate form submission or file validation handlers to existing fields. Fragment initialization also includes the element announcing the fragment.
diff --git a/examples/official-site/assets/landing/js/experience.js b/examples/official-site/assets/landing/js/experience.js
index 0e49903b..4bc422a2 100644
--- a/examples/official-site/assets/landing/js/experience.js
+++ b/examples/official-site/assets/landing/js/experience.js
@@ -61,7 +61,7 @@ export function initExperience(root) {
setStatus("error");
}
}
- stopScroll = initScrollProgress(section, mount, state);
+ stopScroll = initScrollProgress(section, state);
syncMotion();
media.addEventListener("change", syncMotion, { signal: events.signal });
root
diff --git a/examples/official-site/assets/landing/js/scene/database-scene.js b/examples/official-site/assets/landing/js/scene/database-scene.js
index d1f93333..f1380eaa 100644
--- a/examples/official-site/assets/landing/js/scene/database-scene.js
+++ b/examples/official-site/assets/landing/js/scene/database-scene.js
@@ -320,10 +320,17 @@ export function createDatabaseScene({
if (pointers.size) return;
const scale = canvasWidth / 1000;
let { left, right, top, bottom } = screenBounds;
- left = clamp(canvasLeft + left * scale - 12, 0, innerWidth);
- right = clamp(canvasLeft + right * scale + 12, left, innerWidth);
- top = clamp(canvasTop + top * scale - 12, 90, innerHeight);
- bottom = clamp(canvasTop + bottom * scale + 12, top, innerHeight);
+ left = canvasLeft + left * scale - 12;
+ right = canvasLeft + right * scale + 12;
+ top = canvasTop + top * scale - 12;
+ bottom = canvasTop + bottom * scale + 12;
+ // Clip in viewport space, then keep the target in the canvas host's local
+ // coordinates. Rotation can otherwise put gesture centers off the screen.
+ const origin = mount.getBoundingClientRect();
+ left = clamp(left, -origin.left, innerWidth - origin.left);
+ right = clamp(right, left, innerWidth - origin.left);
+ top = clamp(top, 90 - origin.top, innerHeight - origin.top);
+ bottom = clamp(bottom, top, innerHeight - origin.top);
hitArea.style.left = `${left}px`;
hitArea.style.top = `${top}px`;
hitArea.style.width = `${right - left}px`;
@@ -367,10 +374,11 @@ export function createDatabaseScene({
pointers.size > 0 ||
!lastOrientation.equals(dragQuaternion) ||
parallax.lengthSq() > 0.000001;
+ const viewportTop = mount.getBoundingClientRect().top + canvasTop;
const inView =
state.opacity > 0.05 &&
- canvasTop + canvasWidth * 0.81 > 0 &&
- canvasTop + canvasWidth * 0.22 < window.innerHeight;
+ viewportTop + canvasWidth * 0.81 > 0 &&
+ viewportTop + canvasWidth * 0.22 < window.innerHeight;
if (!inView) resetBudget();
if (readySent) {
hitArea.inert = !inView;
diff --git a/examples/official-site/assets/landing/js/scene/landing-frame.js b/examples/official-site/assets/landing/js/scene/landing-frame.js
index af2e6c8e..e4543002 100644
--- a/examples/official-site/assets/landing/js/scene/landing-frame.js
+++ b/examples/official-site/assets/landing/js/scene/landing-frame.js
@@ -57,12 +57,11 @@ function frameAt(width, height, anchor, portrait, bodyWidth = width * 0.48) {
// This small module runs before Three.js downloads, so even the loading layout
// uses the same measured letter, portrait flow, and projection as the live scene.
-export function layoutLandingFrame(mount, anchor) {
- const width = Math.max(1, mount.clientWidth);
+export function layoutLandingFrame(viewport, anchor) {
+ const width = Math.max(1, viewport.clientWidth);
const screenHeight = window.innerHeight;
const portrait = width < 900 && screenHeight >= width;
const shortLandscape = !portrait && screenHeight < 560;
- const viewport = mount.closest(".sqlpage-world").querySelector(".viewport");
const section = viewport.parentElement;
const heading = anchor?.closest(".hero-copy")?.querySelector(".title-from");
const intro = viewport.querySelector(".intro-row");
@@ -80,7 +79,7 @@ export function layoutLandingFrame(mount, anchor) {
`${-Math.max(0, value - screenHeight)}px`,
);
height =
- portrait || shortLandscape ? value : Math.max(1, mount.clientHeight);
+ portrait || shortLandscape ? value : Math.max(1, viewport.clientHeight);
};
const measureLetter = () => {
if (portrait && heading) {
diff --git a/examples/official-site/assets/landing/js/scroll-progress.js b/examples/official-site/assets/landing/js/scroll-progress.js
index 8130aefd..e9a0a42c 100644
--- a/examples/official-site/assets/landing/js/scroll-progress.js
+++ b/examples/official-site/assets/landing/js/scroll-progress.js
@@ -2,7 +2,7 @@
import { layoutLandingFrame, SCULPTURE_FRAME } from "./scene/landing-frame.js";
/** One sculpture: an opening flight, then page anchors with fading handoffs. */
-export function initScrollProgress(section, mount, state) {
+export function initScrollProgress(section, state) {
const root = section.closest(".sqlpage-world");
const preview = root.querySelector(".scene-preview");
const layer = root.querySelector(".scene-layer");
@@ -28,13 +28,45 @@ export function initScrollProgress(section, mount, state) {
const intro = section.querySelector(".intro-row");
const header = section.querySelector(".site-header");
const ribbon = section.querySelector(".sql-ribbon");
+ function dock(host) {
+ if (layer.parentElement === host) return;
+ const before = layer.getBoundingClientRect();
+ // Keep focus and pointer capture when supported; older browsers retain focus
+ // explicitly while moving the same canvas and controls, without a new scene.
+ if (host.moveBefore) host.moveBefore(layer, null);
+ else {
+ const focused = layer.contains(document.activeElement)
+ ? document.activeElement
+ : null;
+ host.append(layer);
+ focused?.focus({ preventScroll: true });
+ }
+ layer.toggleAttribute("data-docked", host !== root);
+ // Preserve the painted frame until the renderer applies the new local pose.
+ const after = layer.getBoundingClientRect();
+ const dx = before.left - after.left;
+ const dy = before.top - after.top;
+ const canvas = layer.querySelector("canvas");
+ if (canvas) {
+ const pose = new DOMMatrix(canvas.style.transform || undefined);
+ pose.e += dx;
+ pose.f += dy;
+ canvas.style.transform = pose.toString();
+ const input = layer.querySelector(".model-interaction");
+ input.style.left = `${parseFloat(input.style.left || "0") + dx}px`;
+ input.style.top = `${parseFloat(input.style.top || "0") + dy}px`;
+ }
+ }
function measure() {
// Read authored page positions even when earlier sections are held under a cover.
root.setAttribute("data-measuring-layout", "");
// Measure the authored hero pose, independent of its current text fade/shift.
section.style.setProperty("--progress", "0");
section.style.setProperty("--intro-shift", "0px");
- heroFrame = layoutLandingFrame(mount, root.querySelector(".anchor-letter"));
+ heroFrame = layoutLandingFrame(
+ viewport,
+ root.querySelector(".anchor-letter"),
+ );
anchors = [];
heroRange = Math.max(0, section.offsetHeight - viewport.offsetHeight);
const maxScroll = Math.max(
@@ -43,39 +75,17 @@ export function initScrollProgress(section, mount, state) {
);
for (const stop of stops) {
const rect = stop.getBoundingClientRect();
- const sheet = stop.closest(".landing-section");
- const sheetRect = sheet.getBoundingClientRect();
- const nextSheet = sheet.nextElementSibling;
// Measure the visible model, not its much larger square canvas.
const size = rect.width / SCULPTURE_FRAME.bodyWidth;
const bodyHeight =
size * (SCULPTURE_FRAME.bounds.bottom - SCULPTURE_FRAME.bounds.top);
const bodyTop = rect.top + scrollY;
const fadeDistance = Math.min(innerHeight * 0.12, bodyHeight * 0.45);
- const heldTop = root.hasAttribute("data-section-motion")
- ? rect.top -
- sheetRect.top +
- (parseFloat(sheet.style.getPropertyValue("--section-stick-top")) || 0)
- : -Infinity;
- // A held section can leave part of its sculpture on screen. In that case
- // its lifetime ends when the next wave covers it, not at its authored Y.
- let exitScroll =
- heldTop + bodyHeight > 0 ? Infinity : bodyTop + bodyHeight;
- if (nextSheet?.matches(".landing-section")) {
- const waveHeight = parseFloat(
- getComputedStyle(nextSheet).getPropertyValue("--wave-height"),
- );
- const coverScroll =
- nextSheet.getBoundingClientRect().top +
- scrollY -
- waveHeight * 0.75 -
- (heldTop + bodyHeight * 0.5);
- exitScroll = Math.min(exitScroll, coverScroll);
- }
anchors.push({
element: stop,
enterScroll: Math.min(bodyTop - innerHeight, maxScroll - fadeDistance),
- exitScroll,
+ // The next section naturally covers a docked sculpture in its own layer.
+ exitScroll: Infinity,
fadeDistance,
bodyHeight,
bodyTop,
@@ -86,7 +96,7 @@ export function initScrollProgress(section, mount, state) {
}
heroDockScroll = Math.min(
maxScroll,
- anchors[0].bodyTop - innerHeight * 0.25,
+ Math.floor(anchors[0].bodyTop - innerHeight * 0.25),
);
anchors[0].enterScroll = -Infinity; // The opening flight supplies its entrance.
anchors.at(-1).exitScroll = Infinity; // Keep the closing composition visible.
@@ -128,11 +138,10 @@ export function initScrollProgress(section, mount, state) {
let sceneOpacity = 1;
if (!heroRange && scroll < heroDockScroll) {
// Reduced motion keeps the opening sculpture attached to the hero layout.
- state.frame = {
- ...heroFrame,
- top: heroFrame.top + viewport.getBoundingClientRect().top,
- };
+ dock(viewport);
+ state.frame = heroFrame;
} else if (heroRange && scroll <= heroDockScroll) {
+ dock(root);
const bounds = SCULPTURE_FRAME.bounds;
const size = Math.min(
(innerWidth * 0.9) / SCULPTURE_FRAME.bodyWidth,
@@ -177,8 +186,9 @@ export function initScrollProgress(section, mount, state) {
)
index++;
const anchor = anchors[index];
- // Follow the actual page anchor, including its section holding beneath the
- // next cover. Only the opening flight interpolates position or size.
+ dock(anchor.element);
+ // Native scrolling and section stickiness own placement. Geometry here
+ // controls fading only; the canvas and input bounds stay local to the host.
const rect = anchor.element.getBoundingClientRect();
if (state.motion) {
sceneOpacity = Math.min(
@@ -191,8 +201,8 @@ export function initScrollProgress(section, mount, state) {
);
}
state.frame = {
- left: rect.left - anchor.size * SCULPTURE_FRAME.bounds.left,
- top: rect.top - anchor.size * SCULPTURE_FRAME.bounds.top,
+ left: -anchor.size * SCULPTURE_FRAME.bounds.left,
+ top: -anchor.size * SCULPTURE_FRAME.bounds.top,
size: anchor.size,
};
}
diff --git a/examples/official-site/assets/landing/styles/frontend.css b/examples/official-site/assets/landing/styles/frontend.css
index a5f059ff..4998ee65 100644
--- a/examples/official-site/assets/landing/styles/frontend.css
+++ b/examples/official-site/assets/landing/styles/frontend.css
@@ -11,7 +11,7 @@
}
.deployment-grid {
display: grid;
- grid-template-columns: 1.6fr 1fr;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 0.2fr);
gap: 25px;
}
.deployment-section .eyebrow {
@@ -22,15 +22,11 @@
margin-bottom: 20px;
}
.deployment-notes {
+ grid-column: 1 / -1;
align-self: end;
text-align: right;
margin: 0 0 20px;
}
-.landing-section .deployment-sculpture {
- top: -85px;
- right: 5%;
- width: 17%;
-}
.frontend-tabs,
.frontend-panels {
grid-column: 1 / -1;
@@ -391,18 +387,13 @@
padding-bottom: 100px;
}
.deployment-grid {
- grid-template-columns: 1fr;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 0.24fr);
gap: 20px;
}
.deployment-section h2 {
font-size: clamp(36px, 10.8vw, 52px);
max-width: 100%;
}
- .landing-section .deployment-sculpture {
- width: 19%;
- top: -10px;
- right: -2%;
- }
.deployment-notes {
text-align: left;
margin: 0;
diff --git a/examples/official-site/assets/landing/styles/scene.css b/examples/official-site/assets/landing/styles/scene.css
index 22eee11e..917173a2 100644
--- a/examples/official-site/assets/landing/styles/scene.css
+++ b/examples/official-site/assets/landing/styles/scene.css
@@ -1,4 +1,4 @@
-/* One fixed sculpture layer, static fallback, and the opening stage’s pinned lifetime. */
+/* One sculpture: a fixed opening flight, then native scrolling in section hosts. */
.sqlpage-world {
position: relative;
overflow-x: clip;
@@ -39,6 +39,10 @@
position: absolute;
height: 100svh;
}
+.scene-layer[data-docked] {
+ position: absolute;
+ height: auto;
+}
.scene-layer .model-interaction[data-ready="true"] {
pointer-events: auto;
}
diff --git a/examples/official-site/assets/landing/styles/sections.css b/examples/official-site/assets/landing/styles/sections.css
index cd5de951..6710501a 100644
--- a/examples/official-site/assets/landing/styles/sections.css
+++ b/examples/official-site/assets/landing/styles/sections.css
@@ -25,39 +25,36 @@
font: 13px / 1.65 var(--mono);
color: #5b9ba9;
}
+.section-heading {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 0.2fr);
+ gap: clamp(16px, 3vw, 36px);
+ align-items: start;
+ margin: 25px 0 40px;
+}
+.section-heading h2 {
+ margin: 0;
+}
.scene-stop {
- position: absolute;
- top: 20px;
- right: 0;
- width: 14%;
+ position: relative;
+ width: 100%;
aspect-ratio: 0.68;
- pointer-events: none;
}
-.purpose-section .scene-stop {
- left: 0;
- right: auto;
- top: 130px;
- width: 18%;
+.purpose-section .section-heading {
+ grid-template-columns: minmax(0, 0.28fr) minmax(0, 1fr);
}
-.landing-section.purpose-section .section-inner > h2 {
- width: 100%;
+.purpose-section .scene-stop {
+ align-self: end;
+ /* Reserve the full square camera below the visible jewel for every drag pose. */
+ aspect-ratio: 0.5;
}
.purpose-section h2 span {
display: block;
- text-align: left;
- width: 76%;
- margin-left: 24%;
font-size: clamp(48px, 5.8vw, 76px);
line-height: 1.05;
}
-.landing-section .section-inner > h2,
-.database-grid {
- width: 80%;
-}
-
-.landing-section .demo-sculpture {
- top: -50px;
- width: 18%;
+.demo-section .section-heading {
+ grid-template-columns: minmax(0, 1fr) minmax(0, 0.24fr);
}
.demo-section {
padding-top: 70px;
@@ -83,7 +80,6 @@
padding-bottom: 105px;
}
.purpose-section h2 {
- min-height: 355px;
font-size: clamp(48px, 6.5vw, 89px);
}
.purpose-intro {
@@ -91,8 +87,7 @@
grid-template-columns: 1.2fr 0.5fr 1fr;
gap: 45px;
align-items: center;
- padding-left: 24%;
- margin: -100px 0 40px;
+ margin: 0 0 40px;
}
.purpose-intro > p:first-child {
color: #a6abbb;
@@ -374,12 +369,8 @@ a.chip:hover {
.landing-section {
padding: 80px 6% 70px;
}
- .purpose-section .scene-stop {
- width: 14%;
- }
.purpose-intro {
gap: 20px;
- margin-top: -90px;
}
.purpose-intro > p:first-child {
font-size: 17px;
@@ -399,18 +390,9 @@ a.chip:hover {
}
}
@media (max-width: 600px) {
- .landing-section .scene-stop,
- .purpose-section .scene-stop {
- width: 8%;
- right: -3%;
- top: 35px;
- }
- .landing-section .section-inner > h2,
- .purpose-intro,
- .purpose-grid,
- .database-grid {
- width: 100%;
- max-width: 100%;
+ .section-heading {
+ grid-template-columns: minmax(0, 1fr) minmax(0, 0.1fr);
+ margin-top: 20px;
}
.landing-section {
padding: 65px clamp(20px, 6vw, 32px);
@@ -422,31 +404,14 @@ a.chip:hover {
.eyebrow {
font-size: 10px;
}
- .demo-section h2 {
- max-width: 78%;
- }
- .purpose-section .scene-stop {
- left: 0;
- right: auto;
- top: 130px;
- width: 22%;
- }
- .landing-section .demo-sculpture {
- width: 19%;
- right: 0;
- top: 0;
- }
.purpose-section h2 span {
font-size: clamp(28px, 7.1vw, 36px);
}
.purpose-section h2 {
- /* Reserve space below the rotating sculpture before the full-width copy. */
- min-height: max(180px, calc(58px + 38vw));
font-size: clamp(33px, 8.2vw, 50px);
text-wrap: balance;
}
.purpose-intro {
- padding-left: 0;
grid-template-columns: 1fr 1fr;
margin: 35px 0;
}
@@ -486,7 +451,6 @@ a.chip:hover {
padding-bottom: 115px;
}
.databases-section h2 {
- max-width: 88%;
font-size: clamp(36px, 10vw, 43px);
}
.databases-section > .section-inner > p .chip {
diff --git a/examples/official-site/index.sql b/examples/official-site/index.sql
index aeacae62..c3e15bc8 100644
--- a/examples/official-site/index.sql
+++ b/examples/official-site/index.sql
@@ -3,6 +3,8 @@ select 'http_header' as component,
'
-- real SQLPage components, a few lines each
--- pick a component
-- Low maintenance, on purpose.
-You need a webapp,
but you don't want to maintain a webapp. Build it, or let your coding agent do it. Don't maintain 1000 lines of code.
-- The yak stays unshaved.
-- App maintenance is not a hobby.
-- What do you have to maintain?
@@ -287,6 +291,7 @@-- Your files. Your deployment.
-- read, edit, and act.
-- forms, tables, charts, and navigation.
-- Tables with manners. CRUD, but polite.
SELECT * FROM your_database;
-We're seeing other databases. A healthy relationship model.
--- A tool for them. Not a second job for you.
-DELETE busy;
-- NULL drama.