This commit is contained in:
pj committed 2026-06-22 12:40:29 -04:00
1 parent 8b83f80d52
commit 72d698e427
16 files changed
+922 -199

No files matched your search

+288 -99
View File
@@ -1,37 +1,70 @@
---
// A faithful CSS mock-up of the Margin desktop app: chapter sidebar, the
// writing surface set in Literata, and the live PDF preview dock. The editor
// shows live proofing underlines; the PDF dock shows the clean typeset page.
// A faithful CSS mock-up of the Margin desktop app: the All-books sidebar with
// front-matter / chapter sections, the writing surface set in Literata with live
// proofing, and the live PDF preview dock with its trim selector.
---
<div class="win" role="img" aria-label="The Margin app: a chapter sidebar, a writing surface with live proofing, and a live PDF page preview.">
<div class="win__bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<span class="win__chrome"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span>
<span class="win__title">The Lighthouse Keeper</span>
<span class="win__menu">Export ▾</span>
<span class="win__actions">
<span class="ibtn"><svg class="i" viewBox="0 0 24 24"><path d="M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4" /></svg></span>
<span class="ibtn"><svg class="i" viewBox="0 0 24 24"><path d="M4 17l4-10 4 10M5.4 13.4h5.2M15 17l2.5 2.5L22 14" /></svg></span>
<span class="ibtn"><svg class="i" viewBox="0 0 24 24"><path d="M4 7h16M4 12h12M4 17h7M13.5 18.5c1-1.2 2-1.2 3 0s2 1.2 3 0" /></svg></span>
<span class="ibtn"><svg class="i" viewBox="0 0 24 24"><path d="M3 5v14M21 5v14M7 12h10M7 12l3-3M7 12l3 3M17 12l-3-3M17 12l-3 3" /></svg></span>
<span class="ibtn"><svg class="i" viewBox="0 0 24 24"><path d="M5 13v6h14v-6M12 16V3M8 7l4-4 4 4" /></svg></span>
<span class="ibtn"><svg class="i" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /></svg></span>
<span class="ibtn is-on"><svg class="i" viewBox="0 0 24 24"><path d="M3 4.5h18v15H3zM14 4.5v15" /></svg></span>
</span>
</div>
<div class="win__body">
<aside class="side">
<p class="side__label">Manuscript</p>
<ul>
<li>Title Page</li>
<li>Dedication</li>
<li class="is-active">1 · The Cliff</li>
<li>2 · A Light at Sea</li>
<li>3 · Salt and Silence</li>
<li>4 · The Long Night</li>
</ul>
<p class="side__label">Book setup</p>
<ul class="side__meta">
<li>Trim · 5 × 8 in</li>
<li>Language · English</li>
</ul>
<span class="side__brand">
<svg class="i" viewBox="0 0 24 24"><path d="M14 7l-5 5 5 5" /></svg>All books
</span>
<span class="side__cover">
<svg class="i" viewBox="0 0 24 24"><path d="M5 4h11l3 3v13H5zM16 4v4h3" /></svg>Cover
</span>
<span class="side__add">
<svg class="i" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14" /></svg>Add page
</span>
<div class="side__scroll">
<div class="side__section">
<p class="side__label">Front matter</p>
<ul class="side__list">
<li class="row"><span class="grip"></span><span class="num"></span><span class="text"><span class="title">Title Page</span></span></li>
<li class="row"><span class="grip"></span><span class="num"></span><span class="text"><span class="title">Dedication</span></span></li>
</ul>
</div>
<div class="side__section">
<p class="side__label">Chapters</p>
<ul class="side__list">
<li class="row is-active">
<span class="grip"><svg class="i" viewBox="0 0 24 24"><path d="M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01" /></svg></span>
<span class="num">1</span>
<span class="text">
<span class="title">The Cliff</span>
<span class="meta">Edited yesterday</span>
</span>
</li>
<li class="row"><span class="grip"></span><span class="num">2</span><span class="text"><span class="title">A Light at Sea</span></span></li>
<li class="row"><span class="grip"></span><span class="num">3</span><span class="text"><span class="title">Salt and Silence</span></span></li>
<li class="row"><span class="grip"></span><span class="num">4</span><span class="text"><span class="title">The Long Night</span></span></li>
</ul>
<span class="side__newchapter">
<svg class="i" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14" /></svg>New chapter
</span>
</div>
</div>
</aside>
<main class="page">
<div class="page__sheet">
<p class="chapno">Chapter One</p>
<p class="chapno">Chapter 1</p>
<h4 class="chaptitle">The Cliff</h4>
<p>
The lighthouse had stood on the headland for ninety years, and in all that time it had
@@ -47,21 +80,36 @@
</main>
<aside class="dock">
<p class="dock__label">Live PDF · pg 7</p>
<div class="dock__pdf">
<span class="rh">THE LIGHTHOUSE KEEPER</span>
<p class="pch">Chapter One</p>
<p class="pdt">The Cliff</p>
<p class="pp pp--first">
The lighthouse had stood on the headland for ninety years, and in all that time it had
never once gone dark. Mara intended to keep it that way.
</p>
<p class="pp">
She climbed the iron stair each evening at dusk, counting the hundred and one steps out
of habit, and watched the lamp turn its slow circle across the water.
</p>
<p class="pp">The sea, tonight, was the colour of pewter.</p>
<span class="pg">7</span>
<div class="dock__head">
<span class="dock__label">Preview</span>
<span class="dock__trim">
5 × 8 in
<svg class="i" viewBox="0 0 24 24"><path d="M8 10l4-4 4 4M8 14l4 4 4-4" /></svg>
</span>
</div>
<div class="dock__pages">
<div class="dock__page">
<p class="pch">Chapter 1</p>
<p class="pdt">The Cliff</p>
<p class="pp pp--first">
The lighthouse had stood on the headland for ninety years, and in all that time it had
never once gone dark. Mara intended to keep it that way.
</p>
<p class="pp">
She climbed the iron stair each evening at dusk, counting the hundred and one steps out
of habit, and watched the lamp turn its slow circle across the water.
</p>
<p class="pp">The sea, tonight, was the colour of pewter.</p>
<span class="pg">1</span>
</div>
<div class="dock__page dock__page--peek">
<p class="pp pp--first">
Morning came grey and slow. She brewed coffee in the keeper's kitchen and listened to the
gulls argue over the rocks below.
</p>
<p class="pp">The radio crackled once, then fell silent.</p>
</div>
</div>
</aside>
</div>
@@ -76,115 +124,231 @@
box-shadow: var(--shadow-deep);
overflow: hidden;
}
.i {
width: 15px;
height: 15px;
flex: none;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.win__bar {
display: flex;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: 8px;
height: 40px;
padding-inline: 16px;
height: 44px;
padding-inline: 14px;
border-bottom: 1px solid var(--line);
background: var(--sidebar);
font-size: 0.78rem;
color: var(--ink-faint);
background: var(--shell);
}
.win__chrome {
display: flex;
gap: 8px;
}
.dot {
width: 11px;
height: 11px;
border-radius: 50%;
}
.win__bar .dot:nth-child(1) {
.win__chrome .dot:nth-child(1) {
background: #ff5f57;
}
.win__bar .dot:nth-child(2) {
.win__chrome .dot:nth-child(2) {
background: #febc2e;
}
.win__bar .dot:nth-child(3) {
.win__chrome .dot:nth-child(3) {
background: #28c840;
}
.win__title {
margin-inline: auto;
font-weight: 500;
}
.win__menu {
grid-column: 2;
font-size: 0.82rem;
letter-spacing: 0.01em;
color: var(--ink-soft);
font-weight: 500;
}
.win__actions {
grid-column: 3;
justify-self: end;
display: flex;
gap: 2px;
}
.ibtn {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: var(--r-sm);
color: var(--ink-soft);
}
.ibtn.is-on {
color: var(--accent);
background: var(--accent-wash);
box-shadow: inset 0 0 0 1px var(--line-strong);
}
.win__body {
display: grid;
grid-template-columns: 172px 1fr 196px;
min-height: 392px;
grid-template-columns: 196px 1fr 200px;
min-height: 432px;
}
.side {
display: flex;
flex-direction: column;
background: var(--sidebar);
border-right: 1px solid var(--line);
padding: 16px 12px;
padding: 12px 10px;
font-size: 0.8rem;
}
.side__brand,
.side__cover,
.side__add,
.side__newchapter {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: var(--r-sm);
color: var(--ink-soft);
}
.side__brand {
color: var(--ink-faint);
font-size: 0.74rem;
}
.side__cover {
color: var(--ink);
margin-top: 2px;
}
.side__add {
color: var(--ink-faint);
margin-bottom: 8px;
}
.side__scroll {
flex: 1;
}
.side__section + .side__section {
margin-top: 16px;
}
.side__label {
font-size: 0.66rem;
letter-spacing: 0.1em;
font-size: 0.62rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-faint);
font-weight: 650;
margin: 0 0 8px;
margin: 0 0 6px;
padding-inline: 8px;
}
.side ul {
.side__list {
list-style: none;
margin: 0 0 18px;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
gap: 1px;
}
.side li {
.row {
display: grid;
grid-template-columns: 14px 16px 1fr;
align-items: center;
gap: 4px;
padding: 5px 8px;
border-radius: var(--r-sm);
color: var(--ink-soft);
}
.side li.is-active {
background: var(--accent-wash);
color: var(--ink);
font-weight: 600;
}
.side__meta li {
.row .grip {
color: var(--ink-faint);
font-size: 0.74rem;
opacity: 0;
display: grid;
place-items: center;
}
.row .grip .i {
width: 13px;
height: 13px;
}
.row .num {
font-size: 0.72rem;
color: var(--ink-faint);
text-align: center;
}
.row .text {
display: flex;
flex-direction: column;
min-width: 0;
}
.row .title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.row .meta {
font-size: 0.66rem;
color: var(--ink-faint);
margin-top: 1px;
}
.row.is-active {
background: var(--raised);
color: var(--ink);
box-shadow: 0 1px 2px rgba(35, 32, 27, 0.08), inset 0 0 0 1px var(--line);
}
.row.is-active .grip {
opacity: 1;
}
.row.is-active .num {
color: var(--ink-soft);
}
.side__newchapter {
color: var(--ink-faint);
font-size: 0.76rem;
margin-top: 4px;
}
.page {
display: flex;
justify-content: center;
padding: 26px 18px;
padding: 30px 18px;
overflow: hidden;
}
.page__sheet {
width: 100%;
max-width: 360px;
max-width: 372px;
font-family: var(--font-book);
font-size: 0.92rem;
line-height: 1.62;
line-height: 1.6;
color: var(--ink);
}
.page__sheet p {
margin: 0 0 0.7em;
text-indent: 1.1em;
margin: 0;
text-indent: 1.3em;
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
}
.page__sheet p + p {
margin-top: 0.2em;
}
.chapno {
text-indent: 0 !important;
text-align: center;
text-align: center !important;
font-family: var(--font-ui);
font-size: 0.62rem;
letter-spacing: 0.18em;
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--ink-faint);
margin-bottom: 0.4em !important;
margin: 0 0 0.5em !important;
}
.chaptitle {
text-indent: 0;
text-align: center;
font-size: 1.5rem;
font-weight: 560;
margin: 0 0 1.1em;
font-weight: 540;
letter-spacing: -0.01em;
margin: 0 0 1.4em;
}
.chaptitle + p {
text-indent: 0;
}
.u-spell {
text-decoration: wavy underline var(--danger);
@@ -200,17 +364,45 @@
.dock {
background: var(--shell);
border-left: 1px solid var(--line);
padding: 16px 14px;
padding: 14px 14px 0;
overflow: hidden;
}
.dock__head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.dock__label {
font-size: 0.66rem;
letter-spacing: 0.1em;
font-size: 0.62rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--ink-faint);
font-weight: 650;
margin: 0 0 12px;
}
.dock__pdf {
.dock__trim {
display: inline-flex;
align-items: center;
gap: 4px;
font-family: var(--font-ui);
font-size: 0.66rem;
color: var(--ink-soft);
padding: 3px 5px 3px 8px;
border: 1px solid var(--line-strong);
border-radius: var(--r-sm);
background: var(--raised);
}
.dock__trim .i {
width: 12px;
height: 12px;
color: var(--ink-faint);
}
.dock__pages {
display: flex;
flex-direction: column;
gap: 14px;
}
.dock__page {
position: relative;
aspect-ratio: 5 / 8;
background: #fcfbf7;
@@ -219,26 +411,18 @@
padding: 18px 16px 22px;
overflow: hidden;
color: #2a2622;
flex: none;
}
.dock__pdf .rh {
display: block;
font-family: var(--font-ui);
font-size: 4.4px;
letter-spacing: 0.16em;
color: #b3ab9c;
text-align: center;
margin-bottom: 14px;
}
.dock__pdf .pch {
.dock__page .pch {
font-family: var(--font-ui);
font-size: 4px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: #9b9484;
text-align: center;
margin: 0 0 4px;
margin: 6px 0 4px;
}
.dock__pdf .pdt {
.dock__page .pdt {
font-family: var(--font-book);
font-size: 9px;
font-weight: 560;
@@ -246,19 +430,21 @@
text-align: center;
margin: 0 0 11px;
}
.dock__pdf .pp {
.dock__page .pp {
font-family: var(--font-book);
font-size: 5.6px;
line-height: 1.62;
text-align: justify;
hyphens: auto;
-webkit-hyphens: auto;
text-indent: 1.3em;
margin: 0 0 0.5em;
color: #2a2622;
}
.dock__pdf .pp--first {
.dock__page .pp--first {
text-indent: 0;
}
.dock__pdf .pg {
.dock__page .pg {
position: absolute;
bottom: 9px;
left: 0;
@@ -268,10 +454,13 @@
font-size: 5px;
color: #b3ab9c;
}
.dock__page--peek {
padding-top: 18px;
}
@media (max-width: 720px) {
.win__body {
grid-template-columns: 1fr 168px;
grid-template-columns: 1fr 180px;
}
.side {
display: none;
-6
View File
@@ -38,12 +38,6 @@ import OfferBadge from "./OfferBadge.astro";
justify-content: center;
margin-top: 32px;
}
.cta__btns :global(.dl) {
align-items: center;
}
.cta__btns :global(.dl__os) {
justify-content: center;
}
.cta__offer {
display: flex;
justify-content: center;
+13 -16
View File
@@ -22,7 +22,6 @@ const { large = false } = Astro.props;
</a>
<div class="dl__os">
<span class="dl__also">Also for</span>
{
platforms.map((p) => (
<a
@@ -32,7 +31,7 @@ const { large = false } = Astro.props;
rel="noopener"
download
aria-label={`Download for ${p.label}`}
title={p.label}
title={`Download for ${p.label}`}
>
<img class="dl__logo" src={`/logos/${p.logo}`} alt={p.label} />
</a>
@@ -45,38 +44,36 @@ const { large = false } = Astro.props;
.dl {
display: flex;
flex-direction: column;
gap: 14px;
align-items: flex-start;
align-items: center;
gap: 16px;
}
.dl__os {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.dl__also {
font-size: 0.82rem;
color: var(--ink-faint);
font-weight: 500;
justify-content: center;
gap: 12px;
}
.dl__chip {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
width: 44px;
height: 44px;
background: #ffffff;
border: 1px solid var(--line-strong);
border-radius: var(--r-md);
transition: box-shadow 0.18s var(--ease), transform 0.18s var(--ease);
box-shadow: var(--shadow-page);
transition:
box-shadow 0.18s var(--ease),
transform 0.18s var(--ease);
}
.dl__chip:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-pop);
}
.dl__logo {
width: 20px;
height: 20px;
width: 22px;
height: 22px;
object-fit: contain;
}
</style>
@@ -36,6 +36,18 @@ const { repo, releasesUrl } = site;
if (label) label.textContent = "Download for " + LABELS[os];
});
document.querySelectorAll(".dl__alt [data-download-os]").forEach((el) => {
if (el.dataset.downloadOs === os) el.remove();
});
document.querySelectorAll("[data-osrow]").forEach((row) => {
const primary = row.querySelector(`[data-download-os="${os}"]`);
if (primary) {
primary.classList.add("is-detected");
row.prepend(primary);
}
});
(async () => {
try {
const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, {
+89 -26
View File
@@ -1,7 +1,7 @@
---
import AppWindow from "./AppWindow.astro";
import DownloadButtons from "./DownloadButtons.astro";
import OfferBadge from "./OfferBadge.astro";
import { site, platforms } from "../data/site";
---
<section class="hero">
@@ -11,63 +11,126 @@ import OfferBadge from "./OfferBadge.astro";
<span class="book-accent">Own</span> every word.
</h1>
<p class="lede hero__lede">
Margin is a calm, offline writing studio for authors. Write your book, format it
beautifully, and send it to every store and to print, all from your own machine. No
subscription, no account. Buy once, own it forever.
A calm, offline studio to write, format, and publish your book to every store and to
print. No subscription, no account. Buy once, own it forever.
</p>
<div class="hero__cta">
<DownloadButtons large />
</div>
<div class="hero__offer">
<OfferBadge />
</div>
</div>
<div class="container hero__art">
<AppWindow />
</div>
<div class="container hero__get" id="download">
<div class="hero__buttons" data-osrow>
{
platforms.map((p) => (
<a
class="osbtn"
href={site.releasesUrl}
data-download-os={p.id}
rel="noopener"
download
>
<img class="osbtn__icon" src={`/logos/${p.logo}`} alt="" />
<span>Download for {p.label}</span>
</a>
))
}
</div>
<p class="hero__meta">
<span data-version>v0.1.2</span> · Works offline · No account
</p>
<div class="hero__offer">
<OfferBadge />
</div>
</div>
</section>
<style>
.hero {
padding-top: clamp(48px, 7vw, 92px);
padding-bottom: clamp(40px, 6vw, 72px);
padding-top: clamp(36px, 4.5vw, 60px);
padding-bottom: clamp(48px, 6vw, 80px);
background:
radial-gradient(120% 90% at 50% -10%, var(--paper) 0%, transparent 60%),
var(--shell);
}
.hero__head {
text-align: center;
max-width: 760px;
max-width: 720px;
}
h1 {
font-size: clamp(2.6rem, 1.6rem + 4.4vw, 4.5rem);
font-size: clamp(2.4rem, 1.6rem + 3.4vw, 3.9rem);
letter-spacing: -0.02em;
margin-bottom: 22px;
margin-bottom: 18px;
}
.hero__lede {
margin-inline: auto;
max-width: 56ch;
max-width: 50ch;
}
.hero__cta {
.hero__art {
margin-top: clamp(32px, 4vw, 48px);
max-width: 1000px;
}
.hero__get {
margin-top: clamp(24px, 3vw, 32px);
scroll-margin-top: 96px;
}
.hero__buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 34px;
gap: 12px;
}
.hero__cta :global(.dl) {
.osbtn {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 0.82em 1.35em;
background: #ffffff;
border: 1px solid var(--line-strong);
border-radius: 999px;
color: #23201b;
font-family: var(--font-ui);
font-size: 1rem;
font-weight: 600;
box-shadow: var(--shadow-page);
transition:
transform 0.18s var(--ease),
box-shadow 0.18s var(--ease),
border-color 0.18s var(--ease);
}
.hero__cta :global(.dl__os) {
justify-content: center;
.osbtn:hover {
transform: translateY(-1px);
box-shadow: var(--shadow-pop);
}
.osbtn.is-detected {
border-color: var(--ink);
}
.osbtn__icon {
width: 18px;
height: 18px;
flex: none;
object-fit: contain;
}
.hero__meta {
margin: 16px 0 0;
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
font-size: 0.8rem;
color: var(--ink-faint);
text-align: center;
}
.hero__offer {
display: flex;
justify-content: center;
margin-top: 30px;
margin-top: 22px;
}
.hero__art {
margin-top: clamp(44px, 6vw, 72px);
max-width: 1000px;
@media (max-width: 480px) {
.hero__buttons {
flex-direction: column;
align-items: stretch;
}
.osbtn {
justify-content: center;
}
}
</style>
+14
View File
@@ -19,6 +19,12 @@ import Logo from "./Logo.astro";
</nav>
<div class="nav__actions">
<a class="btn nav__cta" href="#download">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
<path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
Download
</a>
<button class="iconlink" id="theme-toggle" aria-label="Toggle theme" type="button">
<svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" aria-hidden="true">
<circle cx="12" cy="12" r="4"></circle>
@@ -89,6 +95,14 @@ import Logo from "./Logo.astro";
align-items: center;
gap: 10px;
}
.nav__cta {
padding: 0.5em 1.05em;
font-size: 0.9rem;
}
.nav__cta svg {
width: 1em;
height: 1em;
}
.iconlink {
display: inline-flex;
align-items: center;
-1
View File
@@ -20,7 +20,6 @@ const { cap, left } = site.launch;
border-radius: 999px;
background: color-mix(in srgb, var(--good) 12%, var(--paper));
border: 1px solid color-mix(in srgb, var(--good) 32%, transparent);
box-shadow: var(--shadow-page);
font-size: 0.9rem;
font-weight: 600;
color: var(--ink);
-6
View File
@@ -182,12 +182,6 @@ const included = [
display: flex;
justify-content: center;
}
.price-card__cta :global(.dl) {
align-items: center;
}
.price-card__cta :global(.dl__os) {
justify-content: center;
}
.price-card__note {
margin-top: 26px;
font-size: 0.84rem;
+1 -1
View File
@@ -16,7 +16,7 @@ export const site = {
left: 12,
},
supportEmail: "[email protected]",
supportEmail: "[email protected]",
// Repo that hosts the release files. Used only to resolve the right download
// per OS at runtime. It is never shown to the reader.