From f7fa2508044821e75fb584ed49f52c4a357ae942 Mon Sep 17 00:00:00 2001 From: Disco DeDisco Date: Wed, 13 May 2026 00:43:03 -0400 Subject: [PATCH] =?UTF-8?q?.bud-duplicate-flash:=20auto-ease-out=203s=20af?= =?UTF-8?q?ter=20FYI=20+=20palette=20swap=20=E2=80=94=20`note.js`'s=20`Bri?= =?UTF-8?q?ef.showDuplicateBanner`=20FYI=20handler=20now=20`setTimeout(()?= =?UTF-8?q?=20=3D>=20target.classList.remove('bud-duplicate-flash'),=20300?= =?UTF-8?q?0)`=20after=20the=20`.add()`;=20the=20existing=20`transition:?= =?UTF-8?q?=20color=20600ms=20ease,=20text-shadow=20600ms=20ease`=20rule?= =?UTF-8?q?=20on=20the=20class=20already=20covered=20the=20ease-in=20(defa?= =?UTF-8?q?ult=20=E2=86=92=20flash),=20so=20the=20same=20rule=20now=20also?= =?UTF-8?q?=20covers=20the=20ease-out=20(flash=20=E2=86=92=20default)=20wh?= =?UTF-8?q?en=20the=20class=20drops=20=E2=80=94=20net=20behaviour:=20tap?= =?UTF-8?q?=20FYI=20=E2=86=92=20flash=20peaks=20=E2=86=92=20flash=20visibl?= =?UTF-8?q?y=20fades=20back=20to=20the=20default=20text=20styling=20over?= =?UTF-8?q?=20~600ms=20after=20a=203s=20hold,=20instead=20of=20persisting?= =?UTF-8?q?=20til=20page=20refresh;=20palette=20keys=20swapped=20per=20use?= =?UTF-8?q?r=20steer=20=E2=80=94=20`color:=20var(--terUser);=20text-shadow?= =?UTF-8?q?:=20var(--ninUser)`=20=E2=86=92=20`color:=20var(--ninUser);=20t?= =?UTF-8?q?ext-shadow:=20var(--terUser)`,=20so=20the=20highlight=20reads?= =?UTF-8?q?=20as=20a=20lighter=20handle=20w.=20a=20gold=20glow=20rather=20?= =?UTF-8?q?than=20a=20gold=20handle=20w.=20a=20light=20glow,=20matching=20?= =?UTF-8?q?the=20duplicate-guard=20spec=20the=20user=20re-aligned=20on;=20?= =?UTF-8?q?affects=20all=20three=20flash=20targets=20uniformly=20(`.bud-en?= =?UTF-8?q?try=20.bud-name`=20on=20/billboard/my-buds/,=20`.post-recipient?= =?UTF-8?q?`=20on=20post.html=20share-flow,=20`.gate-slot.filled`=20on=20t?= =?UTF-8?q?he=20gatekeeper=20invite-flow)=20since=20they=20all=20flow=20th?= =?UTF-8?q?rough=20the=20same=20`=5Fbud.scss=20.bud-duplicate-flash`=20sel?= =?UTF-8?q?ector=20+=20the=20same=20`Brief.showDuplicateBanner`=20JS=20han?= =?UTF-8?q?dler;=20new=20Jasmine=20spec=20D7b=20in=20NoteSpec.js=20uses=20?= =?UTF-8?q?`jasmine.clock().install()`=20+=20`clock().tick(3001)`=20to=20f?= =?UTF-8?q?ast-forward=20past=20the=20dismiss=20window=20+=20assert=20the?= =?UTF-8?q?=20class=20is=20gone=20(existing=20D7=20still=20pins=20the=20im?= =?UTF-8?q?mediate-after-FYI=20peak=20state);=20existing=20FTs=20(test=5Fb?= =?UTF-8?q?ill=5Fmy=5Fbuds.test=5Fre=5Fadd=5Fexisting=5Fbud=5Fshows=5Falre?= =?UTF-8?q?ady=5Fpresent=5Fbrief=E2=80=A6=20+=20test=5Fcore=5Fbud=5Fbtn=20?= =?UTF-8?q?duplicate-guard=20FTs)=20still=20green=20because=20they=20asser?= =?UTF-8?q?t=20immediately=20after=20the=20FYI=20click=20(well=20inside=20?= =?UTF-8?q?the=203s=20hold)=20=E2=80=94=20TDD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Code architected by Disco DeDisco Git commit message Co-Authored-By: Claude Opus 4.7 --- .../dashboard/static/apps/dashboard/note.js | 12 +++++++++++- src/static/tests/NoteSpec.js | 19 +++++++++++++++++++ src/static_src/scss/_bud.scss | 9 +++++---- src/static_src/tests/NoteSpec.js | 19 +++++++++++++++++++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/apps/dashboard/static/apps/dashboard/note.js b/src/apps/dashboard/static/apps/dashboard/note.js index d292eea..c3c806d 100644 --- a/src/apps/dashboard/static/apps/dashboard/note.js +++ b/src/apps/dashboard/static/apps/dashboard/note.js @@ -92,7 +92,17 @@ const Brief = (() => { banner.querySelector('.note-banner__fyi').addEventListener('click', function () { if (opts.target_selector) { var target = document.querySelector(opts.target_selector); - if (target) target.classList.add('bud-duplicate-flash'); + if (target) { + target.classList.add('bud-duplicate-flash'); + // Auto-ease-out — the SCSS rule has a 600ms transition + // on color + text-shadow, so removing the class 3s after + // FYI lets the peak state breathe before fading back to + // the default text styling. Without this the flash + // persisted til page refresh. + setTimeout(function () { + target.classList.remove('bud-duplicate-flash'); + }, 3000); + } } banner.remove(); }); diff --git a/src/static/tests/NoteSpec.js b/src/static/tests/NoteSpec.js index 35c8065..71d091a 100644 --- a/src/static/tests/NoteSpec.js +++ b/src/static/tests/NoteSpec.js @@ -230,6 +230,25 @@ describe('Brief.showDuplicateBanner', () => { expect(name.classList.contains('bud-duplicate-flash')).toBeTrue(); }); + it('D7b: .bud-duplicate-flash auto-eases out — class is removed ~3s after FYI', () => { + jasmine.clock().install(); + try { + Brief.showDuplicateBanner({ + display_name: 'alice', + target_selector: '.bud-entry[data-bud-id="42"] .bud-name', + }); + document.querySelector('.note-banner__fyi').click(); + const name = fixture.querySelector('.bud-name'); + // Immediately after FYI: flash is on (the visible peak state). + expect(name.classList.contains('bud-duplicate-flash')).toBeTrue(); + // After the auto-dismiss window: flash is gone. + jasmine.clock().tick(3001); + expect(name.classList.contains('bud-duplicate-flash')).toBeFalse(); + } finally { + jasmine.clock().uninstall(); + } + }); + it('D8: FYI dismisses cleanly when target_selector is missing', () => { Brief.showDuplicateBanner({ display_name: 'alice' }); document.querySelector('.note-banner__fyi').click(); diff --git a/src/static_src/scss/_bud.scss b/src/static_src/scss/_bud.scss index 80f4820..ab14acb 100644 --- a/src/static_src/scss/_bud.scss +++ b/src/static_src/scss/_bud.scss @@ -177,11 +177,12 @@ html:has(#id_kit_bag_dialog[open]) #id_bud_btn { // Eased-in flash applied by Brief.showDuplicateBanner's FYI button to a // caller-supplied target element — one of .bud-entry .bud-name (My Buds), // .post-recipient (post share), or .gate-slot.filled (gatekeeper invite). -// Persists until page refresh; --terUser color + --ninUser text-shadow -// per the duplicate-guard spec. +// Class is auto-removed by note.js 3s after FYI; SCSS transition handles +// both the ease-in (on add) AND the ease-out (on remove). Palette swap +// vs. the original spec: color = --ninUser, text-shadow = --terUser. .bud-duplicate-flash { - color: rgba(var(--terUser), 1); - text-shadow: 0 0 0.5em rgba(var(--ninUser), 1); + color: rgba(var(--ninUser), 1); + text-shadow: 0 0 0.5em rgba(var(--terUser), 1); transition: color 600ms ease, text-shadow 600ms ease; } diff --git a/src/static_src/tests/NoteSpec.js b/src/static_src/tests/NoteSpec.js index 35c8065..71d091a 100644 --- a/src/static_src/tests/NoteSpec.js +++ b/src/static_src/tests/NoteSpec.js @@ -230,6 +230,25 @@ describe('Brief.showDuplicateBanner', () => { expect(name.classList.contains('bud-duplicate-flash')).toBeTrue(); }); + it('D7b: .bud-duplicate-flash auto-eases out — class is removed ~3s after FYI', () => { + jasmine.clock().install(); + try { + Brief.showDuplicateBanner({ + display_name: 'alice', + target_selector: '.bud-entry[data-bud-id="42"] .bud-name', + }); + document.querySelector('.note-banner__fyi').click(); + const name = fixture.querySelector('.bud-name'); + // Immediately after FYI: flash is on (the visible peak state). + expect(name.classList.contains('bud-duplicate-flash')).toBeTrue(); + // After the auto-dismiss window: flash is gone. + jasmine.clock().tick(3001); + expect(name.classList.contains('bud-duplicate-flash')).toBeFalse(); + } finally { + jasmine.clock().uninstall(); + } + }); + it('D8: FYI dismisses cleanly when target_selector is missing', () => { Brief.showDuplicateBanner({ display_name: 'alice' }); document.querySelector('.note-banner__fyi').click();