feat: implement granular section visibility control

Introduce the ability to reveal specific sections of a document
rather than entire files. This includes:

- Updating `revealedPaths` to map normalized file paths to sets of
  revealed section slugs.
- Adding `isPathRevealed` to handle visibility logic for connected
  clients and GMs.
- Enhancing `extractHeadings` to calculate `startLine` and `endLine`
  for each TOC node to support precise section identification.
This commit is contained in:
2026-07-07 07:44:57 +08:00
parent 7cfc0fd4f3
commit c5e1167beb
3 changed files with 92 additions and 16 deletions
+10 -1
View File
@@ -83,7 +83,16 @@ registerMessageType<LinkPayload>({
reducer: (p) => {
journalSetState(
produce((s) => {
s.revealedPaths.add(p.path);
const key = p.path.replace(/^\.?\//, "").replace(/\.md$/, "");
if (!s.revealedPaths[key]) {
s.revealedPaths[key] = new Set();
}
if (p.section) {
s.revealedPaths[key].add(p.section);
} else {
// No section — reveal the whole article
s.revealedPaths[key].clear();
}
}),
);
},
+31 -3
View File
@@ -29,10 +29,12 @@ export interface JournalStreamState {
/** Last sequence number per sender */
senderSeq: Record<string, number>;
/**
* Paths revealed by link messages.
* Paths and sections revealed by link messages.
* Key: normalized path (no .md). Value: set of revealed section slugs.
* An empty set means the whole article is revealed.
* Populated during hydration and live receipt via the type's reducer.
*/
revealedPaths: Set<string>;
revealedPaths: Record<string, Set<string>>;
/** MQTT connection status */
connected: boolean;
/** Granular connection state for UI indicators */
@@ -101,7 +103,7 @@ const [state, setState] = createStore<JournalStreamState>({
sessionName: null,
messages: [],
senderSeq: {},
revealedPaths: new Set(),
revealedPaths: {},
connected: false,
connectionStatus: "disconnected",
connectionError: null,
@@ -604,6 +606,32 @@ export function visibleMessages(): StreamMessage[] {
return state.messages.filter((m) => !m.reverted);
}
/**
* Check whether a path (and optionally section) is revealed.
* GM and disconnected clients see everything.
* Non-GM connected clients only see explicitly revealed content.
*/
export function isPathRevealed(path: string, section?: string): boolean {
if (!state.connected) return true;
if (state.myRole === "gm") return true;
const normalized = normalizePath(path);
const revealed = state.revealedPaths[normalized];
if (!revealed) return false;
// Whole article revealed (empty set)
if (revealed.size === 0) return true;
// Section-specific check
if (section) return revealed.has(section);
// No section asked — at least some sections are revealed, article is partially visible
return revealed.size > 0;
}
/** Normalize a path for lookup: strip .md, leading ./ or / */
function normalizePath(p: string): string {
return p.replace(/^\.?\//, "").replace(/\.md$/, "");
}
export function useJournalStream() {
return state;
}