Set-based bidirectional sync between a <mat-accordion>'s projected
<mat-expansion-panel> tree and the headless CngxAccordion
open-set. The accordion open-set is a multi-membership
Set<string>, so the index-based createMaterialBidirectionalSync
(@cngx/common/data) does not fit — this helper syncs per-panel
expanded against set membership instead.
Installs two effects and one listener-set:
brain→Material — an effect() tracking panels() and, per
panel, accordion.isOpen(id). When the open-set (or the panel
list) changes it writes panel.expanded to match, guarded by a
re-entrancy flag so the resulting synchronous expandedChange
echo does not loop back. The Material writes run inside
untracked() per reference_signal_architecture rule 2.
subscription reconcile — an effect() tracking only
panels(); inside untracked() it subscribes freshly-added
panels' expandedChange and drops removed ones (diff-only churn,
a Map<P, subscription>).
Material→brain — each expandedChange listener flips the
brain via accordion.toggle(id), but only when the incoming
expanded differs from the brain's current (clamped) membership,
so a programmatic echo (or a redundant Material emit) is a no-op.
Routing through toggle (not a raw set-write) means single-mode
arbitration and the exposed openIds model stay identical to the
native CngxAccordionPanel DOM path.
The idempotent membership check plus the re-entrancy flag together
close the loop: the flag drops the synchronous echo CdkAccordionItem
emits from its expanded setter during our own write, and the
membership check drops any Material emit that already agrees with the
brain.