mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
The website on Niveus: window-framed hero, feature cards with drawn icons, screenshot tabs, a light/dark/auto switch, downloads and build steps as cards; still all words in content.js
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
53d8ecbfcd
commit
04c8985374
13 changed files with 4948 additions and 229 deletions
|
|
@ -19,19 +19,19 @@ window.FRY = {
|
|||
},
|
||||
|
||||
features: [
|
||||
{ icon: "▣", title: "Shapes that stay editable", text: "Cube, cylinder, sphere, torus, stairs, arch and more, plus starter models like a table, chair or barrel. Change their sizes any time in Properties." },
|
||||
{ icon: "✎", title: "Draw a shape anywhere", text: "Click an outline on the ground or straight on the face of another object: it becomes a new part, gets added to it, or cuts into it." },
|
||||
{ icon: "⊕", title: "Add, subtract, overlap", text: "Combine objects: add them together, take one out of another, or keep only where they overlap. The result is always closed and ready to print." },
|
||||
{ icon: "◫", title: "Real shape editing", text: "Points, edges and faces. Extrude, inset, round edges, loop cut, knife, merge. Shift+click picks a whole loop, Shift+Ctrl+click a ring." },
|
||||
{ icon: "◎", title: "Holes for screws and nuts", text: "Round, rectangle, triangle or hexagon. M2 to M8 screw and nut sizes ready, with a little room added so the print actually fits." },
|
||||
{ icon: "▤", title: "Copies and thickness", text: "Copies in a row or round in a circle. Make a surface thick, or a solid hollow with walls exactly as thick as you need." },
|
||||
{ icon: "✐", title: "Paint and stickers", text: "Paint on the model or on its flat picture. Paste any picture (Ctrl+V) and it goes on like a sticker that bends over edges and corners." },
|
||||
{ icon: "▦", title: "Layout tab", text: "Move, turn and size the flat frames of the surface by hand. The paint follows, so the model looks the same." },
|
||||
{ icon: "⎙", title: "Made for 3D printing", text: "Your printer's plate in the view, sizes in millimetres, checks for holes and flipped faces with fix buttons, overhangs shown red, weight and filament." },
|
||||
{ icon: "☀", title: "See it rendered", text: "Solid view for modelling, a real renderer with sky, sun and soft shadows when you want to see it properly. Save a picture with F12." },
|
||||
{ icon: "▭", title: "Folders and groups", text: "Layers with folders inside folders, drag and drop, show/hide and lock whole groups, and a button on every row that brings the camera to it." },
|
||||
{ icon: "▯", title: "On your phone too", text: "Not a viewer: the whole modeller. Big buttons, one finger turns the view, two move and pinch to zoom, holding still is a right-click. Files through the phone's own picker." },
|
||||
{ icon: "⟲", title: "Never lose work", text: "Its own packed .fry file, autosave in the background, recovery after a crash, and a Save / Don't save / Cancel when closing." },
|
||||
{ icon: "cube", title: "Shapes that stay editable", text: "Cube, cylinder, sphere, torus, stairs, arch and more, plus starter models like a table, chair or barrel. Change their sizes any time in Properties." },
|
||||
{ icon: "pen", title: "Draw a shape anywhere", text: "Click an outline on the ground or straight on the face of another object: it becomes a new part, gets added to it, or cuts into it." },
|
||||
{ icon: "combine", title: "Add, subtract, overlap", text: "Combine objects: add them together, take one out of another, or keep only where they overlap. The result is always closed and ready to print." },
|
||||
{ icon: "points", title: "Real shape editing", text: "Points, edges and faces. Extrude, inset, round edges, loop cut, knife, merge. Shift+click picks a whole loop, Shift+Ctrl+click a ring." },
|
||||
{ icon: "hole", title: "Holes for screws and nuts", text: "Round, rectangle, triangle or hexagon. M2 to M8 screw and nut sizes ready, with a little room added so the print actually fits." },
|
||||
{ icon: "copies", title: "Copies and thickness", text: "Copies in a row or round in a circle. Make a surface thick, or a solid hollow with walls exactly as thick as you need." },
|
||||
{ icon: "brush", title: "Paint and stickers", text: "Paint on the model or on its flat picture. Paste any picture (Ctrl+V) and it goes on like a sticker that bends over edges and corners." },
|
||||
{ icon: "layout", title: "Layout tab", text: "Move, turn and size the flat frames of the surface by hand. The paint follows, so the model looks the same." },
|
||||
{ icon: "print", title: "Made for 3D printing", text: "Your printer's plate in the view, sizes in millimetres, checks for holes and flipped faces with fix buttons, overhangs shown red, weight and filament." },
|
||||
{ icon: "sun", title: "See it rendered", text: "Solid view for modelling, a real renderer with sky, sun and soft shadows when you want to see it properly. Save a picture with F12." },
|
||||
{ icon: "folder", title: "Folders and groups", text: "Layers with folders inside folders, drag and drop, show/hide and lock whole groups, and a button on every row that brings the camera to it." },
|
||||
{ icon: "phone", title: "On your phone too", text: "Not a viewer: the whole modeller. Big buttons, one finger turns the view, two move and pinch to zoom, holding still is a right-click. Files through the phone's own picker." },
|
||||
{ icon: "save", title: "Never lose work", text: "Its own packed .fry file, autosave in the background, recovery after a crash, and a Save / Don't save / Cancel when closing." },
|
||||
],
|
||||
|
||||
shots: [
|
||||
|
|
|
|||
BIN
docs/fonts/InstrumentSans-Italic.woff2
Normal file
BIN
docs/fonts/InstrumentSans-Italic.woff2
Normal file
Binary file not shown.
BIN
docs/fonts/InstrumentSans.woff2
Normal file
BIN
docs/fonts/InstrumentSans.woff2
Normal file
Binary file not shown.
BIN
docs/fonts/JetBrainsMono.woff2
Normal file
BIN
docs/fonts/JetBrainsMono.woff2
Normal file
Binary file not shown.
|
|
@ -1,8 +1,9 @@
|
|||
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
|
|
@ -43,7 +44,7 @@ new environment.
|
|||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION AND CONDITIONS
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Binary file not shown.
Binary file not shown.
194
docs/index.html
194
docs/index.html
|
|
@ -9,76 +9,162 @@
|
|||
<meta property="og:description" content="A 3D modeller that does what you'd expect. Pick a tool, click, drag.">
|
||||
<meta property="og:image" content="img/model.webp">
|
||||
<link rel="icon" href="img/icon.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<!-- The look is Niveus (github.com/Nexoniarz/niveus), the same as the app's. site.css is only
|
||||
layout: no colours, shadows or type sizes of its own. -->
|
||||
<link rel="stylesheet" href="niveus/niveus.css">
|
||||
<script src="niveus/niveus.js" defer></script>
|
||||
<link rel="stylesheet" href="site.css">
|
||||
<script>
|
||||
// The saved appearance before the first paint, so the page never flashes the wrong one.
|
||||
try {
|
||||
const t = localStorage.getItem("fry-theme");
|
||||
if (t === "light" || t === "dark") document.documentElement.setAttribute("data-theme", t);
|
||||
} catch (e) {}
|
||||
</script>
|
||||
<style>:root { --nv-brand: #ff7828; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- The page is built from content.js by site.js: edit content.js to update it. -->
|
||||
<header class="top">
|
||||
<a class="brand" href="#"><img src="img/logo.svg" alt=""><span>Fry3DX</span></a>
|
||||
<nav>
|
||||
<a href="#features">Features</a>
|
||||
<a href="#look">Look</a>
|
||||
<a href="#get">Get it</a>
|
||||
<a href="#versions">Versions</a>
|
||||
<a class="gh" id="gh-top" href="#">GitHub</a>
|
||||
<nav class="nv-nav" id="top-nav" aria-label="Main">
|
||||
<a class="nv-nav__brand" href="#top"><img class="brand-logo" src="img/logo.svg" alt=""> Fry3DX</a>
|
||||
<ul class="nv-nav__list nav-links">
|
||||
<li><a class="nv-nav__item" href="#features">Features</a></li>
|
||||
<li><a class="nv-nav__item" href="#look">Look</a></li>
|
||||
<li><a class="nv-nav__item" href="#scripting">Scripting</a></li>
|
||||
<li><a class="nv-nav__item" href="#get">Get it</a></li>
|
||||
<li><a class="nv-nav__item" href="#versions">Versions</a></li>
|
||||
</ul>
|
||||
<span class="nv-nav__spacer"></span>
|
||||
<div class="nv-button-group theme-switch" role="group" aria-label="Appearance">
|
||||
<button class="nv-button nv-button--sm" data-nv-theme="light" aria-pressed="false">Light</button>
|
||||
<button class="nv-button nv-button--sm" data-nv-theme="dark" aria-pressed="false">Dark</button>
|
||||
<button class="nv-button nv-button--sm" data-nv-theme="system" aria-pressed="true">Auto</button>
|
||||
</div>
|
||||
<a class="nv-button nv-button--sm nv-button--ghost" id="gh-top" href="#">GitHub</a>
|
||||
<a class="nv-button nv-button--sm nv-button--primary" href="#get">Download</a>
|
||||
</nav>
|
||||
|
||||
<main id="top">
|
||||
<header class="shell hero">
|
||||
<div class="hero__copy">
|
||||
<span class="nv-badge nv-badge--accent"><span class="nv-badge__dot"></span><span id="latest"></span></span>
|
||||
<p class="nv-overline" id="long"></p>
|
||||
<h1 class="nv-display hero__title" id="tagline"></h1>
|
||||
<p class="nv-prose nv-text-secondary" id="intro"></p>
|
||||
<div class="nv-cluster">
|
||||
<a class="nv-button nv-button--primary nv-button--lg" href="#get">Get Fry3DX</a>
|
||||
<a class="nv-button nv-button--lg" id="gh-hero" href="#">Source on GitHub</a>
|
||||
</div>
|
||||
<div class="nv-cluster nv-cluster--tight" id="platforms"></div>
|
||||
</div>
|
||||
<div class="nv-bed hero__bed">
|
||||
<div class="nv-window app-window">
|
||||
<div class="nv-window__titlebar">
|
||||
<div class="nv-window__controls">
|
||||
<span class="nv-window__control nv-window__control--close" aria-hidden="true"></span>
|
||||
<span class="nv-window__control nv-window__control--minimize" aria-hidden="true"></span>
|
||||
<span class="nv-window__control nv-window__control--zoom" aria-hidden="true"></span>
|
||||
</div>
|
||||
<span class="nv-window__title">Fry3DX — kitchen.fry</span>
|
||||
</div>
|
||||
<img class="app-window__shot" src="img/model.webp" alt="Fry3DX with a table, chair, barrel and crate">
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<section class="hero">
|
||||
<div class="hero-text">
|
||||
<p class="kicker" id="long"></p>
|
||||
<h1 id="tagline"></h1>
|
||||
<p class="lead" id="intro"></p>
|
||||
<div class="buttons">
|
||||
<a class="btn hot" href="#get">Get Fry3DX</a>
|
||||
<a class="btn" id="gh-hero" href="#">Source on GitHub</a>
|
||||
<section class="shell section">
|
||||
<div class="nv-card nv-card--inset why">
|
||||
<div class="nv-card__body nv-stack">
|
||||
<h2 class="nv-heading" id="why-title"></h2>
|
||||
<div class="nv-prose nv-stack" id="why-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<figure class="hero-shot"><img src="img/model.webp" alt="Fry3DX with a table, chair, barrel and crate"></figure>
|
||||
</section>
|
||||
|
||||
<section class="why" id="why"></section>
|
||||
|
||||
<section id="features">
|
||||
<h2>What it does</h2>
|
||||
<div class="grid" id="feature-grid"></div>
|
||||
</section>
|
||||
|
||||
<section id="look">
|
||||
<h2>How it looks</h2>
|
||||
<div class="tabs" id="shot-tabs" role="tablist"></div>
|
||||
<figure class="shot"><img id="shot-img" alt=""><figcaption id="shot-text"></figcaption></figure>
|
||||
</section>
|
||||
|
||||
<section class="two">
|
||||
<div>
|
||||
<h2>Files</h2>
|
||||
<div id="formats"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Keys that make sense</h2>
|
||||
<table class="keys" id="keys"></table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="get">
|
||||
<h2>Get it</h2>
|
||||
<section class="shell section" id="features">
|
||||
<div class="section__head">
|
||||
<span class="nv-overline">What it does</span>
|
||||
<h2 class="nv-heading">The useful parts of the big modellers, without the hundred menus.</h2>
|
||||
</div>
|
||||
<div class="features" id="feature-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="shell section" id="look">
|
||||
<div class="section__head">
|
||||
<span class="nv-overline">How it looks</span>
|
||||
<h2 class="nv-heading">Five tabs: what you're doing, not where it's hidden.</h2>
|
||||
</div>
|
||||
<div class="nv-nav-tabs" role="tablist" aria-label="Screenshots" id="shot-tabs" data-nv-tabs></div>
|
||||
<div id="shot-panels" class="shots"></div>
|
||||
</section>
|
||||
|
||||
<section class="shell section" id="scripting" hidden>
|
||||
<div class="split">
|
||||
<div class="section__head">
|
||||
<span class="nv-overline">Scripting</span>
|
||||
<h2 class="nv-heading" id="script-title"></h2>
|
||||
<p class="nv-prose nv-text-secondary" id="script-text"></p>
|
||||
<ul class="ticks" id="script-points"></ul>
|
||||
</div>
|
||||
<div class="nv-card nv-card--elevated code-card">
|
||||
<div class="nv-card__header code-card__head">
|
||||
<span class="nv-overline" id="script-file"></span>
|
||||
<button class="nv-button nv-button--sm nv-button--ghost" data-copy="script-code">Copy</button>
|
||||
</div>
|
||||
<pre class="code" id="script-code"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shell section split">
|
||||
<div class="nv-card">
|
||||
<div class="nv-card__header"><h2 class="nv-card__title">Files</h2></div>
|
||||
<div class="nv-card__body nv-stack" id="formats"></div>
|
||||
</div>
|
||||
<div class="nv-card">
|
||||
<div class="nv-card__header"><h2 class="nv-card__title">Keys that make sense</h2></div>
|
||||
<div class="nv-table-frame"><table class="nv-table nv-table--compact"><tbody id="keys"></tbody></table></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="shell section" id="get">
|
||||
<div class="section__head">
|
||||
<span class="nv-overline">Get it</span>
|
||||
<h2 class="nv-heading">Free, open source, for the computer you've got.</h2>
|
||||
</div>
|
||||
<div class="downloads" id="downloads"></div>
|
||||
<p id="build-text"></p>
|
||||
<div class="code"><button class="copy" id="copy">Copy</button><pre id="build-steps"></pre></div>
|
||||
<p class="muted" id="build-needs"></p>
|
||||
<div class="nv-card nv-card--inset build">
|
||||
<div class="nv-card__body nv-stack">
|
||||
<p class="nv-body" id="build-text"></p>
|
||||
<div class="code-card">
|
||||
<div class="code-card__head">
|
||||
<span class="nv-overline">Terminal</span>
|
||||
<button class="nv-button nv-button--sm nv-button--ghost" data-copy="build-steps">Copy</button>
|
||||
</div>
|
||||
<pre class="code" id="build-steps"></pre>
|
||||
</div>
|
||||
<p class="nv-caption nv-text-tertiary" id="build-needs"></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="versions">
|
||||
<h2>Versions</h2>
|
||||
<div id="version-list"></div>
|
||||
<section class="shell section" id="versions">
|
||||
<div class="section__head">
|
||||
<span class="nv-overline">Versions</span>
|
||||
<h2 class="nv-heading">What's new</h2>
|
||||
</div>
|
||||
<div class="nv-disclosure-group" id="version-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p>Fry3DX, made by <a id="author" href="#"></a>.</p>
|
||||
<p class="muted">Open source under the Apache License 2.0 · Sky pictures from Poly Haven (CC0) · Fonts: Inter, JetBrains Mono (OFL).</p>
|
||||
<footer class="shell footer">
|
||||
<img class="footer__logo" src="img/logo.svg" alt="">
|
||||
<div class="nv-stack nv-stack--tight">
|
||||
<p class="nv-body">Fry3DX, made by <a id="author" href="#"></a>.</p>
|
||||
<p class="nv-caption nv-text-tertiary">Open source under the Apache License 2.0 · Sky pictures from Poly Haven (CC0) ·
|
||||
Fonts: Instrument Sans, JetBrains Mono (OFL) · Look: <a href="https://github.com/Nexoniarz/niveus">Niveus</a>.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="content.js"></script>
|
||||
|
|
|
|||
4163
docs/niveus/niveus.css
Normal file
4163
docs/niveus/niveus.css
Normal file
File diff suppressed because it is too large
Load diff
353
docs/niveus/niveus.js
Normal file
353
docs/niveus/niveus.js
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
/* ==========================================================================
|
||||
Niveus — continuity helpers
|
||||
--------------------------------------------------------------------------
|
||||
Optional. Nothing in Niveus needs this file to render; every component works
|
||||
without it. What it adds is continuity for the changes CSS cannot animate on
|
||||
its own — a selection moving to a different element, a panel replacing
|
||||
another, the whole surface changing appearance. All three are the same
|
||||
problem: two states that exist at different moments, with no single element
|
||||
transitioning between them. A view transition is the browser's answer, and
|
||||
this file is the small amount of wiring it needs.
|
||||
|
||||
Everything here is a thin wrapper. An application that already owns its
|
||||
state (a router, a framework) should call nv.transition() around its own
|
||||
update and ignore the rest.
|
||||
|
||||
nv.transition(update, { direction, kind, scope }) run an update continuously
|
||||
nv.setTheme("dark" | "light" | "system") cross-fade the appearance
|
||||
nv.openDialog(dialog, trigger) open it out of its trigger
|
||||
nv.selectTab(tablist, tab) move through the same surface
|
||||
|
||||
Markup wiring, applied automatically on load:
|
||||
|
||||
[data-nv-theme="dark"] button that sets the appearance
|
||||
[data-nv-dialog="id"] button that opens a dialog from itself
|
||||
[data-nv-tabs] tablist whose [role=tab] children switch
|
||||
their aria-controls panels
|
||||
[data-nv-close] button that closes its dialog or popover
|
||||
[data-nv-scroller] frame whose [data-nv-scroll] controls move
|
||||
its .nv-scroller a page at a time
|
||||
|
||||
Only what changes is lifted. A transition names the selection marker and
|
||||
the items of the groups it is given as `scope`, and only for as long as it
|
||||
runs; everything else stays in the one page snapshot. Naming every item on
|
||||
the page permanently made every transition — a theme change included —
|
||||
tear the page into dozens of layers, which is both slow and what pulled
|
||||
items out from under frosted chrome.
|
||||
|
||||
Reduced motion is honoured by skipping the view transition entirely: the
|
||||
update still runs, it simply arrives instead of travelling.
|
||||
========================================================================== */
|
||||
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var root = document.documentElement;
|
||||
|
||||
function prefersReducedMotion() {
|
||||
return global.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
}
|
||||
|
||||
function canTransition() {
|
||||
return typeof document.startViewTransition === "function" && !prefersReducedMotion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `update` so the user can follow the change.
|
||||
* `direction` ("forward" | "backward") tells content which way to travel;
|
||||
* `kind` ("theme") marks a change the whole surface takes part in;
|
||||
* `scope` (an element or a list of them) names the selection groups whose
|
||||
* marker should travel. Without a scope, nothing is lifted and the change
|
||||
* cross-fades in place.
|
||||
*/
|
||||
function transition(update, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!canTransition()) {
|
||||
return Promise.resolve(update());
|
||||
}
|
||||
|
||||
if (options.direction) root.dataset.nvDirection = options.direction;
|
||||
if (options.kind) root.dataset.nvTransition = options.kind;
|
||||
|
||||
var groups = [].concat(options.scope || []).filter(Boolean);
|
||||
var lifted = [];
|
||||
groups.forEach(function (group) {
|
||||
group.setAttribute("data-nv-travelling", "");
|
||||
lifted = lifted.concat(liftLabels(group));
|
||||
});
|
||||
|
||||
var view = document.startViewTransition(update);
|
||||
|
||||
function clear() {
|
||||
delete root.dataset.nvDirection;
|
||||
delete root.dataset.nvTransition;
|
||||
groups.forEach(function (group) { group.removeAttribute("data-nv-travelling"); });
|
||||
lifted.forEach(function (item) { item.style.viewTransitionName = ""; });
|
||||
}
|
||||
view.finished.then(clear, clear);
|
||||
|
||||
return view.finished;
|
||||
}
|
||||
|
||||
/** "light" | "dark" | "system" — system means: follow the environment. */
|
||||
function setTheme(theme) {
|
||||
var done = transition(function () {
|
||||
if (theme === "system") {
|
||||
root.removeAttribute("data-theme");
|
||||
} else {
|
||||
root.setAttribute("data-theme", theme);
|
||||
}
|
||||
syncThemeControls(theme);
|
||||
}, { kind: "theme" });
|
||||
|
||||
root.dispatchEvent(new CustomEvent("nv:theme", { detail: { theme: theme } }));
|
||||
return done;
|
||||
}
|
||||
|
||||
function syncThemeControls(theme) {
|
||||
var controls = document.querySelectorAll("[data-nv-theme]");
|
||||
for (var i = 0; i < controls.length; i++) {
|
||||
controls[i].setAttribute("aria-pressed", String(controls[i].dataset.nvTheme === theme));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a <dialog> so it appears to come from `trigger`: the entrance scales
|
||||
* away from the trigger's centre instead of from the middle of the screen.
|
||||
*/
|
||||
function openDialog(dialog, trigger) {
|
||||
if (typeof dialog === "string") dialog = document.getElementById(dialog);
|
||||
if (!dialog) return null;
|
||||
|
||||
dialog.showModal();
|
||||
|
||||
if (trigger) {
|
||||
// showModal() has already laid the dialog out, so both boxes are final.
|
||||
var from = trigger.getBoundingClientRect();
|
||||
var to = dialog.getBoundingClientRect();
|
||||
|
||||
// A trigger far outside the viewport — one the page has scrolled past —
|
||||
// would otherwise put the origin hundreds of pixels away and turn a
|
||||
// 3% scale into a lunge across the screen. One dialog-length in any
|
||||
// direction is enough to say where it came from.
|
||||
var clamp = function (value, size) {
|
||||
return Math.max(-size, Math.min(value, size * 2));
|
||||
};
|
||||
|
||||
dialog.style.setProperty(
|
||||
"--nv-origin",
|
||||
clamp(from.left + from.width / 2 - to.left, to.width).toFixed(1) + "px " +
|
||||
clamp(from.top + from.height / 2 - to.top, to.height).toFixed(1) + "px"
|
||||
);
|
||||
} else {
|
||||
dialog.style.removeProperty("--nv-origin");
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
function tabsOf(tablist) {
|
||||
return Array.prototype.slice.call(tablist.querySelectorAll('[role="tab"]'));
|
||||
}
|
||||
|
||||
/** Move the selection, and the panel, in the direction the user is going. */
|
||||
function selectTab(tablist, tab) {
|
||||
var tabs = tabsOf(tablist);
|
||||
var next = tabs.indexOf(tab);
|
||||
var current = -1;
|
||||
|
||||
for (var i = 0; i < tabs.length; i++) {
|
||||
if (tabs[i].getAttribute("aria-selected") === "true") current = i;
|
||||
}
|
||||
if (next < 0 || next === current) return Promise.resolve();
|
||||
|
||||
// The outgoing and incoming panels carry one name between them — the old
|
||||
// one in the old state, the new one in the new — so the browser treats
|
||||
// them as one surface whose content changes. Named only for the length of
|
||||
// the change, so any number of tab sets can live on one page.
|
||||
var panelOf = function (candidate) {
|
||||
return candidate ? document.getElementById(candidate.getAttribute("aria-controls")) : null;
|
||||
};
|
||||
var outgoing = panelOf(tabs[current]);
|
||||
var incoming = panelOf(tab);
|
||||
if (outgoing && canTransition()) outgoing.style.viewTransitionName = "nv-panel";
|
||||
|
||||
var done = transition(function () {
|
||||
tabs.forEach(function (candidate, index) {
|
||||
var selected = index === next;
|
||||
candidate.setAttribute("aria-selected", String(selected));
|
||||
candidate.tabIndex = selected ? 0 : -1;
|
||||
|
||||
var panel = panelOf(candidate);
|
||||
if (panel) panel.hidden = !selected;
|
||||
});
|
||||
if (outgoing) outgoing.style.viewTransitionName = "";
|
||||
if (incoming && canTransition()) incoming.style.viewTransitionName = "nv-panel";
|
||||
}, { direction: next > current ? "forward" : "backward", scope: tablist });
|
||||
|
||||
var settle = function () { if (incoming) incoming.style.viewTransitionName = ""; };
|
||||
done.then(settle, settle);
|
||||
|
||||
tab.focus();
|
||||
return done;
|
||||
}
|
||||
|
||||
function initTabs(tablist) {
|
||||
tablist.addEventListener("click", function (event) {
|
||||
var tab = event.target.closest('[role="tab"]');
|
||||
if (tab && tablist.contains(tab)) {
|
||||
event.preventDefault();
|
||||
selectTab(tablist, tab);
|
||||
}
|
||||
});
|
||||
|
||||
// The ARIA tabs pattern: arrows move selection, Home and End jump to the
|
||||
// ends, and only the selected tab is in the tab order.
|
||||
tablist.addEventListener("keydown", function (event) {
|
||||
var tabs = tabsOf(tablist);
|
||||
var index = tabs.indexOf(document.activeElement);
|
||||
if (index < 0) return;
|
||||
|
||||
var vertical = tablist.getAttribute("aria-orientation") === "vertical";
|
||||
var back = vertical ? "ArrowUp" : "ArrowLeft";
|
||||
var forward = vertical ? "ArrowDown" : "ArrowRight";
|
||||
var target = null;
|
||||
|
||||
if (event.key === back) target = tabs[(index - 1 + tabs.length) % tabs.length];
|
||||
else if (event.key === forward) target = tabs[(index + 1) % tabs.length];
|
||||
else if (event.key === "Home") target = tabs[0];
|
||||
else if (event.key === "End") target = tabs[tabs.length - 1];
|
||||
|
||||
if (target) {
|
||||
event.preventDefault();
|
||||
selectTab(tablist, target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wire a scroller's prev/next controls. Scrolling itself is the browser's;
|
||||
* this only gives pointers without a horizontal axis somewhere to click, and
|
||||
* hides a control once there is nothing left in that direction.
|
||||
*/
|
||||
function initScroller(frame) {
|
||||
var track = frame.querySelector(".nv-scroller");
|
||||
if (!track) return;
|
||||
|
||||
var controls = frame.querySelectorAll("[data-nv-scroll]");
|
||||
|
||||
function step() {
|
||||
var item = track.firstElementChild;
|
||||
if (!item) return track.clientWidth * 0.8;
|
||||
var gap = parseFloat(getComputedStyle(track).columnGap) || 0;
|
||||
return item.getBoundingClientRect().width + gap;
|
||||
}
|
||||
|
||||
function update() {
|
||||
// A sub-pixel slack, or a fractional scrollWidth leaves a control
|
||||
// enabled at an end it cannot move past.
|
||||
var atStart = track.scrollLeft <= 1;
|
||||
var atEnd = track.scrollLeft >= track.scrollWidth - track.clientWidth - 1;
|
||||
|
||||
controls.forEach(function (control) {
|
||||
control.disabled = control.dataset.nvScroll === "prev" ? atStart : atEnd;
|
||||
});
|
||||
|
||||
// The same two facts decide which edge is allowed to fade.
|
||||
frame.toggleAttribute("data-nv-at-start", atStart);
|
||||
frame.toggleAttribute("data-nv-at-end", atEnd);
|
||||
}
|
||||
|
||||
controls.forEach(function (control) {
|
||||
control.addEventListener("click", function () {
|
||||
var direction = control.dataset.nvScroll === "next" ? 1 : -1;
|
||||
track.scrollBy({ left: direction * step(), behavior: "smooth" });
|
||||
});
|
||||
});
|
||||
|
||||
track.addEventListener("scroll", update, { passive: true });
|
||||
if (typeof ResizeObserver === "function") new ResizeObserver(update).observe(track);
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Give every item in `scope` a view-transition name, so it is captured as
|
||||
* its own layer and rides *above* the travelling marker instead of being
|
||||
* painted over by it. Returns the items it named, so the caller can take
|
||||
* the names back off; transition() does this for its `scope` and clears
|
||||
* them when the change is over.
|
||||
*
|
||||
* This cannot live in CSS: a view-transition-name must be unique across the
|
||||
* whole document, and a stylesheet cannot count items. Items already
|
||||
* carrying a name are left alone, so an application can assign its own.
|
||||
*/
|
||||
var carried = 0;
|
||||
|
||||
function liftLabels(scope) {
|
||||
var items = scope.querySelectorAll(".nv-nav__item");
|
||||
var named = [];
|
||||
|
||||
for (var j = 0; j < items.length; j++) {
|
||||
if (items[j].style.viewTransitionName) continue;
|
||||
carried += 1;
|
||||
items[j].style.viewTransitionName = "nv-carried-" + carried;
|
||||
named.push(items[j]);
|
||||
}
|
||||
return named;
|
||||
}
|
||||
|
||||
function wire(scope) {
|
||||
scope = scope || document;
|
||||
|
||||
scope.querySelectorAll("[data-nv-theme]").forEach(function (control) {
|
||||
control.addEventListener("click", function () {
|
||||
setTheme(control.dataset.nvTheme);
|
||||
});
|
||||
});
|
||||
|
||||
scope.querySelectorAll("[data-nv-dialog]").forEach(function (trigger) {
|
||||
trigger.addEventListener("click", function () {
|
||||
openDialog(trigger.dataset.nvDialog, trigger);
|
||||
});
|
||||
});
|
||||
|
||||
scope.querySelectorAll("[data-nv-tabs]").forEach(initTabs);
|
||||
scope.querySelectorAll("[data-nv-scroller]").forEach(initScroller);
|
||||
|
||||
scope.querySelectorAll("[data-nv-close]").forEach(function (control) {
|
||||
control.addEventListener("click", function () {
|
||||
var owner = control.closest("dialog, [popover]");
|
||||
if (!owner) return;
|
||||
if (owner.tagName === "DIALOG") owner.close();
|
||||
else owner.hidePopover();
|
||||
});
|
||||
});
|
||||
|
||||
// A dialog closes when the scrim is clicked, but not when a drag merely
|
||||
// ends there.
|
||||
scope.querySelectorAll("dialog.nv-dialog").forEach(function (dialog) {
|
||||
dialog.addEventListener("mousedown", function (event) {
|
||||
if (event.target === dialog) dialog.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
global.nv = {
|
||||
transition: transition,
|
||||
setTheme: setTheme,
|
||||
syncThemeControls: syncThemeControls,
|
||||
openDialog: openDialog,
|
||||
selectTab: selectTab,
|
||||
initScroller: initScroller,
|
||||
liftLabels: liftLabels,
|
||||
wire: wire,
|
||||
prefersReducedMotion: prefersReducedMotion,
|
||||
};
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", function () { wire(); });
|
||||
} else {
|
||||
wire();
|
||||
}
|
||||
})(window);
|
||||
142
docs/site.css
Normal file
142
docs/site.css
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/* Fry3DX's site: layout only. Every colour, shadow, radius, type size and duration comes from
|
||||
Niveus (niveus/niveus.css); if a hex value turns up here, Niveus is missing a token. */
|
||||
|
||||
html { scroll-padding-top: 5rem; }
|
||||
body { background: var(--nv-color-background); }
|
||||
|
||||
.shell {
|
||||
max-inline-size: var(--nv-layout-max-width);
|
||||
margin-inline: auto;
|
||||
padding-inline: var(--nv-layout-gutter);
|
||||
}
|
||||
.section { padding-block: var(--nv-space-16); }
|
||||
.section__head {
|
||||
max-inline-size: var(--nv-measure);
|
||||
margin-block-end: var(--nv-space-8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--nv-space-3);
|
||||
}
|
||||
|
||||
/* --- Bar ------------------------------------------------------------------- */
|
||||
.brand-logo { inline-size: 1.75rem; block-size: 1.75rem; }
|
||||
@media (max-width: 60rem) { .nav-links { display: none; } }
|
||||
@media (max-width: 34rem) { .theme-switch, #gh-top { display: none; } }
|
||||
|
||||
/* --- Hero ------------------------------------------------------------------ */
|
||||
.hero {
|
||||
display: grid;
|
||||
gap: var(--nv-space-12);
|
||||
padding-block: var(--nv-space-16) var(--nv-space-8);
|
||||
align-items: center;
|
||||
}
|
||||
@media (min-width: 64rem) {
|
||||
.hero { grid-template-columns: 5fr 7fr; gap: var(--nv-space-12); }
|
||||
}
|
||||
.hero__copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--nv-space-5);
|
||||
align-items: flex-start;
|
||||
}
|
||||
.hero__title { font-size: clamp(var(--nv-font-size-4xl), 5.5vw, var(--nv-font-size-5xl)); }
|
||||
.hero__bed { padding: var(--nv-space-6); min-inline-size: 0; }
|
||||
.app-window { max-inline-size: 100%; }
|
||||
.app-window__shot, .shot__img {
|
||||
display: block;
|
||||
inline-size: 100%;
|
||||
block-size: auto;
|
||||
}
|
||||
|
||||
/* --- Why ------------------------------------------------------------------- */
|
||||
.why { max-inline-size: 52rem; margin-inline: auto; }
|
||||
|
||||
/* --- Features -------------------------------------------------------------- */
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
|
||||
gap: var(--nv-space-5);
|
||||
}
|
||||
.feature__icon {
|
||||
inline-size: var(--nv-space-10);
|
||||
block-size: var(--nv-space-10);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin-block-end: var(--nv-space-4);
|
||||
border-radius: var(--nv-radius-md);
|
||||
background-color: var(--nv-color-accent-subtle);
|
||||
color: var(--nv-color-accent-text);
|
||||
}
|
||||
.feature__icon svg { inline-size: var(--nv-space-5); block-size: var(--nv-space-5); }
|
||||
.feature .nv-card__title { margin-block-end: var(--nv-space-2); }
|
||||
|
||||
/* --- Screenshots ----------------------------------------------------------- */
|
||||
.shots { margin-block-start: var(--nv-space-6); }
|
||||
.shot { display: grid; gap: var(--nv-space-4); }
|
||||
.shot[hidden] { display: none; }
|
||||
|
||||
/* --- Two side by side -------------------------------------------------------- */
|
||||
.split {
|
||||
display: grid;
|
||||
gap: var(--nv-space-6);
|
||||
align-items: start;
|
||||
}
|
||||
@media (min-width: 56rem) { .split { grid-template-columns: 1fr 1fr; } }
|
||||
|
||||
.ticks { display: grid; gap: var(--nv-space-2); padding-inline-start: var(--nv-space-5); }
|
||||
|
||||
/* --- Code -------------------------------------------------------------------- */
|
||||
.code-card { overflow: hidden; }
|
||||
.code-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--nv-space-3);
|
||||
}
|
||||
.code {
|
||||
margin: 0;
|
||||
padding: var(--nv-space-4);
|
||||
overflow-x: auto;
|
||||
border-radius: var(--nv-radius-md);
|
||||
background: var(--nv-color-surface-inset);
|
||||
box-shadow: var(--nv-elevation-inset-sm);
|
||||
font: var(--nv-font-code);
|
||||
color: var(--nv-color-text-primary);
|
||||
}
|
||||
.code-card > .code { margin: 0 var(--nv-space-4) var(--nv-space-4); }
|
||||
.code .k { color: var(--nv-color-accent-text); }
|
||||
.code .c { color: var(--nv-color-text-tertiary); }
|
||||
.code .s { color: var(--nv-color-success-text); }
|
||||
|
||||
/* --- Files and keys ------------------------------------------------------------ */
|
||||
.fmt { display: grid; gap: var(--nv-space-2); }
|
||||
kbd {
|
||||
display: inline-block;
|
||||
padding: var(--nv-space-0-5) var(--nv-space-2);
|
||||
border-radius: var(--nv-radius-sm);
|
||||
background: var(--nv-color-surface-elevated);
|
||||
background-image: var(--nv-sheen-raised);
|
||||
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
|
||||
font: var(--nv-font-code);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* --- Downloads ----------------------------------------------------------------- */
|
||||
.downloads {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
|
||||
gap: var(--nv-space-5);
|
||||
margin-block-end: var(--nv-space-8);
|
||||
}
|
||||
.download .nv-card__body { display: grid; gap: var(--nv-space-2); }
|
||||
.download__file { overflow-wrap: anywhere; }
|
||||
|
||||
/* --- Footer -------------------------------------------------------------------- */
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: var(--nv-space-4);
|
||||
align-items: center;
|
||||
padding-block: var(--nv-space-12);
|
||||
border-block-start: var(--nv-border-width) solid var(--nv-color-border-subtle);
|
||||
}
|
||||
.footer__logo { inline-size: var(--nv-space-12); block-size: var(--nv-space-12); }
|
||||
187
docs/site.js
187
docs/site.js
|
|
@ -9,91 +9,176 @@
|
|||
return e;
|
||||
};
|
||||
|
||||
// Feature icons: 24px line drawings in the text's colour.
|
||||
const ICONS = {
|
||||
cube: '<path d="M12 3 20 7.5v9L12 21l-8-4.5v-9z"/><path d="m4 7.5 8 4.5 8-4.5M12 12v9"/>',
|
||||
pen: '<path d="M4 20h4L19 9l-4-4L4 16z"/><path d="m13.5 6.5 4 4"/>',
|
||||
combine: '<circle cx="9" cy="12" r="5.5"/><circle cx="15" cy="12" r="5.5"/>',
|
||||
points: '<rect x="5" y="5" width="14" height="14" rx="1"/><circle cx="5" cy="5" r="1.6" fill="currentColor"/><circle cx="19" cy="5" r="1.6" fill="currentColor"/><circle cx="5" cy="19" r="1.6" fill="currentColor"/><circle cx="19" cy="19" r="1.6" fill="currentColor"/>',
|
||||
hole: '<rect x="3.5" y="3.5" width="17" height="17" rx="2"/><path d="m12 7 4.3 2.5v5L12 17l-4.3-2.5v-5z"/><circle cx="12" cy="12" r="1.8"/>',
|
||||
copies: '<rect x="3" y="9" width="6" height="6" rx="1"/><rect x="10" y="9" width="6" height="6" rx="1"/><rect x="17" y="9" width="4" height="6" rx="1"/>',
|
||||
brush: '<path d="M14.5 4.5 19.5 9.5 11 18l-5-5z"/><path d="M6 13c-2 0-3 1.5-3 3.5S2 20 2 20s3.5.5 5.5-.5S9 17 9 16"/>',
|
||||
layout: '<rect x="3.5" y="3.5" width="17" height="17" rx="2"/><path d="M3.5 12h17M12 3.5v17"/>',
|
||||
print: '<path d="M7 9V4h10v5"/><rect x="3.5" y="9" width="17" height="8" rx="2"/><path d="M7 14h10v6H7z"/>',
|
||||
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 2.5v2.5M12 19v2.5M2.5 12H5M19 12h2.5M5.3 5.3l1.8 1.8M16.9 16.9l1.8 1.8M5.3 18.7l1.8-1.8M16.9 7.1l1.8-1.8"/>',
|
||||
folder: '<path d="M3.5 6.5a1 1 0 0 1 1-1h5l2 2h8a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1z"/>',
|
||||
phone: '<rect x="7" y="2.5" width="10" height="19" rx="2.5"/><path d="M11 18.5h2"/>',
|
||||
save: '<path d="M4 12a8 8 0 1 0 2.4-5.7"/><path d="M4 4v4h4M12 8v4l3 2"/>',
|
||||
code: '<path d="m8 7-5 5 5 5M16 7l5 5-5 5M13.5 4l-3 16"/>',
|
||||
sculpt: '<path d="M4 18c3-1 4-6 8-6s5 5 8 6"/><circle cx="12" cy="7" r="3"/>',
|
||||
sphere: '<circle cx="12" cy="12" r="8.5"/><path d="M3.5 12c2.5 2 14.5 2 17 0"/><circle cx="9" cy="8.5" r="1.5" fill="currentColor" stroke="none"/>',
|
||||
};
|
||||
const icon = (name) => {
|
||||
const s = el("span", "feature__icon");
|
||||
s.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${ICONS[name] || ICONS.cube}</svg>`;
|
||||
return s;
|
||||
};
|
||||
|
||||
// ── Hero ──
|
||||
$("latest").textContent = `Version ${F.versions[0].v}: ${F.versions[0].items[0]}`;
|
||||
$("long").textContent = F.long;
|
||||
$("tagline").textContent = F.tagline;
|
||||
$("intro").textContent = F.intro;
|
||||
for (const id of ["gh-top", "gh-hero"]) $(id).href = F.repo;
|
||||
for (const d of F.downloads) $("platforms").append(el("span", "nv-badge", d.os));
|
||||
$("author").textContent = F.author;
|
||||
$("author").href = F.authorUrl;
|
||||
|
||||
const why = $("why");
|
||||
why.append(el("h2", null, F.why.title));
|
||||
for (const p of F.why.text) why.append(el("p", null, p));
|
||||
$("why-title").textContent = F.why.title;
|
||||
for (const p of F.why.text) $("why-text").append(el("p", "nv-body", p));
|
||||
|
||||
const grid = $("feature-grid");
|
||||
// ── Features ──
|
||||
for (const f of F.features) {
|
||||
const card = el("article", "card");
|
||||
card.append(el("div", "icon", f.icon), el("h3", null, f.title), el("p", null, f.text));
|
||||
grid.append(card);
|
||||
const card = el("article", "nv-card feature");
|
||||
const body = el("div", "nv-card__body");
|
||||
body.append(icon(f.icon), el("h3", "nv-card__title", f.title), el("p", "nv-body nv-text-secondary", f.text));
|
||||
card.append(body);
|
||||
$("feature-grid").append(card);
|
||||
}
|
||||
|
||||
// Screenshots: a tab each.
|
||||
const tabs = $("shot-tabs");
|
||||
const show = (i) => {
|
||||
const s = F.shots[i];
|
||||
$("shot-img").src = s.img;
|
||||
$("shot-img").alt = s.title;
|
||||
$("shot-text").textContent = s.text;
|
||||
[...tabs.children].forEach((b, j) => b.setAttribute("aria-selected", i === j));
|
||||
};
|
||||
// ── Screenshots: a tab each, the picture in a window ──
|
||||
F.shots.forEach((s, i) => {
|
||||
const b = el("button", "tab", s.title);
|
||||
b.setAttribute("role", "tab");
|
||||
b.onclick = () => show(i);
|
||||
tabs.append(b);
|
||||
new Image().src = s.img; // (loaded ahead, so switching is instant)
|
||||
});
|
||||
show(0);
|
||||
const tab = el("button", "nv-nav__item", s.title);
|
||||
Object.assign(tab, { id: `shot-tab-${i}`, role: "tab", tabIndex: i ? -1 : 0 });
|
||||
tab.setAttribute("role", "tab");
|
||||
tab.setAttribute("aria-controls", `shot-${i}`);
|
||||
tab.setAttribute("aria-selected", String(i === 0));
|
||||
$("shot-tabs").append(tab);
|
||||
|
||||
const fm = $("formats");
|
||||
const panel = el("div", "nv-tab-panel shot");
|
||||
Object.assign(panel, { id: `shot-${i}`, hidden: i !== 0 });
|
||||
panel.setAttribute("role", "tabpanel");
|
||||
panel.setAttribute("aria-labelledby", tab.id);
|
||||
const win = el("div", "nv-window");
|
||||
const bar = el("div", "nv-window__titlebar");
|
||||
const lights = el("div", "nv-window__controls");
|
||||
for (const k of ["close", "minimize", "zoom"]) {
|
||||
const l = el("span", `nv-window__control nv-window__control--${k}`);
|
||||
l.setAttribute("aria-hidden", "true");
|
||||
lights.append(l);
|
||||
}
|
||||
bar.append(lights, el("span", "nv-window__title", `Fry3DX — ${s.title}`));
|
||||
const img = el("img", "shot__img");
|
||||
Object.assign(img, { src: s.img, alt: s.title, loading: i ? "lazy" : "eager" });
|
||||
win.append(bar, img);
|
||||
panel.append(win, el("p", "nv-body nv-text-secondary", s.text));
|
||||
$("shot-panels").append(panel);
|
||||
});
|
||||
|
||||
// ── Scripting ──
|
||||
if (F.scripting) {
|
||||
const S = F.scripting;
|
||||
$("scripting").hidden = false;
|
||||
$("script-title").textContent = S.title;
|
||||
$("script-text").textContent = S.text;
|
||||
for (const p of S.points) $("script-points").append(el("li", "nv-body", p));
|
||||
$("script-file").textContent = S.file;
|
||||
$("script-code").innerHTML = highlightLua(S.code);
|
||||
}
|
||||
|
||||
// ── Files and keys ──
|
||||
const row = (label, items) => {
|
||||
const d = el("div", "fmt");
|
||||
d.append(el("span", "fmt-label", label));
|
||||
for (const x of items) d.append(el("span", "chip", x));
|
||||
fm.append(d);
|
||||
d.append(el("span", "nv-overline", label));
|
||||
const c = el("div", "nv-cluster nv-cluster--tight");
|
||||
for (const x of items) c.append(el("span", "nv-badge", x));
|
||||
d.append(c);
|
||||
$("formats").append(d);
|
||||
};
|
||||
row("Save", F.formats.save);
|
||||
row("Export", F.formats.export);
|
||||
fm.append(el("p", "muted", F.formats.note));
|
||||
|
||||
const keys = $("keys");
|
||||
$("formats").append(el("p", "nv-caption nv-text-tertiary", F.formats.note));
|
||||
for (const [k, what] of F.keys) {
|
||||
const tr = el("tr");
|
||||
const td = el("td");
|
||||
td.append(el("kbd", null, k));
|
||||
tr.append(td, el("td", null, what));
|
||||
keys.append(tr);
|
||||
tr.append(td, el("td", "nv-body", what));
|
||||
$("keys").append(tr);
|
||||
}
|
||||
|
||||
const dl = $("downloads");
|
||||
// ── Downloads and building ──
|
||||
for (const d of F.downloads) {
|
||||
const a = el("a", "download");
|
||||
const a = el("a", "nv-card nv-card--interactive download");
|
||||
a.href = F.releases;
|
||||
a.append(el("strong", null, d.os), el("span", "muted", d.what), el("span", "file", "Fry3DX-…-" + d.file), el("span", null, d.how));
|
||||
dl.append(a);
|
||||
const b = el("div", "nv-card__body");
|
||||
b.append(el("h3", "nv-card__title", d.os), el("p", "nv-caption nv-text-tertiary", d.what),
|
||||
el("code", "nv-code download__file", "Fry3DX-…-" + d.file), el("p", "nv-body nv-text-secondary", d.how));
|
||||
a.append(b);
|
||||
$("downloads").append(a);
|
||||
}
|
||||
|
||||
$("build-text").textContent = F.build.text;
|
||||
$("build-steps").textContent = F.build.steps.join("\n");
|
||||
$("build-needs").textContent = F.build.needs;
|
||||
$("copy").onclick = async () => {
|
||||
for (const btn of document.querySelectorAll("[data-copy]")) {
|
||||
btn.onclick = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(F.build.steps.join("\n"));
|
||||
$("copy").textContent = "Copied";
|
||||
await navigator.clipboard.writeText($(btn.dataset.copy).textContent);
|
||||
btn.textContent = "Copied";
|
||||
} catch {
|
||||
$("copy").textContent = "Select and copy";
|
||||
btn.textContent = "Select and copy";
|
||||
}
|
||||
setTimeout(() => ($("copy").textContent = "Copy"), 1800);
|
||||
setTimeout(() => (btn.textContent = "Copy"), 1800);
|
||||
};
|
||||
}
|
||||
|
||||
const vl = $("version-list");
|
||||
for (const v of F.versions) {
|
||||
const d = el("div", "version");
|
||||
const h = el("h3");
|
||||
h.append(el("span", "v", "v" + v.v), el("span", "muted", " " + v.date));
|
||||
const ul = el("ul");
|
||||
for (const it of v.items) ul.append(el("li", null, it));
|
||||
d.append(h, ul);
|
||||
vl.append(d);
|
||||
// ── Versions ──
|
||||
F.versions.forEach((v, i) => {
|
||||
const d = el("details", "nv-disclosure");
|
||||
d.open = i === 0;
|
||||
const s = el("summary", "nv-disclosure__summary");
|
||||
s.append(el("span", "nv-title", "v" + v.v), el("span", "nv-caption nv-text-tertiary", " " + v.date));
|
||||
const ch = el("span", "nv-disclosure__chevron");
|
||||
ch.setAttribute("aria-hidden", "true");
|
||||
s.append(ch);
|
||||
const body = el("div", "nv-disclosure__body");
|
||||
const ul = el("ul", "ticks");
|
||||
for (const it of v.items) ul.append(el("li", "nv-body", it));
|
||||
body.append(ul);
|
||||
d.append(s, body);
|
||||
$("version-list").append(d);
|
||||
});
|
||||
|
||||
// ── The appearance, remembered; the bar's shadow once scrolled ──
|
||||
document.documentElement.addEventListener("nv:theme", (e) => {
|
||||
try { localStorage.setItem("fry-theme", e.detail?.theme ?? "system"); } catch (_) {}
|
||||
});
|
||||
const saved = (() => { try { return localStorage.getItem("fry-theme"); } catch (_) { return null; } })();
|
||||
if (saved) addEventListener("DOMContentLoaded", () => window.nv?.syncThemeControls(saved));
|
||||
const nav = $("top-nav");
|
||||
const onScroll = () => nav.toggleAttribute("data-scrolled", scrollY > 4);
|
||||
addEventListener("scroll", onScroll, { passive: true });
|
||||
onScroll();
|
||||
|
||||
// A little Lua colouring for the scripting sample.
|
||||
function highlightLua(src) {
|
||||
const esc = (t) => t.replace(/&/g, "&").replace(/</g, "<");
|
||||
const words = /\b(local|function|end|for|in|do|if|then|else|elseif|return|while|and|or|not|nil|true|false)\b/g;
|
||||
return src.split("\n").map((line) => {
|
||||
const at = line.indexOf("--");
|
||||
const code = at >= 0 ? line.slice(0, at) : line, comment = at >= 0 ? line.slice(at) : "";
|
||||
const parts = code.split(/("[^"]*")/);
|
||||
const out = parts.map((p) => (p.startsWith('"') ? `<span class="s">${esc(p)}</span>` : esc(p).replace(words, '<span class="k">$1</span>'))).join("");
|
||||
return out + (comment ? `<span class="c">${esc(comment)}</span>` : "");
|
||||
}).join("\n");
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
111
docs/style.css
111
docs/style.css
|
|
@ -1,111 +0,0 @@
|
|||
/* The site's own fonts (SIL Open Font License, fonts/OFL-*.txt), so it looks the same everywhere. */
|
||||
@font-face { font-family: "Inter"; src: url("fonts/inter-latin-wght-normal.woff2") format("woff2"); font-weight: 100 900; font-display: swap; }
|
||||
@font-face { font-family: "JetBrains Mono"; src: url("fonts/jetbrains-mono-latin-wght-normal.woff2") format("woff2"); font-weight: 100 800; font-display: swap; }
|
||||
:root {
|
||||
--bg: #161618;
|
||||
--panel: #1f1f23;
|
||||
--panel2: #26262b;
|
||||
--line: #34343b;
|
||||
--ink: #ecebe6;
|
||||
--dim: #9c9a94;
|
||||
--hot: #ff7828;
|
||||
--hot2: #ffc24a;
|
||||
--radius: 10px;
|
||||
color-scheme: dark;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font: 16px/1.6 "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
a { color: var(--hot); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
img { max-width: 100%; display: block; }
|
||||
h1, h2, h3 { line-height: 1.2; margin: 0 0 .6em; }
|
||||
h2 { font-size: 1.7rem; }
|
||||
h2::after { content: ""; display: block; width: 48px; height: 4px; margin-top: .4em; border-radius: 2px; background: linear-gradient(90deg, var(--hot), var(--hot2)); }
|
||||
.muted { color: var(--dim); }
|
||||
|
||||
.top {
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 16px;
|
||||
padding: 10px max(16px, calc((100% - 1120px) / 2));
|
||||
background: rgba(22, 22, 24, .9); backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 10px; color: var(--ink); font-weight: 700; font-size: 1.15rem; }
|
||||
.brand img { width: 32px; height: 32px; }
|
||||
.brand:hover { text-decoration: none; }
|
||||
nav { display: flex; gap: 18px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
nav a { color: var(--dim); font-size: .95rem; }
|
||||
nav a:hover { color: var(--ink); text-decoration: none; }
|
||||
nav .gh { color: var(--hot); }
|
||||
|
||||
main { max-width: 1120px; margin: 0 auto; padding: 0 16px; }
|
||||
section { padding: 56px 0; border-bottom: 1px solid var(--line); }
|
||||
section:last-child { border-bottom: 0; }
|
||||
|
||||
.hero { display: grid; grid-template-columns: 1fr 1.25fr; gap: 40px; align-items: center; padding-top: 64px; }
|
||||
.kicker { color: var(--hot); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; font-size: .85rem; margin: 0 0 .6em; }
|
||||
.hero h1 { font-size: clamp(2rem, 4.5vw, 3.2rem); }
|
||||
.lead { color: var(--dim); font-size: 1.1rem; }
|
||||
.buttons { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 24px; }
|
||||
.btn { display: inline-block; padding: 12px 20px; border-radius: var(--radius); background: var(--panel2); color: var(--ink); font-weight: 600; border: 1px solid var(--line); }
|
||||
.btn:hover { text-decoration: none; border-color: var(--hot); }
|
||||
.btn.hot { background: linear-gradient(135deg, var(--hot), #ff4a1c); border-color: transparent; color: #fff; }
|
||||
.hero-shot { margin: 0; }
|
||||
.hero-shot img, .shot img { border-radius: var(--radius); border: 1px solid var(--line); box-shadow: 0 20px 60px rgba(0, 0, 0, .45); }
|
||||
|
||||
.why { max-width: 760px; }
|
||||
.why p { font-size: 1.08rem; color: #cfcdc7; }
|
||||
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 16px; }
|
||||
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px; transition: border-color .15s, transform .15s; }
|
||||
.card:hover { border-color: var(--hot); transform: translateY(-2px); }
|
||||
.card .icon { font-size: 1.5rem; color: var(--hot); margin-bottom: 6px; }
|
||||
.card h3 { font-size: 1.05rem; }
|
||||
.card p { margin: 0; color: var(--dim); font-size: .95rem; }
|
||||
|
||||
.tabs { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
|
||||
.tab { font: inherit; color: var(--dim); background: var(--panel); border: 1px solid var(--line); border-radius: 999px; padding: 6px 16px; cursor: pointer; }
|
||||
.tab:hover { color: var(--ink); }
|
||||
.tab[aria-selected="true"] { color: #fff; background: var(--hot); border-color: var(--hot); }
|
||||
.shot { margin: 0; }
|
||||
.shot figcaption { color: var(--dim); margin-top: 12px; }
|
||||
|
||||
.two { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; }
|
||||
.fmt { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 12px; }
|
||||
.fmt-label { width: 64px; color: var(--dim); }
|
||||
.chip { background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 2px 10px; font-size: .9rem; }
|
||||
.keys { border-collapse: collapse; width: 100%; }
|
||||
.keys td { padding: 7px 0; border-bottom: 1px solid var(--line); }
|
||||
.keys td:first-child { width: 45%; }
|
||||
kbd { font: .85rem "JetBrains Mono", ui-monospace, Consolas, monospace; background: var(--panel2); border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 5px; padding: 2px 7px; }
|
||||
|
||||
.downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 28px; }
|
||||
.download { display: flex; flex-direction: column; gap: 4px; padding: 18px 20px; border-radius: var(--radius); background: var(--panel); border: 1px solid var(--line); color: var(--ink); }
|
||||
.download:hover { text-decoration: none; border-color: var(--hot); }
|
||||
.download strong { font-size: 1.15rem; color: var(--hot); }
|
||||
.download .file { font: .85rem "JetBrains Mono", ui-monospace, Consolas, monospace; color: #cfcdc7; }
|
||||
.code { position: relative; background: #0f0f11; border: 1px solid var(--line); border-left: 3px solid var(--hot); border-radius: var(--radius); }
|
||||
.code pre { margin: 0; padding: 18px 20px; overflow-x: auto; font: .92rem/1.7 "JetBrains Mono", ui-monospace, Consolas, monospace; color: #d6d4cd; }
|
||||
.copy { position: absolute; top: 10px; right: 10px; font: inherit; font-size: .85rem; color: var(--ink); background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 4px 12px; cursor: pointer; }
|
||||
.copy:hover { border-color: var(--hot); }
|
||||
|
||||
.version { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 18px 22px; margin-bottom: 12px; }
|
||||
.version h3 { margin-bottom: .3em; }
|
||||
.version .v { color: var(--hot); }
|
||||
.version ul { margin: 0; padding-left: 20px; color: #cfcdc7; }
|
||||
|
||||
footer { text-align: center; padding: 40px 16px; border-top: 1px solid var(--line); color: var(--dim); }
|
||||
footer p { margin: 4px 0; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.hero, .two { grid-template-columns: 1fr; }
|
||||
.hero { padding-top: 32px; gap: 28px; }
|
||||
nav a:not(.gh) { display: none; }
|
||||
section { padding: 40px 0; }
|
||||
}
|
||||
Loading…
Reference in a new issue