mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-02 11:07:04 +00:00
changes
This commit is contained in:
1 parent
8b83f80d52
commit
72d698e427
16 files changed
+905
-182
No files matched your search
Generated
+1
-1
@@ -4704,7 +4704,7 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||
|
||||
[[package]]
|
||||
name = "margin-app"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"harper-core",
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import type { Device } from "../devices";
|
||||
import type { Book } from "../model/book";
|
||||
import { CoverArt } from "./CoverArt";
|
||||
|
||||
interface DeviceFrameProps {
|
||||
device: Device;
|
||||
eyebrow?: ReactNode;
|
||||
title?: ReactNode;
|
||||
html?: string;
|
||||
cover?: Book;
|
||||
}
|
||||
|
||||
function frameVars(device: Device): CSSProperties {
|
||||
return {
|
||||
"--dv-bg": device.bg,
|
||||
"--dv-ink": device.ink,
|
||||
"--dv-fs": device.fontSize,
|
||||
"--dv-lh": device.lineHeight,
|
||||
"--dv-mx": device.marginX,
|
||||
"--dv-my": device.marginY,
|
||||
"--dv-bezel": device.bezel,
|
||||
"--dv-bezel-w": device.bezelWidth,
|
||||
"--dv-radius": device.radius,
|
||||
"--dv-aspect": device.aspect,
|
||||
} as CSSProperties;
|
||||
}
|
||||
|
||||
function StatusBar() {
|
||||
return (
|
||||
<div className="device-statusbar">
|
||||
<span className="device-clock">9:41</span>
|
||||
<span className="device-batt" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DeviceFrame({ device, eyebrow, title, html, cover }: DeviceFrameProps) {
|
||||
return (
|
||||
<div className="device-stage">
|
||||
<div className="device-frame" data-kind={device.kind} style={frameVars(device)}>
|
||||
<div className="device-screen">
|
||||
{device.kind === "tablet" && <StatusBar />}
|
||||
<div className="device-content">
|
||||
{cover ? (
|
||||
<CoverArt book={cover} />
|
||||
) : (
|
||||
<>
|
||||
<div className="device-opener">
|
||||
{eyebrow && <div className="device-eyebrow">{eyebrow}</div>}
|
||||
{title && <div className="device-title">{title}</div>}
|
||||
</div>
|
||||
<div className="device-body" dangerouslySetInnerHTML={{ __html: html ?? "" }} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{device.kind === "eink" && (
|
||||
<div className="device-footer">
|
||||
<span className="device-foot-title">{cover ? device.label : title}</span>
|
||||
<span className="device-batt" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+109
-37
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { generateHTML } from "@tiptap/core";
|
||||
import { COVER_ID, useBook } from "../store/useBook";
|
||||
import { type TrimSize, bodyNumber, chapterKind } from "../model/book";
|
||||
import { type Chapter, type TrimSize, bodyNumber, chapterKind } from "../model/book";
|
||||
import { editorExtensions } from "../editor/extensions";
|
||||
import { chapterToPdfInputs, coverToPdfInputs } from "../export/typst";
|
||||
import { compilePdf, isDesktop } from "../ipc";
|
||||
import { DEVICES, type Device, findDevice } from "../devices";
|
||||
import { usePreviewMode } from "../store/usePreviewMode";
|
||||
import { PdfPreview } from "./PdfPreview";
|
||||
import { CoverArt } from "./CoverArt";
|
||||
import { DeviceFrame } from "./DeviceFrame";
|
||||
|
||||
const TRIMS: { value: TrimSize; label: string }[] = [
|
||||
{ value: "6x9", label: "6 × 9 in" },
|
||||
@@ -15,26 +18,77 @@ const TRIMS: { value: TrimSize; label: string }[] = [
|
||||
{ value: "a5", label: "A5" },
|
||||
];
|
||||
|
||||
function TrimSelect() {
|
||||
function PreviewSelect() {
|
||||
const trim = useBook((s) => s.book?.settings.trim ?? "6x9");
|
||||
const setSettings = useBook((s) => s.setSettings);
|
||||
const mode = usePreviewMode((s) => s.mode);
|
||||
const setMode = usePreviewMode((s) => s.setMode);
|
||||
const value = mode === "print" ? trim : mode;
|
||||
return (
|
||||
<select
|
||||
className="trim-select"
|
||||
value={trim}
|
||||
onChange={(e) => setSettings({ trim: e.target.value as TrimSize })}
|
||||
title="Trim size"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
if (findDevice(v)) {
|
||||
setMode(v as Device["id"]);
|
||||
} else {
|
||||
setSettings({ trim: v as TrimSize });
|
||||
setMode("print");
|
||||
}
|
||||
}}
|
||||
title="Preview format"
|
||||
>
|
||||
<optgroup label="Print">
|
||||
{TRIMS.map((t) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
<optgroup label="Devices">
|
||||
{DEVICES.map((d) => (
|
||||
<option key={d.id} value={d.id}>
|
||||
{d.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
function DockHead({ meta }: { meta?: ReactNode }) {
|
||||
return (
|
||||
<div className="dock-head">
|
||||
<span className="label">Preview</span>
|
||||
<div className="dock-tools">
|
||||
{meta}
|
||||
<PreviewSelect />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function chapterEyebrow(chapter: Chapter, chapters: Chapter[], idx: number): string {
|
||||
const kind = chapterKind(chapter);
|
||||
return kind === "body" ? `Chapter ${bodyNumber(chapters, idx) ?? ""}` : kind === "front" ? "Front matter" : "Back matter";
|
||||
}
|
||||
|
||||
function useActiveChapter() {
|
||||
const book = useBook((s) => s.book);
|
||||
const activeChapterId = useBook((s) => s.activeChapterId);
|
||||
const chapters = book?.chapters ?? [];
|
||||
const idx = chapters.findIndex((c) => c.id === activeChapterId);
|
||||
const chapter = chapters[idx] ?? chapters[0];
|
||||
const html = useMemo(() => (chapter ? generateHTML(chapter.content, editorExtensions) : ""), [chapter]);
|
||||
const eyebrow = chapter ? chapterEyebrow(chapter, chapters, idx) : "";
|
||||
return { book, coverActive: activeChapterId === COVER_ID, chapters, idx, chapter, html, eyebrow };
|
||||
}
|
||||
|
||||
export function Dock() {
|
||||
const mode = usePreviewMode((s) => s.mode);
|
||||
const device = findDevice(mode);
|
||||
if (device) return <DeviceDock device={device} />;
|
||||
return isDesktop ? <PdfDock /> : <HtmlDock />;
|
||||
}
|
||||
|
||||
@@ -67,13 +121,7 @@ function PdfDock() {
|
||||
|
||||
return (
|
||||
<section className="dock">
|
||||
<div className="dock-head">
|
||||
<span className="label">Preview</span>
|
||||
<div className="dock-tools">
|
||||
{busy && <span className="meta">updating…</span>}
|
||||
<TrimSelect />
|
||||
</div>
|
||||
</div>
|
||||
<DockHead meta={busy ? <span className="meta">updating…</span> : null} />
|
||||
{error ? (
|
||||
<pre className="dock-error">{error}</pre>
|
||||
) : pdf ? (
|
||||
@@ -86,26 +134,14 @@ function PdfDock() {
|
||||
}
|
||||
|
||||
function HtmlDock() {
|
||||
const book = useBook((s) => s.book);
|
||||
const activeChapterId = useBook((s) => s.activeChapterId);
|
||||
const coverActive = activeChapterId === COVER_ID;
|
||||
const chapters = book?.chapters ?? [];
|
||||
const idx = chapters.findIndex((c) => c.id === activeChapterId);
|
||||
const chapter = chapters[idx] ?? chapters[0];
|
||||
const html = useMemo(() => (chapter ? generateHTML(chapter.content, editorExtensions) : ""), [chapter]);
|
||||
const { book, coverActive, chapters, idx, chapter, html, eyebrow } = useActiveChapter();
|
||||
|
||||
if (!book) return null;
|
||||
|
||||
if (coverActive) {
|
||||
return (
|
||||
<section className="dock">
|
||||
<div className="dock-head">
|
||||
<span className="label">Preview</span>
|
||||
<div className="dock-tools">
|
||||
<span className="meta">Cover</span>
|
||||
<TrimSelect />
|
||||
</div>
|
||||
</div>
|
||||
<DockHead meta={<span className="meta">Cover</span>} />
|
||||
<div className="cover-stage">
|
||||
<CoverArt book={book} />
|
||||
</div>
|
||||
@@ -115,21 +151,15 @@ function HtmlDock() {
|
||||
|
||||
if (!chapter) return null;
|
||||
|
||||
const kind = chapterKind(chapter);
|
||||
const eyebrow =
|
||||
kind === "body" ? `Chapter ${bodyNumber(chapters, idx) ?? ""}` : kind === "front" ? "Front matter" : "Back matter";
|
||||
|
||||
return (
|
||||
<section className="dock">
|
||||
<div className="dock-head">
|
||||
<span className="label">Preview</span>
|
||||
<div className="dock-tools">
|
||||
<DockHead
|
||||
meta={
|
||||
<span className="meta">
|
||||
{idx + 1} of {chapters.length}
|
||||
</span>
|
||||
<TrimSelect />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="page">
|
||||
<div className="p-opener">
|
||||
<div className="p-num">{eyebrow}</div>
|
||||
@@ -141,3 +171,45 @@ function HtmlDock() {
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function DeviceDock({ device }: { device: Device }) {
|
||||
const { book, coverActive, chapters, idx, chapter, html, eyebrow } = useActiveChapter();
|
||||
|
||||
if (!book) return null;
|
||||
|
||||
if (coverActive) {
|
||||
return (
|
||||
<section className="dock">
|
||||
<DockHead meta={<span className="meta">Cover</span>} />
|
||||
<DeviceFrame device={device} cover={book} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!chapter) {
|
||||
return (
|
||||
<section className="dock">
|
||||
<DockHead />
|
||||
<div className="dock-empty">No chapter selected.</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="dock">
|
||||
<DockHead
|
||||
meta={
|
||||
<span className="meta">
|
||||
{idx + 1} of {chapters.length}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<DeviceFrame
|
||||
device={device}
|
||||
eyebrow={eyebrow}
|
||||
title={chapter.noTitle ? undefined : chapter.title || "Untitled"}
|
||||
html={html}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
export type DeviceId = "kindle" | "kobo" | "ipad" | "iphone" | "android";
|
||||
export type PreviewMode = "print" | DeviceId;
|
||||
export type DeviceKind = "eink" | "phone" | "tablet";
|
||||
|
||||
export interface Device {
|
||||
id: DeviceId;
|
||||
label: string;
|
||||
kind: DeviceKind;
|
||||
bg: string;
|
||||
ink: string;
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
marginX: number;
|
||||
marginY: number;
|
||||
aspect: number;
|
||||
bezel: string;
|
||||
bezelWidth: number;
|
||||
radius: number;
|
||||
}
|
||||
|
||||
export const DEVICES: Device[] = [
|
||||
{
|
||||
id: "kindle",
|
||||
label: "Kindle Paperwhite",
|
||||
kind: "eink",
|
||||
bg: "#f4f3ee",
|
||||
ink: "#2b2b2b",
|
||||
fontSize: 16,
|
||||
lineHeight: 1.55,
|
||||
marginX: 34,
|
||||
marginY: 40,
|
||||
aspect: 1240 / 1680,
|
||||
bezel: "#1c1c1c",
|
||||
bezelWidth: 18,
|
||||
radius: 14,
|
||||
},
|
||||
{
|
||||
id: "kobo",
|
||||
label: "Kobo",
|
||||
kind: "eink",
|
||||
bg: "#f6f5f0",
|
||||
ink: "#262626",
|
||||
fontSize: 16,
|
||||
lineHeight: 1.6,
|
||||
marginX: 34,
|
||||
marginY: 40,
|
||||
aspect: 1264 / 1680,
|
||||
bezel: "#2a2a2a",
|
||||
bezelWidth: 16,
|
||||
radius: 16,
|
||||
},
|
||||
{
|
||||
id: "ipad",
|
||||
label: "iPad",
|
||||
kind: "tablet",
|
||||
bg: "#fbfaf7",
|
||||
ink: "#1d1d1f",
|
||||
fontSize: 18,
|
||||
lineHeight: 1.65,
|
||||
marginX: 56,
|
||||
marginY: 56,
|
||||
aspect: 1640 / 2360,
|
||||
bezel: "#3a3a3c",
|
||||
bezelWidth: 20,
|
||||
radius: 28,
|
||||
},
|
||||
{
|
||||
id: "iphone",
|
||||
label: "iPhone",
|
||||
kind: "phone",
|
||||
bg: "#ffffff",
|
||||
ink: "#1d1d1f",
|
||||
fontSize: 17,
|
||||
lineHeight: 1.6,
|
||||
marginX: 26,
|
||||
marginY: 30,
|
||||
aspect: 1179 / 2556,
|
||||
bezel: "#1a1a1c",
|
||||
bezelWidth: 12,
|
||||
radius: 44,
|
||||
},
|
||||
{
|
||||
id: "android",
|
||||
label: "Android phone",
|
||||
kind: "phone",
|
||||
bg: "#ffffff",
|
||||
ink: "#202124",
|
||||
fontSize: 16,
|
||||
lineHeight: 1.6,
|
||||
marginX: 24,
|
||||
marginY: 28,
|
||||
aspect: 1080 / 2400,
|
||||
bezel: "#0f0f0f",
|
||||
bezelWidth: 11,
|
||||
radius: 36,
|
||||
},
|
||||
];
|
||||
|
||||
const DEVICE_MAP = new Map<string, Device>(DEVICES.map((d) => [d.id, d]));
|
||||
|
||||
const KEY = "margin-preview-mode";
|
||||
|
||||
export function findDevice(id: string): Device | undefined {
|
||||
return DEVICE_MAP.get(id);
|
||||
}
|
||||
|
||||
export function initialPreviewMode(): PreviewMode {
|
||||
const saved = localStorage.getItem(KEY);
|
||||
if (saved === "print" || (saved && DEVICE_MAP.has(saved))) return saved as PreviewMode;
|
||||
return "print";
|
||||
}
|
||||
|
||||
export function persistPreviewMode(mode: PreviewMode) {
|
||||
localStorage.setItem(KEY, mode);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { create } from "zustand";
|
||||
import { type PreviewMode, initialPreviewMode, persistPreviewMode } from "../devices";
|
||||
|
||||
interface PreviewState {
|
||||
mode: PreviewMode;
|
||||
setMode: (mode: PreviewMode) => void;
|
||||
}
|
||||
|
||||
export const usePreviewMode = create<PreviewState>((set) => ({
|
||||
mode: initialPreviewMode(),
|
||||
setMode: (mode) => {
|
||||
persistPreviewMode(mode);
|
||||
set({ mode });
|
||||
},
|
||||
}));
|
||||
@@ -1992,3 +1992,194 @@ body.resizing {
|
||||
background: var(--accent-wash);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.device-stage {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
padding-block: 6px;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.device-frame {
|
||||
--dv-scale: calc(100cqi / 380);
|
||||
width: 100%;
|
||||
padding: calc(var(--dv-bezel-w) * var(--dv-scale));
|
||||
background: var(--dv-bezel);
|
||||
border-radius: calc(var(--dv-radius) * var(--dv-scale));
|
||||
box-shadow: var(--shadow-page);
|
||||
}
|
||||
|
||||
.device-screen {
|
||||
aspect-ratio: var(--dv-aspect);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--dv-bg);
|
||||
color: var(--dv-ink);
|
||||
border-radius: calc(max(0px, (var(--dv-radius) - var(--dv-bezel-w)) * var(--dv-scale)));
|
||||
}
|
||||
|
||||
.device-statusbar {
|
||||
flex: none;
|
||||
height: calc(30 * var(--dv-scale));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 calc(18 * var(--dv-scale));
|
||||
font-family: var(--font-ui);
|
||||
font-size: calc(11 * var(--dv-scale));
|
||||
font-weight: 600;
|
||||
color: var(--dv-ink);
|
||||
}
|
||||
|
||||
.device-clock {
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.device-batt {
|
||||
position: relative;
|
||||
flex: none;
|
||||
width: calc(22 * var(--dv-scale));
|
||||
height: calc(11 * var(--dv-scale));
|
||||
border: calc(1.4 * var(--dv-scale)) solid var(--dv-ink);
|
||||
border-radius: calc(3 * var(--dv-scale));
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.device-batt::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: calc(1.5 * var(--dv-scale));
|
||||
bottom: calc(1.5 * var(--dv-scale));
|
||||
left: calc(1.5 * var(--dv-scale));
|
||||
width: 55%;
|
||||
background: var(--dv-ink);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.device-batt::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: calc(-3 * var(--dv-scale));
|
||||
top: 30%;
|
||||
height: 40%;
|
||||
width: calc(2 * var(--dv-scale));
|
||||
border-radius: 1px;
|
||||
background: var(--dv-ink);
|
||||
}
|
||||
|
||||
.device-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: calc(var(--dv-my) * var(--dv-scale)) calc(var(--dv-mx) * var(--dv-scale));
|
||||
font-family: var(--font-book);
|
||||
font-size: calc(var(--dv-fs) * var(--dv-scale));
|
||||
line-height: var(--dv-lh);
|
||||
}
|
||||
|
||||
.device-frame[data-kind="eink"] .device-content::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.device-opener {
|
||||
text-align: center;
|
||||
margin-bottom: calc(20 * var(--dv-scale));
|
||||
}
|
||||
|
||||
.device-eyebrow {
|
||||
font-family: var(--font-ui);
|
||||
font-size: calc(10 * var(--dv-scale));
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.5;
|
||||
margin-bottom: calc(8 * var(--dv-scale));
|
||||
}
|
||||
|
||||
.device-title {
|
||||
font-family: var(--font-book);
|
||||
font-weight: 500;
|
||||
font-size: calc(24 * var(--dv-scale));
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.device-body p {
|
||||
font-family: var(--font-book);
|
||||
hyphens: auto;
|
||||
margin: 0 0 0.8em;
|
||||
color: var(--dv-ink);
|
||||
}
|
||||
|
||||
.device-body p[data-indent="true"] {
|
||||
text-indent: 1.2em;
|
||||
}
|
||||
|
||||
.device-frame[data-kind="eink"] .device-body p {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.device-frame[data-kind="phone"] .device-body p,
|
||||
.device-frame[data-kind="tablet"] .device-body p {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.device-body h1,
|
||||
.device-body h2,
|
||||
.device-body h3 {
|
||||
font-family: var(--font-book);
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
margin: 1.2em 0 0.5em;
|
||||
}
|
||||
|
||||
.device-body blockquote {
|
||||
margin: 0 0 0.8em;
|
||||
padding-left: 1em;
|
||||
border-left: 2px solid currentColor;
|
||||
font-style: italic;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.device-body hr {
|
||||
border: none;
|
||||
border-top: 1px solid currentColor;
|
||||
width: calc(40 * var(--dv-scale));
|
||||
margin: calc(16 * var(--dv-scale)) auto;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.device-body img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0.8em auto;
|
||||
}
|
||||
|
||||
.device-footer {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: calc(10 * var(--dv-scale));
|
||||
padding: calc(8 * var(--dv-scale)) calc(var(--dv-mx) * var(--dv-scale));
|
||||
font-family: var(--font-ui);
|
||||
font-size: calc(11 * var(--dv-scale));
|
||||
color: var(--dv-ink);
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.device-foot-title {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.device-content .cover-art {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
"preview": "astro preview",
|
||||
"deploy": "astro build && npx wrangler pages deploy dist --project-name=margin --commit-dirty=true"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^5.6.1"
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
<p class="side__label">Book setup</p>
|
||||
<ul class="side__meta">
|
||||
<li>Trim · 5 × 8 in</li>
|
||||
<li>Language · English</li>
|
||||
</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,10 +80,17 @@
|
||||
</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>
|
||||
<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
|
||||
@@ -61,7 +101,15 @@
|
||||
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>
|
||||
<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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`, {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.hero__buttons {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.osbtn {
|
||||
justify-content: center;
|
||||
}
|
||||
.hero__art {
|
||||
margin-top: clamp(44px, 6vw, 72px);
|
||||
max-width: 1000px;
|
||||
}
|
||||
</style>
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user