dsh-offwork 1.1.0 → 1.1.1

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.
package/client.source.js CHANGED
@@ -6,6 +6,7 @@ window.__ModuleLoader__.load({
6
6
  const h = React.createElement;
7
7
  /* PURE_MODULES */
8
8
  /* PET_SPRITES */
9
+ /* PET_CLIPS */
9
10
  const messages = {
10
11
  zh: {
11
12
  ...COMPANION_MESSAGES.zh,
@@ -432,8 +433,10 @@ window.__ModuleLoader__.load({
432
433
  apply(ctx) {
433
434
  for (const [lang, dict] of Object.entries(messages))
434
435
  ctx.effect(() => ctx.locale.register('offwork-clock', lang, dict));
435
- const viewKey = 'dsh.offwork-clock.view.v5';
436
- const previousViewKey = 'dsh.offwork-clock.view.v4';
436
+ const viewKey = 'dsh.offwork-clock.view.v7';
437
+ const previousViewKey = 'dsh.offwork-clock.view.v6';
438
+ const olderViewKey = 'dsh.offwork-clock.view.v5';
439
+ const oldestViewKey = 'dsh.offwork-clock.view.v4';
437
440
  const legacyKey = 'dsh.offwork-clock.view.v3';
438
441
 
439
442
  function Clock(props) {
@@ -505,6 +508,8 @@ window.__ModuleLoader__.load({
505
508
  const [waterConfirm, setWaterConfirm] = React.useState(null);
506
509
  const [waterNotice, setWaterNotice] = React.useState(false);
507
510
  const reactionState = React.useRef(null);
511
+ const petMotion = React.useRef(null);
512
+ const petReactionToken = React.useRef(null);
508
513
  const waterRuntime = React.useRef(null);
509
514
  const waterOwner = React.useRef(null);
510
515
  if (!waterOwner.current)
@@ -655,7 +660,11 @@ window.__ModuleLoader__.load({
655
660
 
656
661
  React.useEffect(() => {
657
662
  try {
658
- const saved = localStorage.getItem(viewKey) ?? localStorage.getItem(previousViewKey);
663
+ const saved =
664
+ localStorage.getItem(viewKey) ??
665
+ localStorage.getItem(previousViewKey) ??
666
+ localStorage.getItem(olderViewKey) ??
667
+ localStorage.getItem(oldestViewKey);
659
668
  const legacy = !saved ? JSON.parse(localStorage.getItem(legacyKey) || 'null') : null;
660
669
  setView(
661
670
  normalizeView(
@@ -1191,7 +1200,10 @@ window.__ModuleLoader__.load({
1191
1200
  d.moved = true;
1192
1201
  petDragged.current = true;
1193
1202
  const next = clamp({ x: e.clientX - d.dx, y: e.clientY - d.dy });
1194
- if (petMode) setDragPosition(next);
1203
+ if (petMode) {
1204
+ setDragPosition(next);
1205
+ petMotion.current?.drag(e.clientX - d.x0);
1206
+ }
1195
1207
  lastPosition.current = next;
1196
1208
  if (box.current) {
1197
1209
  box.current.style.left = next.x + 'px';
@@ -1203,6 +1215,7 @@ window.__ModuleLoader__.load({
1203
1215
  const d = dragState.current;
1204
1216
  if (!d) return;
1205
1217
  dragState.current = null;
1218
+ if (petMode && d.moved) petMotion.current?.release(e.type !== 'pointerup');
1206
1219
  if (e.type === 'pointercancel' || e.type === 'lostpointercapture')
1207
1220
  petDragged.current = false;
1208
1221
  if (d.moved && lastPosition.current) commitPosition(lastPosition.current);
@@ -1472,6 +1485,56 @@ window.__ModuleLoader__.load({
1472
1485
  motion: view.companionMotion && !reactionState.current?.stopped,
1473
1486
  pageVisible: waterVisible && !document.hidden && !editing && !waterEditing,
1474
1487
  });
1488
+ React.useLayoutEffect(() => {
1489
+ if (!petMode) return;
1490
+ const artwork = box.current?.querySelector('.owc-pet-handle > .owc-pet-art');
1491
+ if (!artwork) return;
1492
+ const base = createPetMotion(artwork, window, { enabled: false });
1493
+ const controller = createPetVideoMotion(
1494
+ artwork,
1495
+ window,
1496
+ base,
1497
+ PET_CLIPS.models[stageModel],
1498
+ PET_CLIPS.probe,
1499
+ );
1500
+ petMotion.current = controller;
1501
+ return () => {
1502
+ controller.destroy();
1503
+ petMotion.current = null;
1504
+ };
1505
+ }, [petMode, stageModel]);
1506
+ React.useLayoutEffect(() => {
1507
+ petMotion.current?.configure({
1508
+ enabled:
1509
+ savedView.companionMotion &&
1510
+ waterVisible &&
1511
+ waterReady &&
1512
+ !waterError &&
1513
+ !error &&
1514
+ !editing &&
1515
+ !waterEditing &&
1516
+ !ui.menu,
1517
+ resting: ['quiet', 'rest', 'offline'].includes(companion.pose),
1518
+ });
1519
+ }, [
1520
+ petMode,
1521
+ stageModel,
1522
+ savedView.companionMotion,
1523
+ waterVisible,
1524
+ waterReady,
1525
+ waterError,
1526
+ error,
1527
+ editing,
1528
+ waterEditing,
1529
+ !!ui.menu,
1530
+ companion.pose,
1531
+ ]);
1532
+ React.useLayoutEffect(() => {
1533
+ const token = waterNotice?.token;
1534
+ if (petReactionToken.current === token) return;
1535
+ petReactionToken.current = token;
1536
+ if (petMode && companion.animate) petMotion.current?.play('celebrate');
1537
+ }, [petMode, waterNotice?.token, companion.animate]);
1475
1538
  const companionCard = (compact = false) =>
1476
1539
  companion.visible &&
1477
1540
  h(
@@ -2346,7 +2409,7 @@ window.__ModuleLoader__.load({
2346
2409
  ? petArtwork(
2347
2410
  h,
2348
2411
  companion,
2349
- view.petSize * (petModel(view.petModel).renderer === 'image' ? 0.65 : 1),
2412
+ view.petSize * (petModel(view.petModel).renderer === 'svg' ? 1 : 0.65),
2350
2413
  '',
2351
2414
  view.petModel,
2352
2415
  PET_SPRITES,
@@ -2592,15 +2655,13 @@ window.__ModuleLoader__.load({
2592
2655
  function closePetPanel() {
2593
2656
  navigateScreen(null);
2594
2657
  }
2595
- function togglePetPanel(event) {
2658
+ function interactWithPet(event) {
2596
2659
  if (petDragged.current && event.detail !== 0) {
2597
2660
  petDragged.current = false;
2598
2661
  return;
2599
2662
  }
2600
- navigateScreen(
2601
- event.currentTarget.classList.contains('owc-pet-clock') ? 'income' : 'home',
2602
- event,
2603
- );
2663
+ petDragged.current = false;
2664
+ if (!ui.pending) petMotion.current?.play('greet');
2604
2665
  }
2605
2666
  function closePetMenu(restore = true) {
2606
2667
  dispatchUI({ type: 'dismiss-menu' });
@@ -3071,7 +3132,7 @@ window.__ModuleLoader__.load({
3071
3132
  {
3072
3133
  ref: box,
3073
3134
  className: 'owc-v2 owc-pet-shell',
3074
- 'data-version': '1.1.0',
3135
+ 'data-version': '1.1.1',
3075
3136
  'data-pet-model': stageModel,
3076
3137
  'data-status': safeState.status,
3077
3138
  'aria-label': t('companionLabel'),
@@ -3113,10 +3174,13 @@ window.__ModuleLoader__.load({
3113
3174
  {
3114
3175
  type: 'button',
3115
3176
  className: 'owc-pet-handle',
3116
- 'aria-label': t('petOpen'),
3117
- 'aria-expanded': !!petShowing,
3177
+ 'aria-label': t('petGreet'),
3118
3178
  title: t('companionName') + ' · ' + t('petHint'),
3119
- onClick: togglePetPanel,
3179
+ onClick: interactWithPet,
3180
+ onPointerEnter: (event) => {
3181
+ if (event.pointerType === 'mouse' && !dragState.current)
3182
+ petMotion.current?.play('look');
3183
+ },
3120
3184
  onKeyDown: gripKeys,
3121
3185
  },
3122
3186
  petArtwork(
@@ -3126,14 +3190,14 @@ window.__ModuleLoader__.load({
3126
3190
  waterNotice?.token,
3127
3191
  stageModel,
3128
3192
  PET_SPRITES,
3193
+ true,
3129
3194
  ),
3130
3195
  ),
3131
3196
  h(
3132
- 'button',
3197
+ 'div',
3133
3198
  {
3134
- type: 'button',
3135
3199
  className: 'owc-pet-clock',
3136
- onClick: togglePetPanel,
3200
+ role: 'timer',
3137
3201
  'aria-label': t('petClock'),
3138
3202
  },
3139
3203
  iconSvg('alarm', 12),
@@ -3145,13 +3209,13 @@ window.__ModuleLoader__.load({
3145
3209
  petShowing &&
3146
3210
  ui.screen !== 'home' &&
3147
3211
  h(
3148
- 'button',
3212
+ 'div',
3149
3213
  {
3150
- type: 'button',
3151
3214
  className: 'owc-pet-water-alert owc-water-due',
3152
3215
  'data-water-event': waterState.eventId,
3216
+ role: 'note',
3153
3217
  'aria-label': t('petWaterAlert'),
3154
- onClick: (event) => navigateScreen('home', event),
3218
+ title: t('petWaterAlert'),
3155
3219
  style: {
3156
3220
  left: waterAlertLayout.left,
3157
3221
  top: waterAlertLayout.top,
@@ -3258,7 +3322,7 @@ window.__ModuleLoader__.load({
3258
3322
  (collapsed && waterQuickOpen ? ' owc-quick' : '') +
3259
3323
  (editing || waterEditing ? ' owc-editing' : ''),
3260
3324
  'aria-label': t('title'),
3261
- 'data-version': '1.1.0',
3325
+ 'data-version': '1.1.1',
3262
3326
  'data-status': safeState?.status || 'setup',
3263
3327
  style: cardStyle,
3264
3328
  onPointerDown: dragDown,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-offwork",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Harness 页面陪伴角色、下班倒计时、实时收入与喝水提醒。Character companion, clock-out countdown, earnings and hydration reminders for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -40,6 +40,8 @@
40
40
  "ui-utils.js",
41
41
  "companion.js",
42
42
  "pet-view.js",
43
+ "pet-motion.js",
44
+ "pet-video.js",
43
45
  "pet-interactions.js",
44
46
  "pet-surfaces.js",
45
47
  "hydration.js",
@@ -48,6 +50,7 @@
48
50
  "locale/*.json",
49
51
  "icon.svg",
50
52
  "assets/characters/*",
53
+ "assets/animations/*",
51
54
  "LICENSE",
52
55
  "README.md",
53
56
  "CHANGELOG.md",
@@ -48,7 +48,7 @@ export const PET_INTERACTION_MESSAGES = {
48
48
  petWaterCard: '喝水',
49
49
  petCountdownCard: '下班倒计时',
50
50
  petEarningsCard: '收入',
51
- petWaterAlert: '查看喝水提醒',
51
+ petWaterAlert: '喝水提醒已到,可从右键菜单打开喝水卡片',
52
52
  petPausedHint: '本班已暂停,可手动恢复。',
53
53
  petMenu: '宠物菜单',
54
54
  petBack: '返回',
@@ -77,7 +77,7 @@ export const PET_INTERACTION_MESSAGES = {
77
77
  petWaterCard: 'Water',
78
78
  petCountdownCard: 'Shift countdown',
79
79
  petEarningsCard: 'Earnings',
80
- petWaterAlert: 'View water reminder',
80
+ petWaterAlert: 'Water is due; open Water from the context menu',
81
81
  petPausedHint: 'Paused for this shift. Resume when ready.',
82
82
  petMenu: 'Pet menu',
83
83
  petBack: 'Back',
@@ -106,7 +106,7 @@ export const PET_INTERACTION_MESSAGES = {
106
106
  export const PET_INTERACTION_STYLE = `
107
107
  .owc-pet-workspace .owc-glass{opacity:var(--owc-alpha)}
108
108
  .owc-pet-water-card.owc-water-due{border-radius:14px}
109
- .owc-pet-water-alert{position:fixed!important;width:140px;box-sizing:border-box;min-height:30px!important;border:1px solid var(--owc-accent)!important;background:var(--dsw-alias-bg-overlay)!important;box-shadow:0 4px 12px #18354b18;font-size:11px!important;z-index:2}
109
+ .owc-pet-water-alert{display:flex;align-items:center;justify-content:center;gap:6px;padding:8px 12px;border-radius:12px;cursor:default;position:fixed!important;width:140px;box-sizing:border-box;min-height:30px!important;border:1px solid var(--owc-accent)!important;background:var(--dsw-alias-bg-overlay)!important;box-shadow:0 4px 12px #18354b18;font-size:11px!important;z-index:2}
110
110
  .owc-pet-panel.owc-pet-workspace{display:flex;flex-direction:column;overflow:hidden}
111
111
  .owc-pet-workspace>.owc-head{position:relative;flex:none;min-height:46px;padding:8px 12px;box-sizing:border-box}
112
112
  .owc-pet-workspace>.owc-pet-scroll{position:relative;min-height:0;flex:1;overflow:auto;overscroll-behavior:contain;scrollbar-width:none}
package/pet-motion.js ADDED
@@ -0,0 +1,110 @@
1
+ // Independent, decorative motion only. Never writes preferences or drinking records.
2
+ const PET_ACTIONS = {
3
+ look: { duration: 650, transforms: ['none', 'rotate(-1deg) translateY(-1px)', 'none'] },
4
+ greet: {
5
+ duration: 900,
6
+ transforms: ['none', 'translateY(2px) rotate(-2deg)', 'translateY(-4px) rotate(1deg)', 'none'],
7
+ },
8
+ celebrate: {
9
+ duration: 1200,
10
+ transforms: [
11
+ 'none',
12
+ 'translateY(-7px) rotate(-1deg)',
13
+ 'translateY(1px)',
14
+ 'translateY(-3px) rotate(1deg)',
15
+ 'none',
16
+ ],
17
+ },
18
+ settle: {
19
+ duration: 550,
20
+ transforms: ['none', 'translateY(2px) scaleY(.985)', 'translateY(-2px)', 'none'],
21
+ },
22
+ };
23
+
24
+ export function createPetMotion(element, host, options = {}) {
25
+ const document = host.document;
26
+ const reduced = host.matchMedia?.('(prefers-reduced-motion: reduce)');
27
+ let enabled = options.enabled ?? true;
28
+ let resting = options.resting ?? false;
29
+ let animation = null;
30
+ let destroyed = false;
31
+ const allowed = () => !destroyed && enabled && !document.hidden && !reduced?.matches;
32
+ function stop() {
33
+ const previous = animation;
34
+ animation = null;
35
+ previous?.cancel();
36
+ delete element.dataset.action;
37
+ element.style.transform = '';
38
+ }
39
+ function sync() {
40
+ if (!allowed()) stop();
41
+ element.dataset.motion = allowed() && !resting ? 'idle' : 'off';
42
+ }
43
+ function play(action) {
44
+ if (!allowed() || !Object.hasOwn(PET_ACTIONS, action) || !element.animate) return false;
45
+ if (element.dataset.action === 'drag') return false;
46
+ if (action === 'look' && element.dataset.action) return false;
47
+ if (action === 'greet' && element.dataset.action === 'celebrate') return false;
48
+ stop();
49
+ const spec = PET_ACTIONS[action];
50
+ element.dataset.action = action;
51
+ try {
52
+ const running = element.animate(
53
+ spec.transforms.map((transform) => ({ transform })),
54
+ {
55
+ duration: spec.duration,
56
+ easing: 'ease-in-out',
57
+ },
58
+ );
59
+ animation = running;
60
+ running.finished.then(
61
+ () => {
62
+ if (animation === running) {
63
+ animation = null;
64
+ delete element.dataset.action;
65
+ }
66
+ },
67
+ () => {
68
+ if (animation === running) {
69
+ animation = null;
70
+ delete element.dataset.action;
71
+ }
72
+ },
73
+ );
74
+ return true;
75
+ } catch {
76
+ stop();
77
+ return false;
78
+ }
79
+ }
80
+ document.addEventListener('visibilitychange', sync);
81
+ reduced?.addEventListener?.('change', sync);
82
+ sync();
83
+ return {
84
+ play,
85
+ configure(next) {
86
+ if (next.resting && !resting) stop();
87
+ enabled = next.enabled;
88
+ resting = next.resting;
89
+ sync();
90
+ },
91
+ drag(direction) {
92
+ if (!allowed()) return;
93
+ stop();
94
+ element.dataset.action = 'drag';
95
+ element.style.transform = `translateY(-2px) rotate(${direction < 0 ? -2 : 2}deg)`;
96
+ },
97
+ release(cancelled = false) {
98
+ const dragged = element.dataset.action === 'drag';
99
+ stop();
100
+ if (dragged && !cancelled) play('settle');
101
+ },
102
+ destroy() {
103
+ destroyed = true;
104
+ stop();
105
+ element.dataset.motion = 'off';
106
+ document.removeEventListener('visibilitychange', sync);
107
+ reduced?.removeEventListener?.('change', sync);
108
+ },
109
+ };
110
+ }
package/pet-surfaces.js CHANGED
@@ -122,7 +122,7 @@ export const PET_SURFACE_STYLE = `
122
122
  .owc-v2:not(.owc-pet-shell) .owc-head{position:sticky;top:0;z-index:2;background:var(--owc-surface)}
123
123
  .owc-v2:not(.owc-pet-shell) .owc-save{position:sticky;bottom:0;z-index:2;background:var(--owc-surface);padding:12px 0}
124
124
  .owc-v2 .owc-pet-toast{overflow-wrap:anywhere;overflow:auto;scrollbar-width:none}
125
- .owc-v2 button.owc-pet-water-alert{border-color:var(--owc-accent)!important;color:var(--owc-accent)}
125
+ .owc-v2 .owc-pet-water-alert{border-color:var(--owc-accent)!important;color:var(--owc-accent)}
126
126
  .owc-v2 .owc-glass{background:var(--owc-surface);border:1px solid var(--owc-border);box-shadow:var(--owc-shadow);opacity:var(--owc-alpha,.94)}
127
127
  .owc-v2 .owc-pet-workspace{border-radius:var(--owc-radius);container-type:inline-size}
128
128
  .owc-v2 .owc-head{margin:0;padding:10px 16px;gap:8px;min-height:52px;border-bottom:1px solid var(--owc-border);box-sizing:border-box}
@@ -182,7 +182,7 @@ export const PET_SURFACE_STYLE = `
182
182
  .owc-v2 .owc-menu-natural{padding:6px;display:flow-root;box-sizing:border-box;min-width:0}
183
183
  .owc-v2 .owc-pet-context button{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;justify-content:flex-start;font-size:13px}
184
184
  .owc-v2 .owc-pet-context hr{border-color:var(--owc-border);margin:6px 4px}
185
- .owc-v2 .owc-pet-toast,.owc-v2 button.owc-pet-clock,.owc-v2 button.owc-pet-water-alert{background:var(--owc-surface)!important;color:var(--owc-text);border:1px solid var(--owc-border)!important;box-shadow:var(--owc-shadow)}
185
+ .owc-v2 .owc-pet-toast,.owc-v2 .owc-pet-clock,.owc-v2 .owc-pet-water-alert{background:var(--owc-surface)!important;color:var(--owc-text);border:1px solid var(--owc-border)!important;box-shadow:var(--owc-shadow)}
186
186
  .owc-v2 .owc-pet-toast{border-radius:12px;font-size:12px}
187
187
  .owc-v2 .owc-pet-unsaved{padding:20px 16px}
188
188
  .owc-v2 .owc-pet-unsaved p{font-size:13px;overflow-wrap:anywhere}
package/pet-video.js ADDED
@@ -0,0 +1,231 @@
1
+ // Independent transparent-video playback. Presentation only; never records an action.
2
+ const PET_VIDEO_SUPPORT = new WeakMap();
3
+
4
+ export function petVideoAlphaSupported(host, source) {
5
+ if (PET_VIDEO_SUPPORT.has(host)) return PET_VIDEO_SUPPORT.get(host);
6
+ const result = new Promise((resolve) => {
7
+ const video = host.document.createElement('video');
8
+ let timer;
9
+ const finish = (supported) => {
10
+ host.clearTimeout(timer);
11
+ video.onloadeddata = video.onerror = null;
12
+ video.pause();
13
+ video.removeAttribute('src');
14
+ video.load();
15
+ resolve(supported);
16
+ };
17
+ if (!source || !video.canPlayType('video/webm; codecs="vp9"')) return finish(false);
18
+ video.muted = true;
19
+ video.playsInline = true;
20
+ video.preload = 'auto';
21
+ video.onloadeddata = () => {
22
+ try {
23
+ const canvas = host.document.createElement('canvas');
24
+ canvas.width = canvas.height = 16;
25
+ const context = canvas.getContext('2d', { willReadFrequently: true });
26
+ context.drawImage(video, 0, 0, 16, 16);
27
+ finish(
28
+ context.getImageData(1, 1, 1, 1).data[3] > 240 &&
29
+ context.getImageData(14, 14, 1, 1).data[3] < 8,
30
+ );
31
+ } catch {
32
+ finish(false);
33
+ }
34
+ };
35
+ video.onerror = () => finish(false);
36
+ timer = host.setTimeout(() => finish(false), 3000);
37
+ video.src = source;
38
+ video.load();
39
+ });
40
+ PET_VIDEO_SUPPORT.set(host, result);
41
+ return result;
42
+ }
43
+
44
+ export function createPetVideoMotion(element, host, base, clips, probeSource) {
45
+ const layer = element.querySelector('.owc-pet-video-layer');
46
+ if (!layer || !clips) return base;
47
+ const document = host.document;
48
+ const reduced = host.matchMedia?.('(prefers-reduced-motion: reduce)');
49
+ const capability = petVideoAlphaSupported(host, probeSource);
50
+ const failed = new Set();
51
+ const retired = new Map();
52
+ let enabled = false,
53
+ resting = false,
54
+ dragging = false,
55
+ unsupported = false,
56
+ idleTimer = null,
57
+ destroyed = false,
58
+ current = null;
59
+ const allowed = () => enabled && !destroyed && !document.hidden && !reduced?.matches;
60
+ function clearIdleTimer() {
61
+ host.clearTimeout(idleTimer);
62
+ idleTimer = null;
63
+ }
64
+ function startIdle() {
65
+ if (
66
+ allowed() &&
67
+ !resting &&
68
+ !dragging &&
69
+ !current &&
70
+ !unsupported &&
71
+ Object.hasOwn(clips, 'idle') &&
72
+ !failed.has('idle')
73
+ )
74
+ play('idle');
75
+ }
76
+ function scheduleIdle(delay = 0) {
77
+ clearIdleTimer();
78
+ if (!Object.hasOwn(clips, 'idle') || failed.has('idle') || unsupported || destroyed) return;
79
+ idleTimer = host.setTimeout(() => {
80
+ idleTimer = null;
81
+ startIdle();
82
+ }, delay);
83
+ }
84
+ function dispose(video) {
85
+ video.onplaying = video.onended = video.onerror = video.onwaiting = video.onstalled = null;
86
+ video.pause();
87
+ video.removeAttribute('src');
88
+ video.load();
89
+ video.remove();
90
+ }
91
+ function stop(fade = false) {
92
+ clearIdleTimer();
93
+ const run = current;
94
+ if (!run) return;
95
+ current = null;
96
+ host.clearTimeout(run.timer);
97
+ delete element.dataset.videoPlaying;
98
+ if (element.dataset.action === run.action) delete element.dataset.action;
99
+ if (!run.video) return;
100
+ const video = run.video;
101
+ video.onplaying = video.onended = video.onerror = video.onwaiting = video.onstalled = null;
102
+ video.pause();
103
+ video.style.opacity = '0';
104
+ if (fade && allowed()) {
105
+ const timer = host.setTimeout(() => {
106
+ retired.delete(video);
107
+ dispose(video);
108
+ }, 150);
109
+ retired.set(video, timer);
110
+ } else dispose(video);
111
+ }
112
+ function fallback(run, broken = false) {
113
+ if (current !== run) return;
114
+ if (broken) failed.add(run.action);
115
+ stop();
116
+ if (allowed() && run.action !== 'idle') {
117
+ base.play(run.action);
118
+ scheduleIdle(1400);
119
+ }
120
+ }
121
+ function play(action) {
122
+ if (!allowed() || dragging || element.dataset.action === 'drag') return false;
123
+ if (action === 'idle' && (resting || current || unsupported)) return false;
124
+ if (
125
+ current &&
126
+ (action === 'look' ||
127
+ current.action === action ||
128
+ (current.action === 'celebrate' && action === 'greet'))
129
+ )
130
+ return false;
131
+ if (!Object.hasOwn(clips, action) || failed.has(action) || unsupported) {
132
+ if (action === 'idle') return false;
133
+ if (current?.action === 'idle') stop();
134
+ const played = base.play(action);
135
+ if (played) scheduleIdle(1400);
136
+ return played;
137
+ }
138
+ stop();
139
+ base.release(true);
140
+ const run = { action, video: null, timer: null };
141
+ current = run;
142
+ element.dataset.action = action;
143
+ run.timer = host.setTimeout(() => fallback(run, true), 9000);
144
+ capability.then((supported) => {
145
+ if (current !== run || !allowed()) return;
146
+ if (!supported) {
147
+ unsupported = true;
148
+ return fallback(run);
149
+ }
150
+ const video = document.createElement('video');
151
+ run.video = video;
152
+ video.muted = true;
153
+ video.defaultMuted = true;
154
+ video.playsInline = true;
155
+ video.loop = action === 'idle';
156
+ video.disablePictureInPicture = true;
157
+ video.preload = 'auto';
158
+ video.setAttribute('aria-hidden', 'true');
159
+ video.setAttribute('playsinline', '');
160
+ video.onplaying = () => {
161
+ if (current !== run || !allowed()) return;
162
+ if (!Number.isFinite(video.duration) || video.duration <= 0 || video.duration > 15)
163
+ return fallback(run, true);
164
+ host.clearTimeout(run.timer);
165
+ run.timer =
166
+ action === 'idle'
167
+ ? null
168
+ : host.setTimeout(() => fallback(run, true), video.duration * 1000 + 2000);
169
+ element.dataset.videoPlaying = 'true';
170
+ };
171
+ video.onwaiting = video.onstalled = () => {
172
+ if (current !== run) return;
173
+ host.clearTimeout(run.timer);
174
+ run.timer = host.setTimeout(() => fallback(run, true), 4000);
175
+ };
176
+ video.onended = () => {
177
+ if (current === run) {
178
+ stop(true);
179
+ scheduleIdle();
180
+ }
181
+ };
182
+ video.onerror = () => fallback(run, true);
183
+ video.src = clips[action];
184
+ layer.appendChild(video);
185
+ try {
186
+ Promise.resolve(video.play()).catch(() => fallback(run, true));
187
+ } catch {
188
+ fallback(run, true);
189
+ }
190
+ });
191
+ return true;
192
+ }
193
+ const visibility = () => {
194
+ if (!allowed()) stop();
195
+ else scheduleIdle();
196
+ };
197
+ document.addEventListener('visibilitychange', visibility);
198
+ reduced?.addEventListener?.('change', visibility);
199
+ return {
200
+ play,
201
+ configure(next) {
202
+ if (!next.enabled || (next.resting && !resting)) stop();
203
+ enabled = next.enabled;
204
+ resting = next.resting;
205
+ base.configure(next);
206
+ startIdle();
207
+ },
208
+ drag(direction) {
209
+ dragging = true;
210
+ stop();
211
+ base.drag(direction);
212
+ },
213
+ release(cancelled) {
214
+ dragging = false;
215
+ base.release(cancelled);
216
+ scheduleIdle(cancelled ? 0 : 650);
217
+ },
218
+ destroy() {
219
+ destroyed = true;
220
+ stop();
221
+ for (const [video, timer] of retired) {
222
+ host.clearTimeout(timer);
223
+ dispose(video);
224
+ }
225
+ retired.clear();
226
+ document.removeEventListener('visibilitychange', visibility);
227
+ reduced?.removeEventListener?.('change', visibility);
228
+ base.destroy();
229
+ },
230
+ };
231
+ }