diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts index 599efddb7..d3061f7a0 100644 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts +++ b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.spec.ts @@ -96,10 +96,14 @@ describe('ChatDebugComponent — edge-claim attribute', () => { document.documentElement.removeAttribute('data-ngaf-chat-debug'); }); - it('sets data-ngaf-chat-debug=dock on while open', () => { + it('reads PEER --ngaf-chat-sidebar-claim-right (not aggregate occupy-right)', () => { + // Reading the aggregate occupy-right causes self-feedback: when + // chat-debug docks right, it WRITES occupy-right; if it also READS + // occupy-right, the panel offsets itself by its own width. Read the + // peer-specific sidebar-claim-right instead. const styles = (ChatDebugComponent as unknown as { ɵcmp: { styles: string[] } }).ɵcmp.styles.join('\n'); - expect(styles).toMatch(/\.panel--bottom[^{]*\{[^}]*right:\s*var\(--ngaf-chat-occupy-right/); - expect(styles).toMatch(/\.panel--right[^{]*\{[^}]*right:\s*var\(--ngaf-chat-occupy-right/); + expect(styles).toMatch(/\.panel--bottom[^{]*\{[^}]*right:\s*var\(--ngaf-chat-sidebar-claim-right/); + expect(styles).toMatch(/\.panel--right[^{]*\{[^}]*right:\s*var\(--ngaf-chat-sidebar-claim-right/); }); }); diff --git a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts index d0ccc1857..e54960198 100644 --- a/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts +++ b/libs/chat/src/lib/compositions/chat-debug/chat-debug.component.ts @@ -100,7 +100,7 @@ interface TabEntry { } .panel--right { top: 0; - right: var(--ngaf-chat-occupy-right, 0); + right: var(--ngaf-chat-sidebar-claim-right, 0); bottom: 0; width: var(--panel-size, 420px); border-right: 0; @@ -119,7 +119,7 @@ interface TabEntry { } .panel--bottom { left: 0; - right: var(--ngaf-chat-occupy-right, 0); + right: var(--ngaf-chat-sidebar-claim-right, 0); bottom: 0; height: var(--panel-size, 40vh); border-bottom: 0; diff --git a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.spec.ts b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.spec.ts index 65bc15a88..ae64aae71 100644 --- a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.spec.ts +++ b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.spec.ts @@ -56,15 +56,17 @@ describe('ChatSidebarComponent — edge-claim attribute', () => { }); }); - it('panel CSS includes bottom: var(--ngaf-chat-occupy-bottom)', () => { - // Styles array is the second member of the @Component decorator metadata. - // Easier path: stringify the styles and look for the declaration. + it('panel CSS reads PEER --ngaf-chat-debug-claim-bottom (not aggregate)', () => { + // Components must read PEER per-component claim vars, never the + // aggregate occupy-* (which they write to themselves). The aggregate + // is for external consumer convenience; internal panels read + // peer-specific to avoid self-feedback. const styles = (ChatSidebarComponent as unknown as { ɵcmp: { styles: string[] } }).ɵcmp.styles.join('\n'); - expect(styles).toMatch(/\.chat-sidebar__panel[^{]*\{[^}]*bottom:\s*var\(--ngaf-chat-occupy-bottom/); + expect(styles).toMatch(/\.chat-sidebar__panel[^{]*\{[^}]*bottom:\s*var\(--ngaf-chat-debug-claim-bottom/); }); - it('launcher CSS includes calc(1rem + var(--ngaf-chat-occupy-bottom))', () => { + it('launcher CSS reads PEER --ngaf-chat-debug-claim-bottom (not aggregate)', () => { const styles = (ChatSidebarComponent as unknown as { ɵcmp: { styles: string[] } }).ɵcmp.styles.join('\n'); - expect(styles).toMatch(/\.chat-sidebar__launcher[^{]*\{[^}]*bottom:\s*calc\(1rem\s*\+\s*var\(--ngaf-chat-occupy-bottom/); + expect(styles).toMatch(/\.chat-sidebar__launcher[^{]*\{[^}]*bottom:\s*calc\(1rem\s*\+\s*var\(--ngaf-chat-debug-claim-bottom/); }); }); diff --git a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts index 401c25de9..6fdeed391 100644 --- a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts +++ b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts @@ -27,7 +27,7 @@ import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens'; .chat-sidebar__panel { position: fixed; top: 0; right: 0; - bottom: var(--ngaf-chat-occupy-bottom, 0); + bottom: var(--ngaf-chat-debug-claim-bottom, 0); width: 28rem; background: var(--ngaf-chat-bg); border-left: 1px solid var(--ngaf-chat-separator); @@ -55,7 +55,7 @@ import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens'; .chat-sidebar__close:hover { background: var(--ngaf-chat-surface-alt); color: var(--ngaf-chat-text); } .chat-sidebar__launcher { position: fixed; - bottom: calc(1rem + var(--ngaf-chat-occupy-bottom, 0)); + bottom: calc(1rem + var(--ngaf-chat-debug-claim-bottom, 0)); right: 1rem; z-index: 30; transition: bottom 200ms ease-out; diff --git a/libs/chat/src/lib/styles/chat-tokens.spec.ts b/libs/chat/src/lib/styles/chat-tokens.spec.ts index 58d5dcf21..8010e9d45 100644 --- a/libs/chat/src/lib/styles/chat-tokens.spec.ts +++ b/libs/chat/src/lib/styles/chat-tokens.spec.ts @@ -59,7 +59,7 @@ describe('ROOT_TOKEN_STYLES — edge-claim primitive', () => { it('maps data-ngaf-chat-sidebar="open" to occupy-right', () => { expect(ROOT_TOKEN_STYLES).toMatch( - /:root\[data-ngaf-chat-sidebar="open"\]\s*\{\s*--ngaf-chat-occupy-right:\s*var\(--ngaf-chat-sidebar-width-drawer/, + /:root\[data-ngaf-chat-sidebar="open"\]\s*\{[^}]*--ngaf-chat-occupy-right:\s*var\(--ngaf-chat-sidebar-width-drawer/, ); }); @@ -69,7 +69,37 @@ describe('ROOT_TOKEN_STYLES — edge-claim primitive', () => { ['left', '--ngaf-chat-occupy-left', '--ngaf-chat-debug-panel-size-w'], ])('maps data-ngaf-chat-debug=%s to %s via %s', (dock, occupyVar, sizeVar) => { const pattern = new RegExp( - `:root\\[data-ngaf-chat-debug="${dock}"\\]\\s*\\{\\s*${occupyVar}:\\s*var\\(${sizeVar}`, + `:root\\[data-ngaf-chat-debug="${dock}"\\]\\s*\\{[^}]*${occupyVar}:\\s*var\\(${sizeVar}`, + ); + expect(ROOT_TOKEN_STYLES).toMatch(pattern); + }); + + // ── per-component claim vars (peer-only reads) ──────────────────────── + // Components must NOT read their own aggregate claim (would feedback). + // Each component publishes a per-component claim var that peers read. + it.each([ + '--ngaf-chat-sidebar-claim-right: 0px;', + '--ngaf-chat-debug-claim-top: 0px;', + '--ngaf-chat-debug-claim-right: 0px;', + '--ngaf-chat-debug-claim-bottom: 0px;', + '--ngaf-chat-debug-claim-left: 0px;', + ])('defines per-component default %s', (decl) => { + expect(ROOT_TOKEN_STYLES).toContain(decl); + }); + + it('sidebar attribute mapping also sets per-component claim var', () => { + expect(ROOT_TOKEN_STYLES).toMatch( + /:root\[data-ngaf-chat-sidebar="open"\]\s*\{[^}]*--ngaf-chat-sidebar-claim-right:\s*var\(--ngaf-chat-sidebar-width-drawer/, + ); + }); + + it.each([ + ['bottom', '--ngaf-chat-debug-claim-bottom', '--ngaf-chat-debug-panel-size-h'], + ['right', '--ngaf-chat-debug-claim-right', '--ngaf-chat-debug-panel-size-w'], + ['left', '--ngaf-chat-debug-claim-left', '--ngaf-chat-debug-panel-size-w'], + ])('debug attribute mapping for %s also sets %s', (dock, claimVar, sizeVar) => { + const pattern = new RegExp( + `:root\\[data-ngaf-chat-debug="${dock}"\\]\\s*\\{[^}]*${claimVar}:\\s*var\\(${sizeVar}`, ); expect(ROOT_TOKEN_STYLES).toMatch(pattern); }); diff --git a/libs/chat/src/lib/styles/chat-tokens.ts b/libs/chat/src/lib/styles/chat-tokens.ts index 4c08c740a..a8835ef4d 100644 --- a/libs/chat/src/lib/styles/chat-tokens.ts +++ b/libs/chat/src/lib/styles/chat-tokens.ts @@ -125,12 +125,27 @@ const EDGE_CLAIM_TOKENS = ` Each docked panel publishes the edge it occupies via a data-ngaf-chat-* attribute on ; other panels read these custom properties to leave room. Defaults to 0px so consumers - not using chat-sidebar/chat-debug see zero overhead. */ + not using chat-sidebar/chat-debug see zero overhead. + + TWO LAYERS: + 1. Per-component claim vars (--ngaf-chat--claim-) + are read by PEERS only — never by the component that wrote + them. This eliminates self-feedback (where a right-docked + panel would offset itself by reading its own claim). + 2. Aggregate occupy-* vars are convenience reads for external + consumers and for cases where any-panel-on-edge matters. */ --ngaf-chat-occupy-top: 0px; --ngaf-chat-occupy-right: 0px; --ngaf-chat-occupy-bottom: 0px; --ngaf-chat-occupy-left: 0px; + /* Per-component claims (peer-only reads). */ + --ngaf-chat-sidebar-claim-right: 0px; + --ngaf-chat-debug-claim-top: 0px; + --ngaf-chat-debug-claim-right: 0px; + --ngaf-chat-debug-claim-bottom: 0px; + --ngaf-chat-debug-claim-left: 0px; + /* Sizes the chat-debug dock contributes when it claims an edge. Split by orientation so consumers can override independently. */ --ngaf-chat-debug-panel-size-h: 40vh; @@ -327,15 +342,19 @@ export const ROOT_TOKEN_STYLES = ` chat-sidebar sets data-ngaf-chat-sidebar="open" while its panel is open. chat-debug sets data-ngaf-chat-debug to its current dock while open. */ :root[data-ngaf-chat-sidebar="open"] { + --ngaf-chat-sidebar-claim-right: var(--ngaf-chat-sidebar-width-drawer, 28rem); --ngaf-chat-occupy-right: var(--ngaf-chat-sidebar-width-drawer, 28rem); } :root[data-ngaf-chat-debug="bottom"] { + --ngaf-chat-debug-claim-bottom: var(--ngaf-chat-debug-panel-size-h, 40vh); --ngaf-chat-occupy-bottom: var(--ngaf-chat-debug-panel-size-h, 40vh); } :root[data-ngaf-chat-debug="right"] { + --ngaf-chat-debug-claim-right: var(--ngaf-chat-debug-panel-size-w, 420px); --ngaf-chat-occupy-right: var(--ngaf-chat-debug-panel-size-w, 420px); } :root[data-ngaf-chat-debug="left"] { + --ngaf-chat-debug-claim-left: var(--ngaf-chat-debug-panel-size-w, 420px); --ngaf-chat-occupy-left: var(--ngaf-chat-debug-panel-size-w, 420px); } }