オートセーブと新規パネル
localStorage によるオートセーブ、サイレントな起動時復元、保存状態チップ、New panel によるリセット。
zpd にはサーバー側のプロジェクトストレージがありません——作業中のドキュメントはブラウザタブの中にあります。use-autosave.ts はそれを継続的に localStorage へミラーリングするため、リロード(や意図しないタブのクローズ)で作業を失うことがありません。これはあくまでローカルのみの仕組みです。何もブラウザの外へは出ませんし、ブラウザや端末をまたいで同期されることもありません。
起動時の復元
マウント時、Editor() は遅延評価の useState 初期化子—— readDoc() ?? createDemoDoc()——を通じて保存済みドキュメントを一度だけ 読み込みます。現在のキーは zpd.doc.v2、エンベロープ版は 2 で、config は現在の PanelConfig v5 と厳密に一致する必要があります。有効なら確認なしで 復元します。現在キーがなければ、PanelConfig v1–v4 を含む有効な旧 zpd.doc.v1 エンベロープを移行できます。移行はトランザクショナルで、 正規化した v5 を v2 キーへ書き込めた場合だけ文書を公開し、旧バイトは ロールバック用に残します。
安全に復元できなければ固定スタックのデモ文書へフォールバックします。 壊れた・未対応の現在 v2 バイトは保持され、自動上書きから保護されます。 壊れた旧バイトも保持しますが別キーなので、その後の正当な v2 オートセーブ は妨げません。readDoc() は決して例外を投げません。
保存されるペイロードは、小さなバージョン付きのエンベロープです。
interface StoredDocPayload {
version: 2; // DOC_STORAGE_VERSION
savedAt: number;
config: PanelConfig; // 厳密な v5 serializePanelConfig(doc) の出力
}readDoc() は保存エンベロープを検証し、core の例外を投げない厳密な config parser(防御的パースを参照)を使います。 未対応、将来版、壊れたペイロードはデフォルトとして黙って書き換えず拒否します。 readDoc()/writeDoc() は決して例外を投げず、localStorage への全アクセス (プライバシー設定下でのプロパティ読み取り自体を含む)をタグ付き結果へ還元します。
デバウンスされたオートセーブ
ドキュメントへのすべてのコミットは、500ms 後(AUTOSAVE_DEBOUNCE_MS)に書き込みをスケジュールします。タイマーを保持するエフェクトは doc をキーにしているため、新しいコミットごとに、自身のタイマーをスケジュールする前に直前の実行の保留中タイマーをクリアします——素早い編集(ドラッグや、undo/redo の連打)は、コミットごとに 1 回ではなく、1 回の書き込みにまとまります。
タブが閉じられるときに保留中の書き込みが失われないよう、デバウンスを迂回する早期フラッシュのトリガーが 2 つあります。
pagehide—beforeunloadの現代的な代替。bfcache ナビゲーションやモバイルのタブ切り替えでも発火します。visibilitychangeが'hidden'になったとき — モバイル OS はpagehideを一切発火させずに、バックグラウンド化したタブのプロセスを終了させることがあります。
意図的に beforeunload の確認プロンプト(「サイトを離れますか?変更が保存されない場合があります」)は ありません ——継続的なローカル永続化がその代わりを果たすためです。
保存状態チップ
components/ は、ヘッダーのアプリ名の隣に、小さな非対話的なピル(role="status")をレンダリングします。3 つの状態のいずれかを反映します。
| 状態 | 表示 | スタイル |
|---|---|---|
unsaved | "Unsaved changes…" | ニュートラルなピル |
saved | "Saved locally HH:MM"(24 時間表記、ローカル時刻) | ニュートラルなピル |
failed(quota) | "Save failed (document too large for local storage)" | 赤いピル。title ツールチップも同じ文言 |
failed(その他) | "Save failed" | 赤いピル。ツールチップは "Save failed — local storage is unavailable" |
unsaved は明示的に保存される状態ではなく、導出された値です——このフックは、直近の書き込みの対象ドキュメントを現在の doc と参照で比較します。新しいコミットがあれば、その参照は必ず古くなり、それだけで「unsaved」を意味します。手動でリセットするものは何もありません。
保存の失敗
writeDoc() はすべての失敗に理由——'quota'、'unavailable'、'error'——のタグを付け、オートセーブフックは書き込みが最初に失敗したとき、セッションにつき 1 回だけ警告トースト(toastWarning)を発火します。そのため、以降のデバウンスのたびに quota の失敗が繰り返されても、トーストキューがスパムされることはありません。
quota—QuotaExceededError(あるいは Firefox のNS_ERROR_DOM_QUOTA_REACHED)。zpd のimageレイヤーは base64 のデータ URL を埋め込むため、大きな画像をいくつか含むデザインは、ブラウザのオリジンごとのlocalStorageクォータを実際に超えることがあります。トースト: "This panel is too large to save locally — some changes may be lost if you close the tab."unavailable—localStorageそのものにアクセスできない(プライベートブラウジングのロックダウン、サードパーティストレージのブロック、SSR)。トースト: "Local storage is unavailable in this browser — changes are not being saved."error— その他の書き込み失敗(例: ドキュメントのシリアライズに失敗した場合)。トースト: "Could not save your changes locally."
Note
チップの文言は、何を約束するかについて意図的に控えめです。これはこの ブラウザに限った localStorage であり——アカウントでも、同期されるプロジェクトでも、タブのストレージがクリアされないという保証でもありません。
新規パネル
ヘッダーの New panel ボタン(コマンドパレットの File → New Panel も同じ newPanelAction() を使います)は、新しいドキュメントを開始します。必ず最初に確認を求めます。
Start a new panel? This replaces the current panel with the default starter panel. This cannot be undone. [Cancel] [New panel]
確認すると、ドキュメント全体を createDefaultDoc()——スクリプトや素の @zpd/core 利用者が得るのと同じ、Copper 内にドットグリッドを 1 つ持つ 固定スタックのスターター文書——に置き換えます。上で説明した、よりリッチな 初回訪問時のデモドキュメント ではありません。
replaceDoc()(replace-doc.ts)は、New panel と JSON インポート(ラウンドトリップ → インポートを参照)の両方が使う、ドキュメント全体を置き換える共通のプリミティブです。通常の ctx.commit()/ctx.replace() とは異なり、次を行います。
undo/redo 履歴 全体 を破棄する(
ctx.reset)——新しいドキュメントの編集履歴は、古いドキュメントに対しては意味を持たないため。現在の選択をクリアする(古いドキュメントの古いレイヤー id が残ってはいけないため)。
新しいレイヤーセットにもう存在しないレンダラーの画像キャッシュエントリを退避させる。
New panel は localStorage を直接クリアするわけではありません——インメモリのドキュメントを置き換えるだけで、次のデバウンスされたオートセーブが、他のどんな編集とも同じように、保存済みのエントリを新しいスターターパネルで上書きします。