@nadicodeai/design-system 0.21.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,8 @@
14
14
  "canvas": "#ffffff",
15
15
  "canvas-soft": "#fafafa",
16
16
  "canvas-soft-2": "#f5f5f5",
17
+ "material-workspace": "#f7f7f7",
18
+ "material-solid": "#ffffff",
17
19
  "line": "#e5e5e5",
18
20
  "input": "#757575",
19
21
  "seam": "#d4d4d4",
@@ -73,6 +75,8 @@
73
75
  "dark-canvas": "#000000",
74
76
  "dark-canvas-soft": "#111111",
75
77
  "dark-canvas-soft-2": "#1a1a1a",
78
+ "dark-material-workspace": "#000000",
79
+ "dark-material-solid": "#000000",
76
80
  "dark-ink": "#ededed",
77
81
  "dark-body": "#c7c7c7",
78
82
  "dark-muted": "#8f8f8f",
@@ -136,6 +136,28 @@
136
136
  "hex": "#f5f5f5"
137
137
  }
138
138
  },
139
+ "material-workspace": {
140
+ "$value": {
141
+ "colorSpace": "srgb",
142
+ "components": [
143
+ 0.969,
144
+ 0.969,
145
+ 0.969
146
+ ],
147
+ "hex": "#f7f7f7"
148
+ }
149
+ },
150
+ "material-solid": {
151
+ "$value": {
152
+ "colorSpace": "srgb",
153
+ "components": [
154
+ 1,
155
+ 1,
156
+ 1
157
+ ],
158
+ "hex": "#ffffff"
159
+ }
160
+ },
139
161
  "line": {
140
162
  "$value": {
141
163
  "colorSpace": "srgb",
@@ -785,6 +807,28 @@
785
807
  "hex": "#1a1a1a"
786
808
  }
787
809
  },
