docs(site): vertical pipeline on mobile, click tooltips, consistent chrome
All checks were successful
docs / build-and-deploy (push) Successful in 2s

- Pipeline diagram stacks vertically with downward arrows on mobile (row on
  >=720px) instead of the awkward wrap.
- Tooltip cards now open on CLICK (works on touch too), the card itself is
  clickable and opens the symbol's page in a NEW TAB, and an outside click or
  Escape closes it. Replaces the hover-only behavior.
- Reference pages get their own nav — Home / API Reference / Source ↗ — instead
  of the landing-only Features/Examples/Get started anchors, and now include the
  Source link.
- Unify the container width (1120px) across the landing and all reference pages
  so the header and content align between them.
- Reword extern/@export FFI docs from "C-ABI" to "native" for consistency with
  the site's no-C wording.

Verified in-browser at 375px and 1280px: vertical pipeline, click→card→new-tab,
outside-click close, correct reference nav, aligned container; 158 examples still
compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-29 18:42:40 +03:00
parent c93eca80af
commit 31f1019a69
7 changed files with 62 additions and 41 deletions

View file

@ -5,17 +5,17 @@ category: annotations
kind: annotation
tokens: @export
sig: @export fn name(…) -> R
tip: Expose a function as a C-ABI symbol so a host can call it.
tip: Expose a function as a native symbol so a host can call it.
order: 3
---
<code>@export</code> makes a <code>fn</code> visible outside the module as a plain C-ABI symbol, so a host program or another linked object can call it by name. Without it, functions are internal to the compiled unit; with it, the emitted symbol is externally linkable, which is how Ludic hands entry points to a runtime seam or a foreign caller. It is the outbound counterpart to <code>extern fn</code>, which pulls a foreign symbol in. Keep exported signatures to POD scalars and pointers, since they cross a C boundary where Ludic's richer types do not apply.
<code>@export</code> makes a <code>fn</code> visible outside the module as a plain native symbol, so a host program or another linked object can call it by name. Without it, functions are internal to the compiled unit; with it, the emitted symbol is externally linkable, which is how Ludic hands entry points to a runtime seam or a foreign caller. It is the outbound counterpart to <code>extern fn</code>, which pulls a foreign symbol in. Keep exported signatures to POD scalars and pointers, since they cross a native ABI boundary where Ludic's richer types do not apply.
```ludic
program ScoreModule {
var running_total: int = 0
@export fn add_points(amount: int) -> int { # callable from a C-ABI host
@export fn add_points(amount: int) -> int { # callable from a native host
running_total = running_total + amount
return running_total
}

View file

@ -5,11 +5,11 @@ category: structure
kind: keyword
tokens: extern
sig: extern fn name(a: T) -> R = "symbol"
tip: Bind a name to an external C-ABI symbol — the seam for platform and library calls.
tip: Bind a name to an external native symbol — the seam for platform and library calls.
order: 12
---
An <code>extern fn</code> declares a function whose body lives outside Ludic and binds it to a C-ABI symbol resolved at link time. It is the seam through which Ludic reaches anything with a C interface — a system library, a math routine, or even another `.ludic` file compiled as a `module`. You write the Ludic signature you want to call and give the real symbol name after `=`; the linker connects them (pass `-L`/`-l` to `ludicc` to point at the library). Types must match the foreign ABI, so map each parameter and the return to the right Ludic type (`int`, `fixed`, `ptr`, …).
An <code>extern fn</code> declares a function whose body lives outside Ludic and binds it to a native symbol resolved at link time. It is the seam through which Ludic reaches anything with a native interface — a system library, a math routine, or even another `.ludic` file compiled as a `module`. You write the Ludic signature you want to call and give the real symbol name after `=`; the linker connects them (pass `-L`/`-l` to `ludicc` to point at the library). Types must match the foreign ABI, so map each parameter and the return to the right Ludic type (`int`, `fixed`, `ptr`, …).
Parameters:
- `a` — a typed argument passed straight through to the foreign symbol

View file

@ -14,6 +14,10 @@
{ "label": "Get started", "href": "#start" },
{ "label": "API Reference", "href": "api.html" }
],
"ref_nav_links": [
{ "label": "Home", "href": "index.html" },
{ "label": "API Reference", "href": "api.html" }
],
"hero": {
"pill": "Opinionated · Batteries-included · No framework",
"title_pre": "Build a game,",

View file

@ -4,7 +4,7 @@
--mint:#7cf5c4; --coral:#ff5d73; --violet:#c792ea; --blue:#82aaff; --amber:#f6c177;
--c-bg:#0b0f20; --c-comment:#5b6788; --c-key:#ff7eb6; --c-type:#7cf5c4;
--c-str:#f6c177; --c-num:#a6b8ff; --c-annot:#c792ea; --c-fn:#82aaff; --c-punc:#9aa6cc; --c-arg:#f6c177;
--radius:14px; --max:900px; --hdr:60px;
--radius:14px; --max:1120px; --hdr:60px;
}
*{box-sizing:border-box}
html{scroll-behavior:smooth;-webkit-text-size-adjust:100%}
@ -131,13 +131,15 @@
/* ---- hover card (hover-capable devices only; JS also gates it) ---- */
.hovercard{position:fixed;z-index:100;max-width:min(340px,calc(100vw - 24px));background:var(--panel2);
border:1px solid var(--line2);border-radius:12px;padding:12px 14px;box-shadow:0 20px 50px -18px rgba(0,0,0,.8);pointer-events:none}
border:1px solid var(--line2);border-radius:12px;padding:12px 14px;box-shadow:0 20px 50px -18px rgba(0,0,0,.8);
pointer-events:auto;cursor:pointer}
.hovercard:hover{border-color:var(--mint)}
.hc-top{display:flex;align-items:center;gap:9px;margin-bottom:8px}
.hc-name{font-family:"JetBrains Mono",monospace;color:var(--head);font-weight:700;font-size:14px;overflow-wrap:anywhere}
.hc-sig{display:block;color:var(--c-fn);background:var(--c-bg);border:1px solid var(--line);border-radius:7px;
padding:6px 9px;font-size:12px;margin-bottom:8px;white-space:pre-wrap;overflow-wrap:anywhere}
.hc-tip{color:var(--text);font-size:13.5px;line-height:1.5}
.hc-foot{color:var(--muted);font-size:11.5px;margin-top:9px;letter-spacing:.3px}
.hc-foot{color:var(--mint);font-size:11.5px;margin-top:9px;letter-spacing:.3px}
/* ---- target flash ---- */
@keyframes ludicflash{0%{background:rgba(124,245,196,.28);box-shadow:0 0 0 3px rgba(124,245,196,.28)}

View file

@ -107,13 +107,26 @@
document.querySelectorAll('pre[data-lang="ludic"]').forEach(pre => { pre.innerHTML = highlight(pre.textContent); });
}
/* ---- hover cards ------------------------------------------------------- */
let cardEl = null, cardTimer = null;
/* ---- tooltip cards: click a token to open; click the card to open the ---
---- page in a new tab; click outside (or Escape) to close ------------- */
let cardEl = null, cardAnchor = null;
function ensureCard(){
if (cardEl) return cardEl;
cardEl = document.createElement("div"); cardEl.className = "hovercard"; cardEl.hidden = true;
cardEl.addEventListener("click", () => {
const h = cardEl.dataset.href; if (h) window.open(h, "_blank", "noopener");
});
document.body.appendChild(cardEl); return cardEl;
}
function positionCard(a){
const el = cardEl, r = a.getBoundingClientRect();
const cw = el.offsetWidth, ch = el.offsetHeight;
let left = r.left, top = r.bottom + 8;
if (left + cw > window.innerWidth - 12) left = window.innerWidth - cw - 12;
if (left < 12) left = 12;
if (top + ch > window.innerHeight - 12) top = r.top - ch - 8;
el.style.left = Math.max(12,left) + "px"; el.style.top = Math.max(12,top) + "px";
}
function showCard(a){
const id = a.getAttribute("data-id"); if (!id) return;
const c = CARDS[id]; if (!c) return;
@ -122,30 +135,25 @@
+ (c.kind||"").replace("namespace-method","method") + '</span><span class="hc-name">'+esc(c.name||"")+'</span></div>'
+ (c.sig ? '<code class="hc-sig">'+esc(c.sig)+'</code>' : "")
+ '<div class="hc-tip">'+esc(c.tip||"")+'</div>'
+ '<div class="hc-foot">'+esc(c.section||"")+' · click to open</div>';
el.hidden = false;
const r = a.getBoundingClientRect();
const cw = el.offsetWidth, ch = el.offsetHeight;
let left = r.left, top = r.bottom + 8;
if (left + cw > window.innerWidth - 12) left = window.innerWidth - cw - 12;
if (left < 12) left = 12;
if (top + ch > window.innerHeight - 12) top = r.top - ch - 8;
el.style.left = Math.max(12,left) + "px"; el.style.top = Math.max(12,top) + "px";
+ '<div class="hc-foot">'+esc(c.section||"")+' · open in new tab ↗</div>';
el.dataset.href = a.getAttribute("href") || "";
el.hidden = false; cardAnchor = a; positionCard(a);
}
function hideCard(){ if (cardEl) cardEl.hidden = true; }
function hideCard(){ if (cardEl){ cardEl.hidden = true; cardAnchor = null; } }
function installCards(){
// hover cards are a pointer affordance; skip on touch so a tap just navigates
if (!(window.matchMedia && window.matchMedia("(hover: hover)").matches)) return;
document.addEventListener("mouseover", e => {
document.addEventListener("click", e => {
const a = e.target.closest && e.target.closest("a.tok[data-id]");
if (!a) return;
clearTimeout(cardTimer); cardTimer = setTimeout(()=>showCard(a), 130);
if (a){ e.preventDefault(); showCard(a); return; } // token -> open card
if (cardEl && !cardEl.hidden && !(e.target.closest && e.target.closest(".hovercard"))) hideCard(); // outside -> close
});
document.addEventListener("mouseout", e => {
const a = e.target.closest && e.target.closest("a.tok[data-id]");
if (a){ clearTimeout(cardTimer); hideCard(); }
});
window.addEventListener("scroll", hideCard, {passive:true});
document.addEventListener("keydown", e => { if (e.key === "Escape") hideCard(); });
window.addEventListener("resize", hideCard);
window.addEventListener("scroll", () => {
if (cardEl && !cardEl.hidden && cardAnchor){
const r = cardAnchor.getBoundingClientRect();
if (r.bottom < 0 || r.top > window.innerHeight) hideCard(); else positionCard(cardAnchor);
}
}, {passive:true});
}
/* ---- flash the scrolled-to target ------------------------------------- */

View file

@ -79,12 +79,12 @@
pre a.tok:hover{background:rgba(124,245,196,.14);outline:1px solid rgba(124,245,196,.35)}
/* ---------- pipeline ---------- */
.pipeline{display:flex;align-items:center;justify-content:center;gap:4px;flex-wrap:wrap;
margin:8px 0 0;padding:16px;border:1px dashed var(--line2);border-radius:var(--radius);background:rgba(18,23,43,.4)}
.stage{font-family:"JetBrains Mono",monospace;font-size:12.5px;font-weight:500;color:var(--head);
background:var(--panel);border:1px solid var(--line2);padding:7px 11px;border-radius:9px;white-space:nowrap}
.pipeline{display:flex;flex-direction:column;align-items:center;gap:8px;
margin:8px 0 0;padding:18px 16px;border:1px dashed var(--line2);border-radius:var(--radius);background:rgba(18,23,43,.4)}
.stage{font-family:"JetBrains Mono",monospace;font-size:13px;font-weight:500;color:var(--head);text-align:center;
background:var(--panel);border:1px solid var(--line2);padding:9px 13px;border-radius:9px;white-space:nowrap}
.stage.hl{color:var(--mint);border-color:rgba(124,245,196,.35)}
.arrow{color:var(--muted);padding:0 6px;font-size:14px}
.arrow{color:var(--muted);font-size:16px;line-height:1;transform:rotate(90deg)}
/* ---------- sections ---------- */
section{padding:48px 0}
@ -156,13 +156,15 @@
/* hover-card + kind badges (shared with the reference; hover-devices only) */
.hovercard{position:fixed;z-index:100;max-width:min(340px,calc(100vw - 24px));background:var(--panel2);
border:1px solid var(--line2);border-radius:12px;padding:12px 14px;box-shadow:0 20px 50px -18px rgba(0,0,0,.8);pointer-events:none}
border:1px solid var(--line2);border-radius:12px;padding:12px 14px;box-shadow:0 20px 50px -18px rgba(0,0,0,.8);
pointer-events:auto;cursor:pointer}
.hovercard:hover{border-color:var(--mint)}
.hc-top{display:flex;align-items:center;gap:9px;margin-bottom:8px}
.hc-name{font-family:"JetBrains Mono",monospace;color:var(--head);font-weight:700;font-size:14px;overflow-wrap:anywhere}
.hc-sig{display:block;color:var(--c-fn);background:var(--c-bg);border:1px solid var(--line);border-radius:7px;
padding:6px 9px;font-size:12px;margin-bottom:8px;white-space:pre-wrap;overflow-wrap:anywhere}
.hc-tip{color:var(--text);font-size:13.5px;line-height:1.5}
.hc-foot{color:var(--muted);font-size:11.5px;margin-top:9px;letter-spacing:.3px}
.hc-foot{color:var(--mint);font-size:11.5px;margin-top:9px;letter-spacing:.3px}
.kind-badge{font-size:11px;font-weight:600;letter-spacing:1px;text-transform:uppercase;padding:3px 7px;
border-radius:6px;border:1px solid var(--line2);color:var(--muted);background:var(--panel)}
.kind-keyword{color:var(--c-key)}.kind-type{color:var(--c-type)}.kind-phase{color:var(--blue)}
@ -183,6 +185,9 @@
.nav-links .nav-cta{padding:8px 15px;border-radius:9px;border:1px solid var(--line2);
background:var(--panel);color:var(--head)!important}
.nav-links .nav-cta:hover{border-color:var(--mint)}
.pipeline{flex-direction:row;flex-wrap:wrap;justify-content:center;gap:4px}
.stage{font-size:12.5px;padding:8px 12px}
.arrow{transform:none;padding:0 6px;font-size:14px}
}
@media (min-width:900px){
.hero{padding:72px 0 52px}

View file

@ -161,10 +161,10 @@ NS_TIP = {"Screen": "The 2D drawing surface.", "Color": "The named color palette
# ---------------------------------------------------------------------------
# shared chrome
# ---------------------------------------------------------------------------
def nav_html(cfg, active=None):
def nav_html(cfg, links=None, cta_href="api.html"):
out = []
for n in cfg["nav_links"]:
cls = 'class="nav-cta" ' if n.get("href") == "api.html" else ""
for n in (links if links is not None else cfg["nav_links"]):
cls = 'class="nav-cta" ' if n.get("href") == cta_href else ""
out.append('<a %shref="%s">%s</a>' % (cls, n["href"], esc(n["label"])))
return "".join(out)
@ -186,10 +186,12 @@ def head_html(title, desc, css_file):
</head>""", {"TITLE": esc(title), "DESC": escattr(desc), "CSS": css})
def topbar(cfg):
refnav = nav_html(cfg, links=cfg.get("ref_nav_links", cfg["nav_links"]), cta_href=None) \
+ '<a class="nav-cta" href="%s">Source ↗</a>' % cfg["repo_url"]
return ('<header class="nav"><div class="wrap nav-in">'
'<a class="brand" href="index.html"><span class="logo">L</span> %s</a>'
'<button class="nav-toggle" aria-label="Toggle menu" aria-expanded="false">☰</button>'
'<nav class="nav-links">%s</nav></div></header>') % (esc(cfg["brand"]), nav_html(cfg))
'<nav class="nav-links">%s</nav></div></header>') % (esc(cfg["brand"]), refnav)
# ---------------------------------------------------------------------------
# per-item page