Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
37fae0e
Redesign the official site landing page
lovasoa Oct 4, 2026
692b80e
Refine landing branding, gradients, and diamond reflection
lovasoa Oct 4, 2026
ce427f4
Use built-in SQLPage landing icons and center mobile navigation
lovasoa Oct 4, 2026
c50235c
Build interactive landing sections with live SQLPage demos
lovasoa Oct 4, 2026
6ecf1bc
Restore cinematic landing transitions and polish database-backed demos
lovasoa Oct 4, 2026
3a2443b
Replace landing ASCII yak with a sketched SVG illustration
lovasoa Oct 4, 2026
1bd7dc4
Preserve official-site CSP nonces and stabilize landing CI
lovasoa Oct 4, 2026
6a70ce0
Keep the portrait sculpture clear of landing intro copy
lovasoa Oct 4, 2026
3cfb9ec
Keep landing sculpture moving along curved scroll routes
lovasoa Oct 4, 2026
50970b2
Keep curved mobile transitions visible and verify the full route
lovasoa Oct 4, 2026
feecb49
Let the landing sculpture linger near its scroll anchors
lovasoa Oct 4, 2026
5e627fa
Reserve sculpture landings and animate flowing section edges
lovasoa Oct 4, 2026
e7b5880
Animate the stack comparison and explain SQLPage responsibilities
lovasoa Oct 4, 2026
e977f4d
Refine live examples with a numbered SQL editor and status badges
lovasoa Oct 4, 2026
28d3da0
Tighten the examples heading and sculpture composition
lovasoa Oct 4, 2026
d579453
Keep landing sculpture attached to section compositions
lovasoa Oct 4, 2026
061fed1
Rebalance purpose layout and start visible sculpture transfers earlier
lovasoa Oct 4, 2026
b3d57fb
Keep purpose sculpture clear of introductory copy
lovasoa Oct 4, 2026
49b0cc3
Give departing sculpture clearance around section headings
lovasoa Oct 4, 2026
7466e24
Compact purpose section around its sculpture and introduction
lovasoa Oct 4, 2026
6a550e2
Add saved profile demo and database-driven component catalog
lovasoa Oct 4, 2026
9cac81a
Merge branch 'main' into codex/official-site-landing
lovasoa Oct 4, 2026
74dec07
Redesign homepage frontend section with four interactive views
lovasoa Oct 4, 2026
cf8b81e
Fix homepage frontend diagrams at narrow screen widths
lovasoa Oct 4, 2026
cfb3787
Refine sculpture page anchors and disable invisible input
lovasoa Oct 10, 2026
9bded2e
Preserve reading time in covering landing sections
lovasoa Oct 10, 2026
8079c7a
Verify readable section tails across responsive layouts
lovasoa Oct 10, 2026
f0cda99
Consolidate homepage release notes
lovasoa Oct 10, 2026
f8eb9e6
Merge current main before landing page launch
lovasoa Oct 10, 2026
18c1141
Prepare landing page modules and private demos for launch
lovasoa Oct 10, 2026
50c2356
Preserve homepage SEO metadata and crawlable navigation
lovasoa Oct 10, 2026
5e6a1fd
Reserve landing animation layout before first paint
lovasoa Oct 10, 2026
b167b53
Keep landing DOM motion tests independent of WebGL
lovasoa Oct 10, 2026
f0a7159
Wait for native demo resizing before browser geometry checks
lovasoa Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG.md

