dsh-offwork 1.1.1 → 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.
package/client.source.js CHANGED
@@ -13,7 +13,42 @@ window.__ModuleLoader__.load({
13
13
  ...PET_MESSAGES.zh,
14
14
  ...PET_INTERACTION_MESSAGES.zh,
15
15
  ...PET_SURFACE_MESSAGES.zh,
16
+ detailHelp: '使用说明',
17
+ detailApply: '应用',
18
+ detailShift: '班次详情',
19
+ detailWaterRule: '全天按自然时间提醒,需保持页面打开。',
20
+ detailSavedInterval: '已生效间隔',
21
+ detailDraftHint: '保存后生效',
22
+ detailNextInterval: '用于下一轮计时',
16
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: '下班时间',
17
52
  working: '距离下班',
18
53
  before: '距离开工',
19
54
  lunch: '午休中',
@@ -74,7 +109,7 @@ window.__ModuleLoader__.load({
74
109
  opacityHint: '只影响背景,不影响文字。',
75
110
  waterTitle: '喝水提醒',
76
111
  waterEnable: '开启喝水提醒',
77
- waterInterval: '自定义间隔(分钟)',
112
+ waterInterval: '提醒间隔(分钟)',
78
113
  waterSave: '保存提醒设置',
79
114
  waterEdit: '设置喝水提醒',
80
115
  waterBack: '返回',
@@ -84,21 +119,21 @@ window.__ModuleLoader__.load({
84
119
  waterEffective: '当前间隔',
85
120
  waterManage: '数据管理',
86
121
  waterSaveHint: '修改后点击保存,按新间隔重新计时。',
87
- waterBrief: '按工作时间计时,午休暂停。关闭页面后不提醒。',
122
+ waterBrief: '按自然时间计时,午休、下班和休息日均可提醒。需要保持页面打开。',
88
123
  waterApply: '应用间隔',
89
124
  waterInvalid: '请输入 5–240 的整数。',
90
125
  waterNext: '下次喝水',
91
126
  waterDue: '该喝水啦',
92
127
  waterDrink: '喝了',
93
- waterSnooze: '工作 10 分钟后',
94
- waterPause: '本班暂停',
95
- waterResume: '恢复本班提醒',
96
- waterCount: '本班打卡',
128
+ waterSnooze: '10 分钟后',
129
+ waterPause: '暂停提醒',
130
+ waterResume: '恢复提醒',
131
+ waterCount: '饮水计数',
97
132
  waterManual: '喝了一杯',
98
133
  waterQuick: '喝水快捷操作',
99
134
  waterQuickClose: '收起喝水快捷操作',
100
135
  waterRecorded: '这杯记下啦。',
101
- waterRecordHint: '工作和午休期间可以打卡,关闭提醒也能记录。',
136
+ waterRecordHint: '随时可以打卡,关闭提醒也能记录;计数可手动清零。',
102
137
  waterActionStale: '状态已变化,请再试一次。',
103
138
  waterQuiet: '免打扰',
104
139
  waterQuietAdjust: '调整时长',
@@ -109,32 +144,68 @@ window.__ModuleLoader__.load({
109
144
  waterQuietEnded: '免打扰已结束。',
110
145
  waterQuietHint: '按自然时间,到期自动恢复;已到期的提醒只提示一次。',
111
146
  waterOff: '提醒已关闭',
112
- waterWaiting: '等待工作时段',
113
- waterBreak: '休息中,提醒计时暂停',
114
- waterPaused: '本班已暂停',
115
- waterFinished: '本班不再安排新提醒',
147
+ waterWaiting: '等待开启提醒',
148
+ waterBreak: '提醒已暂停',
149
+ waterPaused: '提醒已暂停',
150
+ waterFinished: '提醒已结束',
116
151
  waterOther: '提醒已到期,等待可见标签页处理',
117
- waterClear: '清除本班次数',
152
+ waterClear: '清除饮水计数',
118
153
  waterReset: '重置喝水提醒',
119
154
  waterConfirm: '确认操作',
120
- waterClearHint: '仅清零本班次数,保留下一次提醒和暂停状态。',
121
- waterResetHint: '清除喝水提醒设置和记录,恢复默认关闭。',
155
+ waterClearHint: '仅清零饮水计数,保留下一次提醒和暂停状态。',
156
+ waterResetHint: '清除当前喝水提醒设置和计数,恢复默认关闭。',
122
157
  waterLocal: '提醒设置和打卡仅保存在当前浏览器,不进入会话。与薪资配置分别保存。',
123
- waterRules: '按工作分钟计时,午休冻结。开启、修改间隔或作息后重新计时;关闭页面后不提醒。',
158
+ waterRules:
159
+ '按自然分钟计时,与作息、薪资和会话无关。开启、修改间隔或手动恢复后重新计时;关闭页面后不提醒。',
124
160
  waterUnavailable:
125
- '喝水提醒存储不可用或数据无效,自动提醒已停止。可重试;重置将清除原喝水数据。',
161
+ '喝水提醒存储不可用或数据无效,自动提醒已停止。可重试;重置将清除当前提醒设置和计数。',
126
162
  waterRetry: '重试读取',
127
- waterSetup: '请先保存薪资与作息。',
163
+ waterSetup: '开启后按所选间隔提醒。',
128
164
  waterLoading: '正在读取喝水提醒…',
129
- waterChanged: '提醒设置已在本浏览器生效',
130
- waterStale: '作息已变化,请点击“读取最新配置”后继续提醒。',
165
+ waterChanged: '提醒设置已保存',
166
+ waterStale: '记录已更新,请重试读取。',
131
167
  },
132
168
  en: {
133
169
  ...COMPANION_MESSAGES.en,
134
170
  ...PET_MESSAGES.en,
135
171
  ...PET_INTERACTION_MESSAGES.en,
136
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',
137
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',
138
209
  working: 'Until clock-out',
139
210
  before: 'Until work starts',
140
211
  lunch: 'On break',
@@ -196,7 +267,7 @@ window.__ModuleLoader__.load({
196
267
  opacityHint: 'Affects the background only.',
197
268
  waterTitle: 'Water reminder',
198
269
  waterEnable: 'Enable water reminders',
199
- waterInterval: 'Custom interval (minutes)',
270
+ waterInterval: 'Interval (minutes)',
200
271
  waterSave: 'Save reminder settings',
201
272
  waterEdit: 'Reminder settings',
202
273
  waterBack: 'Back',
@@ -206,21 +277,23 @@ window.__ModuleLoader__.load({
206
277
  waterEffective: 'Current interval',
207
278
  waterManage: 'Manage data',
208
279
  waterSaveHint: 'Save changes to restart with the new interval.',
209
- 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.',
210
282
  waterApply: 'Apply interval',
211
283
  waterInvalid: 'Enter a whole number from 5 to 240.',
212
284
  waterNext: 'Next water break',
213
285
  waterDue: 'Time for some water',
214
286
  waterDrink: 'Drank water',
215
- waterSnooze: 'In 10 working minutes',
216
- waterPause: 'Pause this shift',
217
- waterResume: 'Resume this shift',
218
- waterCount: 'Drinks this shift',
287
+ waterSnooze: 'In 10 minutes',
288
+ waterPause: 'Pause reminders',
289
+ waterResume: 'Resume reminders',
290
+ waterCount: 'Drinks recorded',
219
291
  waterManual: 'Had a glass',
220
292
  waterQuick: 'Quick water actions',
221
293
  waterQuickClose: 'Close quick water actions',
222
294
  waterRecorded: 'Got it, your drink is recorded.',
223
- 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.',
224
297
  waterActionStale: 'The state changed. Please try again.',
225
298
  waterQuiet: 'Quiet time',
226
299
  waterQuietAdjust: 'Change duration',
@@ -232,27 +305,27 @@ window.__ModuleLoader__.load({
232
305
  waterQuietHint:
233
306
  'Uses clock time. Resumes automatically, with just one reminder if overdue.',
234
307
  waterOff: 'Reminders off',
235
- waterWaiting: 'Waiting for working hours',
236
- waterBreak: 'On break; reminder timer paused',
237
- waterPaused: 'Paused for this shift',
238
- 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',
239
312
  waterOther: 'Reminder due; waiting for a visible tab',
240
- waterClear: 'Clear shift count',
313
+ waterClear: 'Clear drink count',
241
314
  waterReset: 'Reset water reminders',
242
315
  waterConfirm: 'Confirm',
243
316
  waterClearHint: 'Clear the drink count only. Keep the next reminder and pause state.',
244
- waterResetHint: 'Delete water preferences and records, then turn reminders off.',
317
+ waterResetHint: 'Clear current water preferences and count, then turn reminders off.',
245
318
  waterLocal:
246
319
  'Reminder preferences and check-ins stay in this browser, outside conversations. Saved separately from salary settings.',
247
320
  waterRules:
248
- '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.',
249
322
  waterUnavailable:
250
- '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.',
251
324
  waterRetry: 'Retry loading',
252
- waterSetup: 'Save your salary and schedule first.',
325
+ waterSetup: 'Enable reminders to start the selected interval.',
253
326
  waterLoading: 'Loading water reminders…',
254
- waterChanged: 'Reminder preferences saved in this browser',
255
- waterStale: 'Schedule changed. Reload the latest settings to continue reminders.',
327
+ waterChanged: 'Reminder settings saved',
328
+ waterStale: 'Records changed. Retry loading to continue.',
256
329
  },
257
330
  };
258
331
  const STYLE =
@@ -384,6 +457,15 @@ window.__ModuleLoader__.load({
384
457
  water: [
385
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' }],
386
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
+ ],
387
469
  eye: [
388
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' }],
389
471
  ['circle', { cx: 8, cy: 8, r: 2.05 }],
@@ -410,6 +492,7 @@ window.__ModuleLoader__.load({
410
492
  ['path', { d: 'M13.1 3v3.2H9.9' }],
411
493
  ],
412
494
  };
495
+ const LOCAL_SCREENS = [null, 'appearance', 'smoking', 'water-countdown', 'water', 'home'];
413
496
  const iconSvg = (name, size = 15) =>
414
497
  h(
415
498
  'svg',
@@ -457,12 +540,20 @@ window.__ModuleLoader__.load({
457
540
  const waterEditing = ui.screen === 'water';
458
541
  const [appearanceDraft, setAppearanceDraft] = React.useState(null);
459
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
+ );
460
549
  const appearanceWasPet = React.useRef(false);
461
550
  const salaryBase = React.useRef(null);
462
551
  const menuBox = React.useRef(null);
463
552
  const menuTrigger = React.useRef(null);
464
553
  const [dragPosition, setDragPosition] = React.useState(null);
465
- 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));
466
557
  const [error, setError] = React.useState('');
467
558
  const [notice, setNotice] = React.useState('');
468
559
  const [storageError, setStorageError] = React.useState(false);
@@ -488,8 +579,117 @@ window.__ModuleLoader__.load({
488
579
  const dragState = React.useRef(null);
489
580
  const lastPosition = React.useRef(null);
490
581
  const generation = React.useRef(0);
491
- const requestBusy = React.useRef(false);
492
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 });
493
693
  const [water, setWater] = React.useState(defaultHydration);
494
694
  const [waterReady, setWaterReady] = React.useState(false);
495
695
  const [waterError, setWaterError] = React.useState(false);
@@ -507,6 +707,7 @@ window.__ModuleLoader__.load({
507
707
  const [waterInvalid, setWaterInvalid] = React.useState(false);
508
708
  const [waterConfirm, setWaterConfirm] = React.useState(null);
509
709
  const [waterNotice, setWaterNotice] = React.useState(false);
710
+ const [reminderGate, setReminderGate] = React.useState(initialPetReminder);
510
711
  const reactionState = React.useRef(null);
511
712
  const petMotion = React.useRef(null);
512
713
  const petReactionToken = React.useRef(null);
@@ -516,22 +717,23 @@ window.__ModuleLoader__.load({
516
717
  waterOwner.current =
517
718
  window.crypto?.randomUUID?.() || String(Math.random()) + Date.now();
518
719
  const waterInputs = React.useRef(null);
519
- const waterScheduleChange = React.useRef(null);
520
- waterInputs.current = {
521
- settings: data?.configured && !error ? data.settings : null,
522
- editing: editing || waterEditing,
523
- };
720
+ waterInputs.current = { editing: editing || waterEditing };
524
721
 
525
722
  const settings = data?.settings;
526
723
  const workingDraft = draft || settings;
527
724
  const state = settings ? safeCalculate(settings) : null;
528
725
  const preview = workingDraft ? safeCalculate(workingDraft) : null;
529
726
  const safeState = state && !state.error ? state : null;
530
- const petMode =
531
- ready &&
532
- data?.configured &&
533
- !!safeState &&
534
- (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';
535
737
  const stageModel = appearanceOnly ? savedView.petModel : view.petModel;
536
738
  const petGeometry = petLayout(
537
739
  dragPosition ?? view.petPosition,
@@ -539,6 +741,8 @@ window.__ModuleLoader__.load({
539
741
  appearanceOnly ? savedView.petSize : view.petSize,
540
742
  420,
541
743
  stageModel,
744
+ 72,
745
+ 196,
542
746
  );
543
747
  const surfaceScreen = ui.screen || 'reminder';
544
748
  const surfaceWidth = petSurfaceWidth(surfaceScreen, viewport);
@@ -582,7 +786,7 @@ window.__ModuleLoader__.load({
582
786
  );
583
787
  const waterAlertLayout = placePetSurface(anchor, viewport, {
584
788
  width: Math.min(140, Math.max(110, alertRoom)),
585
- height: 34,
789
+ height: 44,
586
790
  kind: 'menu',
587
791
  obstacles: [...(petOpen ? [floatingPanel] : []), ...(ui.menu ? [menuLayout] : [])],
588
792
  });
@@ -591,14 +795,15 @@ window.__ModuleLoader__.load({
591
795
  height: toastHeight,
592
796
  kind: 'menu',
593
797
  });
594
- const collapsed =
595
- !petMode &&
596
- !!safeState &&
597
- !editing &&
598
- !waterEditing &&
599
- view.mini &&
600
- !error &&
601
- !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
+ });
602
807
  const seconds = safeState
603
808
  ? safeState.status === 'before'
604
809
  ? safeState.startRemaining
@@ -626,6 +831,9 @@ window.__ModuleLoader__.load({
626
831
  classicSize,
627
832
  );
628
833
  const cardStyle = {
834
+ width: ui.screen
835
+ ? petSurfaceWidth(ui.screen, viewport)
836
+ : Math.min(collapsed ? 224 : 320, Math.max(1, viewport.width - 24)),
629
837
  '--owc-alpha': String(view.opacity),
630
838
  '--owc-viewport-height': Math.max(1, viewport.height - 24) + 'px',
631
839
  left: classicPosition.x,
@@ -708,17 +916,8 @@ window.__ModuleLoader__.load({
708
916
  store = createHydrationStore(null);
709
917
  }
710
918
  try {
711
- channel = new BroadcastChannel('dsh.offwork-clock.hydration.v1');
919
+ channel = new BroadcastChannel('dsh.offwork-clock.hydration.wall-clock.v3');
712
920
  } catch {}
713
- const contextAt = (time) => {
714
- const current = waterInputs.current.settings;
715
- if (!current) return null;
716
- try {
717
- return hydrationContext(current, calculate(current, time));
718
- } catch {
719
- return null;
720
- }
721
- };
722
921
  const update = async (action = { type: 'tick' }, automatic = false) => {
723
922
  if (!active || (automatic && (running || failed))) return;
724
923
  running++;
@@ -734,59 +933,31 @@ window.__ModuleLoader__.load({
734
933
  let changed = false;
735
934
  let recorded = false;
736
935
  let quietChanged = false;
737
- let usedPermit = null;
738
936
  const result = await store.update(
739
937
  (current) => {
740
938
  const time = Date.now();
741
- const context = contextAt(time);
742
939
  if (action.type === 'sync') return current;
743
- const permit = waterScheduleChange.current;
744
- const operation =
745
- permit && context && permit.schedule === context.schedule
746
- ? { ...action, baseSchedule: permit.baseSchedule }
747
- : action;
748
- let next = transitionHydration(
749
- current,
750
- context,
751
- operation,
752
- time,
753
- waterOwner.current,
754
- );
940
+ let next = transitionHydration(current, action, time, waterOwner.current);
755
941
  recorded =
756
- ['drink', 'manual-drink'].includes(action.type) &&
757
- next.shift?.count >
758
- (current.shift?.id === next.shift?.id ? (current.shift?.count ?? 0) : 0);
942
+ ['drink', 'manual-drink'].includes(action.type) && next.count > current.count;
759
943
  quietChanged =
760
944
  ['quiet', 'end-quiet'].includes(action.type) &&
761
- next.shift?.quietUntil !== current.shift?.quietUntil;
945
+ next.quietUntil !== current.quietUntil;
762
946
  if (automatic) {
763
947
  const allow = !document.hidden && !waterInputs.current.editing;
764
948
  next = transitionHydration(
765
949
  next,
766
- context,
767
950
  { type: allow ? 'claim' : 'release' },
768
951
  time,
769
952
  waterOwner.current,
770
953
  );
771
954
  }
772
- if (
773
- permit &&
774
- context &&
775
- permit.schedule === context.schedule &&
776
- next.shift?.schedule === context.schedule
777
- )
778
- usedPermit = permit;
779
955
  changed = JSON.stringify(next) !== JSON.stringify(current);
780
956
  return next;
781
957
  },
782
958
  { reset: action.type === 'reset' },
783
959
  );
784
960
  if (!active) return;
785
- if (
786
- action.type === 'reset' ||
787
- (usedPermit && waterScheduleChange.current === usedPermit)
788
- )
789
- waterScheduleChange.current = null;
790
961
  failed = false;
791
962
  setWater(result);
792
963
  setWaterReady(true);
@@ -818,11 +989,14 @@ window.__ModuleLoader__.load({
818
989
  const visibility = () => {
819
990
  setWaterVisible(!document.hidden);
820
991
  setNow(Date.now());
821
- if (document.hidden) void update({ type: 'release' });
822
- else tick();
992
+ if (document.hidden) {
993
+ setReminderGate(initialPetReminder);
994
+ void update({ type: 'release' });
995
+ } else tick();
823
996
  };
824
997
  const pageHide = () => {
825
998
  setWaterVisible(false);
999
+ setReminderGate(initialPetReminder);
826
1000
  void update({ type: 'release' });
827
1001
  };
828
1002
  waterRuntime.current = { update, tick };
@@ -847,8 +1021,7 @@ window.__ModuleLoader__.load({
847
1021
  channel?.close();
848
1022
  void store
849
1023
  .update((current) => {
850
- if (current.shift?.claim?.owner === waterOwner.current)
851
- current.shift.claim = null;
1024
+ if (current.claim?.owner === waterOwner.current) current.claim = null;
852
1025
  return current;
853
1026
  })
854
1027
  .catch(() => {})
@@ -858,7 +1031,7 @@ window.__ModuleLoader__.load({
858
1031
 
859
1032
  React.useEffect(() => {
860
1033
  waterRuntime.current?.tick();
861
- }, [settings, editing, waterEditing, error]);
1034
+ }, [editing, waterEditing]);
862
1035
 
863
1036
  React.useEffect(() => {
864
1037
  if (!waterNotice) return;
@@ -904,6 +1077,7 @@ window.__ModuleLoader__.load({
904
1077
  const interval = Number(waterInterval);
905
1078
  if (!Number.isInteger(interval) || interval < 5 || interval > 240) {
906
1079
  setWaterInvalid(true);
1080
+ box.current?.querySelector('[name=waterInterval]')?.focus();
907
1081
  return;
908
1082
  }
909
1083
  const result = await waterAction({
@@ -936,7 +1110,7 @@ window.__ModuleLoader__.load({
936
1110
  setWaterNotice({ key: 'waterRecorded', kind: 'drink', token: result.revision });
937
1111
  if (action.type === 'manual-drink') {
938
1112
  setWaterNotice({
939
- key: result.shift?.recentActions.includes(action.actionId)
1113
+ key: result.recentActions.includes(action.actionId)
940
1114
  ? 'waterRecorded'
941
1115
  : 'waterActionStale',
942
1116
  kind: outcome.recorded ? 'drink' : 'notice',
@@ -947,7 +1121,7 @@ window.__ModuleLoader__.load({
947
1121
  if (action.type === 'quiet')
948
1122
  setWaterNotice({
949
1123
  key:
950
- outcome.quietChanged && result.shift?.quietUntil > Date.now()
1124
+ outcome.quietChanged && result.quietUntil > Date.now()
951
1125
  ? 'waterQuietStarted'
952
1126
  : 'waterActionStale',
953
1127
  kind: 'notice',
@@ -975,13 +1149,9 @@ window.__ModuleLoader__.load({
975
1149
  ? '.owc-pet-handle'
976
1150
  : '.owc-grip,.owc-title'
977
1151
  : action.type === 'manual-drink'
978
- ? petMode
979
- ? '.owc-pet-water-row .owc-primary'
980
- : '.owc-water-actions .owc-primary'
1152
+ ? '.owc-water-record,.owc-water-actions .owc-primary'
981
1153
  : action.type === 'end-quiet'
982
- ? petMode
983
- ? '.owc-pet-quiet-row button'
984
- : '.owc-water-quiet summary'
1154
+ ? '.owc-detail-more summary,.owc-water-quiet summary'
985
1155
  : '.owc-water-form details > summary';
986
1156
  box.current?.querySelector(target)?.focus();
987
1157
  });
@@ -1050,7 +1220,7 @@ window.__ModuleLoader__.load({
1050
1220
  window.visualViewport?.removeEventListener('resize', resize);
1051
1221
  window.visualViewport?.removeEventListener('scroll', resize);
1052
1222
  };
1053
- }, [clamp, petMode]);
1223
+ }, [clamp, petMode, ready]);
1054
1224
 
1055
1225
  React.useEffect(() => {
1056
1226
  if (ui.pending) {
@@ -1102,21 +1272,11 @@ window.__ModuleLoader__.load({
1102
1272
  setBusy(true);
1103
1273
  setError('');
1104
1274
  try {
1105
- const baseSchedule = (await waterRuntime.current?.update({ type: 'sync' }))?.state
1106
- ?.shift?.schedule;
1107
- if (token !== generation.current) return;
1108
1275
  const next = await request('get');
1109
1276
  if (token !== generation.current) return;
1110
- if (next.configured)
1111
- waterScheduleChange.current = {
1112
- baseSchedule,
1113
- schedule: hydrationContext(next.settings, calculate(next.settings, Date.now()))
1114
- .schedule,
1115
- };
1116
1277
  setData(next);
1117
1278
  if (!next.configured) {
1118
1279
  setDraft({ ...next.settings });
1119
- dispatchUI({ type: 'navigate', screen: 'schedule', discard: true });
1120
1280
  salaryBase.current = next.settings;
1121
1281
  }
1122
1282
  if (explicit) setNotice(t('synced'));
@@ -1135,19 +1295,11 @@ window.__ModuleLoader__.load({
1135
1295
  setError('');
1136
1296
  const token = ++generation.current;
1137
1297
  try {
1138
- const baseSchedule = (await waterRuntime.current?.update({ type: 'sync' }))?.state
1139
- ?.shift?.schedule;
1140
- if (token !== generation.current) return;
1141
1298
  const next = await request(
1142
1299
  'set',
1143
1300
  changedSettings(salaryBase.current ?? settings, draft),
1144
1301
  );
1145
1302
  if (token === generation.current) {
1146
- waterScheduleChange.current = {
1147
- baseSchedule,
1148
- schedule: hydrationContext(next.settings, calculate(next.settings, Date.now()))
1149
- .schedule,
1150
- };
1151
1303
  setData(next);
1152
1304
  dispatchUI({ type: 'navigate', screen: ui.back, discard: true });
1153
1305
  setDraft(null);
@@ -1253,7 +1405,7 @@ window.__ModuleLoader__.load({
1253
1405
  petDragged.current = false;
1254
1406
  if (ui.pending) return true;
1255
1407
  if (!petMode) {
1256
- if (!busy && !waterBusy) navigateScreen('appearance', event);
1408
+ if (!requestBusy.current && !waterBusy) navigateScreen('appearance', event);
1257
1409
  return true;
1258
1410
  }
1259
1411
  const trigger =
@@ -1269,7 +1421,7 @@ window.__ModuleLoader__.load({
1269
1421
  : trigger;
1270
1422
  const rect = trigger?.getBoundingClientRect();
1271
1423
  const point =
1272
- event.type === 'keydown'
1424
+ event.type === 'keydown' || (event.type === 'click' && event.detail === 0)
1273
1425
  ? { x: rect?.right ?? petGeometry.x, y: rect?.top ?? petGeometry.y }
1274
1426
  : { x: event.clientX, y: event.clientY };
1275
1427
  dispatchUI({ type: 'menu', position: point });
@@ -1310,8 +1462,19 @@ window.__ModuleLoader__.load({
1310
1462
  setWaterConfirm(null);
1311
1463
  }
1312
1464
  function navigateScreen(screen, event) {
1313
- if (!data?.configured && screen !== 'schedule') return;
1314
- 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
+ )
1315
1478
  return;
1316
1479
  const root = event?.currentTarget?.getAttribute?.('role') === 'menuitem';
1317
1480
  if (event?.currentTarget) focusReturn.current = event.currentTarget;
@@ -1363,7 +1526,7 @@ window.__ModuleLoader__.load({
1363
1526
  navigateScreen('schedule', event);
1364
1527
  }
1365
1528
  function closeEditor() {
1366
- if ((!data?.configured && !appearanceOnly) || busy) return;
1529
+ if (busy) return;
1367
1530
  performNavigation(ui.back);
1368
1531
  }
1369
1532
  function update(key, value) {
@@ -1432,19 +1595,17 @@ window.__ModuleLoader__.load({
1432
1595
  'button',
1433
1596
  {
1434
1597
  type: 'button',
1435
- className: 'owc-text',
1598
+ className: 'owc-icon',
1436
1599
  'aria-pressed': view.masked,
1600
+ 'aria-label': t(view.masked ? 'show' : 'hide'),
1437
1601
  title: t(view.masked ? 'show' : 'hide'),
1438
1602
  onClick: () => setPreference({ masked: !view.masked }),
1439
1603
  },
1440
1604
  iconSvg(view.masked ? 'eyeOff' : 'eye', 13),
1441
- t(view.masked ? 'show' : 'hide'),
1442
1605
  );
1443
1606
 
1444
- const waterContext =
1445
- data?.configured && safeState && !error ? hydrationContext(settings, safeState) : null;
1446
- const waterState = hydrationView(water, waterContext, now);
1447
- const waterClaim = water.shift?.claim;
1607
+ const waterState = hydrationView(water, now);
1608
+ const waterClaim = water.claim;
1448
1609
  const waterPrompt =
1449
1610
  waterReady &&
1450
1611
  !waterError &&
@@ -1454,9 +1615,59 @@ window.__ModuleLoader__.load({
1454
1615
  !document.hidden &&
1455
1616
  waterState.phase === 'due' &&
1456
1617
  waterClaim?.owner === waterOwner.current &&
1457
- waterClaim.sequence === water.shift.sequence &&
1618
+ waterClaim.sequence === water.sequence &&
1458
1619
  waterClaim.until > Date.now() &&
1459
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
+ }
1460
1671
  const reactionSurface = [
1461
1672
  collapsed,
1462
1673
  editing,
@@ -1478,8 +1689,8 @@ window.__ModuleLoader__.load({
1478
1689
  workStatus: safeState?.status,
1479
1690
  phase: waterState.phase,
1480
1691
  ownsPrompt: waterPrompt,
1481
- ready: waterReady && !!safeState,
1482
- failed: waterError || !!error,
1692
+ ready: waterReady,
1693
+ failed: waterError,
1483
1694
  feedback: waterNotice || null,
1484
1695
  visible: view.companion,
1485
1696
  motion: view.companionMotion && !reactionState.current?.stopped,
@@ -1510,11 +1721,10 @@ window.__ModuleLoader__.load({
1510
1721
  waterVisible &&
1511
1722
  waterReady &&
1512
1723
  !waterError &&
1513
- !error &&
1514
1724
  !editing &&
1515
1725
  !waterEditing &&
1516
1726
  !ui.menu,
1517
- resting: ['quiet', 'rest', 'offline'].includes(companion.pose),
1727
+ resting: ['quiet', 'offline'].includes(companion.pose),
1518
1728
  });
1519
1729
  }, [
1520
1730
  petMode,
@@ -1535,6 +1745,60 @@ window.__ModuleLoader__.load({
1535
1745
  petReactionToken.current = token;
1536
1746
  if (petMode && companion.animate) petMotion.current?.play('celebrate');
1537
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
+ ]);
1538
1802
  const companionCard = (compact = false) =>
1539
1803
  companion.visible &&
1540
1804
  h(
@@ -1565,13 +1829,6 @@ window.__ModuleLoader__.load({
1565
1829
  const waterSummary = () => {
1566
1830
  if (!waterReady) return t('waterLoading');
1567
1831
  if (waterError) return t('waterUnavailable');
1568
- if (
1569
- water.enabled &&
1570
- waterContext &&
1571
- water.shift &&
1572
- water.shift.config !== waterContext.config
1573
- )
1574
- return t('waterStale');
1575
1832
  const labels = {
1576
1833
  off: 'waterOff',
1577
1834
  waiting: 'waterWaiting',
@@ -1586,19 +1843,258 @@ window.__ModuleLoader__.load({
1586
1843
  ? t('waterNext') + ' · ' + formatDuration(waterState.remaining / 1000)
1587
1844
  : t(labels[waterState.phase]);
1588
1845
  };
1589
- const waterCanRecord =
1590
- waterReady &&
1591
- !waterError &&
1592
- data?.configured &&
1593
- ['working', 'lunch'].includes(waterContext?.status) &&
1594
- (!water.shift || water.shift.config === waterContext.config);
1595
- 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;
1596
2093
  const recordWater = (event) => {
1597
2094
  if (event.detail > 1) return;
1598
2095
  if (!manualAttempt.current)
1599
2096
  manualAttempt.current = {
1600
2097
  type: 'manual-drink',
1601
- shiftId: waterContext.id,
1602
2098
  actionId: window.crypto?.randomUUID?.() || String(Math.random()) + Date.now(),
1603
2099
  ...(waterState.phase === 'due' ? { eventId: waterState.eventId } : {}),
1604
2100
  };
@@ -1667,14 +2163,10 @@ window.__ModuleLoader__.load({
1667
2163
  'button',
1668
2164
  {
1669
2165
  type: 'button',
1670
- disabled:
1671
- waterError ||
1672
- waterBusy ||
1673
- !waterContext ||
1674
- water.shift?.id !== waterContext.id,
1675
- onClick: () => waterAction({ type: water.shift?.paused ? 'resume' : 'pause' }),
2166
+ disabled: waterError || waterBusy || !waterReady,
2167
+ onClick: () => waterAction({ type: water.paused ? 'resume' : 'pause' }),
1676
2168
  },
1677
- t(water.shift?.paused ? 'waterResume' : 'waterPause'),
2169
+ t(water.paused ? 'waterResume' : 'waterPause'),
1678
2170
  ),
1679
2171
  waterError &&
1680
2172
  h(
@@ -1724,7 +2216,7 @@ window.__ModuleLoader__.load({
1724
2216
  className: 'owc-text',
1725
2217
  style: { marginLeft: 'auto' },
1726
2218
  'aria-label': t('waterEdit'),
1727
- disabled: !waterReady || !data?.configured,
2219
+ disabled: !waterReady,
1728
2220
  onClick: openWaterEditor,
1729
2221
  },
1730
2222
  t('settings'),
@@ -1757,16 +2249,59 @@ window.__ModuleLoader__.load({
1757
2249
  { 'data-water-effective': water.interval },
1758
2250
  t('waterEvery') + ' ' + water.interval + ' ' + t('waterMinutes'),
1759
2251
  ),
1760
- 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
+ ),
1761
2257
  ),
1762
2258
  waterControls(),
1763
- !['working', 'lunch'].includes(waterContext?.status) &&
1764
- h('p', { className: 'owc-hint' }, t('waterRecordHint')),
2259
+ h('p', { className: 'owc-hint' }, t('waterRecordHint')),
1765
2260
  waterState.phase === 'quiet' &&
1766
2261
  h('p', { className: 'owc-hint' }, t('waterQuietHint')),
1767
2262
  waterNotice && !view.companion && h('p', null, t(waterNotice.key)),
1768
2263
  );
1769
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
+ );
1770
2305
  const waterSettings = () =>
1771
2306
  h(
1772
2307
  'form',
@@ -1789,7 +2324,7 @@ window.__ModuleLoader__.load({
1789
2324
  h('input', {
1790
2325
  type: 'checkbox',
1791
2326
  checked: waterEnabledDraft,
1792
- disabled: !waterReady || waterError || waterBusy || !data?.configured,
2327
+ disabled: !waterReady || waterError || waterBusy,
1793
2328
  onChange: (event) => {
1794
2329
  setWaterEnabledDraft(event.target.checked);
1795
2330
  setWaterNotice(false);
@@ -1798,8 +2333,9 @@ window.__ModuleLoader__.load({
1798
2333
  ),
1799
2334
  h(
1800
2335
  'fieldset',
1801
- { disabled: !waterReady || waterError || waterBusy || !data?.configured },
1802
- h('legend', { className: 'owc-muted' }, t('waterPresets')),
2336
+ { disabled: !waterReady || waterError || waterBusy },
2337
+ h('legend', { className: 'owc-sr-only' }, t('waterPresets')),
2338
+ waterIntervalField(),
1803
2339
  h(
1804
2340
  'div',
1805
2341
  { className: 'owc-water-presets' },
@@ -1810,64 +2346,34 @@ window.__ModuleLoader__.load({
1810
2346
  type: 'button',
1811
2347
  key: minutes,
1812
2348
  'aria-pressed': Number(waterInterval) === minutes,
2349
+ 'aria-label': minutes + ' ' + t('waterMinutes'),
1813
2350
  onClick: () => {
1814
2351
  setWaterInterval(String(minutes));
1815
2352
  setWaterInvalid(false);
1816
2353
  },
1817
2354
  },
1818
- minutes + ' ' + t('waterMinutes'),
2355
+ String(minutes),
1819
2356
  ),
1820
2357
  ),
1821
2358
  ),
1822
- h(
1823
- 'label',
1824
- { className: 'owc-field' },
1825
- h('span', null, t('waterInterval')),
1826
- h('input', {
1827
- name: 'waterInterval',
1828
- type: 'number',
1829
- inputMode: 'numeric',
1830
- min: '5',
1831
- max: '240',
1832
- step: '1',
1833
- value: waterInterval,
1834
- 'aria-invalid': waterInvalid,
1835
- onChange: (event) => {
1836
- setWaterInterval(event.target.value);
1837
- setWaterInvalid(false);
1838
- },
1839
- }),
1840
- ),
1841
2359
  ),
1842
2360
  waterInvalid && h('p', { role: 'alert' }, t('waterInvalid')),
1843
- h(
1844
- 'p',
1845
- { 'data-water-saved-interval': water.interval },
1846
- t('waterEffective') + ': ' + water.interval + ' ' + t('waterMinutes'),
1847
- ),
1848
- h('p', { className: 'owc-hint' }, t('waterSaveHint')),
1849
- h('p', { className: 'owc-hint' }, t('waterBrief')),
1850
- waterError && h('p', { role: 'alert' }, t('waterUnavailable')),
1851
- !data?.configured && h('p', null, t('waterSetup')),
1852
- h(
1853
- 'div',
1854
- { className: 'owc-save' },
2361
+ (Number(waterInterval) !== water.interval || waterEnabledDraft !== water.enabled) &&
1855
2362
  h(
1856
- 'button',
1857
- { type: 'button', disabled: waterBusy, onClick: closeWaterEditor },
1858
- 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'),
1859
2372
  ),
1860
- h(
1861
- 'button',
1862
- {
1863
- type: 'submit',
1864
- className: 'owc-primary',
1865
- disabled: !waterReady || waterError || waterBusy || !data?.configured,
1866
- },
1867
- t(waterBusy ? 'busy' : 'waterSave'),
1868
- ),
1869
- ),
1870
- waterError &&
2373
+ h('p', { className: 'owc-hint' }, t('detailWaterRule')),
2374
+ waterError && h('p', { role: 'alert' }, t('waterUnavailable')),
2375
+
2376
+ waterError &&
1871
2377
  h(
1872
2378
  'button',
1873
2379
  {
@@ -1929,6 +2435,7 @@ window.__ModuleLoader__.load({
1929
2435
  ),
1930
2436
  ),
1931
2437
  ),
2438
+ waterSettingsActions(),
1932
2439
  );
1933
2440
  const waterReminder = () =>
1934
2441
  waterPrompt &&
@@ -1943,6 +2450,7 @@ window.__ModuleLoader__.load({
1943
2450
  'button',
1944
2451
  {
1945
2452
  type: 'button',
2453
+ className: 'owc-primary',
1946
2454
  disabled: waterBusy,
1947
2455
  onClick: () => waterAction({ type: 'drink', eventId: waterState.eventId }),
1948
2456
  },
@@ -1960,6 +2468,440 @@ window.__ModuleLoader__.load({
1960
2468
  ),
1961
2469
  );
1962
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
+
1963
2905
  const strip = () =>
1964
2906
  h(
1965
2907
  'div',
@@ -2196,14 +3138,26 @@ window.__ModuleLoader__.load({
2196
3138
  type: 'button',
2197
3139
  className: 'owc-text',
2198
3140
  onClick: () => reload(true),
2199
- disabled: busy || !sessionId,
3141
+ disabled: hostBusy || !sessionId,
2200
3142
  },
2201
3143
  iconSvg('refresh', 12),
2202
3144
  t('retry'),
2203
3145
  ),
2204
3146
  ),
2205
3147
  );
2206
- 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);
2207
3161
  return h(
2208
3162
  'div',
2209
3163
  { className: 'owc-main' },
@@ -2213,10 +3167,15 @@ window.__ModuleLoader__.load({
2213
3167
  section !== 'earnings' &&
2214
3168
  h(
2215
3169
  'div',
2216
- { className: 'owc-time', 'aria-label': status + ' ' + duration },
2217
- 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),
2218
3176
  ),
2219
3177
  section !== 'earnings' &&
3178
+ ['working', 'lunch', 'done'].includes(safeState.status) &&
2220
3179
  h(
2221
3180
  'div',
2222
3181
  {
@@ -2260,24 +3219,29 @@ window.__ModuleLoader__.load({
2260
3219
  ),
2261
3220
  ),
2262
3221
  h(
2263
- 'div',
2264
- { className: 'owc-meta' },
2265
- h(
2266
- 'span',
2267
- null,
2268
- settings.start +
2269
- ' — ' +
2270
- settings.end +
2271
- (settings.end < settings.start ? ' +1' : '') +
2272
- ' · ' +
2273
- (settings.breakStart === settings.breakEnd
2274
- ? t('noBreak')
2275
- : settings.breakStart + '–' + settings.breakEnd),
2276
- ),
3222
+ 'details',
3223
+ { className: 'owc-detail-more' },
3224
+ h('summary', null, t('detailShift')),
2277
3225
  h(
2278
- 'span',
2279
- null,
2280
- 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
+ ),
2281
3245
  ),
2282
3246
  ),
2283
3247
  h(
@@ -2289,7 +3253,7 @@ window.__ModuleLoader__.load({
2289
3253
  type: 'button',
2290
3254
  className: 'owc-text',
2291
3255
  onClick: () => reload(true),
2292
- disabled: busy,
3256
+ disabled: hostBusy,
2293
3257
  },
2294
3258
  iconSvg('refresh', 12),
2295
3259
  t('refresh'),
@@ -2370,7 +3334,6 @@ window.__ModuleLoader__.load({
2370
3334
  onChange: (event) => updateAppearance({ petSize: event.target.valueAsNumber }),
2371
3335
  }),
2372
3336
  ),
2373
- h('p', { className: 'owc-hint' }, t('petAppearanceHint')),
2374
3337
  label(
2375
3338
  t('opacity'),
2376
3339
  h(
@@ -2390,32 +3353,108 @@ window.__ModuleLoader__.load({
2390
3353
  }),
2391
3354
  h(
2392
3355
  'span',
2393
- { className: 'owc-hint', style: { margin: 0 } },
2394
- Math.round(view.opacity * 100) + '% · ' + t('opacityHint'),
3356
+ { className: 'owc-range-value', title: t('opacityHint') },
3357
+ Math.round(view.opacity * 100) + '%',
2395
3358
  ),
2396
3359
  ),
2397
3360
  ),
2398
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
+ ),
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'),
3423
+ ),
3424
+ ),
3425
+ h(
3426
+ 'p',
3427
+ { className: 'owc-hint owc-preview-status', role: 'status', 'aria-atomic': true },
3428
+ message,
3429
+ ),
3430
+ );
3431
+ };
2399
3432
  const editor = () => {
2400
3433
  if (appearanceOnly)
2401
3434
  return h(
2402
3435
  'div',
2403
3436
  { className: 'owc-form owc-appearance', 'data-appearance-settings': true },
2404
- h('p', { className: 'owc-hint' }, t('petAppearancePreview')),
3437
+ h('p', { className: 'owc-sr-only' }, t('petAppearancePreview')),
2405
3438
  h(
2406
3439
  'div',
2407
- { 'data-appearance-preview': true, className: 'owc-appearance-preview' },
3440
+ {
3441
+ ref: previewBox,
3442
+ 'data-appearance-preview': true,
3443
+ className: 'owc-appearance-preview',
3444
+ },
2408
3445
  view.companion
2409
3446
  ? petArtwork(
2410
3447
  h,
2411
- companion,
3448
+ { pose: 'calm', animate: false },
2412
3449
  view.petSize * (petModel(view.petModel).renderer === 'svg' ? 1 : 0.65),
2413
3450
  '',
2414
3451
  view.petModel,
2415
3452
  PET_SPRITES,
3453
+ petModel(view.petModel).renderer === 'video',
2416
3454
  )
2417
3455
  : h('p', null, t('petClassicPreview')),
2418
3456
  ),
3457
+ previewActions(),
2419
3458
  appearanceFields(),
2420
3459
  h(
2421
3460
  'div',
@@ -2423,8 +3462,13 @@ window.__ModuleLoader__.load({
2423
3462
  h('button', { type: 'button', onClick: closeEditor }, t('cancel')),
2424
3463
  h(
2425
3464
  'button',
2426
- { type: 'button', className: 'owc-primary', onClick: applyAppearance },
2427
- t('petApplyAppearance'),
3465
+ {
3466
+ type: 'button',
3467
+ className: 'owc-primary',
3468
+ 'aria-label': t('petApplyAppearance'),
3469
+ onClick: applyAppearance,
3470
+ },
3471
+ t('detailApply'),
2428
3472
  ),
2429
3473
  ),
2430
3474
  );
@@ -2432,21 +3476,7 @@ window.__ModuleLoader__.load({
2432
3476
  const payDaily = workingDraft.payMode === 'daily';
2433
3477
  const salaryKey = payDaily ? 'dailySalary' : 'monthlySalary';
2434
3478
  const salaryControl = view.masked
2435
- ? h(
2436
- 'div',
2437
- { className: 'owc-private' },
2438
- h('span', null, '••••'),
2439
- h(
2440
- 'button',
2441
- {
2442
- type: 'button',
2443
- className: 'owc-text',
2444
- 'aria-label': t('show'),
2445
- onClick: () => setPreference({ masked: false }),
2446
- },
2447
- t('show'),
2448
- ),
2449
- )
3479
+ ? h('div', { className: 'owc-private', 'aria-label': t('masked') }, '••••')
2450
3480
  : numberInput(salaryKey);
2451
3481
  const breakOn = workingDraft.breakStart !== workingDraft.breakEnd;
2452
3482
  const commonZones = [
@@ -2471,6 +3501,20 @@ window.__ModuleLoader__.load({
2471
3501
  const zones = [
2472
3502
  ...new Set([workingDraft.timezone, ...commonZones, ...allZones].filter(Boolean)),
2473
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
+ );
2474
3518
  return h(
2475
3519
  'form',
2476
3520
  {
@@ -2487,7 +3531,22 @@ window.__ModuleLoader__.load({
2487
3531
  h(
2488
3532
  'fieldset',
2489
3533
  { className: 'owc-setting-group' },
2490
- 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
+ ),
2491
3550
  label(
2492
3551
  t('payMode'),
2493
3552
  h(
@@ -2581,22 +3640,10 @@ window.__ModuleLoader__.load({
2581
3640
  ),
2582
3641
  ),
2583
3642
  ),
2584
- label(
2585
- t('timezone'),
2586
- h(
2587
- 'select',
2588
- {
2589
- name: 'timezone',
2590
- value: workingDraft.timezone ?? '',
2591
- disabled: busy,
2592
- onChange: (e) => update('timezone', e.target.value),
2593
- },
2594
- zones.map((zone) => h('option', { key: zone, value: zone }, zone)),
2595
- ),
2596
- ),
2597
3643
  ),
2598
3644
  preview &&
2599
3645
  !preview.error &&
3646
+ !view.masked &&
2600
3647
  h(
2601
3648
  'div',
2602
3649
  { className: 'owc-preview' },
@@ -2620,6 +3667,7 @@ window.__ModuleLoader__.load({
2620
3667
  'details',
2621
3668
  { className: 'owc-advanced' },
2622
3669
  h('summary', null, t('advanced')),
3670
+ timezoneField(),
2623
3671
  h('p', { className: 'owc-hint' }, t('sessionHint')),
2624
3672
  label(t('session'), sessionSelect()),
2625
3673
  ),
@@ -2642,12 +3690,18 @@ window.__ModuleLoader__.load({
2642
3690
  );
2643
3691
  };
2644
3692
 
2645
- const notices = () =>
3693
+ const notices = (showBusinessError = true) =>
2646
3694
  h(
2647
3695
  React.Fragment,
2648
3696
  null,
2649
- error && h('p', { className: 'owc-error', role: 'alert' }, error),
2650
- 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),
2651
3705
  notice && h('p', { className: 'owc-note', role: 'status' }, notice),
2652
3706
  storageError && h('p', { className: 'owc-note' }, t('storage')),
2653
3707
  );
@@ -2685,8 +3739,15 @@ window.__ModuleLoader__.load({
2685
3739
  waterPrompt ? t('waterDue') : waterNotice ? t(waterNotice.key) : '',
2686
3740
  ),
2687
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]);
2688
3749
  const petShowing = petOpen;
2689
- const petFloating = petShowing || waterPrompt;
3750
+ const petFloating = petShowing || autoWaterPrompt;
2690
3751
  React.useLayoutEffect(() => {
2691
3752
  if (!petMode || !petFloating || !cardContent.current) return;
2692
3753
  const content = cardContent.current,
@@ -2790,7 +3851,13 @@ window.__ModuleLoader__.load({
2790
3851
  return () => document.removeEventListener('pointerdown', outside);
2791
3852
  }, [petMode, petShowing, ui.menu, editing, waterEditing, waterBusy]);
2792
3853
  React.useEffect(() => {
2793
- 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();
2794
3861
  }, [ui.menu]);
2795
3862
  React.useEffect(() => {
2796
3863
  if (!petMode || editing || waterEditing || ui.menu) return;
@@ -2806,6 +3873,32 @@ window.__ModuleLoader__.load({
2806
3873
  if (ui.pending && (busy || waterBusy))
2807
3874
  box.current?.querySelector('[data-nav-confirm]')?.focus();
2808
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]);
2809
3902
  const draftGuard = () =>
2810
3903
  h(
2811
3904
  'div',
@@ -2867,6 +3960,8 @@ window.__ModuleLoader__.load({
2867
3960
  );
2868
3961
  const petPageTitle = {
2869
3962
  home: 'petWaterCard',
3963
+ 'water-countdown': 'waterCountdown',
3964
+ smoking: 'smokingCountdown',
2870
3965
  income: 'petCountdownCard',
2871
3966
  earnings: 'petEarningsCard',
2872
3967
  water: 'petWaterSettings',
@@ -2893,9 +3988,22 @@ window.__ModuleLoader__.load({
2893
3988
  ),
2894
3989
  h(
2895
3990
  'strong',
2896
- { 'data-pet-page-title': true, tabIndex: -1, style: { flex: 1, fontSize: 12 } },
3991
+ { 'data-pet-page-title': true, tabIndex: -1, style: { flex: 1 } },
2897
3992
  t(petPageTitle[ui.screen] || 'waterTitle'),
2898
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
+ ),
2899
4007
  h(
2900
4008
  'button',
2901
4009
  {
@@ -2911,102 +4019,91 @@ window.__ModuleLoader__.load({
2911
4019
  const petHome = () =>
2912
4020
  h(
2913
4021
  'div',
2914
- { className: 'owc-pet-home', 'data-pet-home': true },
2915
-
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(),
2916
4028
  h(
2917
4029
  'div',
2918
- {
2919
- className: 'owc-pet-water-card' + (waterPrompt ? ' owc-water-due' : ''),
2920
- 'data-water-event': waterPrompt ? waterState.eventId : undefined,
2921
- },
4030
+ { className: 'owc-detail-primary-actions' },
2922
4031
  h(
2923
- 'p',
2924
- { className: 'owc-water-status', 'data-water-status': true },
2925
- waterPrompt
2926
- ? t('waterDue')
2927
- : waterState.phase === 'quiet'
2928
- ? t('petQuietActive')
2929
- : 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'),
2930
4040
  ),
2931
- h(
2932
- 'div',
2933
- { className: 'owc-pet-water-row' },
4041
+ waterPrompt &&
2934
4042
  h(
2935
4043
  'button',
2936
4044
  {
2937
4045
  type: 'button',
2938
- className: 'owc-primary',
2939
- disabled: !waterCanRecord || waterBusy,
2940
- onClick: recordWater,
4046
+ className: 'owc-text',
4047
+ disabled: waterBusy,
4048
+ onClick: () => waterAction({ type: 'snooze', eventId: waterState.eventId }),
2941
4049
  },
2942
- t('petRecord'),
2943
- ),
2944
- h(
2945
- 'span',
2946
- { 'data-water-count': true },
2947
- t('waterCount') + ': ' + waterState.count,
4050
+ t('waterSnooze'),
2948
4051
  ),
2949
- ),
2950
4052
  ),
2951
4053
  h(
2952
4054
  'div',
2953
- { className: 'owc-pet-quiet-row' },
2954
- [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' &&
2955
4067
  h(
2956
4068
  'button',
2957
4069
  {
2958
4070
  type: 'button',
2959
- key: minutes,
2960
- disabled: !waterCanQuiet || waterBusy,
2961
- onClick: () => waterAction({ type: 'quiet', minutes }),
4071
+ className: 'owc-text',
4072
+ disabled: waterBusy,
4073
+ onClick: () => waterAction({ type: 'end-quiet' }),
2962
4074
  },
2963
- t(minutes === 30 ? 'petQuiet30' : 'petQuiet60'),
4075
+ t('waterQuietEnd'),
2964
4076
  ),
2965
- ),
2966
4077
  ),
2967
4078
  h(
2968
- 'div',
2969
- { className: 'owc-pet-quiet-status' },
2970
- waterPrompt
2971
- ? 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(
2972
4087
  'button',
2973
4088
  {
2974
4089
  type: 'button',
2975
- disabled: waterBusy,
2976
- 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
+ },
2977
4098
  },
2978
- t('waterSnooze'),
2979
- )
2980
- : water.shift?.paused
2981
- ? t('petPausedHint')
2982
- : waterState.phase === 'quiet'
2983
- ? h(
2984
- React.Fragment,
2985
- null,
2986
- t('waterQuiet') +
2987
- ' · ' +
2988
- formatDuration(waterState.quietRemaining / 1000),
2989
- h(
2990
- 'button',
2991
- {
2992
- type: 'button',
2993
- disabled: waterBusy,
2994
- onClick: () => waterAction({ type: 'end-quiet' }),
2995
- },
2996
- t('waterQuietEnd'),
2997
- ),
2998
- )
2999
- : t('petQuietHint'),
3000
- ),
3001
- h(
3002
- 'div',
3003
- { className: 'owc-pet-feedback' },
3004
- waterNotice
3005
- ? t(waterNotice.key)
3006
- : waterError
3007
- ? t('waterUnavailable')
3008
- : t('petHomeHint'),
4099
+ t(minutes === 30 ? 'petQuiet30' : 'petQuiet60'),
4100
+ ),
4101
+ ),
4102
+ ),
4103
+ h('p', { className: 'owc-hint' }, t('petQuietHint')),
3009
4104
  ),
4105
+ waterError &&
4106
+ h('p', { className: 'owc-error', role: 'alert' }, t('waterUnavailable')),
3010
4107
  waterError &&
3011
4108
  h(
3012
4109
  'button',
@@ -3017,26 +4114,6 @@ window.__ModuleLoader__.load({
3017
4114
  },
3018
4115
  t('waterRetry'),
3019
4116
  ),
3020
- h(
3021
- 'div',
3022
- { className: 'owc-pet-home-links' },
3023
- h('button', { type: 'button', onClick: openWaterEditor }, t('petWaterSettings')),
3024
- h(
3025
- 'button',
3026
- {
3027
- type: 'button',
3028
- disabled:
3029
- !water.enabled ||
3030
- busy ||
3031
- waterBusy ||
3032
- waterError ||
3033
- !waterContext ||
3034
- water.shift?.id !== waterContext.id,
3035
- onClick: () => waterAction({ type: water.shift?.paused ? 'resume' : 'pause' }),
3036
- },
3037
- t(water.shift?.paused ? 'waterResume' : 'waterPause'),
3038
- ),
3039
- ),
3040
4117
  );
3041
4118
  const contextMenu = () =>
3042
4119
  ui.menu &&
@@ -3091,6 +4168,8 @@ window.__ModuleLoader__.load({
3091
4168
  [
3092
4169
  ['income', 'petCountdownCard'],
3093
4170
  ['home', 'petWaterCard'],
4171
+ ['water-countdown', 'waterCountdown'],
4172
+ ['smoking', 'smokingCountdown'],
3094
4173
  ['earnings', 'petEarningsCard'],
3095
4174
  ['appearance', 'petAppearance'],
3096
4175
  ['schedule', 'petWorkSettings'],
@@ -3101,14 +4180,34 @@ window.__ModuleLoader__.load({
3101
4180
  key: screen,
3102
4181
  type: 'button',
3103
4182
  role: 'menuitem',
4183
+ 'data-pet-menu-screen': screen,
3104
4184
  tabIndex: -1,
3105
- disabled: busy || waterBusy,
4185
+ disabled:
4186
+ requestBusy.current ||
4187
+ waterBusy ||
4188
+ (!LOCAL_SCREENS.includes(screen) &&
4189
+ (hostBusy || !data || (!data.configured && screen !== 'schedule'))),
3106
4190
  onClick: (event) => navigateScreen(screen, event),
3107
4191
  },
3108
4192
  t(label),
3109
4193
  ),
3110
4194
  ),
3111
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
+ ),
3112
4211
  h('hr', null),
3113
4212
  h(
3114
4213
  'button',
@@ -3126,15 +4225,23 @@ window.__ModuleLoader__.load({
3126
4225
  ),
3127
4226
  );
3128
4227
 
4228
+ if (!ready) return null;
3129
4229
  if (petMode)
3130
4230
  return h(
3131
4231
  'section',
3132
4232
  {
3133
4233
  ref: box,
3134
4234
  className: 'owc-v2 owc-pet-shell',
3135
- 'data-version': '1.1.1',
4235
+ 'data-version': '1.1.2',
3136
4236
  'data-pet-model': stageModel,
3137
- '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',
3138
4245
  'aria-label': t('companionLabel'),
3139
4246
  style: {
3140
4247
  '--owc-alpha': String(view.opacity),
@@ -3160,8 +4267,7 @@ window.__ModuleLoader__.load({
3160
4267
  else if (ui.pending) dispatchUI({ type: 'stay' });
3161
4268
  else if (editing || waterEditing) navigateScreen(ui.back);
3162
4269
  else if (petShowing) closePetPanel();
3163
- else if (waterPrompt)
3164
- void waterAction({ type: 'snooze', eventId: waterState.eventId });
4270
+ else if (autoWaterPrompt) dismissWaterPopup();
3165
4271
  },
3166
4272
  },
3167
4273
  h('style', null, STYLE),
@@ -3196,26 +4302,79 @@ window.__ModuleLoader__.load({
3196
4302
  h(
3197
4303
  'div',
3198
4304
  {
3199
- className: 'owc-pet-clock',
3200
- role: 'timer',
3201
- 'aria-label': t('petClock'),
4305
+ className: 'owc-pet-timers',
4306
+ role: 'group',
4307
+ 'aria-label': t('petCountdownGroup'),
3202
4308
  },
3203
- iconSvg('alarm', 12),
3204
- h('span', { className: 'owc-pet-clock-label' }, t(safeState.status)),
3205
- ['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
+ ),
3206
4363
  ),
3207
4364
  ),
3208
4365
  waterPrompt &&
3209
- petShowing &&
4366
+ !autoWaterPrompt &&
3210
4367
  ui.screen !== 'home' &&
3211
4368
  h(
3212
- 'div',
4369
+ 'button',
3213
4370
  {
4371
+ type: 'button',
3214
4372
  className: 'owc-pet-water-alert owc-water-due',
3215
4373
  'data-water-event': waterState.eventId,
3216
- role: 'note',
3217
4374
  'aria-label': t('petWaterAlert'),
3218
4375
  title: t('petWaterAlert'),
4376
+ disabled: busy || waterBusy || !!ui.pending,
4377
+ onClick: openWaterPopup,
3219
4378
  style: {
3220
4379
  left: waterAlertLayout.left,
3221
4380
  top: waterAlertLayout.top,
@@ -3224,7 +4383,8 @@ window.__ModuleLoader__.load({
3224
4383
  },
3225
4384
  },
3226
4385
  iconSvg('water', 13),
3227
- t('petWaterBadge'),
4386
+ h('span', { className: 'owc-pet-water-label' }, t('petWaterBadge')),
4387
+ iconSvg('chevronUp', 10),
3228
4388
  ),
3229
4389
  contextMenu(),
3230
4390
  !petShowing &&
@@ -3249,8 +4409,8 @@ window.__ModuleLoader__.load({
3249
4409
  'div',
3250
4410
  {
3251
4411
  ref: petPanel,
3252
- className: 'owc-pet-panel owc-pet-workspace',
3253
- 'data-pet-panel': petShowing ? 'open' : 'bubble',
4412
+ className: 'owc-pet-panel owc-pet-workspace owc-detail-page',
4413
+ 'data-pet-panel': petShowing ? 'open' : 'reminder',
3254
4414
  'data-pet-screen': ui.screen || 'reminder',
3255
4415
  'data-side': floatingPanel.side,
3256
4416
  role: 'region',
@@ -3275,10 +4435,10 @@ window.__ModuleLoader__.load({
3275
4435
  {
3276
4436
  type: 'button',
3277
4437
  className: 'owc-icon',
3278
- 'aria-label': t('waterSnooze'),
4438
+ 'aria-label': t('petDismissReminder'),
4439
+ title: t('petDismissReminder'),
3279
4440
  disabled: waterBusy,
3280
- onClick: () =>
3281
- waterAction({ type: 'snooze', eventId: waterState.eventId }),
4441
+ onClick: dismissWaterPopup,
3282
4442
  },
3283
4443
  iconSvg('close', 14),
3284
4444
  ),
@@ -3301,9 +4461,13 @@ window.__ModuleLoader__.load({
3301
4461
  ? editor()
3302
4462
  : petDetails
3303
4463
  ? panelBody('clock')
3304
- : ui.screen === 'earnings'
3305
- ? panelBody('earnings')
3306
- : petHome(),
4464
+ : ui.screen === 'water-countdown'
4465
+ ? waterCountdownCard()
4466
+ : ui.screen === 'smoking'
4467
+ ? smokingCountdownCard()
4468
+ : ui.screen === 'earnings'
4469
+ ? panelBody('earnings')
4470
+ : petHome(),
3307
4471
  notices(),
3308
4472
  )
3309
4473
  : waterReminder(),
@@ -3317,13 +4481,14 @@ window.__ModuleLoader__.load({
3317
4481
  {
3318
4482
  ref: box,
3319
4483
  className:
3320
- 'owc-v2' +
4484
+ 'owc-v2 owc-classic' +
4485
+ (!collapsed ? ' owc-detail-page' : '') +
3321
4486
  (collapsed ? ' owc-compact' : '') +
3322
4487
  (collapsed && waterQuickOpen ? ' owc-quick' : '') +
3323
4488
  (editing || waterEditing ? ' owc-editing' : ''),
3324
4489
  'aria-label': t('title'),
3325
- 'data-version': '1.1.1',
3326
- 'data-status': safeState?.status || 'setup',
4490
+ 'data-version': '1.1.2',
4491
+ 'data-status': classicClock.phase,
3327
4492
  style: cardStyle,
3328
4493
  onPointerDown: dragDown,
3329
4494
  onPointerMove: dragMove,
@@ -3342,8 +4507,8 @@ window.__ModuleLoader__.load({
3342
4507
  return;
3343
4508
  event.preventDefault();
3344
4509
  if (ui.pending) dispatchUI({ type: 'stay' });
3345
- else if (editing || waterEditing) navigateScreen(ui.back);
3346
- else if (waterQuickOpen) closeWaterQuick();
4510
+ else if (ui.screen)
4511
+ navigateScreen(PET_EDITOR_SCREENS.includes(ui.screen) ? ui.back : null);
3347
4512
  else setPreference({ mini: true });
3348
4513
  },
3349
4514
  },
@@ -3365,33 +4530,28 @@ window.__ModuleLoader__.load({
3365
4530
  waterPrompt ? t('waterDue') : waterNotice ? t(waterNotice.key) : '',
3366
4531
  ),
3367
4532
  ),
3368
- collapsed
3369
- ? h(
3370
- React.Fragment,
3371
- null,
3372
- strip(),
3373
- waterQuickOpen && waterOverview(true),
3374
- !waterQuickOpen &&
3375
- waterNotice &&
3376
- (!view.companion || companion.announce) &&
3377
- h('p', { className: 'owc-mini-feedback' }, t(waterNotice.key)),
3378
- )
3379
- : h(
3380
- React.Fragment,
3381
- null,
3382
- panelHead(),
3383
- !editing && !waterEditing && companionCard(),
3384
- ui.pending
3385
- ? draftGuard()
3386
- : waterEditing
3387
- ? waterSettings()
3388
- : editing
3389
- ? editor()
3390
- : panelBody(),
3391
- notices(),
3392
- !editing && !waterEditing && waterOverview(),
3393
- ),
3394
- !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(),
3395
4555
  ),
3396
4556
  );
3397
4557
  }