810
+ "dark-material-workspace": {
811
+ "$value": {
812
+ "colorSpace": "srgb",
813
+ "components": [
814
+ 0,
815
+ 0,
816
+ 0
817
+ ],
818
+ "hex": "#000000"
819
+ }
820
+ },
821
+ "dark-material-solid": {
822
+ "$value": {
823
+ "colorSpace": "srgb",
824
+ "components": [
825
+ 0,
826
+ 0,
827
+ 0
828
+ ],
829
+ "hex": "#000000"
830
+ }
831
+ },
788
832
  "dark-ink": {
789
833
  "$value": {
790
834
  "colorSpace": "srgb",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nadicodeai/design-system",
3
- "version": "0.21.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,7 +12,6 @@
12
12
  "./tokens/dtcg": "./dist/tokens/nadicode.dtcg.json",
13
13
  "./tailwind": "./dist/tailwind/nadicode.tailwind.json",
14
14
  "./css": "./dist/css/index.css",
15
- "./css/chat-autoplay.js": "./dist/css/chat-autoplay.js",
16
15
  "./icons/*": "./dist/icons/*",
17
16
  "./favicon/*": "./dist/favicon/*",
18
17
  "./assets/logo-mark": {
@@ -155,6 +154,8 @@
155
154
  "lint:design": "design.md lint DESIGN.md",
156
155
  "lint": "eslint --max-warnings=0",
157
156
  "typecheck": "tsc -p tsconfig.json",
157
+ "check": "npm run lint:design && npm run lint && npm run typecheck",
158
+ "check:static": "npm run check",
158
159
  "test": "vitest run",
159
160
  "test:visual": "playwright test --config playwright.visual.config.ts",
160
161
  "prepublishOnly": "npm run build"
@@ -1,537 +0,0 @@
1
- /*
2
- * chat-autoplay.js — the single optional vanilla enhancer for NadicodeAI chat
3
- * threads. The ONLY JavaScript in @nadicodeai/design-system.
4
- *
5
- * It is pure progressive enhancement, framework-free, dependency-free, with no
6
- * module-loader statements and no third-party runtime. It turns an
7
- * already-static `.nc-chat-thread` into an auto-playing, looping agent
8
- * conversation.
9
- *
10
- * Runtime behavior:
11
- *
12
- * (a) Motion on: messages are hidden, then revealed in scripted source order by
13
- * toggling the `.nc-anim-msg-in` hook (the rise-in). A typing indicator
14
- * (`.nc-typing-dots`) shows between turns, the thread body auto-scrolls to
15
- * the newest message, the `.nc-usage-meter-fill` is driven via `--nc-usage`,
16
- * and after `LOOP_HOLD` the cycle restarts.
17
- *
18
- * (b) `prefers-reduced-motion: reduce`: the FULL thread is shown immediately,
19
- * every message visible, NO loop, no typing animation. (We also stop if the
20
- * preference changes to reduce mid-play.)
21
- *
22
- * (c) Script absent: the conversation renders FULLY and statically from CSS
23
- * alone. The atoms set `animation-name: nc-msg-in` but, without the
24
- * `.nc-anim-msg-in` hook supplying a duration, that is inert and every
25
- * message is visible. This file is the ONLY thing that ever hides a message
26
- * (via the per-element inline `display` it sets while playing). No hidden
27
- * class is ever present in static markup, so no-JS == full static thread.
28
- *
29
- * (d) Playback starts on scroll-into-view via `IntersectionObserver`; it pauses
30
- * (resets to the static full thread and re-arms) when scrolled fully out.
31
- *
32
- * Timing parity with `src/css/motion.css`: the loop reads the duration token
33
- * (`--nc-duration-stagger`) from the thread's computed style and uses the
34
- * per-role `defaultTiming()` rhythm below. The stagger unit comes from CSS, not
35
- * a re-typed magic number; this module owns the sequencing cadence because it
36
- * is runtime behavior rather than a reusable visual token.
37
- *
38
- * Opt-in: only `.nc-chat-thread[data-nc-autoplay]` elements are enhanced. A
39
- * thread without the attribute is left fully static.
40
- *
41
- * Usage (no bundler assumed):
42
- * <script src="…/css/chat-autoplay.js"></script>
43
- * It self-initializes on DOMContentLoaded and also exposes
44
- * `window.ncChatAutoplay.init(root?)` for manual / dynamic init.
45
- */
46
- (function () {
47
- "use strict";
48
-
49
- if (typeof window === "undefined" || typeof document === "undefined") return;
50
-
51
- // The CSS token owns visual stagger. These values own runtime sequencing.
52
- var LOOP_HOLD = 6000; // Keep the completed state visible for most of the loop.
53
- var START_DELAY = 240;
54
-
55
- function defaultTiming(role, textLen) {
56
- var len = textLen || 0;
57
- switch (role) {
58
- case "user":
59
- return { typingMs: 0, holdMs: 1100 + len * 8 };
60
- case "tool":
61
- return { typingMs: 380, holdMs: 700 };
62
- case "state":
63
- return { typingMs: 220, holdMs: 700 };
64
- case "memory":
65
- return { typingMs: 380, holdMs: 900 };
66
- case "handoff":
67
- return { typingMs: 420, holdMs: 1300 };
68
- case "approval":
69
- return { typingMs: 460, holdMs: 1600 };
70
- case "artifact":
71
- return { typingMs: 600, holdMs: 1800 };
72
- case "agent":
73
- default:
74
- return { typingMs: 680, holdMs: 900 + len * 12 };
75
- }
76
- }
77
-
78
- // Infer a message's role from its NadicodeAI atom class so authors don't have
79
- // to duplicate the role in a data attribute. An explicit `data-nc-role`
80
- // overrides. Falls back to "agent" (the slowest, safest cadence).
81
- var ROLE_BY_CLASS = [
82
- ["nc-chat-bubble-user", "user"],
83
- ["nc-chat-bubble-agent", "agent"],
84
- ["nc-tool-call-trace", "tool"],
85
- ["nc-memory-item", "memory"],
86
- ["nc-handoff-banner", "handoff"],
87
- ["nc-approval-gate", "approval"],
88
- ["nc-artifact-preview", "artifact"],
89
- ["nc-state-row", "state"],
90
- ];
91
-
92
- function roleOf(el) {
93
- var explicit = el.getAttribute("data-nc-role");
94
- if (explicit) return explicit;
95
- for (var i = 0; i < ROLE_BY_CLASS.length; i++) {
96
- if (el.classList.contains(ROLE_BY_CLASS[i][0])) return ROLE_BY_CLASS[i][1];
97
- }
98
- return "agent";
99
- }
100
-
101
- function reducedMotion() {
102
- return (
103
- typeof window.matchMedia === "function" &&
104
- window.matchMedia("(prefers-reduced-motion: reduce)").matches
105
- );
106
- }
107
-
108
- // Read a duration token (e.g. "--nc-duration-stagger") off a live element and
109
- // return milliseconds. Falls back to a documented default that equals the
110
- // motion.css value if the bundle is not loaded (graceful, never throws).
111
- function durationToken(el, name, fallbackMs) {
112
- var raw = getComputedStyle(el).getPropertyValue(name).trim();
113
- if (!raw) return fallbackMs;
114
- if (raw.slice(-2) === "ms") return parseFloat(raw) || fallbackMs;
115
- if (raw.slice(-1) === "s") return (parseFloat(raw) || 0) * 1000 || fallbackMs;
116
- var n = parseFloat(raw);
117
- return isNaN(n) ? fallbackMs : n;
118
- }
119
-
120
- // ── One thread's player ───────────────────────────────────────────────────
121
- function ChatThreadPlayer(thread) {
122
- this.thread = thread;
123
- this.body = thread.querySelector(".nc-chat-thread-body");
124
- // Stable, source-ordered list of message atoms inside the body. The typing
125
- // indicator and any element flagged to ignore are NOT messages.
126
- this.messages = this.body
127
- ? Array.prototype.filter.call(this.body.children, function (el) {
128
- return (
129
- el.nodeType === 1 &&
130
- !el.classList.contains("nc-typing-dots") &&
131
- el.getAttribute("data-nc-autoplay-ignore") === null
132
- );
133
- })
134
- : [];
135
- this.typing = this.body ? this.body.querySelector(".nc-typing-dots") : null;
136
- this.meterFill = thread.querySelector(".nc-usage-meter-fill");
137
- this.staggerMs = 160; // overwritten from the CSS token at enhance()
138
- this.timers = [];
139
- this.running = false;
140
- this.cancelled = false;
141
- // Monotonic loop epoch. `_runLoop` captures the value live at start and every
142
- // guard checks it. `stop()`/`rearm()` bump it so a stale continuation — a
143
- // `_sleep` timer that already fired but whose `.then` is still queued when the
144
- // player is stopped and immediately re-armed on scroll-in — sees a mismatched
145
- // epoch and dies permanently, instead of resuming once `cancelled` flips back
146
- // to false and racing the fresh loop. Single, uninterrupted playback never
147
- // changes the epoch mid-run, so the observed sequence is unchanged.
148
- this.generation = 0;
149
- // WCAG 2.2.2 user-pause latch. Set true when the user presses the pause button;
150
- // cleared when the user presses resume. play() and the in-view observer branch
151
- // both check it; a paused thread stays paused across scroll-out/in.
152
- this.userPaused = false;
153
- // Set to true once the sequence has reached its end at least once. Used by
154
- // the IntersectionObserver entry branch: a thread that has completed is shown
155
- // in its final visible state on re-entry rather than replayed from blank, so
156
- // a reader who scrolls back sees the payoff frame instead of an empty/typing state.
157
- this.completedOnce = false;
158
- // Accent for an auto-created typing bubble, inferred from the first agent
159
- // bubble's data-accent (default green, matching the prototype).
160
- this.accent = "green";
161
- var firstAgent = thread.querySelector(".nc-chat-bubble-agent[data-accent]");
162
- if (firstAgent) this.accent = firstAgent.getAttribute("data-accent") || "green";
163
- }
164
-
165
- ChatThreadPlayer.prototype._clearTimers = function () {
166
- for (var i = 0; i < this.timers.length; i++) clearTimeout(this.timers[i]);
167
- this.timers = [];
168
- };
169
-
170
- ChatThreadPlayer.prototype._sleep = function (ms) {
171
- var timers = this.timers;
172
- return new Promise(function (resolve) {
173
- var t = setTimeout(resolve, ms);
174
- timers.push(t);
175
- });
176
- };
177
-
178
- // Fully static, fully visible end-state. Used for no-loop (reduced-motion),
179
- // for pause-out-of-view, and as the baseline each loop resets from. Removes
180
- // every trace the player added so the DOM matches the no-JS static thread.
181
- ChatThreadPlayer.prototype._showAll = function () {
182
- for (var i = 0; i < this.messages.length; i++) {
183
- var m = this.messages[i];
184
- m.style.display = "";
185
- m.classList.remove("nc-anim-msg-in");
186
- }
187
- this._hideTyping();
188
- this.thread.removeAttribute("data-nc-playing");
189
- };
190
-
191
- ChatThreadPlayer.prototype._hideAllMessages = function () {
192
- for (var i = 0; i < this.messages.length; i++) {
193
- this.messages[i].style.display = "none";
194
- this.messages[i].classList.remove("nc-anim-msg-in");
195
- }
196
- };
197
-
198
- ChatThreadPlayer.prototype._scrollToBottom = function () {
199
- var body = this.body;
200
- if (!body) return;
201
- requestAnimationFrame(function () {
202
- body.scrollTop = body.scrollHeight;
203
- });
204
- };
205
-
206
- // Ensure a typing indicator exists; create one if the markup has none so the
207
- // "typing between turns" behaviour works on any thread. Created element is
208
- // tagged so _showAll can drop it.
209
- ChatThreadPlayer.prototype._ensureTyping = function () {
210
- if (this.typing) return this.typing;
211
- if (!this.body) return null;
212
- var el = document.createElement("div");
213
- el.className = "nc-typing-dots";
214
- el.setAttribute("data-accent", this.accent);
215
- el.setAttribute("aria-hidden", "true");
216
- el.setAttribute("data-nc-autoplay-created", "");
217
- for (var i = 0; i < 3; i++) {
218
- var dot = document.createElement("span");
219
- dot.className = "nc-typing-dots-dot nc-anim-typing";
220
- dot.style.setProperty("--nc-anim-delay", i * this.staggerMs + "ms");
221
- el.appendChild(dot);
222
- }
223
- el.style.display = "none";
224
- this.body.appendChild(el);
225
- this.typing = el;
226
- return el;
227
- };
228
-
229
- ChatThreadPlayer.prototype._showTyping = function () {
230
- var el = this._ensureTyping();
231
- if (!el) return;
232
- // Move the indicator to the end of the body so it sits below revealed turns.
233
- this.body.appendChild(el);
234
- el.style.display = "";
235
- this._scrollToBottom();
236
- };
237
-
238
- ChatThreadPlayer.prototype._hideTyping = function () {
239
- if (this.typing) this.typing.style.display = "none";
240
- };
241
-
242
- ChatThreadPlayer.prototype._setMeter = function (fraction) {
243
- if (!this.meterFill) return;
244
- var pct = Math.max(0, Math.min(1, fraction)) * 100;
245
- this.meterFill.style.setProperty("--nc-usage", pct + "%");
246
- };
247
-
248
- ChatThreadPlayer.prototype._reveal = function (msg, index) {
249
- msg.style.display = "";
250
- // Re-trigger the rise-in: the hook supplies duration/easing; the atom
251
- // already carries `animation-name: nc-msg-in`. Force a reflow so re-adding
252
- // the class restarts the animation on a re-loop.
253
- msg.style.setProperty("--nc-anim-delay", "0ms");
254
- msg.classList.remove("nc-anim-msg-in");
255
- void msg.offsetWidth;
256
- msg.classList.add("nc-anim-msg-in");
257
- this._setMeter((index + 1) / this.messages.length);
258
- this._scrollToBottom();
259
- };
260
-
261
- // A loop is "stale" once it is cancelled OR a newer epoch has started; either
262
- // way it must stop touching the DOM. `myGen` is captured once at start so a queued
263
- // continuation can detect that the player was stopped-and-re-armed underneath
264
- // it (see the `generation` note in the constructor).
265
- ChatThreadPlayer.prototype._runLoop = function () {
266
- var myGen = this.generation;
267
- function stale(player) {
268
- return player.cancelled || player.generation !== myGen;
269
- }
270
- function loop(player) {
271
- if (stale(player)) return Promise.resolve();
272
- player._hideAllMessages();
273
- player._hideTyping();
274
- player._setMeter(0);
275
- return player
276
- ._sleep(START_DELAY)
277
- .then(function () {
278
- var chain = Promise.resolve();
279
- player.messages.forEach(function (msg, i) {
280
- chain = chain.then(function () {
281
- if (stale(player)) return;
282
- var role = roleOf(msg);
283
- var t = defaultTiming(role, (msg.textContent || "").trim().length);
284
- var step = function () {
285
- if (stale(player)) return;
286
- player._hideTyping();
287
- player._reveal(msg, i);
288
- return player._sleep(t.holdMs);
289
- };
290
- if (t.typingMs > 0) {
291
- player._showTyping();
292
- return player._sleep(t.typingMs).then(step);
293
- }
294
- return step();
295
- });
296
- });
297
- return chain;
298
- })
299
- .then(function () {
300
- if (stale(player)) return;
301
- // Mark completion before the hold so the observer entry branch can
302
- // show the final state on re-entry without replaying from blank.
303
- player.completedOnce = true;
304
- return player._sleep(LOOP_HOLD);
305
- })
306
- .then(function () {
307
- if (stale(player)) return;
308
- return loop(player);
309
- });
310
- }
311
- return loop(this);
312
- };
313
-
314
- ChatThreadPlayer.prototype.play = function () {
315
- if (this.running || this.cancelled) return;
316
- // WCAG 2.2.2: user-pressed pause wins over every automatic restart.
317
- if (this.userPaused) return;
318
- // (b) Reduced motion: show the full thread immediately, no loop, no typing.
319
- if (reducedMotion()) {
320
- this._showAll();
321
- this._setMeter(1);
322
- return;
323
- }
324
- if (!this.messages.length) return;
325
- this.running = true;
326
- this.thread.setAttribute("data-nc-playing", "");
327
- this._runLoop();
328
- };
329
-
330
- // Stop and reset to the fully-visible static thread (re-armable).
331
- ChatThreadPlayer.prototype.stop = function () {
332
- this.cancelled = true;
333
- this.generation++; // retire the running loop's queued continuations
334
- this._clearTimers();
335
- this.running = false;
336
- this._showAll();
337
- this._setMeter(1);
338
- };
339
-
340
- // Re-arm a stopped player (used when the thread scrolls back into view). Bump
341
- // the epoch again so a still-queued continuation from the just-stopped loop can
342
- // never resume now that `cancelled` is cleared — only the loop `play()` starts
343
- // next will match the current epoch.
344
- ChatThreadPlayer.prototype.rearm = function () {
345
- this.cancelled = false;
346
- this.generation++;
347
- this.running = false;
348
- };
349
-
350
- // Inject the enhancer-owned pause/resume button into the thread card.
351
- // One button per thread, positioned top-right above the body. Uses
352
- // data-nc-autoplay-name for an accessible per-thread subject, e.g.
353
- // "Pause <value>"/"Resume <value>"; falls back to "Pause demonstration".
354
- // Never called in reduced-motion mode (contract (b): no button for a path
355
- // that plays nothing).
356
- ChatThreadPlayer.prototype._injectPauseButton = function () {
357
- var thread = this.thread;
358
- var subject = thread.getAttribute("data-nc-autoplay-name") || "demonstration";
359
- var btn = document.createElement("button");
360
- btn.type = "button";
361
- btn.className = "nc-chat-pause";
362
- btn.setAttribute("aria-pressed", "false");
363
- btn.textContent = "Pause " + subject;
364
- btn.addEventListener(
365
- "click",
366
- function () {
367
- if (this.userPaused) {
368
- // Resume: clear the latch, rearm and play.
369
- this.userPaused = false;
370
- btn.setAttribute("aria-pressed", "false");
371
- btn.textContent = "Pause " + subject;
372
- this.rearm();
373
- this.play();
374
- } else {
375
- // Pause: set the latch, stop and reveal full content.
376
- this.userPaused = true;
377
- btn.setAttribute("aria-pressed", "true");
378
- btn.textContent = "Resume " + subject;
379
- this.stop();
380
- }
381
- }.bind(this),
382
- );
383
- // Insert before the body so the button sits above the scrolling content.
384
- var body = thread.querySelector(".nc-chat-thread-body");
385
- if (body) {
386
- thread.insertBefore(btn, body);
387
- } else {
388
- thread.appendChild(btn);
389
- }
390
- this.pauseBtn = btn;
391
- };
392
-
393
- // ── Initialisation ──────────────────────────────────────────────────────
394
- var players = [];
395
- var observer = null;
396
-
397
- function enhance(thread) {
398
- if (thread.__ncAutoplayPlayer) return thread.__ncAutoplayPlayer;
399
- var player = new ChatThreadPlayer(thread);
400
- // Read the stagger token off the live thread (parity with motion.css);
401
- // fallback equals the motion.css default (60ms) if the bundle is absent.
402
- player.staggerMs = durationToken(thread, "--nc-duration-stagger", 60);
403
- thread.__ncAutoplayPlayer = player;
404
- players.push(player);
405
- // Inject the pause/resume button unless reduced-motion is on (that path
406
- // plays nothing and must inject no button — WCAG 2.2.2 scope follows the
407
- // animation).
408
- if (!reducedMotion()) {
409
- player._injectPauseButton();
410
- }
411
- return player;
412
- }
413
-
414
- function startObserving() {
415
- if (typeof window.IntersectionObserver !== "function") {
416
- // No IntersectionObserver: degrade by playing immediately (still respects
417
- // reduced motion inside play()).
418
- for (var i = 0; i < players.length; i++) players[i].play();
419
- return;
420
- }
421
- // Manual re-init (the documented `window.ncChatAutoplay.init(root)` path for
422
- // dynamic content) calls this again; tear down the prior observer first so a
423
- // repeat init replaces — rather than stacks — observers. A single self-init
424
- // (the common path) hits this once and `observer` is null, so behavior is
425
- // unchanged; only repeated manual init benefits (no leaked observers, no
426
- // duplicate intersection callbacks per thread).
427
- if (observer) observer.disconnect();
428
- observer = new IntersectionObserver(
429
- function (entries) {
430
- for (var i = 0; i < entries.length; i++) {
431
- var entry = entries[i];
432
- var player = entry.target.__ncAutoplayPlayer;
433
- if (!player) continue;
434
- if (entry.isIntersecting) {
435
- // (d) Start when scrolled into view — but only if the player is not
436
- // already running its loop. Re-arming a mid-loop player kills and
437
- // restarts it from blank (the long right thread reads as dead under
438
- // scroll crossings). A player is "running" once play() sets the flag
439
- // and until stop() clears it; the loop recursion holds this state.
440
- // WCAG 2.2.2: do not override a user-pressed pause on scroll-in.
441
- if (player.userPaused) continue;
442
- if (!player.running) {
443
- if (player.completedOnce) {
444
- // The sequence finished at least once. Show the full final state
445
- // immediately so the reader sees the payoff frame, not a blank/
446
- // typing restart. The loop resumes from this state on the next
447
- // natural cycle if the thread stays in view.
448
- player._showAll();
449
- player._setMeter(1);
450
- } else {
451
- player.rearm();
452
- player.play();
453
- }
454
- }
455
- } else if (entry.intersectionRatio === 0) {
456
- // Stop only when the thread is FULLY out of view (ratio exactly 0).
457
- // At partial visibility (e.g. ratio 0.228) do nothing — collapsing
458
- // to the finished card while the thread is still on screen is the
459
- // bug this guard fixes.
460
- if (player.running) {
461
- player.stop();
462
- }
463
- }
464
- }
465
- },
466
- { threshold: [0, 0.25] },
467
- );
468
- for (var j = 0; j < players.length; j++) observer.observe(players[j].thread);
469
- }
470
-
471
- // ── Enhancer-armed, run-once scroll entrance ─────────────────────────────
472
- // Arms .nc-settle elements that are fully below the viewport at init.
473
- // On first IntersectionObserver entry, switches data-nc-settle from "armed"
474
- // to "in" (triggers the CSS nc-msg-in animation) and immediately unobserves
475
- // so the rise-in fires once and is never reversed on scroll-back. Elements
476
- // already visible at load are left unarmed (no flash; hero region untouched).
477
- // prefers-reduced-motion: skip entirely — page fully visible without JS.
478
- function initSettle(scope) {
479
- if (reducedMotion()) return;
480
- if (typeof window.IntersectionObserver !== "function") return;
481
- var settleElements = (scope || document).querySelectorAll(".nc-settle");
482
- if (!settleElements.length) return;
483
- var settleObserver = new IntersectionObserver(
484
- function (entries, obs) {
485
- for (var i = 0; i < entries.length; i++) {
486
- if (entries[i].isIntersecting) {
487
- entries[i].target.setAttribute("data-nc-settle", "in");
488
- obs.unobserve(entries[i].target);
489
- }
490
- }
491
- },
492
- { threshold: 0.1 },
493
- );
494
- for (var i = 0; i < settleElements.length; i++) {
495
- var el = settleElements[i];
496
- var rect = el.getBoundingClientRect();
497
- // Arm only elements whose top edge is fully below the viewport.
498
- if (rect.top > window.innerHeight) {
499
- el.setAttribute("data-nc-settle", "armed");
500
- settleObserver.observe(el);
501
- }
502
- }
503
- }
504
-
505
- function init(root) {
506
- var scope = root || document;
507
- var threads = scope.querySelectorAll(".nc-chat-thread[data-nc-autoplay]");
508
- for (var i = 0; i < threads.length; i++) enhance(threads[i]);
509
- initSettle(scope);
510
- startObserving();
511
- return players;
512
- }
513
-
514
- // Respond to a runtime change of the reduced-motion preference: collapse any
515
- // running player to the static full thread.
516
- if (typeof window.matchMedia === "function") {
517
- var mq = window.matchMedia("(prefers-reduced-motion: reduce)");
518
- var onChange = function () {
519
- if (!mq.matches) return;
520
- for (var i = 0; i < players.length; i++) {
521
- if (players[i].running) players[i].stop();
522
- }
523
- };
524
- if (typeof mq.addEventListener === "function") mq.addEventListener("change", onChange);
525
- else if (typeof mq.addListener === "function") mq.addListener(onChange);
526
- }
527
-
528
- window.ncChatAutoplay = { init: init, players: players };
529
-
530
- if (document.readyState === "loading") {
531
- document.addEventListener("DOMContentLoaded", function () {
532
- init();
533
- });
534
- } else {
535
- init();
536
- }
537
- })();