diff --git a/src/index.html b/src/index.html
index 0b50b12..2645cba 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1770,6 +1770,13 @@
.bookPageTile.reflowSegment {
background: linear-gradient(135deg, #f8fafc 25%, transparent 25%, transparent 50%, #f8fafc 50%, #f8fafc 75%, transparent 75%) 0 0/8px 8px;
}
+ .bookPageTileGateChip {
+ position: absolute; bottom: 4px; left: 4px;
+ padding: 2px 8px; font-size: .7rem;
+ background: #fef3c7; color: #92400e;
+ border: 1px solid #fbbf24; border-radius: 999px;
+ z-index: 5; pointer-events: none;
+ }
.bookPageTileUpdateChip {
position: absolute; top: 4px; left: 4px;
padding: 2px 8px; font-size: .72rem;
@@ -4667,6 +4674,7 @@
+
@@ -8863,6 +8871,44 @@
// label. Layers for the master live in state.layers under
// `book-master:`. The first master created becomes the
// default automatically.
+ // Per-page paywall / premium gating (Phase H1). Records the
+ // user's intent on the manifest entry — the actual encrypted-
+ // publish wiring (route through _publishPremiumDesign for
+ // premium pages) is a follow-up slice. For v1 we surface the
+ // chip on tiles + the publish-modal note so authors can prep
+ // their pricing.
+ function openBookPageGatingDialog(entryId) {
+ const book = state.book;
+ const entry = book?.pages?.find(p => p.id === entryId);
+ if (!entry) return;
+ const isPremium = entry.gating?.mode === 'premium';
+ const currentPrice = entry.gating?.priceSats || 1000;
+ const choice = prompt(
+ `Gating for "${entry.label || 'this page'}"\n` +
+ `\n` +
+ `Type a price in sats to mark this page premium (readers will see a watermarked preview + an "Unlock with zap" CTA in Reader Mode).\n` +
+ `Type "0" or leave empty to keep it free.\n` +
+ `\n` +
+ `Note: v1 persists the intent on the manifest; full encrypted-publish wiring lands in H1.2.`,
+ isPremium ? String(currentPrice) : ''
+ );
+ if (choice === null) return; // cancelled
+ const trimmed = String(choice).trim();
+ pushHistory('book: page gating');
+ if (!trimmed || trimmed === '0') {
+ entry.gating = null;
+ } else {
+ const n = Math.max(1, Math.floor(Number(trimmed) || 0));
+ if (!Number.isFinite(n) || n <= 0) {
+ toastError && toastError('Invalid price — enter a positive number of sats or 0 for free.');
+ return;
+ }
+ entry.gating = { mode: 'premium', priceSats: n, previewWatermark: true };
+ }
+ render();
+ syncBookPageEditBar();
+ }
+
// Convert the current deck into a book (Phase F2). Each slide
// becomes a designed page; layers letterbox-fit into the trim
// (contain-scaled + centered). Switches templateMode to 'book'.
@@ -9624,6 +9670,15 @@
});
actions.appendChild(del);
tile.appendChild(actions);
+ // Premium chip (Phase H1). Surfaces the gating intent on the
+ // tile so authors can scan paywall coverage at a glance.
+ if (p.gating?.mode === 'premium') {
+ const gateChip = document.createElement('div');
+ gateChip.className = 'bookPageTileGateChip';
+ gateChip.textContent = `⚡ ${p.gating.priceSats || 0}`;
+ gateChip.title = `Premium — ${p.gating.priceSats || 0} sats`;
+ tile.appendChild(gateChip);
+ }
// Upstream-update chip (D1.7). Shown when the resolver's
// background probe found a newer version of this entry's
// ref upstream. Click applies it.
@@ -11503,6 +11558,7 @@
if (next) next.style.display = '';
if (tfBtn) tfBtn.style.display = 'none';
if (back) back.textContent = '← Back to Pages';
+ const gateBtn = document.getElementById('bookPageEditGatingBtn');
if (pubBtn) {
const entry = book.pages.find(p => p.id === book.editingEntryId);
const visTag = state.visibility === 'private' ? ' 🔒' : '';
@@ -11510,6 +11566,22 @@
pubBtn.title = entry?.ref
? `Replace ${entry.ref.dTag} on relays${visTag ? ' (private)' : ''}`
: `Publish this page as a casewrap-page event${visTag ? ' (private, end-to-end encrypted)' : ''}`;
+ if (gateBtn) {
+ gateBtn.style.display = '';
+ if (entry?.gating?.mode === 'premium') {
+ gateBtn.textContent = `⚡ ${entry.gating.priceSats || 0} sats`;
+ gateBtn.title = `Premium page — readers pay ${entry.gating.priceSats || 0} sats to unlock. Click to change.`;
+ gateBtn.style.background = '#fef3c7';
+ gateBtn.style.color = '#92400e';
+ gateBtn.style.borderColor = '#fbbf24';
+ } else {
+ gateBtn.textContent = '⚡ Free';
+ gateBtn.title = 'Click to add a paywall for this page (premium / zap to unlock)';
+ gateBtn.style.background = '';
+ gateBtn.style.color = '';
+ gateBtn.style.borderColor = '';
+ }
+ }
}
const designed = book.pages.filter(p => p.type === 'design');
const idx = designed.findIndex(p => p.id === book.editingEntryId);
@@ -35921,6 +35993,10 @@
on('bookPageEditNext', 'click', () => gotoBookPageRelative(1));
on('bookMasterAddTextFrame', 'click', () => addBookTextFrameToActiveMaster());
on('bookPageEditPublish', 'click', publishBookSurface);
+ on('bookPageEditGatingBtn', 'click', () => {
+ const id = state.book?.editingEntryId;
+ if (id) openBookPageGatingDialog(id);
+ });
on('bookPageEditProofPdf', 'click', async () => {
const book = state.book;
const entryId = book?.editingEntryId;