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: [
|
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: "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: "✎", 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: "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: "⊕", 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: "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: "◫", 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: "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: "◎", 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: "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: "▤", 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: "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: "✐", 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: "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: "▦", 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: "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: "⎙", 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: "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: "☀", 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: "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: "▭", 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: "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: "▯", 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: "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: "⟲", 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: "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: [
|
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 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:
|
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
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
|
@ -43,7 +44,7 @@ new environment.
|
||||||
"Author" refers to any designer, engineer, programmer, technical
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
writer or other person who contributed to the Font Software.
|
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
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
redistribute, and sell modified and unmodified copies of the Font
|
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:description" content="A 3D modeller that does what you'd expect. Pick a tool, click, drag.">
|
||||||
<meta property="og:image" content="img/model.webp">
|
<meta property="og:image" content="img/model.webp">
|
||||||
<link rel="icon" href="img/icon.png">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- The page is built from content.js by site.js: edit content.js to update it. -->
|
<!-- The page is built from content.js by site.js: edit content.js to update it. -->
|
||||||
<header class="top">
|
<nav class="nv-nav" id="top-nav" aria-label="Main">
|
||||||
<a class="brand" href="#"><img src="img/logo.svg" alt=""><span>Fry3DX</span></a>
|
<a class="nv-nav__brand" href="#top"><img class="brand-logo" src="img/logo.svg" alt=""> Fry3DX</a>
|
||||||
<nav>
|
<ul class="nv-nav__list nav-links">
|
||||||
<a href="#features">Features</a>
|
<li><a class="nv-nav__item" href="#features">Features</a></li>
|
||||||
<a href="#look">Look</a>
|
<li><a class="nv-nav__item" href="#look">Look</a></li>
|
||||||
<a href="#get">Get it</a>
|
<li><a class="nv-nav__item" href="#scripting">Scripting</a></li>
|
||||||
<a href="#versions">Versions</a>
|
<li><a class="nv-nav__item" href="#get">Get it</a></li>
|
||||||
<a class="gh" id="gh-top" href="#">GitHub</a>
|
<li><a class="nv-nav__item" href="#versions">Versions</a></li>
|
||||||
</nav>
|
</ul>
|
||||||
</header>
|
<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>
|
<main id="top">
|
||||||
<section class="hero">
|
<header class="shell hero">
|
||||||
<div class="hero-text">
|
<div class="hero__copy">
|
||||||
<p class="kicker" id="long"></p>
|
<span class="nv-badge nv-badge--accent"><span class="nv-badge__dot"></span><span id="latest"></span></span>
|
||||||
<h1 id="tagline"></h1>
|
<p class="nv-overline" id="long"></p>
|
||||||
<p class="lead" id="intro"></p>
|
<h1 class="nv-display hero__title" id="tagline"></h1>
|
||||||
<div class="buttons">
|
<p class="nv-prose nv-text-secondary" id="intro"></p>
|
||||||
<a class="btn hot" href="#get">Get Fry3DX</a>
|
<div class="nv-cluster">
|
||||||
<a class="btn" id="gh-hero" href="#">Source on GitHub</a>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<figure class="hero-shot"><img src="img/model.webp" alt="Fry3DX with a table, chair, barrel and crate"></figure>
|
</header>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="why" id="why"></section>
|
<section class="shell section">
|
||||||
|
<div class="nv-card nv-card--inset why">
|
||||||
<section id="features">
|
<div class="nv-card__body nv-stack">
|
||||||
<h2>What it does</h2>
|
<h2 class="nv-heading" id="why-title"></h2>
|
||||||
<div class="grid" id="feature-grid"></div>
|
<div class="nv-prose nv-stack" id="why-text"></div>
|
||||||
</section>
|
</div>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="get">
|
<section class="shell section" id="features">
|
||||||
<h2>Get it</h2>
|
<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>
|
<div class="downloads" id="downloads"></div>
|
||||||
<p id="build-text"></p>
|
<div class="nv-card nv-card--inset build">
|
||||||
<div class="code"><button class="copy" id="copy">Copy</button><pre id="build-steps"></pre></div>
|
<div class="nv-card__body nv-stack">
|
||||||
<p class="muted" id="build-needs"></p>
|
<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>
|
||||||
|
|
||||||
<section id="versions">
|
<section class="shell section" id="versions">
|
||||||
<h2>Versions</h2>
|
<div class="section__head">
|
||||||
<div id="version-list"></div>
|
<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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer class="shell footer">
|
||||||
<p>Fry3DX, made by <a id="author" href="#"></a>.</p>
|
<img class="footer__logo" src="img/logo.svg" alt="">
|
||||||
<p class="muted">Open source under the Apache License 2.0 · Sky pictures from Poly Haven (CC0) · Fonts: Inter, JetBrains Mono (OFL).</p>
|
<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>
|
</footer>
|
||||||
|
|
||||||
<script src="content.js"></script>
|
<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); }
|
||||||
195
docs/site.js
195
docs/site.js
|
|
@ -9,91 +9,176 @@
|
||||||
return e;
|
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;
|
$("long").textContent = F.long;
|
||||||
$("tagline").textContent = F.tagline;
|
$("tagline").textContent = F.tagline;
|
||||||
$("intro").textContent = F.intro;
|
$("intro").textContent = F.intro;
|
||||||
for (const id of ["gh-top", "gh-hero"]) $(id).href = F.repo;
|
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").textContent = F.author;
|
||||||
$("author").href = F.authorUrl;
|
$("author").href = F.authorUrl;
|
||||||
|
|
||||||
const why = $("why");
|
$("why-title").textContent = F.why.title;
|
||||||
why.append(el("h2", null, F.why.title));
|
for (const p of F.why.text) $("why-text").append(el("p", "nv-body", p));
|
||||||
for (const p of F.why.text) why.append(el("p", null, p));
|
|
||||||
|
|
||||||
const grid = $("feature-grid");
|
// ── Features ──
|
||||||
for (const f of F.features) {
|
for (const f of F.features) {
|
||||||
const card = el("article", "card");
|
const card = el("article", "nv-card feature");
|
||||||
card.append(el("div", "icon", f.icon), el("h3", null, f.title), el("p", null, f.text));
|
const body = el("div", "nv-card__body");
|
||||||
grid.append(card);
|
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.
|
// ── Screenshots: a tab each, the picture in a window ──
|
||||||
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));
|
|
||||||
};
|
|
||||||
F.shots.forEach((s, i) => {
|
F.shots.forEach((s, i) => {
|
||||||
const b = el("button", "tab", s.title);
|
const tab = el("button", "nv-nav__item", s.title);
|
||||||
b.setAttribute("role", "tab");
|
Object.assign(tab, { id: `shot-tab-${i}`, role: "tab", tabIndex: i ? -1 : 0 });
|
||||||
b.onclick = () => show(i);
|
tab.setAttribute("role", "tab");
|
||||||
tabs.append(b);
|
tab.setAttribute("aria-controls", `shot-${i}`);
|
||||||
new Image().src = s.img; // (loaded ahead, so switching is instant)
|
tab.setAttribute("aria-selected", String(i === 0));
|
||||||
});
|
$("shot-tabs").append(tab);
|
||||||
show(0);
|
|
||||||
|
|
||||||
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 row = (label, items) => {
|
||||||
const d = el("div", "fmt");
|
const d = el("div", "fmt");
|
||||||
d.append(el("span", "fmt-label", label));
|
d.append(el("span", "nv-overline", label));
|
||||||
for (const x of items) d.append(el("span", "chip", x));
|
const c = el("div", "nv-cluster nv-cluster--tight");
|
||||||
fm.append(d);
|
for (const x of items) c.append(el("span", "nv-badge", x));
|
||||||
|
d.append(c);
|
||||||
|
$("formats").append(d);
|
||||||
};
|
};
|
||||||
row("Save", F.formats.save);
|
row("Save", F.formats.save);
|
||||||
row("Export", F.formats.export);
|
row("Export", F.formats.export);
|
||||||
fm.append(el("p", "muted", F.formats.note));
|
$("formats").append(el("p", "nv-caption nv-text-tertiary", F.formats.note));
|
||||||
|
|
||||||
const keys = $("keys");
|
|
||||||
for (const [k, what] of F.keys) {
|
for (const [k, what] of F.keys) {
|
||||||
const tr = el("tr");
|
const tr = el("tr");
|
||||||
const td = el("td");
|
const td = el("td");
|
||||||
td.append(el("kbd", null, k));
|
td.append(el("kbd", null, k));
|
||||||
tr.append(td, el("td", null, what));
|
tr.append(td, el("td", "nv-body", what));
|
||||||
keys.append(tr);
|
$("keys").append(tr);
|
||||||
}
|
}
|
||||||
|
|
||||||
const dl = $("downloads");
|
// ── Downloads and building ──
|
||||||
for (const d of F.downloads) {
|
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.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));
|
const b = el("div", "nv-card__body");
|
||||||
dl.append(a);
|
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-text").textContent = F.build.text;
|
||||||
$("build-steps").textContent = F.build.steps.join("\n");
|
$("build-steps").textContent = F.build.steps.join("\n");
|
||||||
$("build-needs").textContent = F.build.needs;
|
$("build-needs").textContent = F.build.needs;
|
||||||
$("copy").onclick = async () => {
|
for (const btn of document.querySelectorAll("[data-copy]")) {
|
||||||
try {
|
btn.onclick = async () => {
|
||||||
await navigator.clipboard.writeText(F.build.steps.join("\n"));
|
try {
|
||||||
$("copy").textContent = "Copied";
|
await navigator.clipboard.writeText($(btn.dataset.copy).textContent);
|
||||||
} catch {
|
btn.textContent = "Copied";
|
||||||
$("copy").textContent = "Select and copy";
|
} catch {
|
||||||
}
|
btn.textContent = "Select and copy";
|
||||||
setTimeout(() => ($("copy").textContent = "Copy"), 1800);
|
}
|
||||||
};
|
setTimeout(() => (btn.textContent = "Copy"), 1800);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const vl = $("version-list");
|
// ── Versions ──
|
||||||
for (const v of F.versions) {
|
F.versions.forEach((v, i) => {
|
||||||
const d = el("div", "version");
|
const d = el("details", "nv-disclosure");
|
||||||
const h = el("h3");
|
d.open = i === 0;
|
||||||
h.append(el("span", "v", "v" + v.v), el("span", "muted", " " + v.date));
|
const s = el("summary", "nv-disclosure__summary");
|
||||||
const ul = el("ul");
|
s.append(el("span", "nv-title", "v" + v.v), el("span", "nv-caption nv-text-tertiary", " " + v.date));
|
||||||
for (const it of v.items) ul.append(el("li", null, it));
|
const ch = el("span", "nv-disclosure__chevron");
|
||||||
d.append(h, ul);
|
ch.setAttribute("aria-hidden", "true");
|
||||||
vl.append(d);
|
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