dsh-offwork 1.1.0 → 1.1.2

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +50 -32
  3. package/assets/animations/NOTICE.md +15 -0
  4. package/assets/animations/alpha-probe.webm +0 -0
  5. package/assets/animations/ornate-drink.webm +0 -0
  6. package/assets/animations/ornate-greet.webm +0 -0
  7. package/assets/animations/ornate-idle.webm +0 -0
  8. package/assets/animations/whale-drink.webm +0 -0
  9. package/assets/animations/whale-greet.webm +0 -0
  10. package/assets/characters/NOTICE.md +4 -0
  11. package/assets/characters/whale-actions.webp +0 -0
  12. package/assets/characters/whale-ornate-actions.webp +0 -0
  13. package/client.js +2970 -770
  14. package/client.source.js +1668 -444
  15. package/companion.js +6 -9
  16. package/countdown.js +83 -0
  17. package/hydration-store.js +55 -16
  18. package/hydration.js +231 -250
  19. package/index.js +2 -0
  20. package/locale/en.json +1 -1
  21. package/locale/zh.json +1 -1
  22. package/media-manifest.js +29 -0
  23. package/media-server.js +123 -0
  24. package/package.json +9 -2
  25. package/pet-interactions.js +78 -7
  26. package/pet-motion.js +111 -0
  27. package/pet-reminder.js +20 -0
  28. package/pet-surfaces.js +130 -6
  29. package/pet-video.js +380 -0
  30. package/pet-view.js +129 -13
  31. package/tests/classic-overview.test.mjs +77 -0
  32. package/tests/companion.test.mjs +12 -12
  33. package/tests/countdown.test.mjs +115 -0
  34. package/tests/host.test.mjs +6 -0
  35. package/tests/hydration-extras.test.mjs +168 -204
  36. package/tests/hydration-store.test.mjs +199 -213
  37. package/tests/hydration.test.mjs +116 -186
  38. package/tests/media-server.test.mjs +179 -0
  39. package/tests/pet-character.test.mjs +8 -2
  40. package/tests/pet-motion.test.mjs +139 -0
  41. package/tests/pet-reminder.test.mjs +71 -0
  42. package/tests/pet-video.test.mjs +503 -0
  43. package/tools/build.mjs +62 -13
  44. package/tools/check-package.mjs +19 -0
  45. package/ui-utils.js +51 -1
package/client.source.js CHANGED
@@ -6,13 +6,49 @@ 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,
12
13
  ...PET_MESSAGES.zh,
13
14
  ...PET_INTERACTION_MESSAGES.zh,
14
15
  ...PET_SURFACE_MESSAGES.zh,
16
+ detailHelp: '使用说明',
17
+ detailApply: '应用',
18
+ detailShift: '班次详情',
19
+ detailWaterRule: '全天按自然时间提醒,需保持页面打开。',
20
+ detailSavedInterval: '已生效间隔',
21
+ detailDraftHint: '保存后生效',
22
+ detailNextInterval: '用于下一轮计时',
15
23
  title: '下班计时器',
24
+ classicMiniWorking: '距下班',
25
+ classicMiniBefore: '距开工',
26
+ classicMiniLunch: '午休中',
27
+ classicMiniStatus: '本班状态',
28
+ classicMiniWaiting: '待开启',
29
+ classicMiniFinished: '已结束',
30
+ classicOverview: '今日概览',
31
+ classicReturn: '返回概览',
32
+ classicMore: '展开作息详情',
33
+ classicLess: '收起作息详情',
34
+ classicNoSession: '等待会话',
35
+ classicUnavailable: '暂时无法计时',
36
+ classicRetryHint: '重新读取作息后恢复计时。',
37
+ classicLoading: '正在准备计时',
38
+ classicLoadingHint: '正在读取已保存的作息。',
39
+ classicBeforeHint: '班次尚未开始,显示距离开工的时间。',
40
+ classicWorkingHint: '本班工作进行中。',
41
+ classicLunchRemaining: '距离午休结束',
42
+ classicLunchHint: '午休结束后继续工作;喝水提醒照常计时。',
43
+ classicDoneHint: '本班计时已结束,下一班开始后继续。',
44
+ classicRestHint: '今天不计工作时长,计时会在工作日恢复。',
45
+ classicWaterHint: '自然时间计时',
46
+ classicSmokeHint: '自然时间计时',
47
+ classicConfigure: '设置作息与薪资',
48
+ classicIncome: '本班收入',
49
+ classicIncomeEntry: '查看收入详情',
50
+ classicShift: '本班作息',
51
+ classicEnd: '下班时间',
16
52
  working: '距离下班',
17
53
  before: '距离开工',
18
54
  lunch: '午休中',
