release: v0.17.0 (#677)

See CHANGELOG.md, section [0.17.0] — 2026-09-23.
This commit is contained in:
Ralksta
2026-09-23 12:09:08 +02:00
committed by GitHub
parent a3a6319c29
commit 89a8fac564
78 changed files with 9485 additions and 2050 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import {
DndContext,
closestCenter,
@@ -41,6 +41,8 @@ import {
} from './page-builder/types';
import { useScrollLock } from './useScrollLock';
import { useUnsavedGuard } from './useUnsavedGuard';
import { useDraft } from './useDraft';
import DraftNotice from './DraftNotice';
import { reportIfSessionExpired } from './sessionExpiry';
import {
IconCamera,
@@ -273,6 +275,11 @@ export default function PageBuilder() {
useUnsavedGuard(dirty);
// Unsaved edits survive leaving the builder — a tab link, back, Reload,
// Logout (#592). `serverState` is what a discard goes back to.
const draft = useDraft<GalleryState>('page-builder', gallery, dirty);
const serverState = useRef<GalleryState | null>(null);
// A subpage opened from a `?album=` link can hold dozens of albums: bring the
// linked one into view, and let the mark fade after a moment. The mark itself
// is a prop on the tile — a class added to the DOM here would be wiped by the
@@ -312,8 +319,11 @@ export default function PageBuilder() {
const { gallery: raw } = await galleryRes.json();
const parsed = parseGalleryYaml(raw);
setGallery(parsed);
openAlbumFromLink(parsed);
serverState.current = parsed;
const restored = draft.load(JSON.stringify(parsed));
setGallery(restored ?? parsed);
if (restored) setDirty(true);
openAlbumFromLink(restored ?? parsed);
// A failed album list is survivable — it only empties the picker, and
// saving with it empty changes nothing in gallery.yaml.
@@ -418,6 +428,20 @@ export default function PageBuilder() {
return { hero, albums, subpages };
}
function discardDraft() {
draft.discard();
if (serverState.current) setGallery(serverState.current);
setDirty(false);
setSaveMessage('');
}
function restoreConflictingDraft() {
const value = draft.takeConflicting();
if (!value) return;
setGallery(value);
setDirty(true);
}
const markDirty = useCallback(() => {
setDirty(true);
setSaveMessage('');
@@ -482,6 +506,13 @@ export default function PageBuilder() {
if (res.ok) {
const data = await res.json();
// Fingerprint what the next load will see, the way it will see it: the
// file is exactly `yamlData`, read back through the same parser. The
// editor's own state can differ in shape (an `undefined` here, a
// default there) and would make every later draft look outdated.
const asLoaded = parseGalleryYaml(JSON.parse(JSON.stringify(yamlData)));
serverState.current = asLoaded;
draft.saved(JSON.stringify(asLoaded));
setDirty(false);
setSaveMessage(data.message || 'Saved successfully!');
setTimeout(() => setSaveMessage(''), 5000);
@@ -850,6 +881,14 @@ export default function PageBuilder() {
showPreview
/>
<DraftNotice
status={draft.status}
subject="page structure"
onDiscard={discardDraft}
onRestore={restoreConflictingDraft}
onDismiss={draft.dismiss}
/>
{/* Search Bar */}
<div className="builder-search-container">
<div className="builder-search-wrapper">