Skip to content

Commit cb20ed4

Browse files
feat(audience): add demo footer, event log polish, and alias validation
Final UX polish for the demo: Footer and accessibility - Footer renders the SDK version (read from SDK_VERSION exported from cdn.ts — adds the const + a cdn.test.ts for the guard) - Event log marked with aria-live="polite" so screen readers announce new entries Event log - Copy button copies the full session's log to the clipboard (named just 'Copy' — clearer than a longer label) - Auto-scroll to bottom on new entries, but only while the user is already at the bottom — if they scroll up to inspect older events, auto-scroll locks so the view doesn't jump away Alias validation - Real-time check on the Alias button: disabled while either ID is empty or (fromId, fromType) === (toId, toType). Mirrors core's isAliasValid() so the user gets immediate feedback instead of discovering the problem after clicking Cleanup - Remove dead #panel-slot selectors from demo.css and the empty <div id="panel-slot"> from index.html (never used) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fef712a commit cb20ed4

5 files changed

Lines changed: 128 additions & 7 deletions

File tree

packages/audience/sdk/demo/demo.css

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -340,13 +340,11 @@ button.active {
340340
background: var(--accent);
341341
}
342342

343-
.controls > .panel,
344-
.controls > #panel-slot {
343+
.controls > .panel {
345344
margin-bottom: 16px;
346345
}
347346

348-
.controls > .panel:last-child,
349-
.controls > #panel-slot:last-child {
347+
.controls > .panel:last-child {
350348
margin-bottom: 0;
351349
}
352350

@@ -371,3 +369,28 @@ button.active {
371369
resize: vertical;
372370
}
373371
}
372+
373+
footer {
374+
text-align: center;
375+
color: var(--text-muted);
376+
font-size: 12px;
377+
padding: 24px 0 8px;
378+
font-family: var(--mono);
379+
}
380+
381+
footer a {
382+
color: var(--text-muted);
383+
text-decoration: none;
384+
border-bottom: 1px dotted var(--text-muted);
385+
transition: color 0.12s ease, border-color 0.12s ease;
386+
}
387+
388+
footer a:hover {
389+
color: var(--text);
390+
border-bottom-color: var(--text);
391+
}
392+
393+
footer .footer-sep {
394+
margin: 0 8px;
395+
color: var(--text-dim);
396+
}

packages/audience/sdk/demo/demo.js

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
var Audience = window.ImmutableAudience.Audience;
1818
var IdentityType = window.ImmutableAudience.IdentityType;
19+
var SDK_VERSION = (window.ImmutableAudience && window.ImmutableAudience.version) || 'unknown';
1920

2021
// State
2122
var audience = null;
@@ -24,6 +25,12 @@
2425
var logEntries = [];
2526
var MAX_LOG_ENTRIES = 500;
2627

28+
// Track whether the user has scrolled up inside the event log. When true,
29+
// renderLog stops auto-pinning to the bottom so new entries don't yank the
30+
// user away from whatever they were reading.
31+
var logAutoScroll = true;
32+
var LOG_BOTTOM_THRESHOLD = 20; // px from bottom still counts as "at the bottom"
33+
2734
// DOM helpers
2835
function $(id) { return document.getElementById(id); }
2936

@@ -35,6 +42,12 @@
3542
return el;
3643
}
3744

45+
function isLogAtBottom() {
46+
var el = $('log');
47+
if (!el) return true;
48+
return el.scrollHeight - el.scrollTop - el.clientHeight <= LOG_BOTTOM_THRESHOLD;
49+
}
50+
3851
function getRadio(name) {
3952
var radios = document.querySelectorAll('input[name="' + name + '"]');
4053
for (var i = 0; i < radios.length; i++) {
@@ -211,16 +224,53 @@
211224

212225
container.appendChild(entry);
213226
}
214-
container.scrollTop = container.scrollHeight;
227+
if (logAutoScroll) {
228+
container.scrollTop = container.scrollHeight;
229+
}
215230
var countText = logEntries.length + ' entries';
216231
if (logEntries.length >= MAX_LOG_ENTRIES) {
217232
countText += ' (capped at ' + MAX_LOG_ENTRIES + ')';
218233
}
219234
text($('log-count'), countText);
220235
}
221236

