@@ -339,34 +339,33 @@ rendered through `<SettingsActionChips actions={…} />` from
339339` @/components/settings/settings-header ` — that is the shared chip path, and it
340340is what keeps tone/icon/variant/tooltip handling from drifting between the two
341341shells. Reach for it before hand-rolling a ` Chip ` .
342- - ** ` useSettingsUnsavedGuard({ isDirty }) ` ** (` …/settings/hooks/use-settings-unsaved-guard ` )
343- — syncs the page's local ` isDirty ` into the shared ` useSettingsDirtyStore ` (so
344- the sidebar's ** section-switch** confirm + the centralized ` beforeunload ` both
345- apply for free) and returns `{ showUnsavedModal, setShowUnsavedModal, guardBack,
346- confirmDiscard }` for a detail view's ** in-view back** chip.
347- - ** Top-level pages** (whitelabeling, sso): call it ** unassigned** —
348- ` useSettingsUnsavedGuard({ isDirty: hasChanges }) ` — they only need the
349- store-sync; the sidebar/` beforeunload ` do the rest.
350- - ** Detail sub-views** (data-retention, access-control group-detail): route the
351- back chip through ` onClick={() => guard.guardBack(closeFn)} ` and render the
352- shared `<UnsavedChangesModal open={guard.showUnsavedModal}
353- onOpenChange={guard.setShowUnsavedModal} onDiscard={guard.confirmDiscard} />`
354- (from ` @/app/workspace/[workspaceId]/components/credential-detail ` ). The
355- in-view header ** Discard** chip (via ` saveDiscardActions({ onDiscard }) ` ) is a
356- * reset to original* — distinct from the back-confirm's discard, which leaves.
357- - ** ` useSettingsBeforeUnload ` ** is mounted by the settings shells
358- (` settings/layout.tsx ` and ` components/settings/standalone-settings-shell.tsx ` ) —
359- never add a per-page ` beforeunload ` .
360- - ** Dirty * computation* stays local** (shapes differ: field-compare vs
361- normalize+stringify) — only how dirty is * consumed* is shared. Derive it (a
362- ` const ` /` useMemo ` ), never store it in ` useState ` .
363- - ** CRITICAL — rules of hooks:** call ` useSettingsUnsavedGuard(...) `
364- ** unconditionally, before every early-return gate** (entitlement / loading /
365- not-entitled ` return <SettingsEmptyState> ` ). A hook placed after a gate is
366- skipped on gated renders and crashes.
367- - The route-based credential detail keeps its own ` useUnsavedChangesGuard ` (it
368- guards real ` router.push ` navigation + browser Back via a history sentinel);
369- it already shares ` UnsavedChangesModal ` , so copy stays unified.
342+ - ** ` useSettingsUnsavedGuard({ isDirty, navigationBlocked, onDiscard }) ` ** from
343+ ` @/components/settings/use-settings-unsaved-guard ` registers one editor with the
344+ shared settings store. Register every full-page draft, including inline creation
345+ forms. Dirty computation stays local and derived; fetched defaults stay clean.
346+ - ** ` onDiscard ` resets the registered draft.** The root ` SettingsNavigationGuard `
347+ owns the discard dialog, internal link interception, browser Back/Forward, and
348+ refresh protection for registered editors. Do not add a per-page dialog or
349+ history listener for an editor using this hook. Skill create/detail pages retain
350+ their existing ` useUnsavedChangesGuard ` and local dialog until they migrate to
351+ the shared registration.
352+ - ** Native history needs entry indexes.** The Navigation API supplies them for
353+ legacy entries. Without it, native traversal to an unindexed entry cannot be
354+ cancelled reliably; never guess a direction or rewrite the history stack.
355+ - ** History confirmation authorizes traversal before discarding.** Drafts are
356+ discarded only when the browser reports the confirmed traversal, so a no-op
357+ Back or Forward retains edits. Known cross-document traversal uses native unload
358+ protection. An unknown cross-document target on a classic History browser can
359+ require a second native confirmation after the shared dialog.
360+ - ** Detail back controls** call ` guard.guardBack(closeFn) ` . Other destructive view
361+ transitions, such as switching an editor's direction or environment, also use
362+ the shared ` requestLeave ` action before resetting their draft.
363+ - ** ` navigationBlocked ` covers pending saves and uploads.** The shared guard blocks
364+ navigation while requests are pending. Attempts are not queued; retry after they
365+ settle. Disable or preserve edits made during requests;
366+ failed saves retain drafts, and successful saves only clear committed values.
367+ - ** Call the hook unconditionally, before every early-return gate** (entitlement,
368+ loading, or empty-state return), so gated renders preserve hook order.
370369
371370## Detail sub-views
372371
0 commit comments