## v0.47.0 (unreleased)
- 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.
- **Mac users:** the downloadable `sqlpage-macos.tgz` is now code-signed with a Developer ID certificate and notarized by Apple. macOS may still ask you to confirm opening it the first time. The binary can still load third-party ODBC database drivers.
- **Mac users:** the downloadable `sqlpage-macos.tgz` now runs natively on Apple silicon (M-series Macs) and no longer runs on Intel Macs. Homebrew remains the recommended and easiest installation method. On an Intel Mac, [install Homebrew](https://brew.sh/) if needed, then run `brew install sqlpage` (or `brew update` followed by `brew upgrade sqlpage` if you already installed it with Homebrew). Open Terminal in your existing website folder and run `sqlpage` instead of `./sqlpage.bin`; keep your SQL files, database, and `sqlpage` configuration folder in place. Intel installations may build from source and take longer; see the [macOS installation guide](https://sql-page.com/your-first-sql-website/?os=macos#download) for setup and older macOS requirements.
Expand Down
55 changes: 53 additions & 2 deletions examples/official-site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,61 @@
The SQLPage website is of course built with SQLPage !

Things that you may be interested to look at:
- The [custom component we use for our stylish home page](./sqlpage/templates/shell-home.handlebars)
- The [custom component we use for our stylish home page](./sqlpage/templates/landing-page.handlebars)
- The [migrations](./sqlpage/migrations) that populate the database with the documentation for all components
- The [advanced multistep form example](./examples/multistep-form/)

It is hosted as a simple Docker container on [sql-page.com](https://sql-page.com).

Feel free to [open a pull request](https://github.com/lovasoa/sqlpage/pulls) if you would like to add or change anything !
Feel free to [open a pull request](https://github.com/lovasoa/sqlpage/pulls) if you would like to add or change anything !

The home page is rendered by `index.sql` and the `landing-page` custom template.
Its styles, ES modules, model, lighting map, and static preview are under
`assets/landing/`; no separate HTML entry point or frontend build is required.
Internal navigation uses site-relative URLs so it also works in local previews.

A small nonce-protected marker in the template reserves the animated opening
before first paint; it does not run when JavaScript is disabled. The static
preview appears once its page anchor is measured, and WebGL readiness changes
only the renderer, so slow or failed 3D loading does not shift the page.

The landing page runs as native ES modules. `main.js` starts independent
navigation, selectors, streaming timeline, section transitions, waves, and scene
controllers; each returns its cleanup function. `sections.js` shares keyboard tab
behavior through `tabs.js` and delegates iframe/source switching to
`component-demos.js`. Each live preview uses the normal SQLPage shell and built-in
components in `landing-demos/`, with `demo.css` sizing the rendered content.

Demo customer, ticket, feedback and profile defaults come from
`78_landing_demos.sql`. The source endpoint reads the same SQL files from a fixed
allowlist, never an arbitrary request path. Profile submissions are validated on
the server and rendered in that POST response only. They do not change the shared
database, create visitor storage, or put submitted values in URLs. A new GET of
the form restores its seeded defaults.

`section-motion.js` measures authored content before applying sticky positioning
and element entrance/outro progress. A short section holds once it fits; a long
section scrolls through its full content before its tail holds beneath the next
section's wave. `wave-motion.js` combines slow idle waves with scroll-driven wind
impulses that decay after scrolling stops. Visual poses and responsive layout
remain in the CSS modules rather than the JavaScript controllers.

`experience.js` isolates the optional WebGL lifecycle. `scroll-progress.js` owns
page geometry and scroll state; `scene/database-scene.js` owns rendering and
interaction. One shared canvas flies from the opening composition to the demo,
then stays at each section's `data-scene-stop` page anchor while rotating. Later
handoffs fade between anchors without interpolating position. Anchor measurements
refresh after content and viewport changes, including iframe resizing.

Scene modules use pinned Three.js 0.186.1 ES modules from jsDelivr. Keep their core
module URLs identical when upgrading. Shaders are ordinary `.glsl` files in
`js/scene/shaders/`, fetched relative to `shader-sources.js`; successful loads are
shared, while failed loads can be retried. The model, lighting map, and static
preview are versioned local assets. The official site's `sqlpage.yaml` allows
jsDelivr scripts while preserving SQLPage's per-request nonce.

Without JavaScript the document retains its content, initial live example, and
navigation. CDN or WebGL failure leaves the static sculpture preview and working
controls, and a retry is available for the optional scene. Reduced motion disables
the pinned opening, covering transitions, and idle animations. SQL source uses a
pinned SQL-only Highlight.js grammar with readable plain text if its CDN fails.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions examples/official-site/assets/landing/assets/yak.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
224 changes: 224 additions & 0 deletions examples/official-site/assets/landing/js/component-demos.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import { initSqlHighlight } from "./sql-highlight.js";
import { initTabs, selectTab } from "./tabs.js";

/** Prepare the next live example before changing the visible preview and SQL. */
export function initComponentDemos(root, signal) {
const panel = root.querySelector("#demo-panel");
const viewport = panel.querySelector(".demo-preview");
const source = root.querySelector("#demo-source");
const status = root.querySelector(".demo-status");
const renderSource = initSqlHighlight(source, signal);
const media = matchMedia("(prefers-reduced-motion: reduce)");
const picker = root.querySelector(".component-picker");
let preview = viewport.querySelector("iframe");
let component = "table";
let sourceFiles = { main: source.textContent };
let request;
let pending;
let animation;
let fade;
let resizePreview;
const previewHeight = (content) =>
`${Math.ceil(content.getBoundingClientRect().height) + 2}px`;

function resizePanel(change, entering = false) {
const before = panel.getBoundingClientRect().height;
animation?.cancel();
fade?.cancel();
change();
const after = panel.getBoundingClientRect().height;
if (media.matches) return;
animation = panel.animate(
[
{ height: `${before}px`, opacity: entering ? 0 : 1 },
{ height: `${after}px`, opacity: 1 },
],
{ duration: 320, easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
);
}

function fitPreview() {
resizePreview?.disconnect();
const content = preview.contentDocument?.querySelector(
"#sqlpage_main_wrapper",
);
if (!content) return;
const fit = () => {
const height = previewHeight(content);
if (viewport.style.height !== height)
resizePanel(() => {
viewport.style.height = height;
});
};
resizePreview = new ResizeObserver(fit);
resizePreview.observe(content);
fit();
}
preview.addEventListener("load", fitPreview, { signal });
fitPreview();

root.querySelector("[data-copy-sql]").addEventListener(
"click",
async () => {
try {
await navigator.clipboard.writeText(source.textContent);
status.textContent = "SQL copied.";
} catch {
status.textContent = "Copy unavailable. Select the SQL below.";
}
},
{ signal },
);
initTabs(
root.querySelector(".source-tabs"),
"aria-selected",
(button) => {
source.setAttribute("aria-labelledby", button.id);
status.textContent = "";
resizePanel(() => {
void renderSource(sourceFiles[button.dataset.file]);
});
},
signal,
);

function loadPreview(frame, requestSignal) {
return new Promise((resolve, reject) => {
let observer;
const abort = () => {
observer?.disconnect();
reject(new DOMException("Demo cancelled", "AbortError"));
};
requestSignal.addEventListener("abort", abort, { once: true });
frame.addEventListener(
"load",
async () => {
const document = frame.contentDocument;
if (!document?.querySelector("#sqlpage_main_wrapper")) {
reject(new Error("Preview unavailable"));
return;
}
const ready = () => {
if (document.querySelector("[data-pre-init]")) return;
observer?.disconnect();
requestSignal.removeEventListener("abort", abort);
resolve();
};
await document.fonts.ready;
if (requestSignal.aborted) return;
observer = new MutationObserver(ready);
observer.observe(document, { attributes: true, subtree: true });
ready();
},
{ once: true, signal: requestSignal },
);
});
}

async function selectDemo(button) {
request?.abort();
pending?.remove();
fade?.cancel();
const next = button.dataset.demo;
if (next === component) {
panel.removeAttribute("aria-busy");
return;
}
const controller = new AbortController();
request = controller;
const incoming = preview.cloneNode(false);
pending = incoming;
incoming.title = "Loading SQLPage component demo";
incoming.setAttribute("aria-hidden", "true");
incoming.inert = true;
incoming.loading = "eager";
const loaded = loadPreview(incoming, controller.signal);
incoming.src = `/landing-demos/demo.sql?component=${next}`;
viewport.append(incoming);
panel.setAttribute("aria-busy", "true");
status.textContent = "";
try {
const [files] = await Promise.all([
next === "catalog"
? null
: fetch(`/landing-demos/source.sql?component=${next}`, {
signal: controller.signal,
}).then((response) => {
if (!response.ok) throw new Error("Source unavailable");
return response.json();
}),
loaded,
]);
if (controller.signal.aborted) return;
if (!media.matches) {
fade = panel.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: 100,
fill: "forwards",
});
await fade.finished;
}
if (controller.signal.aborted) return;
resizePreview?.disconnect();
preview.removeEventListener("load", fitPreview);
const outgoing = preview;
resizePanel(() => {
outgoing.remove();
preview = incoming;
pending = null;
component = next;
incoming.title = "Live SQLPage component demo";
incoming.removeAttribute("aria-hidden");
incoming.inert = false;
viewport.style.height = previewHeight(
incoming.contentDocument.querySelector("#sqlpage_main_wrapper"),
);
panel.dataset.demo = next;
panel.setAttribute("aria-labelledby", button.id);
root.querySelector('[data-file="save"]').hidden = next !== "form";
root.querySelector("#demo-filename").textContent = `${next}.sql`;
const link = panel.querySelector(".demo-docs");
link.href = `/component.sql?component=${next}`;
link.firstChild.textContent = `Explore the ${next} component `;
if (files) {
sourceFiles = { main: files.source, save: files.save_source };
// Avoid an intermediate one-line loading state or a nested resize.
const main = root.querySelector("#demo-filename");
selectTab(root.querySelector(".source-tabs"), main);
source.setAttribute("aria-labelledby", main.id);
void renderSource(sourceFiles.main);
}
}, true);
preview.addEventListener("load", fitPreview, { signal });
fitPreview();
panel.removeAttribute("aria-busy");
} catch {
if (controller.signal.aborted) return;
controller.abort();
incoming.remove();
fade?.cancel();
panel.removeAttribute("aria-busy");
selectTab(picker, picker.querySelector(`[data-demo="${component}"]`));
status.textContent = "The demo could not load. Please try again.";
}
}
initTabs(
picker,
"aria-selected",
(button) => void selectDemo(button),
signal,
);
media.addEventListener(
"change",
() => {
if (media.matches) animation?.cancel();
},
{ signal },
);
return () => {
request?.abort();
pending?.remove();
resizePreview?.disconnect();
animation?.cancel();
fade?.cancel();
};
}
Loading
Loading