237+
function onCopyLog() {
238+
var btn = $('btn-copy-log');
239+
if (!btn) return;
240+
var originalText = btn.textContent;
241+
function flashLabel(msg) {
242+
btn.textContent = msg;
243+
setTimeout(function () {
244+
btn.textContent = originalText;
245+
}, 1500);
246+
}
247+
248+
var payload;
249+
try {
250+
payload = JSON.stringify(logEntries, null, 2);
251+
} catch (err) {
252+
log('WARN', 'Copy session failed: ' + String(err && err.message || err), 'warn');
253+
flashLabel('Copy failed');
254+
return;
255+
}
256+
257+
if (!navigator.clipboard || !navigator.clipboard.writeText) {
258+
log('WARN', 'Clipboard API unavailable in this browser', 'warn');
259+
flashLabel('Not supported');
260+
return;
261+
}
262+
263+
navigator.clipboard.writeText(payload).then(function () {
264+
flashLabel('Copied!');
265+
}).catch(function (err) {
266+
log('WARN', 'Copy session failed: ' + String(err && err.message || err), 'warn');
267+
flashLabel('Copy failed');
268+
});
269+
}
270+
222271
function clearLog() {
223272
logEntries = [];
273+
logAutoScroll = true;
224274
renderLog();
225275
}
226276

@@ -260,6 +310,21 @@
260310
initBtn.disabled = pkInput.value.trim().length === 0;
261311
}
262312

313+
// Sync the Alias button's enabled state based on from/to inputs.
314+
// Mirrors core's isAliasValid: button is disabled if the SDK is not initialised,
315+
// if either ID is empty, or if (fromId, fromType) === (toId, toType).
316+
function syncAliasButton() {
317+
var btn = $('btn-alias');
318+
if (!audience) { btn.disabled = true; return; }
319+
var fromId = $('alias-from-id').value.trim();
320+
var toId = $('alias-to-id').value.trim();
321+
var fromType = $('alias-from-type').value;
322+
var toType = $('alias-to-type').value;
323+
if (!fromId || !toId) { btn.disabled = true; return; }
324+
if (fromId === toId && fromType === toType) { btn.disabled = true; return; }
325+
btn.disabled = false;
326+
}
327+
263328
// Enable/disable controls based on init state
264329
function setInitState(on) {
265330
$('btn-init').disabled = on;
@@ -279,6 +344,10 @@
279344
var consentRadios = document.querySelectorAll('input[name="initial-consent"]');
280345
for (var j = 0; j < consentRadios.length; j++) consentRadios[j].disabled = on;
281346
if (!on) syncInitEnabled();
347+
// Alias button needs the finer-grained check (inputs + equality). Called
348+
// unconditionally because syncAliasButton handles both the enabled and
349+
// disabled cases internally.
350+
syncAliasButton();
282351
}
283352

284353
// onError handler passed to Audience.init
@@ -518,7 +587,11 @@
518587
$('btn-shutdown').addEventListener('click', onShutdown);
519588
$('btn-reset').addEventListener('click', onReset);
520589
$('btn-flush').addEventListener('click', onFlush);
590+
$('btn-copy-log').addEventListener('click', onCopyLog);
521591
$('btn-clear-log').addEventListener('click', clearLog);
592+
$('log').addEventListener('scroll', function () {
593+
logAutoScroll = isLogAtBottom();
594+
});
522595

523596
$('btn-consent-none').addEventListener('click', function () { onSetConsent('none'); });
524597
$('btn-consent-anon').addEventListener('click', function () { onSetConsent('anonymous'); });
@@ -527,12 +600,24 @@
527600
$('btn-page').addEventListener('click', onPage);
528601
$('btn-track').addEventListener('click', onTrack);
529602