@@ -73,7 +109,7 @@ window.__ModuleLoader__.load({
73
109
  opacityHint: '只影响背景,不影响文字。',
74
110
  waterTitle: '喝水提醒',
75
111
  waterEnable: '开启喝水提醒',
76
- waterInterval: '自定义间隔(分钟)',
112
+ waterInterval: '提醒间隔(分钟)',
77
113
  waterSave: '保存提醒设置',
78
114
  waterEdit: '设置喝水提醒',
79
115
  waterBack: '返回',
@@ -83,21 +119,21 @@ window.__ModuleLoader__.load({
83
119
  waterEffective: '当前间隔',
84
120
  waterManage: '数据管理',
85
121
  waterSaveHint: '修改后点击保存,按新间隔重新计时。',
86
- waterBrief: '按工作时间计时,午休暂停。关闭页面后不提醒。',
122
+ waterBrief: '按自然时间计时,午休、下班和休息日均可提醒。需要保持页面打开。',
87
123
  waterApply: '应用间隔',
88
124
  waterInvalid: '请输入 5–240 的整数。',
89
125
  waterNext: '下次喝水',
90
126
  waterDue: '该喝水啦',
91
127
  waterDrink: '喝了',
92
- waterSnooze: '工作 10 分钟后',
93
- waterPause: '本班暂停',
94
- waterResume: '恢复本班提醒',
95
- waterCount: '本班打卡',
128
+ waterSnooze: '10 分钟后',
129
+ waterPause: '暂停提醒',
130
+ waterResume: '恢复提醒',
131
+ waterCount: '饮水计数',
96
132
  waterManual: '喝了一杯',
97
133
  waterQuick: '喝水快捷操作',
98
134
  waterQuickClose: '收起喝水快捷操作',
99
135
  waterRecorded: '这杯记下啦。',
100
- waterRecordHint: '工作和午休期间可以打卡,关闭提醒也能记录。',
136
+ waterRecordHint: '随时可以打卡,关闭提醒也能记录;计数可手动清零。',
101
137
  waterActionStale: '状态已变化,请再试一次。',
102
138
  waterQuiet: '免打扰',
103
139
  waterQuietAdjust: '调整时长',
@@ -108,32 +144,68 @@ window.__ModuleLoader__.load({
108
144
  waterQuietEnded: '免打扰已结束。',
109
145
  waterQuietHint: '按自然时间,到期自动恢复;已到期的提醒只提示一次。',
110
146
  waterOff: '提醒已关闭',
111
- waterWaiting: '等待工作时段',
112
- waterBreak: '休息中,提醒计时暂停',
113
- waterPaused: '本班已暂停',
114
- waterFinished: '本班不再安排新提醒',
147
+ waterWaiting: '等待开启提醒',
148
+ waterBreak: '提醒已暂停',
149
+ waterPaused: '提醒已暂停',
150
+ waterFinished: '提醒已结束',
115
151
  waterOther: '提醒已到期,等待可见标签页处理',
116
- waterClear: '清除本班次数',
152
+ waterClear: '清除饮水计数',
117
153
  waterReset: '重置喝水提醒',
118
154
  waterConfirm: '确认操作',
119
- waterClearHint: '仅清零本班次数,保留下一次提醒和暂停状态。',
120
- waterResetHint: '清除喝水提醒设置和记录,恢复默认关闭。',
155
+ waterClearHint: '仅清零饮水计数,保留下一次提醒和暂停状态。',
156
+ waterResetHint: '清除当前喝水提醒设置和计数,恢复默认关闭。',
121
157
  waterLocal: '提醒设置和打卡仅保存在当前浏览器,不进入会话。与薪资配置分别保存。',
122
- waterRules: '按工作分钟计时,午休冻结。开启、修改间隔或作息后重新计时;关闭页面后不提醒。',
158
+ waterRules:
159
+ '按自然分钟计时,与作息、薪资和会话无关。开启、修改间隔或手动恢复后重新计时;关闭页面后不提醒。',
123
160
  waterUnavailable:
124
- '喝水提醒存储不可用或数据无效,自动提醒已停止。可重试;重置将清除原喝水数据。',
161
+ '喝水提醒存储不可用或数据无效,自动提醒已停止。可重试;重置将清除当前提醒设置和计数。',
125
162
  waterRetry: '重试读取',
126
- waterSetup: '请先保存薪资与作息。',
163
+ waterSetup: '开启后按所选间隔提醒。',
127
164
  waterLoading: '正在读取喝水提醒…',
128
- waterChanged: '提醒设置已在本浏览器生效',
129
- waterStale: '作息已变化,请点击“读取最新配置”后继续提醒。',
165
+ waterChanged: '提醒设置已保存',
166
+ waterStale: '记录已更新,请重试读取。',
130
167
  },
131
168
  en: {
132
169
  ...COMPANION_MESSAGES.en,
133
170
  ...PET_MESSAGES.en,
134
171
  ...PET_INTERACTION_MESSAGES.en,
135
172
  ...PET_SURFACE_MESSAGES.en,
173
+ detailHelp: 'How it works',
174
+ detailApply: 'Apply',
175
+ detailShift: 'Shift details',
176
+ detailWaterRule: 'Reminds you throughout the day. Keep the page open.',
177
+ detailSavedInterval: 'Saved interval',
178
+ detailDraftHint: 'Applies when saved',
179
+ detailNextInterval: 'Applies to the next timer',
136
180
  title: 'Offwork Clock',
181
+ classicMiniWorking: 'To end',
182
+ classicMiniBefore: 'To start',
183
+ classicMiniLunch: 'Break',
184
+ classicMiniStatus: 'Status',
185
+ classicMiniWaiting: 'Waiting',
186
+ classicMiniFinished: 'Done',
187
+ classicOverview: 'Today at a glance',
188
+ classicReturn: 'Back to overview',
189
+ classicMore: 'Show schedule details',
190
+ classicLess: 'Hide schedule details',
191
+ classicNoSession: 'Waiting for a session',
192
+ classicUnavailable: 'Timer unavailable',
193
+ classicRetryHint: 'Reload your schedule to restore the timer.',
194
+ classicLoading: 'Preparing your timer',
195
+ classicLoadingHint: 'Loading your saved schedule.',
196
+ classicBeforeHint: 'Your shift has not started. This is the time until work begins.',
197
+ classicWorkingHint: 'Your shift is in progress.',
198
+ classicLunchRemaining: 'Until lunch ends',
199
+ classicLunchHint: 'Work resumes after lunch. Water reminders continue as usual.',
200
+ classicDoneHint: 'This shift has ended. Timing resumes with your next shift.',
201
+ classicRestHint: 'No work time is counted today. Timing resumes on a scheduled workday.',
202
+ classicWaterHint: 'Clock time',
203
+ classicSmokeHint: 'Clock time',
204
+ classicConfigure: 'Set schedule and pay',
205
+ classicIncome: 'Shift earnings',
206
+ classicIncomeEntry: 'View earnings',
207
+ classicShift: 'Shift schedule',
208
+ classicEnd: 'Shift ends',
137
209
  working: 'Until clock-out',
138
210
  before: 'Until work starts',
139
211
  lunch: 'On break',
@@ -195,7 +267,7 @@ window.__ModuleLoader__.load({
195
267
  opacityHint: 'Affects the background only.',
196
268
  waterTitle: 'Water reminder',
197
269
  waterEnable: 'Enable water reminders',
198
- waterInterval: 'Custom interval (minutes)',
270
+ waterInterval: 'Interval (minutes)',
199
271
  waterSave: 'Save reminder settings',
200
272
  waterEdit: 'Reminder settings',
201
273
  waterBack: 'Back',
@@ -205,21 +277,23 @@ window.__ModuleLoader__.load({
205
277
  waterEffective: 'Current interval',
206
278
  waterManage: 'Manage data',
207
279
  waterSaveHint: 'Save changes to restart with the new interval.',
208
- waterBrief: 'Counts working time and pauses on unpaid breaks. Keep the page open.',
280
+ waterBrief:
281
+ 'Uses clock time, including breaks, off-hours and days off. Keep the page open.',
209
282
  waterApply: 'Apply interval',
210
283
  waterInvalid: 'Enter a whole number from 5 to 240.',
211
284
  waterNext: 'Next water break',
212
285
  waterDue: 'Time for some water',
213
286
  waterDrink: 'Drank water',
214
- waterSnooze: 'In 10 working minutes',
215
- waterPause: 'Pause this shift',
216
- waterResume: 'Resume this shift',
217
- waterCount: 'Drinks this shift',
287
+ waterSnooze: 'In 10 minutes',
288
+ waterPause: 'Pause reminders',
289
+ waterResume: 'Resume reminders',
290
+ waterCount: 'Drinks recorded',
218
291
  waterManual: 'Had a glass',
219
292
  waterQuick: 'Quick water actions',
220
293
  waterQuickClose: 'Close quick water actions',
221
294
  waterRecorded: 'Got it, your drink is recorded.',
222
- waterRecordHint: 'Check in during work or breaks, even with reminders off.',
295
+ waterRecordHint:
296
+ 'Check in at any time, even with reminders off. Clear the count manually when needed.',
223
297
  waterActionStale: 'The state changed. Please try again.',
224
298
  waterQuiet: 'Quiet time',
225
299
  waterQuietAdjust: 'Change duration',
@@ -231,27 +305,27 @@ window.__ModuleLoader__.load({
231
305
  waterQuietHint:
232
306
  'Uses clock time. Resumes automatically, with just one reminder if overdue.',
233
307
  waterOff: 'Reminders off',
234
- waterWaiting: 'Waiting for working hours',
235
- waterBreak: 'On break; reminder timer paused',
236
- waterPaused: 'Paused for this shift',
237
- waterFinished: 'No more reminders this shift',
308
+ waterWaiting: 'Waiting for reminders to be enabled',
309
+ waterBreak: 'Reminders paused',
310
+ waterPaused: 'Reminders paused',
311
+ waterFinished: 'Reminders ended',
238
312
  waterOther: 'Reminder due; waiting for a visible tab',
239
- waterClear: 'Clear shift count',
313
+ waterClear: 'Clear drink count',
240
314
  waterReset: 'Reset water reminders',
241
315
  waterConfirm: 'Confirm',
242
316
  waterClearHint: 'Clear the drink count only. Keep the next reminder and pause state.',
243
- waterResetHint: 'Delete water preferences and records, then turn reminders off.',
317
+ waterResetHint: 'Clear current water preferences and count, then turn reminders off.',
244
318
  waterLocal:
245
319
  'Reminder preferences and check-ins stay in this browser, outside conversations. Saved separately from salary settings.',
246
320
  waterRules:
247
- 'Counts working minutes; unpaid breaks pause the timer. Enabling or changing interval or schedule restarts it. The page must stay open.',
321
+ 'Uses clock time, independently of work, salary and sessions. Enabling, changing the interval or manually resuming starts a new interval. Keep the page open.',
248
322
  waterUnavailable:
249
- 'Water storage is unavailable or invalid; automatic reminders stopped. Retry, or reset to delete old water data.',
323
+ 'Water storage is unavailable or invalid; automatic reminders stopped. Retry, or reset current water preferences and count.',
250
324
  waterRetry: 'Retry loading',
251
- waterSetup: 'Save your salary and schedule first.',
325
+ waterSetup: 'Enable reminders to start the selected interval.',
252
326
  waterLoading: 'Loading water reminders…',
253
- waterChanged: 'Reminder preferences saved in this browser',
254
- waterStale: 'Schedule changed. Reload the latest settings to continue reminders.',
327
+ waterChanged: 'Reminder settings saved',
328
+ waterStale: 'Records changed. Retry loading to continue.',
255
329
  },
256
330
  };
257
331
  const STYLE =
@@ -383,6 +457,15 @@ window.__ModuleLoader__.load({
383
457
  water: [
384
458
  ['path', { d: 'M8 1.5C6.6 4 3.5 7.1 3.5 10a4.5 4.5 0 0 0 9 0C12.5 7.1 9.4 4 8 1.5Z' }],
385
459
  ],
460
+ smoking: [
461
+ ['rect', { x: 1.5, y: 10, width: 11, height: 3, rx: 0.5 }],
462
+ [
463
+ 'path',
464
+ {
465
+ d: 'M9.5 10v3M14.5 10v3M10 7.5c1-1 .8-1.8 0-2.5s-1-1.5 0-2.5M13 6c1-1 .8-1.8 0-2.5s-1-1.5 0-2.5',
466
+ },
467
+ ],
468
+ ],
386
469
  eye: [
387
470
  ['path', { d: 'M1.7 8S4.3 3.9 8 3.9 14.3 8 14.3 8 11.7 12.1 8 12.1 1.7 8 1.7 8Z' }],
388
471
  ['circle', { cx: 8, cy: 8, r: 2.05 }],
@@ -409,6 +492,7 @@ window.__ModuleLoader__.load({
409
492
  ['path', { d: 'M13.1 3v3.2H9.9' }],
410
493
  ],
411
494
  };
495
+ const LOCAL_SCREENS = [null, 'appearance', 'smoking', 'water-countdown', 'water', 'home'];
412
496
  const iconSvg = (name, size = 15) =>
413
497
  h(
414
498
  'svg',
@@ -432,8 +516,10 @@ window.__ModuleLoader__.load({
432
516
  apply(ctx) {
433
517
  for (const [lang, dict] of Object.entries(messages))
434
518
  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';
519
+ const viewKey = 'dsh.offwork-clock.view.v7';
520
+ const previousViewKey = 'dsh.offwork-clock.view.v6';
521
+ const olderViewKey = 'dsh.offwork-clock.view.v5';
522
+ const oldestViewKey = 'dsh.offwork-clock.view.v4';
437
523
  const legacyKey = 'dsh.offwork-clock.view.v3';
438
524
 
439
525
  function Clock(props) {
@@ -454,12 +540,20 @@ window.__ModuleLoader__.load({
454
540
  const waterEditing = ui.screen === 'water';
455
541
  const [appearanceDraft, setAppearanceDraft] = React.useState(null);
456
542
  const appearanceBase = React.useRef(null);
543
+ const previewBox = React.useRef(null);
544
+ const previewMotion = React.useRef(null);
545
+ const [previewState, setPreviewState] = React.useState({ phase: 'idle', action: null });
546
+ const [previewReduced, setPreviewReduced] = React.useState(
547
+ () => window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false,
548
+ );
457
549
  const appearanceWasPet = React.useRef(false);
458
550
  const salaryBase = React.useRef(null);
459
551
  const menuBox = React.useRef(null);
460
552
  const menuTrigger = React.useRef(null);
461
553
  const [dragPosition, setDragPosition] = React.useState(null);
462
- const [busy, setBusy] = React.useState(false);
554
+ const requestBusy = React.useRef(false);
555
+ const [hostBusy, setBusy] = React.useState(false);
556
+ const busy = hostBusy && (requestBusy.current || !LOCAL_SCREENS.includes(ui.screen));
463
557
  const [error, setError] = React.useState('');
464
558
  const [notice, setNotice] = React.useState('');
465
559
  const [storageError, setStorageError] = React.useState(false);
@@ -485,8 +579,117 @@ window.__ModuleLoader__.load({
485
579
  const dragState = React.useRef(null);
486
580
  const lastPosition = React.useRef(null);
487
581
  const generation = React.useRef(0);
488
- const requestBusy = React.useRef(false);
489
582
  const focusReturn = React.useRef(null);
583
+ const [smoking, setSmoking] = React.useState(defaultCountdown);
584
+ const [smokingReady, setSmokingReady] = React.useState(false);
585
+ const [smokingBusy, setSmokingBusy] = React.useState(false);
586
+ const [smokingError, setSmokingError] = React.useState('');
587
+ const smokingRuntime = React.useRef(null);
588
+ React.useEffect(() => {
589
+ let alive = true,
590
+ reading = false,
591
+ writing = false,
592
+ issued = 0,
593
+ accepted = 0,
594
+ channel;
595
+ const makeStore = () => {
596
+ try {
597
+ return createStateStore(
598
+ window.indexedDB,
599
+ SMOKING_TIMER_DB,
600
+ defaultCountdown,
601
+ validateCountdown,
602
+ );
603
+ } catch {
604
+ return createStateStore(
605
+ null,
606
+ SMOKING_TIMER_DB,
607
+ defaultCountdown,
608
+ validateCountdown,
609
+ );
610
+ }
611
+ };
612
+ let store = makeStore();
613
+ const accept = (next, order) => {
614
+ if (!alive || order < accepted) return;
615
+ accepted = order;
616
+ setSmoking(next);
617
+ setNow(Date.now());
618
+ setSmokingReady(true);
619
+ };
620
+ const refresh = async () => {
621
+ if (!alive || reading || writing) return;
622
+ reading = true;
623
+ const source = store,
624
+ order = ++issued;
625
+ try {
626
+ const next = await source.update((current) => current);
627
+ if (source === store) accept(next, order);
628
+ } catch {
629
+ if (alive && source === store && order >= accepted)
630
+ setSmokingError('timerStorageError');
631
+ } finally {
632
+ reading = false;
633
+ }
634
+ };
635
+ const run = async (action) => {
636
+ if (!alive || writing) return;
637
+ writing = true;
638
+ const order = ++issued;
639
+ setSmokingBusy(true);
640
+ try {
641
+ if (action.type === 'retry') {
642
+ store.close();
643
+ store = makeStore();
644
+ }
645
+ let changedElsewhere = false;
646
+ const next = await store.update((current) => {
647
+ if (action.type === 'retry') return current;
648
+ if (current.revision !== action.revision) {
649
+ changedElsewhere = true;
650
+ return current;
651
+ }
652
+ return transitionCountdown(current, action, Date.now());
653
+ });
654
+ accept(next, order);
655
+ if (alive) setSmokingError(changedElsewhere ? 'timerChanged' : '');
656
+ try {
657
+ channel?.postMessage(next.revision);
658
+ } catch {
659
+ /* Polling also synchronizes. */
660
+ }
661
+ } catch {
662
+ if (alive) setSmokingError('timerStorageError');
663
+ } finally {
664
+ writing = false;
665
+ if (alive) setSmokingBusy(false);
666
+ }
667
+ };
668
+ smokingRuntime.current = { run };
669
+ try {
670
+ channel = new BroadcastChannel(SMOKING_TIMER_DB);
671
+ channel.onmessage = refresh;
672
+ } catch {
673
+ /* Polling is sufficient. */
674
+ }
675
+ void refresh();
676
+ const timer = window.setInterval(refresh, 2000);
677
+ window.addEventListener('focus', refresh);
678
+ document.addEventListener('visibilitychange', refresh);
679
+ return () => {
680
+ alive = false;
681
+ smokingRuntime.current = null;
682
+ window.clearInterval(timer);
683
+ window.removeEventListener('focus', refresh);
684
+ document.removeEventListener('visibilitychange', refresh);
685
+ channel?.close();
686
+ store.close();
687
+ };
688
+ }, []);
689
+ const smokingState = countdownView(smoking, now);
690
+ const smokingUnavailable = !smokingReady || smokingError === 'timerStorageError';
691
+ const smokingAction = (action) =>
692
+ smokingRuntime.current?.run({ ...action, revision: smoking.revision });
490
693
  const [water, setWater] = React.useState(defaultHydration);
491
694
  const [waterReady, setWaterReady] = React.useState(false);
492
695
  const [waterError, setWaterError] = React.useState(false);
@@ -504,29 +707,33 @@ window.__ModuleLoader__.load({
504
707
  const [waterInvalid, setWaterInvalid] = React.useState(false);
505
708
  const [waterConfirm, setWaterConfirm] = React.useState(null);
506
709
  const [waterNotice, setWaterNotice] = React.useState(false);
710
+ const [reminderGate, setReminderGate] = React.useState(initialPetReminder);
507
711
  const reactionState = React.useRef(null);
712
+ const petMotion = React.useRef(null);
713
+ const petReactionToken = React.useRef(null);
508
714
  const waterRuntime = React.useRef(null);
509
715
  const waterOwner = React.useRef(null);
510
716
  if (!waterOwner.current)
511
717
  waterOwner.current =
512
718
  window.crypto?.randomUUID?.() || String(Math.random()) + Date.now();
513
719
  const waterInputs = React.useRef(null);
514
- const waterScheduleChange = React.useRef(null);
515
- waterInputs.current = {
516
- settings: data?.configured && !error ? data.settings : null,
517
- editing: editing || waterEditing,
518
- };
720
+ waterInputs.current = { editing: editing || waterEditing };
519
721
 
520
722
  const settings = data?.settings;
521
723
  const workingDraft = draft || settings;
522
724
  const state = settings ? safeCalculate(settings) : null;
523
725
  const preview = workingDraft ? safeCalculate(workingDraft) : null;
524
726
  const safeState = state && !state.error ? state : null;
525
- const petMode =
526
- ready &&
527
- data?.configured &&
528
- !!safeState &&
529
- (appearanceOnly ? appearanceWasPet.current : view.companion);
727
+ const petMode = ready && (appearanceOnly ? appearanceWasPet.current : view.companion);
728
+ const petClockStatus = !sessionId
729
+ ? 'petNeedsSession'
730
+ : error || state?.error
731
+ ? 'petLoadFailed'
732
+ : !data
733
+ ? 'loading'
734
+ : !data.configured
735
+ ? 'petNeedsSetup'
736
+ : safeState?.status || 'loading';
530
737
  const stageModel = appearanceOnly ? savedView.petModel : view.petModel;
531
738
  const petGeometry = petLayout(
532
739
  dragPosition ?? view.petPosition,
@@ -534,6 +741,8 @@ window.__ModuleLoader__.load({
534
741
  appearanceOnly ? savedView.petSize : view.petSize,
535
742
  420,
536
743
  stageModel,
744
+ 72,
745
+ 196,
537
746
  );
538
747
  const surfaceScreen = ui.screen || 'reminder';
539
748
  const surfaceWidth = petSurfaceWidth(surfaceScreen, viewport);
@@ -577,7 +786,7 @@ window.__ModuleLoader__.load({
577
786
  );
578
787
  const waterAlertLayout = placePetSurface(anchor, viewport, {
579
788
  width: Math.min(140, Math.max(110, alertRoom)),
580
- height: 34,
789
+ height: 44,
581
790
  kind: 'menu',
582
791
  obstacles: [...(petOpen ? [floatingPanel] : []), ...(ui.menu ? [menuLayout] : [])],
583
792
  });
@@ -586,14 +795,15 @@ window.__ModuleLoader__.load({
586
795
  height: toastHeight,
587
796
  kind: 'menu',
588
797
  });
589
- const collapsed =
590
- !petMode &&
591
- !!safeState &&
592
- !editing &&
593
- !waterEditing &&
594
- view.mini &&
595
- !error &&
596
- !state?.error;
798
+ const collapsed = !petMode && ui.screen === null && view.mini;
799
+ const classicClock = classicClockPresentation({
800
+ hasSession: !!sessionId,
801
+ loaded: !!data,
802
+ configured: !!data?.configured,
803
+ failed: !!error || !!state?.error,
804
+ state: safeState,
805
+ now,
806
+ });
597
807
  const seconds = safeState
598
808
  ? safeState.status === 'before'
599
809
  ? safeState.startRemaining
@@ -621,6 +831,9 @@ window.__ModuleLoader__.load({
621
831
  classicSize,
622
832
  );
623
833
  const cardStyle = {
834
+ width: ui.screen
835
+ ? petSurfaceWidth(ui.screen, viewport)
836
+ : Math.min(collapsed ? 224 : 320, Math.max(1, viewport.width - 24)),
624
837
  '--owc-alpha': String(view.opacity),
625
838
  '--owc-viewport-height': Math.max(1, viewport.height - 24) + 'px',
626
839
  left: classicPosition.x,
@@ -655,7 +868,11 @@ window.__ModuleLoader__.load({
655
868
 
656
869
  React.useEffect(() => {
657
870
  try {
658
- const saved = localStorage.getItem(viewKey) ?? localStorage.getItem(previousViewKey);
871
+ const saved =
872
+ localStorage.getItem(viewKey) ??
873
+ localStorage.getItem(previousViewKey) ??
874
+ localStorage.getItem(olderViewKey) ??
875
+ localStorage.getItem(oldestViewKey);
659
876
  const legacy = !saved ? JSON.parse(localStorage.getItem(legacyKey) || 'null') : null;
660
877
  setView(
661
878
  normalizeView(
@@ -699,17 +916,8 @@ window.__ModuleLoader__.load({
699
916
  store = createHydrationStore(null);
700
917
  }
701
918
  try {
702
- channel = new BroadcastChannel('dsh.offwork-clock.hydration.v1');
919
+ channel = new BroadcastChannel('dsh.offwork-clock.hydration.wall-clock.v3');
703
920
  } catch {}
704
- const contextAt = (time) => {
705
- const current = waterInputs.current.settings;
706
- if (!current) return null;
707
- try {
708
- return hydrationContext(current, calculate(current, time));
709
- } catch {
710
- return null;
711
- }
712
- };
713
921
  const update = async (action = { type: 'tick' }, automatic = false) => {
714
922
  if (!active || (automatic && (running || failed))) return;
715
923
  running++;
@@ -725,59 +933,31 @@ window.__ModuleLoader__.load({
725
933
  let changed = false;
726
934
  let recorded = false;
727
935
  let quietChanged = false;
728
- let usedPermit = null;
729
936
  const result = await store.update(
730
937
  (current) => {
731
938
  const time = Date.now();
732
- const context = contextAt(time);
733
939
  if (action.type === 'sync') return current;
734
- const permit = waterScheduleChange.current;
735
- const operation =
736
- permit && context && permit.schedule === context.schedule
737
- ? { ...action, baseSchedule: permit.baseSchedule }
738
- : action;
739
- let next = transitionHydration(
740
- current,
741
- context,
742
- operation,
743
- time,
744
- waterOwner.current,
745
- );
940
+ let next = transitionHydration(current, action, time, waterOwner.current);
746
941
  recorded =
747
- ['drink', 'manual-drink'].includes(action.type) &&
748
- next.shift?.count >
749
- (current.shift?.id === next.shift?.id ? (current.shift?.count ?? 0) : 0);
942
+ ['drink', 'manual-drink'].includes(action.type) && next.count > current.count;
750
943
  quietChanged =
751
944
  ['quiet', 'end-quiet'].includes(action.type) &&
752
- next.shift?.quietUntil !== current.shift?.quietUntil;
945
+ next.quietUntil !== current.quietUntil;
753
946
  if (automatic) {
754
947
  const allow = !document.hidden && !waterInputs.current.editing;
755
948
  next = transitionHydration(
756
949
  next,
757
- context,
758
950
  { type: allow ? 'claim' : 'release' },
759
951
  time,
760
952
  waterOwner.current,
761
953
  );
762
954
  }
763
- if (
764
- permit &&
765
- context &&
766
- permit.schedule === context.schedule &&
767
- next.shift?.schedule === context.schedule
768
- )
769
- usedPermit = permit;
770
955
  changed = JSON.stringify(next) !== JSON.stringify(current);
771
956
  return next;
772
957
  },
773
958
  { reset: action.type === 'reset' },
774
959
  );
775
960
  if (!active) return;
776
- if (
777
- action.type === 'reset' ||
778
- (usedPermit && waterScheduleChange.current === usedPermit)
779
- )
780
- waterScheduleChange.current = null;
781
961
  failed = false;
782
962
  setWater(result);
783
963
  setWaterReady(true);
@@ -809,11 +989,14 @@ window.__ModuleLoader__.load({
809
989
  const visibility = () => {
810
990
  setWaterVisible(!document.hidden);
811
991
  setNow(Date.now());
812
- if (document.hidden) void update({ type: 'release' });
813
- else tick();
992
+ if (document.hidden) {
993
+ setReminderGate(initialPetReminder);
994
+ void update({ type: 'release' });
995
+ } else tick();
814
996
  };
815
997
  const pageHide = () => {
816
998
  setWaterVisible(false);
999
+ setReminderGate(initialPetReminder);
817
1000
  void update({ type: 'release' });
818
1001
  };
819
1002
  waterRuntime.current = { update, tick };
@@ -838,8 +1021,7 @@ window.__ModuleLoader__.load({
838
1021
  channel?.close();
839
1022
  void store
840
1023
  .update((current) => {
841
- if (current.shift?.claim?.owner === waterOwner.current)
842
- current.shift.claim = null;
1024
+ if (current.claim?.owner === waterOwner.current) current.claim = null;
843
1025
  return current;
844
1026
  })
845
1027
  .catch(() => {})
@@ -849,7 +1031,7 @@ window.__ModuleLoader__.load({
849
1031
 
850
1032
  React.useEffect(() => {
851
1033
  waterRuntime.current?.tick();
852
- }, [settings, editing, waterEditing, error]);
1034
+ }, [editing, waterEditing]);
853
1035
 
854
1036
  React.useEffect(() => {
855
1037
  if (!waterNotice) return;
@@ -895,6 +1077,7 @@ window.__ModuleLoader__.load({
895
1077
  const interval = Number(waterInterval);
896
1078
  if (!Number.isInteger(interval) || interval < 5 || interval > 240) {
897
1079
  setWaterInvalid(true);
1080
+ box.current?.querySelector('[name=waterInterval]')?.focus();
898
1081
  return;
899
1082
  }
900
1083
  const result = await waterAction({
@@ -927,7 +1110,7 @@ window.__ModuleLoader__.load({
927
1110
  setWaterNotice({ key: 'waterRecorded', kind: 'drink', token: result.revision });
928
1111
  if (action.type === 'manual-drink') {
929
1112
  setWaterNotice({
930
- key: result.shift?.recentActions.includes(action.actionId)
1113
+ key: result.recentActions.includes(action.actionId)
931
1114
  ? 'waterRecorded'
932
1115
  : 'waterActionStale',
933
1116
  kind: outcome.recorded ? 'drink' : 'notice',
@@ -938,7 +1121,7 @@ window.__ModuleLoader__.load({
938
1121
  if (action.type === 'quiet')
939
1122
  setWaterNotice({
940
1123
  key:
941
- outcome.quietChanged && result.shift?.quietUntil > Date.now()
1124
+ outcome.quietChanged && result.quietUntil > Date.now()
942
1125
  ? 'waterQuietStarted'
943
1126
  : 'waterActionStale',
944
1127
  kind: 'notice',
@@ -966,13 +1149,9 @@ window.__ModuleLoader__.load({
966
1149
  ? '.owc-pet-handle'
967
1150
  : '.owc-grip,.owc-title'
968
1151
  : action.type === 'manual-drink'
969
- ? petMode
970
- ? '.owc-pet-water-row .owc-primary'
971
- : '.owc-water-actions .owc-primary'
1152
+ ? '.owc-water-record,.owc-water-actions .owc-primary'
972
1153
  : action.type === 'end-quiet'
973
- ? petMode
974
- ? '.owc-pet-quiet-row button'
975
- : '.owc-water-quiet summary'
1154
+ ? '.owc-detail-more summary,.owc-water-quiet summary'
976
1155
  : '.owc-water-form details > summary';
977
1156
  box.current?.querySelector(target)?.focus();
978
1157
  });
@@ -1041,7 +1220,7 @@ window.__ModuleLoader__.load({
1041
1220
  window.visualViewport?.removeEventListener('resize', resize);
1042
1221
  window.visualViewport?.removeEventListener('scroll', resize);
1043
1222
  };
1044
- }, [clamp, petMode]);
1223
+ }, [clamp, petMode, ready]);
1045
1224
 
1046
1225
  React.useEffect(() => {
1047
1226
  if (ui.pending) {
@@ -1093,21 +1272,11 @@ window.__ModuleLoader__.load({
1093
1272
  setBusy(true);
1094
1273
  setError('');
1095
1274
  try {
1096
- const baseSchedule = (await waterRuntime.current?.update({ type: 'sync' }))?.state
1097
- ?.shift?.schedule;
1098
- if (token !== generation.current) return;
1099
1275
  const next = await request('get');
1100
1276
  if (token !== generation.current) return;
1101
- if (next.configured)
1102
- waterScheduleChange.current = {
1103
- baseSchedule,
1104
- schedule: hydrationContext(next.settings, calculate(next.settings, Date.now()))
1105
- .schedule,
1106
- };
1107
1277
  setData(next);
1108
1278
  if (!next.configured) {
1109
1279
  setDraft({ ...next.settings });
1110
- dispatchUI({ type: 'navigate', screen: 'schedule', discard: true });
1111
1280
  salaryBase.current = next.settings;
1112
1281
  }
1113
1282
  if (explicit) setNotice(t('synced'));
@@ -1126,19 +1295,11 @@ window.__ModuleLoader__.load({
1126
1295
  setError('');
1127
1296
  const token = ++generation.current;
1128
1297
  try {
1129
- const baseSchedule = (await waterRuntime.current?.update({ type: 'sync' }))?.state
1130
- ?.shift?.schedule;
1131
- if (token !== generation.current) return;
1132
1298
  const next = await request(
1133
1299
  'set',
1134
1300
  changedSettings(salaryBase.current ?? settings, draft),
1135
1301
  );
1136
1302
  if (token === generation.current) {
1137
- waterScheduleChange.current = {
1138
- baseSchedule,
1139
- schedule: hydrationContext(next.settings, calculate(next.settings, Date.now()))
1140
- .schedule,
1141
- };
1142
1303
  setData(next);
1143
1304
  dispatchUI({ type: 'navigate', screen: ui.back, discard: true });
1144
1305
  setDraft(null);
@@ -1191,7 +1352,10 @@ window.__ModuleLoader__.load({
1191
1352
  d.moved = true;
1192
1353
  petDragged.current = true;
1193
1354
  const next = clamp({ x: e.clientX - d.dx, y: e.clientY - d.dy });
1194
- if (petMode) setDragPosition(next);
1355
+ if (petMode) {
1356
+ setDragPosition(next);
1357
+ petMotion.current?.drag(e.clientX - d.x0);
1358
+ }
1195
1359
  lastPosition.current = next;
1196
1360
  if (box.current) {
1197
1361
  box.current.style.left = next.x + 'px';
@@ -1203,6 +1367,7 @@ window.__ModuleLoader__.load({
1203
1367
  const d = dragState.current;
1204
1368
  if (!d) return;
1205
1369
  dragState.current = null;
1370
+ if (petMode && d.moved) petMotion.current?.release(e.type !== 'pointerup');
1206
1371
  if (e.type === 'pointercancel' || e.type === 'lostpointercapture')
1207
1372
  petDragged.current = false;
1208
1373
  if (d.moved && lastPosition.current) commitPosition(lastPosition.current);
@@ -1240,7 +1405,7 @@ window.__ModuleLoader__.load({
1240
1405
  petDragged.current = false;
1241
1406
  if (ui.pending) return true;
1242
1407
  if (!petMode) {
1243
- if (!busy && !waterBusy) navigateScreen('appearance', event);
1408
+ if (!requestBusy.current && !waterBusy) navigateScreen('appearance', event);
1244
1409
  return true;
1245
1410
  }
1246
1411
  const trigger =
@@ -1256,7 +1421,7 @@ window.__ModuleLoader__.load({
1256
1421
  : trigger;
1257
1422
  const rect = trigger?.getBoundingClientRect();
1258
1423
  const point =
1259
- event.type === 'keydown'
1424
+ event.type === 'keydown' || (event.type === 'click' && event.detail === 0)
1260
1425
  ? { x: rect?.right ?? petGeometry.x, y: rect?.top ?? petGeometry.y }
1261
1426
  : { x: event.clientX, y: event.clientY };
1262
1427
  dispatchUI({ type: 'menu', position: point });
@@ -1297,8 +1462,19 @@ window.__ModuleLoader__.load({
1297
1462
  setWaterConfirm(null);
1298
1463
  }
1299
1464
  function navigateScreen(screen, event) {
1300
- if (!data?.configured && screen !== 'schedule') return;
1301
- if (ui.pending || busy || waterBusy || requestBusy.current || waterPending.current)
1465
+ if (
1466
+ screen !== null &&
1467
+ !LOCAL_SCREENS.includes(screen) &&
1468
+ (!data || (!data.configured && screen !== 'schedule'))
1469
+ )
1470
+ return;
1471
+ if (
1472
+ ui.pending ||
1473
+ waterBusy ||
1474
+ requestBusy.current ||
1475
+ waterPending.current ||
1476
+ (hostBusy && !LOCAL_SCREENS.includes(screen))
1477
+ )
1302
1478
  return;
1303
1479
  const root = event?.currentTarget?.getAttribute?.('role') === 'menuitem';
1304
1480
  if (event?.currentTarget) focusReturn.current = event.currentTarget;
@@ -1350,7 +1526,7 @@ window.__ModuleLoader__.load({
1350
1526
  navigateScreen('schedule', event);
1351
1527
  }
1352
1528
  function closeEditor() {
1353
- if ((!data?.configured && !appearanceOnly) || busy) return;
1529
+ if (busy) return;
1354
1530
  performNavigation(ui.back);
1355
1531
  }
1356
1532
  function update(key, value) {
@@ -1419,19 +1595,17 @@ window.__ModuleLoader__.load({
1419
1595
  'button',
1420
1596
  {
1421
1597
  type: 'button',
1422
- className: 'owc-text',
1598
+ className: 'owc-icon',
1423
1599
  'aria-pressed': view.masked,
1600
+ 'aria-label': t(view.masked ? 'show' : 'hide'),
1424
1601
  title: t(view.masked ? 'show' : 'hide'),
1425
1602
  onClick: () => setPreference({ masked: !view.masked }),
1426
1603
  },
1427
1604
  iconSvg(view.masked ? 'eyeOff' : 'eye', 13),
1428
- t(view.masked ? 'show' : 'hide'),
1429
1605
  );
1430
1606
 
1431
- const waterContext =
1432
- data?.configured && safeState && !error ? hydrationContext(settings, safeState) : null;
1433
- const waterState = hydrationView(water, waterContext, now);
1434
- const waterClaim = water.shift?.claim;
1607
+ const waterState = hydrationView(water, now);
1608
+ const waterClaim = water.claim;
1435
1609
  const waterPrompt =
1436
1610
  waterReady &&
1437
1611
  !waterError &&
@@ -1441,9 +1615,59 @@ window.__ModuleLoader__.load({
1441
1615
  !document.hidden &&
1442
1616
  waterState.phase === 'due' &&
1443
1617
  waterClaim?.owner === waterOwner.current &&
1444
- waterClaim.sequence === water.shift.sequence &&
1618
+ waterClaim.sequence === water.sequence &&
1445
1619
  waterClaim.until > Date.now() &&
1446
1620
  waterClaim.until <= Date.now() + HYDRATION_LEASE_MS;
1621
+ const reminderReady = !!(petMode && waterReady && !waterError);
1622
+ const reminderVisible = waterVisible && !document.hidden;
1623
+ const reminderAvailable = !petOpen && !ui.menu && !ui.pending && !busy;
1624
+ React.useEffect(() => {
1625
+ setReminderGate((current) =>
1626
+ nextPetReminder(current, {
1627
+ ready: reminderReady,
1628
+ eventId: waterState.eventId,
1629
+ phase: waterState.phase,
1630
+ visible: reminderVisible,
1631
+ available: reminderAvailable,
1632
+ ownsPrompt: waterPrompt,
1633
+ }),
1634
+ );
1635
+ }, [
1636
+ reminderReady,
1637
+ waterState.eventId,
1638
+ waterState.phase,
1639
+ reminderVisible,
1640
+ reminderAvailable,
1641
+ waterPrompt,
1642
+ ]);
1643
+ const autoWaterPrompt =
1644
+ reminderReady &&
1645
+ reminderVisible &&
1646
+ reminderAvailable &&
1647
+ waterPrompt &&
1648
+ reminderGate.open &&
1649
+ reminderGate.eventId === waterState.eventId;
1650
+ function openWaterPopup(event) {
1651
+ if (!waterPrompt || waterBusy) return;
1652
+ if (!reminderAvailable) {
1653
+ navigateScreen('home', event);
1654
+ return;
1655
+ }
1656
+ const trigger = event.currentTarget;
1657
+ setReminderGate({ eventId: waterState.eventId, armed: false, open: true });
1658
+ requestAnimationFrame(() => {
1659
+ const active = document.activeElement;
1660
+ if (active !== trigger && active?.tagName !== 'BODY') return;
1661
+ if (petPanel.current?.dataset.petScreen === 'reminder')
1662
+ petPanel.current.querySelector('.owc-water-actions button')?.focus();
1663
+ });
1664
+ }
1665
+ function dismissWaterPopup() {
1666
+ const hadFocus = petPanel.current?.contains(document.activeElement);
1667
+ setReminderGate(dismissPetReminder);
1668
+ if (hadFocus)
1669
+ requestAnimationFrame(() => box.current?.querySelector('.owc-pet-handle')?.focus());
1670
+ }
1447
1671
  const reactionSurface = [
1448
1672
  collapsed,
1449
1673
  editing,
@@ -1465,13 +1689,116 @@ window.__ModuleLoader__.load({
1465
1689
  workStatus: safeState?.status,
1466
1690
  phase: waterState.phase,
1467
1691
  ownsPrompt: waterPrompt,
1468
- ready: waterReady && !!safeState,
1469
- failed: waterError || !!error,
1692
+ ready: waterReady,
1693
+ failed: waterError,
1470
1694
  feedback: waterNotice || null,
1471
1695
  visible: view.companion,
1472
1696
  motion: view.companionMotion && !reactionState.current?.stopped,
1473
1697
  pageVisible: waterVisible && !document.hidden && !editing && !waterEditing,
1474
1698
  });
1699
+ React.useLayoutEffect(() => {
1700
+ if (!petMode) return;
1701
+ const artwork = box.current?.querySelector('.owc-pet-handle > .owc-pet-art');
1702
+ if (!artwork) return;
1703
+ const base = createPetMotion(artwork, window, { enabled: false });
1704
+ const controller = createPetVideoMotion(
1705
+ artwork,
1706
+ window,
1707
+ base,
1708
+ PET_CLIPS.models[stageModel],
1709
+ PET_CLIPS.probe,
1710
+ );
1711
+ petMotion.current = controller;
1712
+ return () => {
1713
+ controller.destroy();
1714
+ petMotion.current = null;
1715
+ };
1716
+ }, [petMode, stageModel]);
1717
+ React.useLayoutEffect(() => {
1718
+ petMotion.current?.configure({
1719
+ enabled:
1720
+ savedView.companionMotion &&
1721
+ waterVisible &&
1722
+ waterReady &&
1723
+ !waterError &&
1724
+ !editing &&
1725
+ !waterEditing &&
1726
+ !ui.menu,
1727
+ resting: ['quiet', 'offline'].includes(companion.pose),
1728
+ });
1729
+ }, [
1730
+ petMode,
1731
+ stageModel,
1732
+ savedView.companionMotion,
1733
+ waterVisible,
1734
+ waterReady,
1735
+ waterError,
1736
+ error,
1737
+ editing,
1738
+ waterEditing,
1739
+ !!ui.menu,
1740
+ companion.pose,
1741
+ ]);
1742
+ React.useLayoutEffect(() => {
1743
+ const token = waterNotice?.token;
1744
+ if (petReactionToken.current === token) return;
1745
+ petReactionToken.current = token;
1746
+ if (petMode && companion.animate) petMotion.current?.play('celebrate');
1747
+ }, [petMode, waterNotice?.token, companion.animate]);
1748
+ React.useEffect(() => {
1749
+ const media = window.matchMedia?.('(prefers-reduced-motion: reduce)');
1750
+ const update = () => setPreviewReduced(!!media?.matches);
1751
+ media?.addEventListener?.('change', update);
1752
+ return () => media?.removeEventListener?.('change', update);
1753
+ }, []);
1754
+ React.useLayoutEffect(() => {
1755
+ if (!appearanceOnly || ui.pending) return;
1756
+ setPreviewState({ phase: 'idle', action: null });
1757
+ if (!view.companion || petModel(view.petModel).renderer !== 'video') return;
1758
+ const artwork = previewBox.current?.querySelector('.owc-pet-art');
1759
+ if (!artwork) return;
1760
+ const base = createPetMotion(artwork, window, { enabled: false, ambient: false });
1761
+ const controller = createPetVideoMotion(
1762
+ artwork,
1763
+ window,
1764
+ base,
1765
+ PET_CLIPS.models[view.petModel],
1766
+ PET_CLIPS.probe,
1767
+ {
1768
+ autoIdle: false,
1769
+ fallback: false,
1770
+ interruptible: true,
1771
+ onStateChange: setPreviewState,
1772
+ },
1773
+ );
1774
+ previewMotion.current = controller;
1775
+ return () => {
1776
+ controller.destroy();
1777
+ previewMotion.current = null;
1778
+ };
1779
+ }, [appearanceOnly, view.companion, view.petModel, !!ui.pending]);
1780
+ React.useLayoutEffect(() => {
1781
+ previewMotion.current?.configure({
1782
+ enabled:
1783
+ appearanceOnly &&
1784
+ view.companion &&
1785
+ view.companionMotion &&
1786
+ waterVisible &&
1787
+ !previewReduced &&
1788
+ !ui.menu &&
1789
+ !ui.pending,
1790
+ resting: false,
1791
+ });
1792
+ }, [
1793
+ appearanceOnly,
1794
+ view.companion,
1795
+ view.petModel,
1796
+ view.companionMotion,
1797
+ waterVisible,
1798
+ previewReduced,
1799
+ !!ui.menu,
1800
+ !!ui.pending,
1801
+ ]);
1475
1802
  const companionCard = (compact = false) =>
1476
1803
  companion.visible &&
1477
1804
  h(
@@ -1502,13 +1829,6 @@ window.__ModuleLoader__.load({
1502
1829
  const waterSummary = () => {
1503
1830
  if (!waterReady) return t('waterLoading');
1504
1831
  if (waterError) return t('waterUnavailable');
1505
- if (
1506
- water.enabled &&
1507
- waterContext &&
1508
- water.shift &&
1509
- water.shift.config !== waterContext.config
1510
- )
1511
- return t('waterStale');
1512
1832
  const labels = {
1513
1833
  off: 'waterOff',
1514
1834
  waiting: 'waterWaiting',
@@ -1523,19 +1843,258 @@ window.__ModuleLoader__.load({
1523
1843
  ? t('waterNext') + ' · ' + formatDuration(waterState.remaining / 1000)
1524
1844
  : t(labels[waterState.phase]);
1525
1845
  };
1526
- const waterCanRecord =
1527
- waterReady &&
1528
- !waterError &&
1529
- data?.configured &&
1530
- ['working', 'lunch'].includes(waterContext?.status) &&
1531
- (!water.shift || water.shift.config === waterContext.config);
1532
- const waterCanQuiet = waterCanRecord && water.enabled && !water.shift?.paused;
1846
+ const waterCountdownText = () => {
1847
+ if (waterError) return t('timerUnavailable');
1848
+ if (!waterReady) return t('timerLoading');
1849
+ if (['countdown', 'due', 'break'].includes(waterState.phase))
1850
+ return formatDuration(Math.ceil(waterState.remaining / 1000));
1851
+ if (waterState.phase === 'quiet')
1852
+ return formatDuration(Math.ceil(waterState.quietRemaining / 1000));
1853
+ return t(
1854
+ {
1855
+ off: 'timerOff',
1856
+ waiting: 'timerWaiting',
1857
+ paused: 'timerPaused',
1858
+ finished: 'timerFinished',
1859
+ }[waterState.phase] || 'timerUnavailable',
1860
+ );
1861
+ };
1862
+ const smokingCountdownText = () =>
1863
+ smokingUnavailable
1864
+ ? t(smokingError ? 'timerUnavailable' : 'timerLoading')
1865
+ : smokingState.phase === 'idle'
1866
+ ? t('timerIdle')
1867
+ : formatDuration(Math.ceil(smokingState.remaining / 1000));
1868
+ const timerRow = (kind, value, due, title) => {
1869
+ const phase = kind === 'water' ? waterState.phase : smokingState.phase;
1870
+ const extra = {
1871
+ paused: 'timerPaused',
1872
+ break: 'timerBreak',
1873
+ quiet: 'timerQuiet',
1874
+ due: 'timerDue',
1875
+ }[phase];
1876
+ return h(
1877
+ 'div',
1878
+ {
1879
+ className: 'owc-pet-timer-row',
1880
+ 'data-countdown': kind,
1881
+ 'data-countdown-due': due,
1882
+ 'data-countdown-phase': phase,
1883
+ role: 'timer',
1884
+ 'aria-label': t(kind === 'water' ? 'waterCountdown' : 'smokingCountdown'),
1885
+ title,
1886
+ },
1887
+ h(
1888
+ 'span',
1889
+ { className: 'owc-pet-timer-label' },
1890
+ iconSvg(kind, 14),
1891
+ extra && h('span', { className: 'owc-sr-only' }, t(extra)),
1892
+ ),
1893
+ h(
1894
+ 'strong',
1895
+ null,
1896
+ ['paused', 'break'].includes(phase) &&
1897
+ /^\d/.test(value) &&
1898
+ h('span', { className: 'owc-timer-pause', 'aria-hidden': true }, 'Ⅱ'),
1899
+ phase === 'quiet' &&
1900
+ h('span', { className: 'owc-timer-pause', 'aria-hidden': true }, '☾'),
1901
+ value.startsWith('00:') ? value.slice(3) : value.replace(/^0(?=\d:)/, ''),
1902
+ ),
1903
+ );
1904
+ };
1905
+ const waterHero = () => {
1906
+ const ticking =
1907
+ waterReady && !waterError && ['countdown', 'due', 'quiet'].includes(waterState.phase);
1908
+ return h(
1909
+ 'div',
1910
+ { className: 'owc-detail-hero' },
1911
+ h(
1912
+ 'p',
1913
+ { className: 'owc-detail-kicker', 'data-water-status': true },
1914
+ t(
1915
+ ticking
1916
+ ? waterState.phase === 'quiet'
1917
+ ? 'waterQuiet'
1918
+ : waterState.phase === 'due'
1919
+ ? 'waterDue'
1920
+ : 'waterNext'
1921
+ : 'waterTitle',
1922
+ ),
1923
+ ),
1924
+ h(
1925
+ 'div',
1926
+ {
1927
+ className: 'owc-detail-value' + (ticking ? '' : ' owc-detail-state'),
1928
+ role: 'timer',
1929
+ 'aria-label': t('waterCountdown'),
1930
+ },
1931
+ waterCountdownText(),
1932
+ ),
1933
+ h(
1934
+ 'div',
1935
+ { className: 'owc-detail-meta' },
1936
+ h(
1937
+ 'span',
1938
+ { 'data-water-count': true },
1939
+ t('waterCount') + ' · ' + (waterReady && !waterError ? waterState.count : '—'),
1940
+ ),
1941
+ h(
1942
+ 'span',
1943
+ { className: 'owc-detail-feedback' },
1944
+ waterNotice ? t(waterNotice.key) : '',
1945
+ ),
1946
+ ),
1947
+ );
1948
+ };
1949
+ const waterCountdownCard = () =>
1950
+ h(
1951
+ 'div',
1952
+ { className: 'owc-countdown-card', 'data-water-countdown-card': true },
1953
+ waterHero(),
1954
+ h(
1955
+ 'div',
1956
+ { className: 'owc-countdown-actions' },
1957
+ h(
1958
+ 'button',
1959
+ {
1960
+ type: 'button',
1961
+ className: 'owc-primary owc-water-record',
1962
+ disabled: !waterCanRecord || waterBusy,
1963
+ onClick: recordWater,
1964
+ },
1965
+ t('petRecord'),
1966
+ ),
1967
+ h(
1968
+ 'button',
1969
+ {
1970
+ type: 'button',
1971
+ className: 'owc-text',
1972
+ onClick: (event) => navigateScreen('home', event),
1973
+ },
1974
+ t('timerWaterDetails'),
1975
+ ),
1976
+ ),
1977
+ );
1978
+ const smokingCountdownCard = () =>
1979
+ h(
1980
+ 'div',
1981
+ { className: 'owc-countdown-card', 'data-smoking-countdown-card': true },
1982
+ h(
1983
+ 'div',
1984
+ {
1985
+ className: 'owc-countdown-value',
1986
+ role: 'timer',
1987
+ 'aria-label': t('smokingCountdown'),
1988
+ },
1989
+ !smokingUnavailable && smokingState.phase === 'idle'
1990
+ ? formatDuration(smoking.minutes * 60)
1991
+ : smokingCountdownText(),
1992
+ ),
1993
+ h(
1994
+ 'p',
1995
+ { role: 'status' },
1996
+ smokingUnavailable
1997
+ ? t(smokingError ? 'timerUnavailable' : 'timerLoading')
1998
+ : t(
1999
+ {
2000
+ idle: 'timerIdle',
2001
+ running: 'timerRunning',
2002
+ paused: 'timerPaused',
2003
+ due: 'timerDue',
2004
+ }[smokingState.phase],
2005
+ ),
2006
+ ),
2007
+ h(
2008
+ 'label',
2009
+ null,
2010
+ t('timerInterval'),
2011
+ h(
2012
+ 'select',
2013
+ {
2014
+ value: smoking.minutes,
2015
+ disabled: smokingUnavailable || smokingBusy,
2016
+ onChange: (event) =>
2017
+ smokingAction({ type: 'interval', minutes: Number(event.target.value) }),
2018
+ },
2019
+ COUNTDOWN_INTERVALS.map((minutes) =>
2020
+ h('option', { key: minutes, value: minutes }, String(minutes)),
2021
+ ),
2022
+ ),
2023
+ ),
2024
+ h(
2025
+ 'div',
2026
+ { className: 'owc-countdown-actions' },
2027
+ h(
2028
+ 'button',
2029
+ {
2030
+ type: 'button',
2031
+ className: 'owc-primary',
2032
+ disabled: smokingUnavailable || smokingBusy,
2033
+ onClick: () =>
2034
+ smokingAction({
2035
+ type:
2036
+ smokingState.phase === 'running'
2037
+ ? 'pause'
2038
+ : smokingState.phase === 'paused'
2039
+ ? 'resume'
2040
+ : 'start',
2041
+ }),
2042
+ },
2043
+ t(
2044
+ smokingState.phase === 'running'
2045
+ ? 'timerPause'
2046
+ : smokingState.phase === 'paused'
2047
+ ? 'timerResume'
2048
+ : 'timerStart',
2049
+ ),
2050
+ ),
2051
+ smokingState.phase !== 'idle' &&
2052
+ h(
2053
+ 'button',
2054
+ {
2055
+ type: 'button',
2056
+ disabled: smokingUnavailable || smokingBusy,
2057
+ onClick: () => smokingAction({ type: 'restart' }),
2058
+ },
2059
+ t('timerRestart'),
2060
+ ),
2061
+ smokingState.phase !== 'idle' &&
2062
+ h(
2063
+ 'button',
2064
+ {
2065
+ type: 'button',
2066
+ disabled: smokingUnavailable || smokingBusy,
2067
+ onClick: () => smokingAction({ type: 'stop' }),
2068
+ },
2069
+ t('timerStop'),
2070
+ ),
2071
+ ),
2072
+ h('p', { className: 'owc-hint' }, t('detailNextInterval')),
2073
+ h(
2074
+ 'details',
2075
+ { className: 'owc-detail-more' },
2076
+ h('summary', null, t('detailHelp')),
2077
+ h('p', null, t('timerHint')),
2078
+ ),
2079
+ smokingError && h('p', { className: 'owc-error', role: 'alert' }, t(smokingError)),
2080
+ smokingError === 'timerStorageError' &&
2081
+ h(
2082
+ 'button',
2083
+ {
2084
+ type: 'button',
2085
+ disabled: smokingBusy,
2086
+ onClick: () => smokingAction({ type: 'retry' }),
2087
+ },
2088
+ t('timerRetry'),
2089
+ ),
2090
+ );
2091
+ const waterCanRecord = waterReady && !waterError;
2092
+ const waterCanQuiet = waterCanRecord && water.enabled && !water.paused;
1533
2093
  const recordWater = (event) => {
1534
2094
  if (event.detail > 1) return;
1535
2095
  if (!manualAttempt.current)
1536
2096
  manualAttempt.current = {
1537
2097
  type: 'manual-drink',
1538
- shiftId: waterContext.id,
1539
2098
  actionId: window.crypto?.randomUUID?.() || String(Math.random()) + Date.now(),
1540
2099
  ...(waterState.phase === 'due' ? { eventId: waterState.eventId } : {}),
1541
2100
  };
@@ -1604,14 +2163,10 @@ window.__ModuleLoader__.load({
1604
2163
  'button',
1605
2164
  {
1606
2165
  type: 'button',
1607
- disabled:
1608
- waterError ||
1609
- waterBusy ||
1610
- !waterContext ||
1611
- water.shift?.id !== waterContext.id,
1612
- onClick: () => waterAction({ type: water.shift?.paused ? 'resume' : 'pause' }),
2166
+ disabled: waterError || waterBusy || !waterReady,
2167
+ onClick: () => waterAction({ type: water.paused ? 'resume' : 'pause' }),
1613
2168
  },
1614
- t(water.shift?.paused ? 'waterResume' : 'waterPause'),
2169
+ t(water.paused ? 'waterResume' : 'waterPause'),
1615
2170
  ),
1616
2171
  waterError &&
1617
2172
  h(
@@ -1661,7 +2216,7 @@ window.__ModuleLoader__.load({
1661
2216
  className: 'owc-text',
1662
2217
  style: { marginLeft: 'auto' },
1663
2218
  'aria-label': t('waterEdit'),
1664
- disabled: !waterReady || !data?.configured,
2219
+ disabled: !waterReady,
1665
2220
  onClick: openWaterEditor,
1666
2221
  },
1667
2222
  t('settings'),
@@ -1694,16 +2249,59 @@ window.__ModuleLoader__.load({
1694
2249
  { 'data-water-effective': water.interval },
1695
2250
  t('waterEvery') + ' ' + water.interval + ' ' + t('waterMinutes'),
1696
2251
  ),
1697
- h('span', { 'data-water-count': true }, t('waterCount') + ': ' + waterState.count),
2252
+ h(
2253
+ 'span',
2254
+ { 'data-water-count': true },
2255
+ t('waterCount') + ': ' + (waterReady && !waterError ? waterState.count : '—'),
2256
+ ),
1698
2257
  ),
1699
2258
  waterControls(),
1700
- !['working', 'lunch'].includes(waterContext?.status) &&
1701
- h('p', { className: 'owc-hint' }, t('waterRecordHint')),
2259
+ h('p', { className: 'owc-hint' }, t('waterRecordHint')),
1702
2260
  waterState.phase === 'quiet' &&
1703
2261
  h('p', { className: 'owc-hint' }, t('waterQuietHint')),
1704
2262
  waterNotice && !view.companion && h('p', null, t(waterNotice.key)),
1705
2263
  );
1706
2264
 
2265
+ const waterIntervalField = () =>
2266
+ h(
2267
+ 'label',
2268
+ { className: 'owc-water-interval' },
2269
+ h('span', null, t('waterInterval')),
2270
+ h('input', {
2271
+ name: 'waterInterval',
2272
+ type: 'number',
2273
+ inputMode: 'numeric',
2274
+ min: '5',
2275
+ max: '240',
2276
+ step: '1',
2277
+ value: waterInterval,
2278
+ 'aria-invalid': waterInvalid,
2279
+ onChange: (event) => {
2280
+ setWaterInterval(event.target.value);
2281
+ setWaterInvalid(false);
2282
+ },
2283
+ }),
2284
+ );
2285
+ const waterSettingsActions = () =>
2286
+ h(
2287
+ 'div',
2288
+ { className: 'owc-save' },
2289
+ h(
2290
+ 'button',
2291
+ { type: 'button', disabled: waterBusy, onClick: closeWaterEditor },
2292
+ t('cancel'),
2293
+ ),
2294
+ h(
2295
+ 'button',
2296
+ {
2297
+ type: 'submit',
2298
+ className: 'owc-primary',
2299
+ 'aria-label': t('waterSave'),
2300
+ disabled: !waterReady || waterError || waterBusy,
2301
+ },
2302
+ t(waterBusy ? 'busy' : 'save'),
2303
+ ),
2304
+ );
1707
2305
  const waterSettings = () =>
1708
2306
  h(
1709
2307
  'form',
@@ -1726,7 +2324,7 @@ window.__ModuleLoader__.load({
1726
2324
  h('input', {
1727
2325
  type: 'checkbox',
1728
2326
  checked: waterEnabledDraft,
1729
- disabled: !waterReady || waterError || waterBusy || !data?.configured,
2327
+ disabled: !waterReady || waterError || waterBusy,
1730
2328
  onChange: (event) => {
1731
2329
  setWaterEnabledDraft(event.target.checked);
1732
2330
  setWaterNotice(false);
@@ -1735,8 +2333,9 @@ window.__ModuleLoader__.load({
1735
2333
  ),
1736
2334
  h(
1737
2335
  'fieldset',
1738
- { disabled: !waterReady || waterError || waterBusy || !data?.configured },
1739
- h('legend', { className: 'owc-muted' }, t('waterPresets')),
2336
+ { disabled: !waterReady || waterError || waterBusy },
2337
+ h('legend', { className: 'owc-sr-only' }, t('waterPresets')),
2338
+ waterIntervalField(),
1740
2339
  h(
1741
2340
  'div',
1742
2341
  { className: 'owc-water-presets' },
@@ -1747,64 +2346,34 @@ window.__ModuleLoader__.load({
1747
2346
  type: 'button',
1748
2347
  key: minutes,
1749
2348
  'aria-pressed': Number(waterInterval) === minutes,
2349
+ 'aria-label': minutes + ' ' + t('waterMinutes'),
1750
2350
  onClick: () => {
1751
2351
  setWaterInterval(String(minutes));
1752
2352
  setWaterInvalid(false);
1753
2353
  },
1754
2354
  },
1755
- minutes + ' ' + t('waterMinutes'),
2355
+ String(minutes),
1756
2356
  ),
1757
2357
  ),
1758
2358
  ),
1759
- h(
1760
- 'label',
1761
- { className: 'owc-field' },
1762
- h('span', null, t('waterInterval')),
1763
- h('input', {
1764
- name: 'waterInterval',
1765
- type: 'number',
1766
- inputMode: 'numeric',
1767
- min: '5',
1768
- max: '240',
1769
- step: '1',
1770
- value: waterInterval,
1771
- 'aria-invalid': waterInvalid,
1772
- onChange: (event) => {
1773
- setWaterInterval(event.target.value);
1774
- setWaterInvalid(false);
1775
- },
1776
- }),
1777
- ),
1778
2359
  ),
1779
2360
  waterInvalid && h('p', { role: 'alert' }, t('waterInvalid')),
1780
- h(
1781
- 'p',
1782
- { 'data-water-saved-interval': water.interval },
1783
- t('waterEffective') + ': ' + water.interval + ' ' + t('waterMinutes'),
1784
- ),
1785
- h('p', { className: 'owc-hint' }, t('waterSaveHint')),
1786
- h('p', { className: 'owc-hint' }, t('waterBrief')),
1787
- waterError && h('p', { role: 'alert' }, t('waterUnavailable')),
1788
- !data?.configured && h('p', null, t('waterSetup')),
1789
- h(
1790
- 'div',
1791
- { className: 'owc-save' },
2361
+ (Number(waterInterval) !== water.interval || waterEnabledDraft !== water.enabled) &&
1792
2362
  h(
1793
- 'button',
1794
- { type: 'button', disabled: waterBusy, onClick: closeWaterEditor },
1795
- t('cancel'),
2363
+ 'p',
2364
+ { className: 'owc-hint', 'data-water-saved-interval': water.interval },
2365
+ t('detailSavedInterval') +
2366
+ ': ' +
2367
+ water.interval +
2368
+ ' ' +
2369
+ t('waterMinutes') +
2370
+ ' · ' +
2371
+ t('detailDraftHint'),
1796
2372
  ),
1797
- h(
1798
- 'button',
1799
- {
1800
- type: 'submit',
1801
- className: 'owc-primary',
1802
- disabled: !waterReady || waterError || waterBusy || !data?.configured,
1803
- },
1804
- t(waterBusy ? 'busy' : 'waterSave'),
1805
- ),
1806
- ),
1807
- waterError &&
2373
+ h('p', { className: 'owc-hint' }, t('detailWaterRule')),
2374
+ waterError && h('p', { role: 'alert' }, t('waterUnavailable')),
2375
+
2376
+ waterError &&
1808
2377
  h(
1809
2378
  'button',
1810
2379
  {
@@ -1866,6 +2435,7 @@ window.__ModuleLoader__.load({
1866
2435
  ),
1867
2436
  ),
1868
2437
  ),
2438
+ waterSettingsActions(),
1869
2439
  );
1870
2440
  const waterReminder = () =>
1871
2441
  waterPrompt &&
@@ -1880,6 +2450,7 @@ window.__ModuleLoader__.load({
1880
2450
  'button',
1881
2451
  {
1882
2452
  type: 'button',
2453
+ className: 'owc-primary',
1883
2454
  disabled: waterBusy,
1884
2455
  onClick: () => waterAction({ type: 'drink', eventId: waterState.eventId }),
1885
2456
  },
@@ -1897,6 +2468,440 @@ window.__ModuleLoader__.load({
1897
2468
  ),
1898
2469
  );
1899
2470
 
2471
+ const classicMini = () => {
2472
+ const countdown = classicClock.seconds !== null;
2473
+ const caption =
2474
+ {
2475
+ working: 'classicMiniWorking',
2476
+ before: 'classicMiniBefore',
2477
+ lunch: 'classicMiniLunch',
2478
+ }[classicClock.phase] || 'classicMiniStatus';
2479
+ const stateTitle =
2480
+ { setup: 'petNeedsSetup', error: 'petLoadFailed', waiting: 'petNeedsSession' }[
2481
+ classicClock.phase
2482
+ ] || classicClock.title;
2483
+ const valid = !!data?.configured && !!safeState && !error;
2484
+ const showIncome = valid && ['working', 'lunch', 'done'].includes(safeState.status);
2485
+ const compact = (value) => {
2486
+ if (value === t('timerUnavailable')) return '—';
2487
+ if (value === t('timerWaiting')) return t('classicMiniWaiting');
2488
+ if (value === t('timerFinished')) return t('classicMiniFinished');
2489
+ return value.startsWith('00:') ? value.slice(3) : value.replace(/^0(?=\d:)/, '');
2490
+ };
2491
+ const tile = (kind, screen, label, value, summary, due) =>
2492
+ h(
2493
+ 'button',
2494
+ {
2495
+ type: 'button',
2496
+ className: 'owc-classic-mini-timer',
2497
+ 'data-classic-timer': kind,
2498
+ 'data-countdown-due': due,
2499
+ 'aria-label': t(label),
2500
+ title: t(label) + ' · ' + summary,
2501
+ disabled: busy || waterBusy,
2502
+ onClick: (event) => navigateScreen(screen, event),
2503
+ },
2504
+ iconSvg(kind, 13),
2505
+ h('strong', null, compact(value)),
2506
+ );
2507
+ return h(
2508
+ 'div',
2509
+ {
2510
+ className: 'owc-classic-mini',
2511
+ 'data-classic-overview': true,
2512
+ 'data-clock-phase': classicClock.phase,
2513
+ },
2514
+ h(
2515
+ 'div',
2516
+ { className: 'owc-classic-mini-top' },
2517
+ h(
2518
+ 'div',
2519
+ {
2520
+ className: 'owc-grip',
2521
+ tabIndex: 0,
2522
+ title: t(classicClock.hint) + ' · ' + t('drag'),
2523
+ onKeyDown: gripKeys,
2524
+ },
2525
+ h('span', { className: 'owc-classic-mini-label' }, t(caption)),
2526
+ countdown
2527
+ ? h(
2528
+ 'strong',
2529
+ {
2530
+ className: 'owc-classic-mini-time',
2531
+ role: 'timer',
2532
+ 'aria-label': t(classicClock.title),
2533
+ },
2534
+ formatDuration(classicClock.seconds),
2535
+ )
2536
+ : ['setup', 'error'].includes(classicClock.phase)
2537
+ ? h(
2538
+ 'button',
2539
+ {
2540
+ type: 'button',
2541
+ className: 'owc-classic-mini-state',
2542
+ disabled: hostBusy,
2543
+ 'aria-label': t(
2544
+ classicClock.phase === 'setup' ? 'classicConfigure' : 'retry',
2545
+ ),
2546
+ onClick:
2547
+ classicClock.phase === 'setup' ? openEditor : () => reload(true),
2548
+ },
2549
+ t(stateTitle),
2550
+ )
2551
+ : h(
2552
+ 'strong',
2553
+ { className: 'owc-classic-mini-state', role: 'status' },
2554
+ t(stateTitle),
2555
+ ),
2556
+ ),
2557
+ h(
2558
+ 'button',
2559
+ {
2560
+ type: 'button',
2561
+ className: 'owc-icon',
2562
+ title: t('expand'),
2563
+ 'aria-label': t('expand'),
2564
+ onClick: () => setPreference({ mini: false }),
2565
+ },
2566
+ iconSvg('chevronDown', 14),
2567
+ ),
2568
+ ),
2569
+ h(
2570
+ 'div',
2571
+ { className: 'owc-classic-mini-timers' },
2572
+ tile(
2573
+ 'water',
2574
+ 'water-countdown',
2575
+ 'waterCountdown',
2576
+ waterCountdownText(),
2577
+ waterSummary(),
2578
+ waterState.phase === 'due',
2579
+ ),
2580
+ tile(
2581
+ 'smoking',
2582
+ 'smoking',
2583
+ 'smokingCountdown',
2584
+ smokingCountdownText(),
2585
+ t(
2586
+ smokingState.phase === 'paused'
2587
+ ? 'timerPaused'
2588
+ : smokingState.phase === 'due'
2589
+ ? 'timerDue'
2590
+ : smokingState.phase === 'idle'
2591
+ ? 'timerIdle'
2592
+ : 'timerRunning',
2593
+ ),
2594
+ smokingState.phase === 'due',
2595
+ ),
2596
+ h(
2597
+ 'div',
2598
+ { className: 'owc-classic-mini-income' },
2599
+ showIncome &&
2600
+ !view.masked &&
2601
+ h(
2602
+ 'button',
2603
+ {
2604
+ type: 'button',
2605
+ className: 'owc-classic-mini-amount',
2606
+ 'aria-label': t('classicIncomeEntry'),
2607
+ 'aria-description': moneyText,
2608
+ title: moneyText,
2609
+ onClick: (event) => navigateScreen('earnings', event),
2610
+ },
2611
+ moneyText,
2612
+ ),
2613
+ h(
2614
+ 'button',
2615
+ {
2616
+ type: 'button',
2617
+ className: 'owc-icon',
2618
+ disabled: !valid,
2619
+ 'aria-label': t(
2620
+ showIncome ? (view.masked ? 'show' : 'hide') : 'classicIncomeEntry',
2621
+ ),
2622
+ title: t(showIncome ? (view.masked ? 'show' : 'hide') : 'classicIncomeEntry'),
2623
+ 'aria-pressed': showIncome ? !view.masked : undefined,
2624
+ onClick: (event) =>
2625
+ showIncome
2626
+ ? setPreference({ masked: !view.masked })
2627
+ : navigateScreen('earnings', event),
2628
+ },
2629
+ iconSvg(view.masked || !showIncome ? 'eyeOff' : 'eye', 13),
2630
+ ),
2631
+ ),
2632
+ ),
2633
+ );
2634
+ };
2635
+
2636
+ const classicHeader = () =>
2637
+ h(
2638
+ 'div',
2639
+ { className: 'owc-head owc-classic-head' },
2640
+ ui.screen &&
2641
+ h(
2642
+ 'button',
2643
+ {
2644
+ type: 'button',
2645
+ className: 'owc-icon',
2646
+ 'aria-label': t(
2647
+ PET_EDITOR_SCREENS.includes(ui.screen) && ui.back
2648
+ ? 'petBack'
2649
+ : 'classicReturn',
2650
+ ),
2651
+ title: t(
2652
+ PET_EDITOR_SCREENS.includes(ui.screen) && ui.back
2653
+ ? 'petBack'
2654
+ : 'classicReturn',
2655
+ ),
2656
+ disabled: busy || waterBusy || !!ui.pending,
2657
+ onClick: () =>
2658
+ navigateScreen(PET_EDITOR_SCREENS.includes(ui.screen) ? ui.back : null),
2659
+ },
2660
+ '←',
2661
+ ),
2662
+ h(
2663
+ 'strong',
2664
+ { className: 'owc-title', tabIndex: 0, title: t('drag'), onKeyDown: gripKeys },
2665
+ t(ui.screen ? petPageTitle[ui.screen] || 'classicOverview' : 'classicOverview'),
2666
+ ),
2667
+ ['home', 'water-countdown'].includes(ui.screen) &&
2668
+ h(
2669
+ 'button',
2670
+ {
2671
+ type: 'button',
2672
+ className: 'owc-icon',
2673
+ 'aria-label': t('petWaterSettings'),
2674
+ title: t('petWaterSettings'),
2675
+ disabled: busy || waterBusy || !!ui.pending,
2676
+ onClick: openWaterEditor,
2677
+ },
2678
+ iconSvg('sliders', 14),
2679
+ ),
2680
+ waterPrompt &&
2681
+ ui.screen &&
2682
+ !['home', 'water-countdown'].includes(ui.screen) &&
2683
+ h(
2684
+ 'button',
2685
+ {
2686
+ type: 'button',
2687
+ className: 'owc-icon',
2688
+ 'aria-label': t('waterDue'),
2689
+ title: t('waterDue'),
2690
+ onClick: (event) => navigateScreen('home', event),
2691
+ },
2692
+ iconSvg('water'),
2693
+ ),
2694
+ !ui.screen &&
2695
+ h(
2696
+ React.Fragment,
2697
+ null,
2698
+ h(
2699
+ 'button',
2700
+ { type: 'button', className: 'owc-text', onClick: openAppearance },
2701
+ t('appearanceGroup'),
2702
+ ),
2703
+ h(
2704
+ 'button',
2705
+ {
2706
+ type: 'button',
2707
+ className: 'owc-icon',
2708
+ 'aria-label': t('petWorkSettings'),
2709
+ title: t('petWorkSettings'),
2710
+ disabled: hostBusy || !data,
2711
+ onClick: openEditor,
2712
+ },
2713
+ iconSvg('sliders'),
2714
+ ),
2715
+ h(
2716
+ 'button',
2717
+ {
2718
+ type: 'button',
2719
+ className: 'owc-icon',
2720
+ 'aria-label': t(collapsed ? 'classicMore' : 'classicLess'),
2721
+ title: t(collapsed ? 'classicMore' : 'classicLess'),
2722
+ onClick: () => setPreference({ mini: !view.mini }),
2723
+ },
2724
+ iconSvg(collapsed ? 'chevronDown' : 'chevronUp'),
2725
+ ),
2726
+ ),
2727
+ );
2728
+ const classicOverview = () => {
2729
+ const countdown = classicClock.seconds !== null;
2730
+ const valid = !!data?.configured && !!safeState && !error;
2731
+ const showIncome = valid && ['working', 'lunch', 'done'].includes(safeState.status);
2732
+ const tile = (kind, screen, label, value, summary) =>
2733
+ h(
2734
+ 'button',
2735
+ {
2736
+ type: 'button',
2737
+ className: 'owc-classic-tile',
2738
+ 'data-classic-timer': kind,
2739
+ 'aria-label': t(label),
2740
+ disabled: busy || waterBusy,
2741
+ onClick: (event) => navigateScreen(screen, event),
2742
+ },
2743
+ h(
2744
+ 'span',
2745
+ { className: 'owc-classic-tile-title' },
2746
+ iconSvg(kind, 14),
2747
+ t(kind === 'water' ? 'waterTimerShort' : 'smokingTimerShort'),
2748
+ ),
2749
+ h('strong', null, value),
2750
+ h('span', { className: 'owc-classic-tile-hint' }, summary),
2751
+ );
2752
+ return h(
2753
+ 'div',
2754
+ {
2755
+ className: 'owc-classic-overview',
2756
+ 'data-classic-overview': true,
2757
+ 'data-clock-phase': classicClock.phase,
2758
+ },
2759
+ h(
2760
+ 'div',
2761
+ { className: 'owc-classic-hero' },
2762
+ h(
2763
+ 'div',
2764
+ { className: 'owc-classic-kicker' },
2765
+ iconSvg('alarm', 14),
2766
+ t(countdown ? classicClock.title : 'title'),
2767
+ ),
2768
+ countdown
2769
+ ? h(
2770
+ 'div',
2771
+ {
2772
+ className: 'owc-classic-time',
2773
+ role: 'timer',
2774
+ 'aria-label': t(classicClock.title),
2775
+ },
2776
+ formatDuration(classicClock.seconds),
2777
+ )
2778
+ : h(
2779
+ 'h3',
2780
+ { className: 'owc-classic-state', role: 'status' },
2781
+ t(classicClock.title),
2782
+ ),
2783
+ h('p', { className: 'owc-classic-hint' }, t(classicClock.hint)),
2784
+ classicClock.phase === 'setup' &&
2785
+ h(
2786
+ 'button',
2787
+ { type: 'button', className: 'owc-primary', onClick: openEditor },
2788
+ t('classicConfigure'),
2789
+ ),
2790
+ classicClock.phase === 'error' &&
2791
+ h(
2792
+ 'button',
2793
+ {
2794
+ type: 'button',
2795
+ disabled: hostBusy || !sessionId,
2796
+ onClick: () => reload(true),
2797
+ },
2798
+ iconSvg('refresh', 12),
2799
+ t('retry'),
2800
+ ),
2801
+ valid &&
2802
+ h(
2803
+ 'div',
2804
+ { className: 'owc-classic-schedule' },
2805
+ h(
2806
+ 'span',
2807
+ null,
2808
+ settings.start + ' — ' + settings.end + (safeState.overnight ? ' +1' : ''),
2809
+ ),
2810
+ h('span', null, t('classicEnd') + ' ' + settings.end),
2811
+ ),
2812
+ ),
2813
+ h(
2814
+ 'div',
2815
+ { className: 'owc-classic-timers' },
2816
+ tile(
2817
+ 'water',
2818
+ 'water-countdown',
2819
+ 'waterCountdown',
2820
+ waterCountdownText(),
2821
+ waterState.phase === 'countdown' ? t('classicWaterHint') : waterSummary(),
2822
+ ),
2823
+ tile(
2824
+ 'smoking',
2825
+ 'smoking',
2826
+ 'smokingCountdown',
2827
+ smokingCountdownText(),
2828
+ smokingUnavailable
2829
+ ? t(smokingError ? 'timerUnavailable' : 'timerLoading')
2830
+ : t(
2831
+ {
2832
+ idle: 'classicSmokeHint',
2833
+ running: 'classicSmokeHint',
2834
+ paused: 'timerPaused',
2835
+ due: 'timerDue',
2836
+ }[smokingState.phase],
2837
+ ),
2838
+ ),
2839
+ ),
2840
+ h(
2841
+ 'div',
2842
+ { className: 'owc-classic-income' },
2843
+ h(
2844
+ 'button',
2845
+ {
2846
+ type: 'button',
2847
+ className: 'owc-classic-income-link',
2848
+ disabled: !valid,
2849
+ onClick: (event) => navigateScreen('earnings', event),
2850
+ },
2851
+ h('span', null, t(showIncome ? 'classicIncome' : 'classicIncomeEntry')),
2852
+ showIncome &&
2853
+ h('strong', { 'aria-label': view.masked ? t('masked') : moneyText }, moneyText),
2854
+ h('span', { 'aria-hidden': true }, '›'),
2855
+ ),
2856
+ showIncome && salaryToggle(),
2857
+ ),
2858
+ !collapsed &&
2859
+ h(
2860
+ 'div',
2861
+ { className: 'owc-classic-details' },
2862
+ valid
2863
+ ? h(
2864
+ React.Fragment,
2865
+ null,
2866
+ h(
2867
+ 'div',
2868
+ { className: 'owc-classic-detail-title' },
2869
+ t('classicShift'),
2870
+ h('span', null, Math.floor(safeState.progress * 100) + '%'),
2871
+ ),
2872
+ h(
2873
+ 'div',
2874
+ {
2875
+ className: 'owc-progress',
2876
+ role: 'progressbar',
2877
+ 'aria-label': t('progress'),
2878
+ 'aria-valuenow': Math.floor(safeState.progress * 100),
2879
+ 'aria-valuemin': 0,
2880
+ 'aria-valuemax': 100,
2881
+ },
2882
+ h('div', { style: { width: safeState.progress * 100 + '%' } }),
2883
+ ),
2884
+ h(
2885
+ 'p',
2886
+ null,
2887
+ t('shiftDate') + ' ' + safeState.shiftDate + ' · ' + settings.timezone,
2888
+ ),
2889
+ h(
2890
+ 'p',
2891
+ null,
2892
+ settings.breakStart === settings.breakEnd
2893
+ ? t('noBreak')
2894
+ : t('lunch') + ' ' + settings.breakStart + '–' + settings.breakEnd,
2895
+ ),
2896
+ h('button', { type: 'button', onClick: openEditor }, t('petWorkSettings')),
2897
+ )
2898
+ : h('p', null, t(classicClock.hint)),
2899
+ ),
2900
+ waterNotice &&
2901
+ h('p', { className: 'owc-classic-feedback', role: 'status' }, t(waterNotice.key)),
2902
+ );
2903
+ };
2904
+
1900
2905
  const strip = () =>
1901
2906
  h(
1902
2907
  'div',
@@ -2133,14 +3138,26 @@ window.__ModuleLoader__.load({
2133
3138
  type: 'button',
2134
3139
  className: 'owc-text',
2135
3140
  onClick: () => reload(true),
2136
- disabled: busy || !sessionId,
3141
+ disabled: hostBusy || !sessionId,
2137
3142
  },
2138
3143
  iconSvg('refresh', 12),
2139
3144
  t('retry'),
2140
3145
  ),
2141
3146
  ),
2142
3147
  );
2143
- const status = safeState.status === 'done' ? '' : t(safeState.status);
3148
+ const clockSeconds = ['done', 'rest'].includes(safeState.status) ? null : seconds;
3149
+ const clockTitle =
3150
+ safeState.status === 'before'
3151
+ ? 'before'
3152
+ : ['working', 'lunch'].includes(safeState.status)
3153
+ ? 'working'
3154
+ : safeState.status;
3155
+ const status =
3156
+ clockSeconds === null
3157
+ ? ''
3158
+ : safeState.status === 'lunch'
3159
+ ? t('lunch') + ' · ' + t('working')
3160
+ : t(clockTitle);
2144
3161
  return h(
2145
3162
  'div',
2146
3163
  { className: 'owc-main' },
@@ -2150,10 +3167,15 @@ window.__ModuleLoader__.load({
2150
3167
  section !== 'earnings' &&
2151
3168
  h(
2152
3169
  'div',
2153
- { className: 'owc-time', 'aria-label': status + ' ' + duration },
2154
- duration,
3170
+ {
3171
+ className: 'owc-time' + (clockSeconds === null ? ' owc-detail-state' : ''),
3172
+ role: clockSeconds === null ? 'status' : 'timer',
3173
+ 'aria-label': t(clockTitle),
3174
+ },
3175
+ clockSeconds === null ? t(clockTitle) : formatDuration(clockSeconds),
2155
3176
  ),
2156
3177
  section !== 'earnings' &&
3178
+ ['working', 'lunch', 'done'].includes(safeState.status) &&
2157
3179
  h(
2158
3180
  'div',
2159
3181
  {
@@ -2197,24 +3219,29 @@ window.__ModuleLoader__.load({
2197
3219
  ),
2198
3220
  ),
2199
3221
  h(
2200
- 'div',
2201
- { className: 'owc-meta' },
2202
- h(
2203
- 'span',
2204
- null,
2205
- settings.start +
2206
- ' — ' +
2207
- settings.end +
2208
- (settings.end < settings.start ? ' +1' : '') +
2209
- ' · ' +
2210
- (settings.breakStart === settings.breakEnd
2211
- ? t('noBreak')
2212
- : settings.breakStart + '–' + settings.breakEnd),
2213
- ),
3222
+ 'details',
3223
+ { className: 'owc-detail-more' },
3224
+ h('summary', null, t('detailShift')),
2214
3225
  h(
2215
- 'span',
2216
- null,
2217
- t('shiftDate') + ' ' + safeState.shiftDate + ' · ' + settings.timezone,
3226
+ 'div',
3227
+ { className: 'owc-meta' },
3228
+ h(
3229
+ 'span',
3230
+ null,
3231
+ settings.start +
3232
+ ' — ' +
3233
+ settings.end +
3234
+ (settings.end < settings.start ? ' +1' : '') +
3235
+ ' · ' +
3236
+ (settings.breakStart === settings.breakEnd
3237
+ ? t('noBreak')
3238
+ : settings.breakStart + '–' + settings.breakEnd),
3239
+ ),
3240
+ h(
3241
+ 'span',
3242
+ null,
3243
+ t('shiftDate') + ' ' + safeState.shiftDate + ' · ' + settings.timezone,
3244
+ ),
2218
3245
  ),
2219
3246
  ),
2220
3247
  h(
@@ -2226,7 +3253,7 @@ window.__ModuleLoader__.load({
2226
3253
  type: 'button',
2227
3254
  className: 'owc-text',
2228
3255
  onClick: () => reload(true),
2229
- disabled: busy,
3256
+ disabled: hostBusy,
2230
3257
  },
2231
3258
  iconSvg('refresh', 12),
2232
3259
  t('refresh'),
@@ -2307,7 +3334,6 @@ window.__ModuleLoader__.load({
2307
3334
  onChange: (event) => updateAppearance({ petSize: event.target.valueAsNumber }),
2308
3335
  }),
2309
3336
  ),
2310
- h('p', { className: 'owc-hint' }, t('petAppearanceHint')),
2311
3337
  label(
2312
3338
  t('opacity'),
2313
3339
  h(
@@ -2327,32 +3353,108 @@ window.__ModuleLoader__.load({
2327
3353
  }),
2328
3354
  h(
2329
3355
  'span',
2330
- { className: 'owc-hint', style: { margin: 0 } },
2331
- Math.round(view.opacity * 100) + '% · ' + t('opacityHint'),
3356
+ { className: 'owc-range-value', title: t('opacityHint') },
3357
+ Math.round(view.opacity * 100) + '%',
3358
+ ),
3359
+ ),
3360
+ ),
3361
+ );
3362
+ const previewActions = () => {
3363
+ if (!view.companion || petModel(view.petModel).renderer !== 'video') return null;
3364
+ const labels = {
3365
+ idle: 'petPreviewIdle',
3366
+ greet: 'petPreviewGreet',
3367
+ celebrate: 'petPreviewDrink',
3368
+ };
3369
+ const names = { idle: 'petTryIdle', greet: 'petTryGreet', celebrate: 'petTryDrink' };
3370
+ const clips = PET_CLIPS.models[view.petModel] || {};
3371
+ const blocked =
3372
+ !view.companionMotion || previewReduced || previewState.phase === 'unsupported';
3373
+ const message = previewReduced
3374
+ ? t('petPreviewReduced')
3375
+ : !view.companionMotion
3376
+ ? t('petPreviewEnable')
3377
+ : previewState.phase === 'loading'
3378
+ ? t('petPreviewLoading')
3379
+ : previewState.phase === 'playing'
3380
+ ? t('petPreviewPlaying') + ' · ' + t(labels[previewState.action])
3381
+ : previewState.phase === 'unsupported'
3382
+ ? t('petPreviewUnsupported')
3383
+ : previewState.phase === 'unavailable'
3384
+ ? t('petPreviewUnavailable')
3385
+ : t('petPreviewHint');
3386
+ return h(
3387
+ 'div',
3388
+ { className: 'owc-preview-tools' },
3389
+ h(
3390
+ 'div',
3391
+ {
3392
+ className: 'owc-preview-actions',
3393
+ role: 'group',
3394
+ 'aria-label': t('petPreviewActions'),
3395
+ },
3396
+ ...Object.keys(labels)
3397
+ .filter((action) => Object.hasOwn(clips, action))
3398
+ .map((action) =>
3399
+ h(
3400
+ 'button',
3401
+ {
3402
+ type: 'button',
3403
+ key: action,
3404
+ 'aria-label': t(names[action]),
3405
+ 'aria-pressed':
3406
+ previewState.action === action &&
3407
+ ['loading', 'playing'].includes(previewState.phase),
3408
+ disabled: blocked,
3409
+ onClick: () => previewMotion.current?.retry?.(action),
3410
+ },
3411
+ t(labels[action]),
3412
+ ),
2332
3413
  ),
3414
+ h(
3415
+ 'button',
3416
+ {
3417
+ type: 'button',
3418
+ 'aria-label': t('petStopPreview'),
3419
+ disabled: !['loading', 'playing'].includes(previewState.phase),
3420
+ onClick: () => previewMotion.current?.stop?.(),
3421
+ },
3422
+ t('petPreviewStop'),
2333
3423
  ),
2334
3424
  ),
3425
+ h(
3426
+ 'p',
3427
+ { className: 'owc-hint owc-preview-status', role: 'status', 'aria-atomic': true },
3428
+ message,
3429
+ ),
2335
3430
  );
3431
+ };
2336
3432
  const editor = () => {
2337
3433
  if (appearanceOnly)
2338
3434
  return h(
2339
3435
  'div',
2340
3436
  { className: 'owc-form owc-appearance', 'data-appearance-settings': true },
2341
- h('p', { className: 'owc-hint' }, t('petAppearancePreview')),
3437
+ h('p', { className: 'owc-sr-only' }, t('petAppearancePreview')),
2342
3438
  h(
2343
3439
  'div',
2344
- { 'data-appearance-preview': true, className: 'owc-appearance-preview' },
3440
+ {
3441
+ ref: previewBox,
3442
+ 'data-appearance-preview': true,
3443
+ className: 'owc-appearance-preview',
3444
+ },
2345
3445
  view.companion
2346
3446
  ? petArtwork(
2347
3447
  h,
2348
- companion,
2349
- view.petSize * (petModel(view.petModel).renderer === 'image' ? 0.65 : 1),
3448
+ { pose: 'calm', animate: false },
3449
+ view.petSize * (petModel(view.petModel).renderer === 'svg' ? 1 : 0.65),
2350
3450
  '',
2351
3451
  view.petModel,
2352
3452
  PET_SPRITES,
3453
+ petModel(view.petModel).renderer === 'video',
2353
3454
  )
2354
3455
  : h('p', null, t('petClassicPreview')),
2355
3456
  ),
3457
+ previewActions(),
2356
3458
  appearanceFields(),
2357
3459
  h(
2358
3460
  'div',
@@ -2360,8 +3462,13 @@ window.__ModuleLoader__.load({
2360
3462
  h('button', { type: 'button', onClick: closeEditor }, t('cancel')),
2361
3463
  h(
2362
3464
  'button',
2363
- { type: 'button', className: 'owc-primary', onClick: applyAppearance },
2364
- t('petApplyAppearance'),
3465
+ {
3466
+ type: 'button',
3467
+ className: 'owc-primary',
3468
+ 'aria-label': t('petApplyAppearance'),
3469
+ onClick: applyAppearance,
3470
+ },
3471
+ t('detailApply'),
2365
3472
  ),
2366
3473
  ),
2367
3474
  );
@@ -2369,21 +3476,7 @@ window.__ModuleLoader__.load({
2369
3476
  const payDaily = workingDraft.payMode === 'daily';
2370
3477
  const salaryKey = payDaily ? 'dailySalary' : 'monthlySalary';
2371
3478
  const salaryControl = view.masked
2372
- ? h(
2373
- 'div',
2374
- { className: 'owc-private' },
2375
- h('span', null, '••••'),
2376
- h(
2377
- 'button',
2378
- {
2379
- type: 'button',
2380
- className: 'owc-text',
2381
- 'aria-label': t('show'),
2382
- onClick: () => setPreference({ masked: false }),
2383
- },
2384
- t('show'),
2385
- ),
2386
- )
3479
+ ? h('div', { className: 'owc-private', 'aria-label': t('masked') }, '••••')
2387
3480
  : numberInput(salaryKey);
2388
3481
  const breakOn = workingDraft.breakStart !== workingDraft.breakEnd;
2389
3482
  const commonZones = [
@@ -2408,6 +3501,20 @@ window.__ModuleLoader__.load({
2408
3501
  const zones = [
2409
3502
  ...new Set([workingDraft.timezone, ...commonZones, ...allZones].filter(Boolean)),
2410
3503
  ];
3504
+ const timezoneField = () =>
3505
+ label(
3506
+ t('timezone'),
3507
+ h(
3508
+ 'select',
3509
+ {
3510
+ name: 'timezone',
3511
+ value: workingDraft.timezone ?? '',
3512
+ disabled: busy,
3513
+ onChange: (e) => update('timezone', e.target.value),
3514
+ },
3515
+ zones.map((zone) => h('option', { key: zone, value: zone }, zone)),
3516
+ ),
3517
+ );
2411
3518
  return h(
2412
3519
  'form',
2413
3520
  {
@@ -2424,7 +3531,22 @@ window.__ModuleLoader__.load({
2424
3531
  h(
2425
3532
  'fieldset',
2426
3533
  { className: 'owc-setting-group' },
2427
- h('legend', null, t('salaryGroup')),
3534
+ h(
3535
+ 'legend',
3536
+ { className: 'owc-salary-legend' },
3537
+ h('span', null, t('salaryGroup')),
3538
+ h(
3539
+ 'button',
3540
+ {
3541
+ type: 'button',
3542
+ className: 'owc-icon',
3543
+ 'aria-label': t(view.masked ? 'show' : 'hide'),
3544
+ title: t(view.masked ? 'show' : 'hide'),
3545
+ onClick: () => setPreference({ masked: !view.masked }),
3546
+ },
3547
+ iconSvg(view.masked ? 'eyeOff' : 'eye', 13),
3548
+ ),
3549
+ ),
2428
3550
  label(
2429
3551
  t('payMode'),
2430
3552
  h(
@@ -2518,22 +3640,10 @@ window.__ModuleLoader__.load({
2518
3640
  ),
2519
3641
  ),
2520
3642
  ),
2521
- label(
2522
- t('timezone'),
2523
- h(
2524
- 'select',
2525
- {
2526
- name: 'timezone',
2527
- value: workingDraft.timezone ?? '',
2528
- disabled: busy,
2529
- onChange: (e) => update('timezone', e.target.value),
2530
- },
2531
- zones.map((zone) => h('option', { key: zone, value: zone }, zone)),
2532
- ),
2533
- ),
2534
3643
  ),
2535
3644
  preview &&
2536
3645
  !preview.error &&
3646
+ !view.masked &&
2537
3647
  h(
2538
3648
  'div',
2539
3649
  { className: 'owc-preview' },
@@ -2557,6 +3667,7 @@ window.__ModuleLoader__.load({
2557
3667
  'details',
2558
3668
  { className: 'owc-advanced' },
2559
3669
  h('summary', null, t('advanced')),
3670
+ timezoneField(),
2560
3671
  h('p', { className: 'owc-hint' }, t('sessionHint')),
2561
3672
  label(t('session'), sessionSelect()),
2562
3673
  ),
@@ -2579,12 +3690,18 @@ window.__ModuleLoader__.load({
2579
3690
  );
2580
3691
  };
2581
3692
 
2582
- const notices = () =>
3693
+ const notices = (showBusinessError = true) =>
2583
3694
  h(
2584
3695
  React.Fragment,
2585
3696
  null,
2586
- error && h('p', { className: 'owc-error', role: 'alert' }, error),
2587
- state?.error && h('p', { className: 'owc-error', role: 'alert' }, state.error),
3697
+ showBusinessError &&
3698
+ !LOCAL_SCREENS.includes(ui.screen) &&
3699
+ error &&
3700
+ h('p', { className: 'owc-error', role: 'alert' }, error),
3701
+ showBusinessError &&
3702
+ !LOCAL_SCREENS.includes(ui.screen) &&
3703
+ state?.error &&
3704
+ h('p', { className: 'owc-error', role: 'alert' }, state.error),
2588
3705
  notice && h('p', { className: 'owc-note', role: 'status' }, notice),
2589
3706
  storageError && h('p', { className: 'owc-note' }, t('storage')),
2590
3707
  );
@@ -2592,15 +3709,13 @@ window.__ModuleLoader__.load({
2592
3709
  function closePetPanel() {
2593
3710
  navigateScreen(null);
2594
3711
  }
2595
- function togglePetPanel(event) {
3712
+ function interactWithPet(event) {
2596
3713
  if (petDragged.current && event.detail !== 0) {
2597
3714
  petDragged.current = false;
2598
3715
  return;
2599
3716
  }
2600
- navigateScreen(
2601
- event.currentTarget.classList.contains('owc-pet-clock') ? 'income' : 'home',
2602
- event,
2603
- );
3717
+ petDragged.current = false;
3718
+ if (!ui.pending) petMotion.current?.play('greet');
2604
3719
  }
2605
3720
  function closePetMenu(restore = true) {
2606
3721
  dispatchUI({ type: 'dismiss-menu' });
@@ -2624,8 +3739,15 @@ window.__ModuleLoader__.load({
2624
3739
  waterPrompt ? t('waterDue') : waterNotice ? t(waterNotice.key) : '',
2625
3740
  ),
2626
3741
  );
3742
+ React.useLayoutEffect(() => {
3743
+ if (!ready) return;
3744
+ const scroll = petMode
3745
+ ? petPanel.current?.querySelector('.owc-pet-scroll')
3746
+ : box.current?.querySelector('.owc-content');
3747
+ if (scroll) scroll.scrollTop = 0;
3748
+ }, [ready, petMode, ui.screen]);
2627
3749
  const petShowing = petOpen;
2628
- const petFloating = petShowing || waterPrompt;
3750
+ const petFloating = petShowing || autoWaterPrompt;
2629
3751
  React.useLayoutEffect(() => {
2630
3752
  if (!petMode || !petFloating || !cardContent.current) return;
2631
3753
  const content = cardContent.current,
@@ -2729,7 +3851,13 @@ window.__ModuleLoader__.load({
2729
3851
  return () => document.removeEventListener('pointerdown', outside);
2730
3852
  }, [petMode, petShowing, ui.menu, editing, waterEditing, waterBusy]);
2731
3853
  React.useEffect(() => {
2732
- if (ui.menu) menuBox.current?.querySelector('[role=menuitem]:not(:disabled)')?.focus();
3854
+ if (!ui.menu) return;
3855
+ const waterEntry = menuTrigger.current?.classList.contains('owc-pet-water-alert')
3856
+ ? menuBox.current?.querySelector('[data-pet-menu-screen="home"]:not(:disabled)')
3857
+ : null;
3858
+ (
3859
+ waterEntry || menuBox.current?.querySelector('[role=menuitem]:not(:disabled)')
3860
+ )?.focus();
2733
3861
  }, [ui.menu]);
2734
3862
  React.useEffect(() => {
2735
3863
  if (!petMode || editing || waterEditing || ui.menu) return;
@@ -2745,6 +3873,32 @@ window.__ModuleLoader__.load({
2745
3873
  if (ui.pending && (busy || waterBusy))
2746
3874
  box.current?.querySelector('[data-nav-confirm]')?.focus();
2747
3875
  }, [ui.pending, busy, waterBusy]);
3876
+ const classicNavigation = React.useRef({ screen: ui.screen, pending: !!ui.pending });
3877
+ React.useEffect(() => {
3878
+ const previous = classicNavigation.current;
3879
+ classicNavigation.current = { screen: ui.screen, pending: !!ui.pending };
3880
+ if (
3881
+ petMode ||
3882
+ !ready ||
3883
+ (previous.screen === ui.screen && previous.pending === !!ui.pending)
3884
+ )
3885
+ return;
3886
+ const frame = requestAnimationFrame(() => {
3887
+ if (ui.pending) box.current?.querySelector('[data-keep-editing]')?.focus();
3888
+ else if (!editing && !waterEditing) {
3889
+ const kind = focusReturn.current?.getAttribute?.('data-classic-timer');
3890
+ const target =
3891
+ !ui.screen && ['water', 'smoking'].includes(kind)
3892
+ ? box.current?.querySelector(`[data-classic-timer="${kind}"]`)
3893
+ : box.current?.querySelector(
3894
+ '.owc-classic-head .owc-title,.owc-classic-mini .owc-grip',
3895
+ );
3896
+ target?.focus();
3897
+ } else if (previous.pending)
3898
+ box.current?.querySelector('form input,form select,form button')?.focus();
3899
+ });
3900
+ return () => cancelAnimationFrame(frame);
3901
+ }, [petMode, ready, ui.screen, !!ui.pending]);
2748
3902
  const draftGuard = () =>
2749
3903
  h(
2750
3904
  'div',
@@ -2806,6 +3960,8 @@ window.__ModuleLoader__.load({
2806
3960
  );
2807
3961
  const petPageTitle = {
2808
3962
  home: 'petWaterCard',
3963
+ 'water-countdown': 'waterCountdown',
3964
+ smoking: 'smokingCountdown',
2809
3965
  income: 'petCountdownCard',
2810
3966
  earnings: 'petEarningsCard',
2811
3967
  water: 'petWaterSettings',
@@ -2832,9 +3988,22 @@ window.__ModuleLoader__.load({
2832
3988
  ),
2833
3989
  h(
2834
3990
  'strong',
2835
- { 'data-pet-page-title': true, tabIndex: -1, style: { flex: 1, fontSize: 12 } },
3991
+ { 'data-pet-page-title': true, tabIndex: -1, style: { flex: 1 } },
2836
3992
  t(petPageTitle[ui.screen] || 'waterTitle'),
2837
3993
  ),
3994
+ ['home', 'water-countdown'].includes(ui.screen) &&
3995
+ h(
3996
+ 'button',
3997
+ {
3998
+ type: 'button',
3999
+ className: 'owc-icon',
4000
+ 'aria-label': t('petWaterSettings'),
4001
+ title: t('petWaterSettings'),
4002
+ disabled: busy || waterBusy || !!ui.pending,
4003
+ onClick: openWaterEditor,
4004
+ },
4005
+ iconSvg('sliders', 14),
4006
+ ),
2838
4007
  h(
2839
4008
  'button',
2840
4009
  {
@@ -2850,102 +4019,91 @@ window.__ModuleLoader__.load({
2850
4019
  const petHome = () =>
2851
4020
  h(
2852
4021
  'div',
2853
- { className: 'owc-pet-home', 'data-pet-home': true },
2854
-
4022
+ {
4023
+ className: 'owc-pet-home' + (waterPrompt ? ' owc-water-due' : ''),
4024
+ 'data-pet-home': true,
4025
+ 'data-water-event': waterPrompt ? waterState.eventId : undefined,
4026
+ },
4027
+ waterHero(),
2855
4028
  h(
2856
4029
  'div',
2857
- {
2858
- className: 'owc-pet-water-card' + (waterPrompt ? ' owc-water-due' : ''),
2859
- 'data-water-event': waterPrompt ? waterState.eventId : undefined,
2860
- },
4030
+ { className: 'owc-detail-primary-actions' },
2861
4031
  h(
2862
- 'p',
2863
- { className: 'owc-water-status', 'data-water-status': true },
2864
- waterPrompt
2865
- ? t('waterDue')
2866
- : waterState.phase === 'quiet'
2867
- ? t('petQuietActive')
2868
- : waterSummary(),
4032
+ 'button',
4033
+ {
4034
+ type: 'button',
4035
+ className: 'owc-primary owc-water-record',
4036
+ disabled: !waterCanRecord || waterBusy,
4037
+ onClick: recordWater,
4038
+ },
4039
+ t('petRecord'),
2869
4040
  ),
2870
- h(
2871
- 'div',
2872
- { className: 'owc-pet-water-row' },
4041
+ waterPrompt &&
2873
4042
  h(
2874
4043
  'button',
2875
4044
  {
2876
4045
  type: 'button',
2877
- className: 'owc-primary',
2878
- disabled: !waterCanRecord || waterBusy,
2879
- onClick: recordWater,
4046
+ className: 'owc-text',
4047
+ disabled: waterBusy,
4048
+ onClick: () => waterAction({ type: 'snooze', eventId: waterState.eventId }),
2880
4049
  },
2881
- t('petRecord'),
4050
+ t('waterSnooze'),
2882
4051
  ),
2883
- h(
2884
- 'span',
2885
- { 'data-water-count': true },
2886
- t('waterCount') + ': ' + waterState.count,
2887
- ),
2888
- ),
2889
4052
  ),
2890
4053
  h(
2891
4054
  'div',
2892
- { className: 'owc-pet-quiet-row' },
2893
- [30, 60].map((minutes) =>
4055
+ { className: 'owc-detail-secondary' },
4056
+ h(
4057
+ 'button',
4058
+ {
4059
+ type: 'button',
4060
+ className: 'owc-text',
4061
+ disabled: !water.enabled || busy || waterBusy || waterError || !waterReady,
4062
+ onClick: () => waterAction({ type: water.paused ? 'resume' : 'pause' }),
4063
+ },
4064
+ t(water.paused ? 'waterResume' : 'waterPause'),
4065
+ ),
4066
+ waterState.phase === 'quiet' &&
2894
4067
  h(
2895
4068
  'button',
2896
4069
  {
2897
4070
  type: 'button',
2898
- key: minutes,
2899
- disabled: !waterCanQuiet || waterBusy,
2900
- onClick: () => waterAction({ type: 'quiet', minutes }),
4071
+ className: 'owc-text',
4072
+ disabled: waterBusy,
4073
+ onClick: () => waterAction({ type: 'end-quiet' }),
2901
4074
  },
2902
- t(minutes === 30 ? 'petQuiet30' : 'petQuiet60'),
4075
+ t('waterQuietEnd'),
2903
4076
  ),
2904
- ),
2905
4077
  ),
2906
4078
  h(
2907
- 'div',
2908
- { className: 'owc-pet-quiet-status' },
2909
- waterPrompt
2910
- ? h(
4079
+ 'details',
4080
+ { className: 'owc-detail-more' },
4081
+ h('summary', null, t('waterQuiet')),
4082
+ h(
4083
+ 'div',
4084
+ { className: 'owc-detail-options' },
4085
+ [30, 60].map((minutes) =>
4086
+ h(
2911
4087
  'button',
2912
4088
  {
2913
4089
  type: 'button',
2914
- disabled: waterBusy,
2915
- onClick: () => waterAction({ type: 'snooze', eventId: waterState.eventId }),
4090
+ key: minutes,
4091
+ disabled: !waterCanQuiet || waterBusy,
4092
+ onClick: (event) => {
4093
+ const menu = event.currentTarget.closest('details');
4094
+ menu.open = false;
4095
+ menu.querySelector('summary')?.focus();
4096
+ void waterAction({ type: 'quiet', minutes });
4097
+ },
2916
4098
  },
2917
- t('waterSnooze'),
2918
- )
2919
- : water.shift?.paused
2920
- ? t('petPausedHint')
2921
- : waterState.phase === 'quiet'
2922
- ? h(
2923
- React.Fragment,
2924
- null,
2925
- t('waterQuiet') +
2926
- ' · ' +
2927
- formatDuration(waterState.quietRemaining / 1000),
2928
- h(
2929
- 'button',
2930
- {
2931
- type: 'button',
2932
- disabled: waterBusy,
2933
- onClick: () => waterAction({ type: 'end-quiet' }),
2934
- },
2935
- t('waterQuietEnd'),
2936
- ),
2937
- )
2938
- : t('petQuietHint'),
2939
- ),
2940
- h(
2941
- 'div',
2942
- { className: 'owc-pet-feedback' },
2943
- waterNotice
2944
- ? t(waterNotice.key)
2945
- : waterError
2946
- ? t('waterUnavailable')
2947
- : t('petHomeHint'),
4099
+ t(minutes === 30 ? 'petQuiet30' : 'petQuiet60'),
4100
+ ),
4101
+ ),
4102
+ ),
4103
+ h('p', { className: 'owc-hint' }, t('petQuietHint')),
2948
4104
  ),
4105
+ waterError &&
4106
+ h('p', { className: 'owc-error', role: 'alert' }, t('waterUnavailable')),
2949
4107
  waterError &&
2950
4108
  h(
2951
4109
  'button',
@@ -2956,26 +4114,6 @@ window.__ModuleLoader__.load({
2956
4114
  },
2957
4115
  t('waterRetry'),
2958
4116
  ),
2959
- h(
2960
- 'div',
2961
- { className: 'owc-pet-home-links' },
2962
- h('button', { type: 'button', onClick: openWaterEditor }, t('petWaterSettings')),
2963
- h(
2964
- 'button',
2965
- {
2966
- type: 'button',
2967
- disabled:
2968
- !water.enabled ||
2969
- busy ||
2970
- waterBusy ||
2971
- waterError ||
2972
- !waterContext ||
2973
- water.shift?.id !== waterContext.id,
2974
- onClick: () => waterAction({ type: water.shift?.paused ? 'resume' : 'pause' }),
2975
- },
2976
- t(water.shift?.paused ? 'waterResume' : 'waterPause'),
2977
- ),
2978
- ),
2979
4117
  );
2980
4118
  const contextMenu = () =>
2981
4119
  ui.menu &&
@@ -3030,6 +4168,8 @@ window.__ModuleLoader__.load({
3030
4168
  [
3031
4169
  ['income', 'petCountdownCard'],
3032
4170
  ['home', 'petWaterCard'],
4171
+ ['water-countdown', 'waterCountdown'],
4172
+ ['smoking', 'smokingCountdown'],
3033
4173
  ['earnings', 'petEarningsCard'],
3034
4174
  ['appearance', 'petAppearance'],
3035
4175
  ['schedule', 'petWorkSettings'],
@@ -3040,14 +4180,34 @@ window.__ModuleLoader__.load({
3040
4180
  key: screen,
3041
4181
  type: 'button',
3042
4182
  role: 'menuitem',
4183
+ 'data-pet-menu-screen': screen,
3043
4184
  tabIndex: -1,
3044
- disabled: busy || waterBusy,
4185
+ disabled:
4186
+ requestBusy.current ||
4187
+ waterBusy ||
4188
+ (!LOCAL_SCREENS.includes(screen) &&
4189
+ (hostBusy || !data || (!data.configured && screen !== 'schedule'))),
3045
4190
  onClick: (event) => navigateScreen(screen, event),
3046
4191
  },
3047
4192
  t(label),
3048
4193
  ),
3049
4194
  ),
3050
4195
 
4196
+ (!data || error || state?.error) &&
4197
+ h(
4198
+ 'button',
4199
+ {
4200
+ type: 'button',
4201
+ role: 'menuitem',
4202
+ tabIndex: -1,
4203
+ disabled: hostBusy || !sessionId,
4204
+ onClick: () => {
4205
+ closePetMenu();
4206
+ void reload(true);
4207
+ },
4208
+ },
4209
+ t('retry'),
4210
+ ),
3051
4211
  h('hr', null),
3052
4212
  h(
3053
4213
  'button',
@@ -3065,15 +4225,23 @@ window.__ModuleLoader__.load({
3065
4225
  ),
3066
4226
  );
3067
4227
 
4228
+ if (!ready) return null;
3068
4229
  if (petMode)
3069
4230
  return h(
3070
4231
  'section',
3071
4232
  {
3072
4233
  ref: box,
3073
4234
  className: 'owc-v2 owc-pet-shell',
3074
- 'data-version': '1.1.0',
4235
+ 'data-version': '1.1.2',
3075
4236
  'data-pet-model': stageModel,
3076
- 'data-status': safeState.status,
4237
+ 'data-status':
4238
+ error || state?.error
4239
+ ? 'error'
4240
+ : data?.configured && safeState
4241
+ ? safeState.status
4242
+ : data
4243
+ ? 'setup'
4244
+ : 'loading',
3077
4245
  'aria-label': t('companionLabel'),
3078
4246
  style: {
3079
4247
  '--owc-alpha': String(view.opacity),
@@ -3099,8 +4267,7 @@ window.__ModuleLoader__.load({
3099
4267
  else if (ui.pending) dispatchUI({ type: 'stay' });
3100
4268
  else if (editing || waterEditing) navigateScreen(ui.back);
3101
4269
  else if (petShowing) closePetPanel();
3102
- else if (waterPrompt)
3103
- void waterAction({ type: 'snooze', eventId: waterState.eventId });
4270
+ else if (autoWaterPrompt) dismissWaterPopup();
3104
4271
  },
3105
4272
  },
3106
4273
  h('style', null, STYLE),
@@ -3113,10 +4280,13 @@ window.__ModuleLoader__.load({
3113
4280
  {
3114
4281
  type: 'button',
3115
4282
  className: 'owc-pet-handle',
3116
- 'aria-label': t('petOpen'),
3117
- 'aria-expanded': !!petShowing,
4283
+ 'aria-label': t('petGreet'),
3118
4284
  title: t('companionName') + ' · ' + t('petHint'),
3119
- onClick: togglePetPanel,
4285
+ onClick: interactWithPet,
4286
+ onPointerEnter: (event) => {
4287
+ if (event.pointerType === 'mouse' && !dragState.current)
4288
+ petMotion.current?.play('look');
4289
+ },
3120
4290
  onKeyDown: gripKeys,
3121
4291
  },
3122
4292
  petArtwork(
@@ -3126,23 +4296,74 @@ window.__ModuleLoader__.load({
3126
4296
  waterNotice?.token,
3127
4297
  stageModel,
3128
4298
  PET_SPRITES,
4299
+ true,
3129
4300
  ),
3130
4301
  ),
3131
4302
  h(
3132
- 'button',
4303
+ 'div',
3133
4304
  {
3134
- type: 'button',
3135
- className: 'owc-pet-clock',
3136
- onClick: togglePetPanel,
3137
- 'aria-label': t('petClock'),
4305
+ className: 'owc-pet-timers',
4306
+ role: 'group',
4307
+ 'aria-label': t('petCountdownGroup'),
3138
4308
  },
3139
- iconSvg('alarm', 12),
3140
- h('span', { className: 'owc-pet-clock-label' }, t(safeState.status)),
3141
- ['done', 'rest'].includes(safeState.status) ? null : h('strong', null, duration),
4309
+ h(
4310
+ 'div',
4311
+ {
4312
+ className: 'owc-pet-clock',
4313
+ role: data?.configured && safeState ? 'timer' : 'status',
4314
+ 'aria-label': t('petClock'),
4315
+ title:
4316
+ error || state?.error
4317
+ ? t('petRetryHint')
4318
+ : !sessionId
4319
+ ? t('noSession')
4320
+ : undefined,
4321
+ },
4322
+ iconSvg('alarm', 12),
4323
+ h(
4324
+ 'span',
4325
+ { className: 'owc-pet-clock-label', title: t(petClockStatus) },
4326
+ t(petClockStatus),
4327
+ ),
4328
+ data?.configured &&
4329
+ safeState &&
4330
+ !error &&
4331
+ sessionId &&
4332
+ !['done', 'rest'].includes(safeState.status)
4333
+ ? h('strong', null, duration)
4334
+ : null,
4335
+ ),
4336
+ h(
4337
+ 'div',
4338
+ { className: 'owc-pet-timer-pair' },
4339
+ timerRow(
4340
+ 'water',
4341
+ waterCountdownText(),
4342
+ waterState.phase === 'due',
4343
+ t('waterCountdown') + ' · ' + waterSummary(),
4344
+ ),
4345
+ timerRow(
4346
+ 'smoking',
4347
+ smokingCountdownText(),
4348
+ smokingState.phase === 'due',
4349
+ t('smokingCountdown') +
4350
+ ' · ' +
4351
+ (smokingUnavailable
4352
+ ? t('timerUnavailable')
4353
+ : t(
4354
+ {
4355
+ idle: 'timerIdle',
4356
+ running: 'timerRunning',
4357
+ paused: 'timerPaused',
4358
+ due: 'timerDue',
4359
+ }[smokingState.phase],
4360
+ )),
4361
+ ),
4362
+ ),
3142
4363
  ),
3143
4364
  ),
3144
4365
  waterPrompt &&
3145
- petShowing &&
4366
+ !autoWaterPrompt &&
3146
4367
  ui.screen !== 'home' &&
3147
4368
  h(
3148
4369
  'button',
@@ -3151,7 +4372,9 @@ window.__ModuleLoader__.load({
3151
4372
  className: 'owc-pet-water-alert owc-water-due',
3152
4373
  'data-water-event': waterState.eventId,
3153
4374
  'aria-label': t('petWaterAlert'),
3154
- onClick: (event) => navigateScreen('home', event),
4375
+ title: t('petWaterAlert'),
4376
+ disabled: busy || waterBusy || !!ui.pending,
4377
+ onClick: openWaterPopup,
3155
4378
  style: {
3156
4379
  left: waterAlertLayout.left,
3157
4380
  top: waterAlertLayout.top,
@@ -3160,7 +4383,8 @@ window.__ModuleLoader__.load({
3160
4383
  },
3161
4384
  },
3162
4385
  iconSvg('water', 13),
3163
- t('petWaterBadge'),
4386
+ h('span', { className: 'owc-pet-water-label' }, t('petWaterBadge')),
4387
+ iconSvg('chevronUp', 10),
3164
4388
  ),
3165
4389
  contextMenu(),
3166
4390
  !petShowing &&
@@ -3185,8 +4409,8 @@ window.__ModuleLoader__.load({
3185
4409
  'div',
3186
4410
  {
3187
4411
  ref: petPanel,
3188
- className: 'owc-pet-panel owc-pet-workspace',
3189
- 'data-pet-panel': petShowing ? 'open' : 'bubble',
4412
+ className: 'owc-pet-panel owc-pet-workspace owc-detail-page',
4413
+ 'data-pet-panel': petShowing ? 'open' : 'reminder',
3190
4414
  'data-pet-screen': ui.screen || 'reminder',
3191
4415
  'data-side': floatingPanel.side,
3192
4416
  role: 'region',
@@ -3211,10 +4435,10 @@ window.__ModuleLoader__.load({
3211
4435
  {
3212
4436
  type: 'button',
3213
4437
  className: 'owc-icon',
3214
- 'aria-label': t('waterSnooze'),
4438
+ 'aria-label': t('petDismissReminder'),
4439
+ title: t('petDismissReminder'),
3215
4440
  disabled: waterBusy,
3216
- onClick: () =>
3217
- waterAction({ type: 'snooze', eventId: waterState.eventId }),
4441
+ onClick: dismissWaterPopup,
3218
4442
  },
3219
4443
  iconSvg('close', 14),
3220
4444
  ),
@@ -3237,9 +4461,13 @@ window.__ModuleLoader__.load({
3237
4461
  ? editor()
3238
4462
  : petDetails
3239
4463
  ? panelBody('clock')
3240
- : ui.screen === 'earnings'
3241
- ? panelBody('earnings')
3242
- : petHome(),
4464
+ : ui.screen === 'water-countdown'
4465
+ ? waterCountdownCard()
4466
+ : ui.screen === 'smoking'
4467
+ ? smokingCountdownCard()
4468
+ : ui.screen === 'earnings'
4469
+ ? panelBody('earnings')
4470
+ : petHome(),
3243
4471
  notices(),
3244
4472
  )
3245
4473
  : waterReminder(),
@@ -3253,13 +4481,14 @@ window.__ModuleLoader__.load({
3253
4481
  {
3254
4482
  ref: box,
3255
4483
  className:
3256
- 'owc-v2' +
4484
+ 'owc-v2 owc-classic' +
4485
+ (!collapsed ? ' owc-detail-page' : '') +
3257
4486
  (collapsed ? ' owc-compact' : '') +
3258
4487
  (collapsed && waterQuickOpen ? ' owc-quick' : '') +
3259
4488
  (editing || waterEditing ? ' owc-editing' : ''),
3260
4489
  'aria-label': t('title'),
3261
- 'data-version': '1.1.0',
3262
- 'data-status': safeState?.status || 'setup',
4490
+ 'data-version': '1.1.2',
4491
+ 'data-status': classicClock.phase,
3263
4492
  style: cardStyle,
3264
4493
  onPointerDown: dragDown,
3265
4494
  onPointerMove: dragMove,
@@ -3278,8 +4507,8 @@ window.__ModuleLoader__.load({
3278
4507
  return;
3279
4508
  event.preventDefault();
3280
4509
  if (ui.pending) dispatchUI({ type: 'stay' });
3281
- else if (editing || waterEditing) navigateScreen(ui.back);
3282
- else if (waterQuickOpen) closeWaterQuick();
4510
+ else if (ui.screen)
4511
+ navigateScreen(PET_EDITOR_SCREENS.includes(ui.screen) ? ui.back : null);
3283
4512
  else setPreference({ mini: true });
3284
4513
  },
3285
4514
  },
@@ -3301,33 +4530,28 @@ window.__ModuleLoader__.load({
3301
4530
  waterPrompt ? t('waterDue') : waterNotice ? t(waterNotice.key) : '',
3302
4531
  ),
3303
4532
  ),
3304
- collapsed
3305
- ? h(
3306
- React.Fragment,
3307
- null,
3308
- strip(),
3309
- waterQuickOpen && waterOverview(true),
3310
- !waterQuickOpen &&
3311
- waterNotice &&
3312
- (!view.companion || companion.announce) &&
3313
- h('p', { className: 'owc-mini-feedback' }, t(waterNotice.key)),
3314
- )
3315
- : h(
3316
- React.Fragment,
3317
- null,
3318
- panelHead(),
3319
- !editing && !waterEditing && companionCard(),
3320
- ui.pending
3321
- ? draftGuard()
3322
- : waterEditing
3323
- ? waterSettings()
3324
- : editing
3325
- ? editor()
3326
- : panelBody(),
3327
- notices(),
3328
- !editing && !waterEditing && waterOverview(),
3329
- ),
3330
- !collapsed && waterReminder(),
4533
+ !collapsed && classicHeader(),
4534
+ ui.pending
4535
+ ? draftGuard()
4536
+ : waterEditing
4537
+ ? waterSettings()
4538
+ : editing
4539
+ ? editor()
4540
+ : ui.screen === 'water-countdown'
4541
+ ? waterCountdownCard()
4542
+ : ui.screen === 'smoking'
4543
+ ? smokingCountdownCard()
4544
+ : ui.screen === 'home'
4545
+ ? petHome()
4546
+ : ui.screen === 'earnings'
4547
+ ? panelBody('earnings')
4548
+ : ui.screen === 'income'
4549
+ ? panelBody('clock')
4550
+ : collapsed
4551
+ ? classicMini()
4552
+ : classicOverview(),
4553
+ notices(ui.screen !== null),
4554
+ ui.screen === null && waterReminder(),
3331
4555
  ),
3332
4556
  );
3333
4557
  }