// Which nav rows a viewer may see, when the answer belongs to a module. // // Phase 2, PR 8 of docs/website/MODULE_SYSTEM.md §2.7 (§1.5 states the problem); // the contract is docs/website/MODULE_API.md §3.3. // // Nine of the sixteen rows in the public header used to carry a `feature`, and // every one of them was a shard surface an admin can disable or gate to a higher // audience. The provider that answered those questions moved out with the module // in Phase 3 slice 3, and core cannot call it directly and still be a core. It // keeps this generic seam instead, and the module fills it. // // **The namespace comes from the registration, not from the string.** A row's // `feature` is resolved by the provider its OWN module registered, so a module // author writes `feature: 'status'` exactly as it reads today: nothing parses a // prefix, and a typo'd namespace is not a thing that can exist. Core's own rows // carry no `moduleId` and resolve against the owner id `core` — which nothing // registers now that the shard rows are gone, and that is the correct resting // state rather than a gap: no core nav row carries a `feature`. // // Everything here fails OPEN, and that is deliberate: this is presentation, the // gate is server-side // (a disabled feature 404s and an out-of-rung one 403s whether or not a link was // rendered), so an unknown answer shows the link rather than blanking the nav. // The one thing a UI mistake must never do here is hide a page from someone // entitled to it. /** * The predicate the layouts filter their nav with. * * @param {Map boolean} | null | undefined>} flagsByOwner * one entry per registered provider, keyed by the id of the module that * registered it. The value is whatever that provider's hook returned this * render: a Set-like of the flags this viewer may see, or `null` while the * answer is still in flight. * @returns {(item: object) => boolean} */ export function buildFeatureGate(flagsByOwner) { return function isVisible(item) { if (!item || !item.feature) return true const owner = item.moduleId ?? 'core' // No provider for this owner: the row names a flag nothing answers for. That // is the no-module-installed case — no core row carries a `feature` once the // module is out — and it is a correct no-op rather than a hidden row. if (!flagsByOwner || !flagsByOwner.has(owner)) return true const flags = flagsByOwner.get(owner) // Still loading, or a provider that returned something unusable. Both are // "we do not know yet", and both show the link. if (!flags || typeof flags.has !== 'function') return true return flags.has(item.feature) } } /** The gate an area with no providers gets: everything is visible. */ export const OPEN_GATE = () => true export default buildFeatureGate