603+
var versionEl = $('sdk-version');
604+
if (versionEl) versionEl.textContent = SDK_VERSION;
605+
530606
populateIdentityDropdowns();
531607
initDemoGutter();
532608
$('btn-identify').addEventListener('click', onIdentify);
533609
$('btn-identify-traits').addEventListener('click', onIdentifyTraits);
534610
$('btn-alias').addEventListener('click', onAlias);
535611

612+
// Real-time alias validity: disable the button when either ID is empty or
613+
// when (fromId, fromType) === (toId, toType). Matches the design spec and
614+
// mirrors the SDK's isAliasValid() — user gets immediate feedback instead
615+
// of discovering the problem only after clicking.
616+
$('alias-from-id').addEventListener('input', syncAliasButton);
617+
$('alias-to-id').addEventListener('input', syncAliasButton);
618+
$('alias-from-type').addEventListener('change', syncAliasButton);
619+
$('alias-to-type').addEventListener('change', syncAliasButton);
620+
536621
// Enable Init only when the publishable key input has non-whitespace content.
537622
$('pk').addEventListener('input', syncInitEnabled);
538623
syncInitEnabled();

packages/audience/sdk/demo/index.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ <h2 class="panel-title">Alias</h2>
144144
</div>
145145
</section>
146146

147-
<div id="panel-slot"></div>
148147
</div>
149148
<div class="demo-gutter" id="demo-gutter" role="separator" aria-orientation="vertical" aria-label="Resize columns" tabindex="0"></div>
150149
<div class="log-column">
@@ -154,12 +153,20 @@ <h2 class="panel-title">Event Log</h2>
154153
<span class="log-count" id="log-count">0 entries</span>
155154
</div>
156155
<div class="button-row compact">
156+
<button class="secondary" id="btn-copy-log">Copy</button>
157157
<button class="secondary" id="btn-clear-log">Clear</button>
158158
</div>
159-
<div class="log" id="log"></div>
159+
<div class="log" id="log" role="log" aria-live="polite" aria-label="SDK event log"></div>
160160
</section>
161161
</div>
162162
</div>
163+
<footer>
164+
<span>@imtbl/audience <span id="sdk-version">0.0.0</span></span>
165+
<span class="footer-sep">·</span>
166+
<a href="./README.md">Demo README</a>
167+
<span class="footer-sep">·</span>
168+
<a href="../README.md">SDK README</a>
169+
</footer>
163170
</main>
164171

165172
<script src="../dist/cdn/imtbl-audience.global.js"></script>

packages/audience/sdk/src/cdn.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ describe('cdn entry point', () => {
2424
Audience: { init: Function };
2525
AudienceError: typeof Error;
2626
IdentityType: Record<string, string>;
27+
version: string;
2728
};
2829
}).ImmutableAudience;
2930

3031
expect(g).toBeDefined();
3132
expect(typeof g!.Audience.init).toBe('function');
3233
expect(g!.IdentityType.Passport).toBe('passport');
34+
expect(typeof g!.version).toBe('string');
35+
expect(g!.version.length).toBeGreaterThan(0);
3336
expect(g!.IdentityType.Steam).toBe('steam');
3437
expect(g!.IdentityType.Custom).toBe('custom');
3538

packages/audience/sdk/src/cdn.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
import { IdentityType } from '@imtbl/audience-core';
1414

1515
import { Audience } from './sdk';
16+
import { LIBRARY_VERSION } from './config';
1617
import { AudienceError } from './types';
1718

1819
type GlobalShape = {
1920
Audience: typeof Audience;
2021
AudienceError: typeof AudienceError;
2122
IdentityType: typeof IdentityType;
23+
version: string;
2224
};
2325

2426
// globalThis is ES2020; tsup targets es2018, so provide a runtime fallback
@@ -37,5 +39,6 @@ if (globalObj.ImmutableAudience) {
3739
Audience,
3840
AudienceError,
3941
IdentityType,
42+
version: LIBRARY_VERSION,
4043
};
4144
}

0 commit comments

Comments
 (0)