dsh-offwork 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +38 -20
  3. package/assets/animations/NOTICE.md +15 -0
  4. package/assets/animations/alpha-probe.webm +0 -0
  5. package/assets/animations/ornate-drink.webm +0 -0
  6. package/assets/animations/ornate-greet.webm +0 -0
  7. package/assets/animations/ornate-idle.webm +0 -0
  8. package/assets/animations/whale-drink.webm +0 -0
  9. package/assets/animations/whale-greet.webm +0 -0
  10. package/assets/characters/NOTICE.md +11 -0
  11. package/assets/characters/whale-actions.webp +0 -0
  12. package/assets/characters/whale-dress.webp +0 -0
  13. package/assets/characters/whale-ornate-actions.webp +0 -0
  14. package/assets/characters/whale-ornate.webp +0 -0
  15. package/client.js +4033 -188
  16. package/client.source.js +2449 -214
  17. package/companion.js +185 -0
  18. package/hydration-store.js +101 -0
  19. package/hydration.js +303 -0
  20. package/locale/en.json +1 -1
  21. package/locale/zh.json +1 -1
  22. package/package.json +13 -2
  23. package/pet-interactions.js +138 -0
  24. package/pet-motion.js +110 -0
  25. package/pet-surfaces.js +194 -0
  26. package/pet-video.js +231 -0
  27. package/pet-view.js +323 -0
  28. package/tests/companion.test.mjs +86 -0
  29. package/tests/hydration-extras.test.mjs +226 -0
  30. package/tests/hydration-store.test.mjs +287 -0
  31. package/tests/hydration.test.mjs +211 -0
  32. package/tests/pet-character.test.mjs +62 -0
  33. package/tests/pet-interactions.test.mjs +88 -0
  34. package/tests/pet-motion.test.mjs +139 -0
  35. package/tests/pet-surfaces.test.mjs +128 -0
  36. package/tests/pet-video.test.mjs +337 -0
  37. package/tests/pet-view.test.mjs +48 -0
  38. package/tests/ui.test.mjs +22 -2
  39. package/tools/build.mjs +62 -6
  40. package/tools/check-package.mjs +20 -0
  41. package/ui-utils.js +22 -4
package/client.source.js CHANGED
@@ -5,8 +5,14 @@ window.__ModuleLoader__.load({
5
5
  const React = require('react');
6
6
  const h = React.createElement;
7
7
  /* PURE_MODULES */
8
+ /* PET_SPRITES */
9
+ /* PET_CLIPS */
8
10
  const messages = {
9
11
  zh: {
12
+ ...COMPANION_MESSAGES.zh,
13
+ ...PET_MESSAGES.zh,
14
+ ...PET_INTERACTION_MESSAGES.zh,
15
+ ...PET_SURFACE_MESSAGES.zh,
10
16
  title: '下班计时器',
11
17
  working: '距离下班',
12
18
  before: '距离开工',
@@ -53,6 +59,7 @@ window.__ModuleLoader__.load({
53
59
  salaryGroup: '薪资',
54
60
  scheduleGroup: '作息',
55
61
  appearanceGroup: '外观',
62
+ appearanceDone: '完成',
56
63
  advanced: '高级设置',
57
64
  sessionHint: '仅用于记录配置读写操作,不是账本,不影响工资计算。',
58
65
  session: '操作记录会话',
@@ -65,8 +72,68 @@ window.__ModuleLoader__.load({
65
72
  shiftDate: '班次',
66
73
  opacity: '背景透明度',
67
74
  opacityHint: '只影响背景,不影响文字。',
75
+ waterTitle: '喝水提醒',
76
+ waterEnable: '开启喝水提醒',
77
+ waterInterval: '自定义间隔(分钟)',
78
+ waterSave: '保存提醒设置',
79
+ waterEdit: '设置喝水提醒',
80
+ waterBack: '返回',
81
+ waterPresets: '常用间隔',
82
+ waterMinutes: '分钟',
83
+ waterEvery: '每',
84
+ waterEffective: '当前间隔',
85
+ waterManage: '数据管理',
86
+ waterSaveHint: '修改后点击保存,按新间隔重新计时。',
87
+ waterBrief: '按工作时间计时,午休暂停。关闭页面后不提醒。',
88
+ waterApply: '应用间隔',
89
+ waterInvalid: '请输入 5–240 的整数。',
90
+ waterNext: '下次喝水',
91
+ waterDue: '该喝水啦',
92
+ waterDrink: '喝了',
93
+ waterSnooze: '工作 10 分钟后',
94
+ waterPause: '本班暂停',
95
+ waterResume: '恢复本班提醒',
96
+ waterCount: '本班打卡',
97
+ waterManual: '喝了一杯',
98
+ waterQuick: '喝水快捷操作',
99
+ waterQuickClose: '收起喝水快捷操作',
100
+ waterRecorded: '这杯记下啦。',
101
+ waterRecordHint: '工作和午休期间可以打卡,关闭提醒也能记录。',
102
+ waterActionStale: '状态已变化,请再试一次。',
103
+ waterQuiet: '免打扰',
104
+ waterQuietAdjust: '调整时长',
105
+ waterQuiet30: '30 分钟',
106
+ waterQuiet60: '1 小时',
107
+ waterQuietEnd: '结束免打扰',
108
+ waterQuietStarted: '好,先安静一会儿,到时再提醒你。',
109
+ waterQuietEnded: '免打扰已结束。',
110
+ waterQuietHint: '按自然时间,到期自动恢复;已到期的提醒只提示一次。',
111
+ waterOff: '提醒已关闭',
112
+ waterWaiting: '等待工作时段',
113
+ waterBreak: '休息中,提醒计时暂停',
114
+ waterPaused: '本班已暂停',
115
+ waterFinished: '本班不再安排新提醒',
116
+ waterOther: '提醒已到期,等待可见标签页处理',
117
+ waterClear: '清除本班次数',
118
+ waterReset: '重置喝水提醒',
119
+ waterConfirm: '确认操作',
120
+ waterClearHint: '仅清零本班次数,保留下一次提醒和暂停状态。',
121
+ waterResetHint: '清除喝水提醒设置和记录,恢复默认关闭。',
122
+ waterLocal: '提醒设置和打卡仅保存在当前浏览器,不进入会话。与薪资配置分别保存。',
123
+ waterRules: '按工作分钟计时,午休冻结。开启、修改间隔或作息后重新计时;关闭页面后不提醒。',
124
+ waterUnavailable:
125
+ '喝水提醒存储不可用或数据无效,自动提醒已停止。可重试;重置将清除原喝水数据。',
126
+ waterRetry: '重试读取',
127
+ waterSetup: '请先保存薪资与作息。',
128
+ waterLoading: '正在读取喝水提醒…',
129
+ waterChanged: '提醒设置已在本浏览器生效',
130
+ waterStale: '作息已变化,请点击“读取最新配置”后继续提醒。',
68
131
  },
69
132
  en: {
133
+ ...COMPANION_MESSAGES.en,
134
+ ...PET_MESSAGES.en,
135
+ ...PET_INTERACTION_MESSAGES.en,
136
+ ...PET_SURFACE_MESSAGES.en,
70
137
  title: 'Offwork Clock',
71
138
  working: 'Until clock-out',
72
139
  before: 'Until work starts',
@@ -113,6 +180,7 @@ window.__ModuleLoader__.load({
113
180
  salaryGroup: 'Salary',
114
181
  scheduleGroup: 'Schedule',
115
182
  appearanceGroup: 'Appearance',
183
+ appearanceDone: 'Done',
116
184
  advanced: 'Advanced',
117
185
  sessionHint:
118
186
  'Logs configuration operations only; not a ledger and does not affect pay calculations.',
@@ -126,17 +194,79 @@ window.__ModuleLoader__.load({
126
194
  shiftDate: 'Shift',
127
195
  opacity: 'Surface opacity',
128
196
  opacityHint: 'Affects the background only.',
197
+ waterTitle: 'Water reminder',
198
+ waterEnable: 'Enable water reminders',
199
+ waterInterval: 'Custom interval (minutes)',
200
+ waterSave: 'Save reminder settings',
201
+ waterEdit: 'Reminder settings',
202
+ waterBack: 'Back',
203
+ waterPresets: 'Quick intervals',
204
+ waterMinutes: 'min',
205
+ waterEvery: 'Every',
206
+ waterEffective: 'Current interval',
207
+ waterManage: 'Manage data',
208
+ waterSaveHint: 'Save changes to restart with the new interval.',
209
+ waterBrief: 'Counts working time and pauses on unpaid breaks. Keep the page open.',
210
+ waterApply: 'Apply interval',
211
+ waterInvalid: 'Enter a whole number from 5 to 240.',
212
+ waterNext: 'Next water break',
213
+ waterDue: 'Time for some water',
214
+ waterDrink: 'Drank water',
215
+ waterSnooze: 'In 10 working minutes',
216
+ waterPause: 'Pause this shift',
217
+ waterResume: 'Resume this shift',
218
+ waterCount: 'Drinks this shift',
219
+ waterManual: 'Had a glass',
220
+ waterQuick: 'Quick water actions',
221
+ waterQuickClose: 'Close quick water actions',
222
+ waterRecorded: 'Got it, your drink is recorded.',
223
+ waterRecordHint: 'Check in during work or breaks, even with reminders off.',
224
+ waterActionStale: 'The state changed. Please try again.',
225
+ waterQuiet: 'Quiet time',
226
+ waterQuietAdjust: 'Change duration',
227
+ waterQuiet30: '30 minutes',
228
+ waterQuiet60: '1 hour',
229
+ waterQuietEnd: 'End quiet time',
230
+ waterQuietStarted: 'Enjoy some quiet. I’ll remind you when it ends.',
231
+ waterQuietEnded: 'Quiet time has ended.',
232
+ waterQuietHint:
233
+ 'Uses clock time. Resumes automatically, with just one reminder if overdue.',
234
+ 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',
239
+ waterOther: 'Reminder due; waiting for a visible tab',
240
+ waterClear: 'Clear shift count',
241
+ waterReset: 'Reset water reminders',
242
+ waterConfirm: 'Confirm',
243
+ waterClearHint: 'Clear the drink count only. Keep the next reminder and pause state.',
244
+ waterResetHint: 'Delete water preferences and records, then turn reminders off.',
245
+ waterLocal:
246
+ 'Reminder preferences and check-ins stay in this browser, outside conversations. Saved separately from salary settings.',
247
+ waterRules:
248
+ 'Counts working minutes; unpaid breaks pause the timer. Enabling or changing interval or schedule restarts it. The page must stay open.',
249
+ waterUnavailable:
250
+ 'Water storage is unavailable or invalid; automatic reminders stopped. Retry, or reset to delete old water data.',
251
+ waterRetry: 'Retry loading',
252
+ waterSetup: 'Save your salary and schedule first.',
253
+ waterLoading: 'Loading water reminders…',
254
+ waterChanged: 'Reminder preferences saved in this browser',
255
+ waterStale: 'Schedule changed. Reload the latest settings to continue reminders.',
129
256
  },
130
257
  };
131
- const STYLE = `
258
+ const STYLE =
259
+ `
132
260
  .owc-v2{--owc-accent:color-mix(in srgb,var(--dsw-alias-brand-primary) 65%,#2563eb);pointer-events:auto;position:fixed;top:96px;right:24px;z-index:40;max-width:calc(100vw - 24px);max-height:calc(100dvh - 24px);border-radius:20px;color:var(--dsw-alias-label-primary);font:13px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif;cursor:grab;touch-action:manipulation;user-select:none;-webkit-user-select:none}
133
261
  .owc-v2:active{cursor:grabbing}
134
262
  .owc-v2.owc-compact{touch-action:none;border-radius:18px}
263
+ .owc-v2.owc-compact.owc-quick{width:296px;touch-action:manipulation}
135
264
  .owc-v2:not(.owc-compact){width:296px}
136
265
  .owc-v2 :is(button,input,select,label,a){cursor:pointer;user-select:auto;-webkit-user-select:auto}
137
266
  .owc-v2 :is(input,select){user-select:text;-webkit-user-select:text}
138
267
  .owc-glass{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:var(--owc-alpha,.82);background:color-mix(in srgb,var(--dsw-alias-bg-overlay) 97%,var(--owc-accent));-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);border:1px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 18%,transparent);box-shadow:inset 0 1px 0 color-mix(in srgb,var(--dsw-alias-bg-base) 70%,transparent),0 8px 20px color-mix(in srgb,var(--dsw-alias-label-primary) 7%,transparent),0 14px 34px color-mix(in srgb,var(--owc-accent) 5%,transparent);transition:opacity .18s ease}
139
- .owc-content{position:relative;border-radius:inherit;max-height:calc(100dvh - 24px);overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}
268
+ .owc-content{position:relative;border-radius:inherit;max-height:calc(100dvh - 24px);overflow:auto;overscroll-behavior:contain;scrollbar-width:none}
269
+ .owc-content::-webkit-scrollbar{display:none}
140
270
  .owc-v2 button{appearance:none;-webkit-appearance:none;border:0;margin:0;background:transparent;color:inherit;font:inherit;display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:6px;min-height:28px;border-radius:10px;transition:background .15s ease,color .15s ease}
141
271
  .owc-v2 button:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 7%,transparent)}
142
272
  .owc-v2 button:focus-visible,.owc-v2 :is(input,select):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
@@ -212,9 +342,48 @@ window.__ModuleLoader__.load({
212
342
  @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.owc-glass{opacity:1}}
213
343
  @media(prefers-reduced-transparency:reduce),(prefers-contrast:more){.owc-glass{opacity:1;-webkit-backdrop-filter:none;backdrop-filter:none}}
214
344
  @media(prefers-reduced-motion:reduce){.owc-v2 button,.owc-glass,.owc-progress>div{transition:none}}
345
+ .owc-water{padding:10px 14px;border-top:1px solid color-mix(in srgb,var(--dsw-alias-label-primary) 12%,transparent);font-size:12px;cursor:default}
346
+ .owc-water summary{cursor:pointer;font-weight:600;margin-bottom:8px}
347
+ .owc-water p{margin:6px 0;font-size:11px;line-height:1.6;overflow-wrap:anywhere}
348
+ .owc-water-actions{display:flex;gap:4px;flex-wrap:wrap;margin-top:6px}
349
+ .owc-water-actions button{white-space:normal;text-align:left;font-size:11px}
350
+ .owc-water-due{background:color-mix(in srgb,var(--owc-accent) 9%,transparent);border-radius:0 0 18px 18px}
351
+ .owc-water-summary{font-size:11px!important;text-align:left;white-space:normal}
352
+ .owc-water input[type=number]{max-width:100px}
353
+ .owc-water .owc-check{margin-top:8px}
354
+ .owc-water-confirm{padding:8px;border:1px solid var(--owc-accent);border-radius:8px}
355
+ .owc-water-head{display:flex;align-items:center;justify-content:space-between;gap:8px}
356
+ .owc-water-head strong{display:flex;align-items:center;gap:5px;font-size:12px}
357
+ .owc-water-quiet{margin:0!important;align-self:center;flex:1;min-width:90px}
358
+ .owc-water-quiet summary{cursor:pointer;font-size:11px;padding:6px}
359
+ .owc-water-quiet[open]{flex-basis:100%}
360
+ .owc-water-overview{padding:12px 17px}
361
+ .owc-water-overview .owc-water-status{font-size:12px;font-variant-numeric:tabular-nums;margin:7px 0}
362
+ .owc-water-meta{display:flex;justify-content:space-between;gap:8px;color:var(--dsw-alias-label-secondary);font-size:11px}
363
+ .owc-water-form{border-top:0;padding:4px 17px 16px}
364
+ .owc-water-toggle{display:flex;align-items:center;justify-content:space-between;padding:10px 0;font-weight:600}
365
+ .owc-water-toggle input{width:18px;height:18px;accent-color:var(--owc-accent)}
366
+ .owc-water-presets{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;margin:8px 0 14px}
367
+ .owc-water-presets button{border:1px solid color-mix(in srgb,var(--dsw-alias-label-primary) 12%,transparent);border-radius:10px;padding:8px 4px}
368
+ .owc-water-presets button[aria-pressed=true]{background:color-mix(in srgb,var(--owc-accent) 12%,transparent);color:var(--owc-accent);border-color:var(--owc-accent)}
369
+ .owc-water-form input[type=number]{max-width:none}
370
+ .owc-water-form fieldset{border:0;padding:0;margin:0;min-width:0}
371
+ .owc-water-form fieldset:disabled{opacity:.5}
372
+ .owc-water-form .owc-save{margin:12px 0}
373
+ .owc-water-form details{margin-top:14px;padding-top:10px;border-top:1px solid color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent)}
374
+ .owc-pill-salary{display:flex;align-items:center;gap:3px}
375
+ .owc-pill-water{font-size:11px!important;gap:4px;white-space:nowrap;font-variant-numeric:tabular-nums}
376
+ .owc-water-summary{justify-content:flex-start!important}
215
377
  @media(max-width:420px){.owc-pill{min-width:136px;padding:8px 8px 7px 12px}.owc-pill-status{display:none}.owc-pill-time{font-size:22px}.owc-main,.owc-form{padding-left:14px;padding-right:14px}.owc-time{font-size:31px}}
216
- `;
378
+ ` +
379
+ COMPANION_STYLE +
380
+ PET_STYLE +
381
+ PET_INTERACTION_STYLE +
382
+ PET_SURFACE_STYLE;
217
383
  const ICONS = {
384
+ water: [
385
+ ['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
+ ],
218
387
  eye: [
219
388
  ['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' }],
220
389
  ['circle', { cx: 8, cy: 8, r: 2.05 }],
@@ -264,7 +433,10 @@ window.__ModuleLoader__.load({
264
433
  apply(ctx) {
265
434
  for (const [lang, dict] of Object.entries(messages))
266
435
  ctx.effect(() => ctx.locale.register('offwork-clock', lang, dict));
267
- const viewKey = 'dsh.offwork-clock.view.v4';
436
+ const viewKey = 'dsh.offwork-clock.view.v7';
437
+ const previousViewKey = 'dsh.offwork-clock.view.v6';
438
+ const olderViewKey = 'dsh.offwork-clock.view.v5';
439
+ const oldestViewKey = 'dsh.offwork-clock.view.v4';
268
440
  const legacyKey = 'dsh.offwork-clock.view.v3';
269
441
 
270
442
  function Clock(props) {
@@ -279,27 +451,154 @@ window.__ModuleLoader__.load({
279
451
  const sessionId = sessionIds.includes(sessionChoice) ? sessionChoice : sessionIds[0];
280
452
  const [data, setData] = React.useState(null);
281
453
  const [draft, setDraft] = React.useState(null);
282
- const [editing, setEditing] = React.useState(false);
454
+ const [ui, dispatchUI] = React.useReducer(petUIReducer, initialPetUI);
455
+ const editing = ui.screen === 'schedule' || ui.screen === 'appearance';
456
+ const appearanceOnly = ui.screen === 'appearance';
457
+ const waterEditing = ui.screen === 'water';
458
+ const [appearanceDraft, setAppearanceDraft] = React.useState(null);
459
+ const appearanceBase = React.useRef(null);
460
+ const appearanceWasPet = React.useRef(false);
461
+ const salaryBase = React.useRef(null);
462
+ const menuBox = React.useRef(null);
463
+ const menuTrigger = React.useRef(null);
464
+ const [dragPosition, setDragPosition] = React.useState(null);
283
465
  const [busy, setBusy] = React.useState(false);
284
466
  const [error, setError] = React.useState('');
285
467
  const [notice, setNotice] = React.useState('');
286
468
  const [storageError, setStorageError] = React.useState(false);
287
469
  const [now, setNow] = React.useState(Date.now());
288
- const [view, setView] = React.useState(normalizeView(null));
470
+ const [savedView, setView] = React.useState(normalizeView(null));
471
+ const view =
472
+ appearanceOnly && appearanceDraft ? { ...savedView, ...appearanceDraft } : savedView;
289
473
  const [ready, setReady] = React.useState(false);
290
474
  const box = React.useRef(null);
475
+ const petPanel = React.useRef(null);
476
+ const cardContent = React.useRef(null);
477
+ const menuContent = React.useRef(null);
478
+ const toastContent = React.useRef(null);
479
+ const [toastHeight, setToastHeight] = React.useState(48);
480
+ const [cardMeasure, setCardMeasure] = React.useState({ key: '', height: 320 });
481
+ const [menuMeasure, setMenuMeasure] = React.useState({ width: 0, height: 260 });
482
+ const panelPlacement = React.useRef(null);
483
+ const petDragged = React.useRef(false);
484
+ const petOpen = ui.screen !== null;
485
+ const petDetails = ui.screen === 'income';
486
+ const [classicSize, setClassicSize] = React.useState({ width: 340, height: 240 });
487
+ const [viewport, setViewport] = React.useState(() => petViewport(window));
291
488
  const dragState = React.useRef(null);
292
489
  const lastPosition = React.useRef(null);
293
490
  const generation = React.useRef(0);
294
491
  const requestBusy = React.useRef(false);
295
492
  const focusReturn = React.useRef(null);
493
+ const [water, setWater] = React.useState(defaultHydration);
494
+ const [waterReady, setWaterReady] = React.useState(false);
495
+ const [waterError, setWaterError] = React.useState(false);
496
+ const [waterBusy, setWaterBusy] = React.useState(false);
497
+ const [waterVisible, setWaterVisible] = React.useState(!document.hidden);
498
+ const [waterInterval, setWaterInterval] = React.useState('60');
499
+ const [waterQuickOpen, setWaterQuickOpen] = React.useState(false);
500
+ const waterQuickTrigger = React.useRef(null);
501
+ const [waterEnabledDraft, setWaterEnabledDraft] = React.useState(false);
502
+ const waterPending = React.useRef(false);
503
+ const manualAttempt = React.useRef(null);
504
+ const waterPreferencesBase = React.useRef(null);
505
+ const editorWasOpen = React.useRef(false);
506
+ const previousPetMode = React.useRef(null);
507
+ const [waterInvalid, setWaterInvalid] = React.useState(false);
508
+ const [waterConfirm, setWaterConfirm] = React.useState(null);
509
+ const [waterNotice, setWaterNotice] = React.useState(false);
510
+ const reactionState = React.useRef(null);
511
+ const petMotion = React.useRef(null);
512
+ const petReactionToken = React.useRef(null);
513
+ const waterRuntime = React.useRef(null);
514
+ const waterOwner = React.useRef(null);
515
+ if (!waterOwner.current)
516
+ waterOwner.current =
517
+ window.crypto?.randomUUID?.() || String(Math.random()) + Date.now();
518
+ 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
+ };
296
524
 
297
525
  const settings = data?.settings;
298
526
  const workingDraft = draft || settings;
299
527
  const state = settings ? safeCalculate(settings) : null;
300
528
  const preview = workingDraft ? safeCalculate(workingDraft) : null;
301
529
  const safeState = state && !state.error ? state : null;
302
- const collapsed = !!safeState && !editing && view.mini && !error && !state?.error;
530
+ const petMode =
531
+ ready &&
532
+ data?.configured &&
533
+ !!safeState &&
534
+ (appearanceOnly ? appearanceWasPet.current : view.companion);
535
+ const stageModel = appearanceOnly ? savedView.petModel : view.petModel;
536
+ const petGeometry = petLayout(
537
+ dragPosition ?? view.petPosition,
538
+ viewport,
539
+ appearanceOnly ? savedView.petSize : view.petSize,
540
+ 420,
541
+ stageModel,
542
+ );
543
+ const surfaceScreen = ui.screen || 'reminder';
544
+ const surfaceWidth = petSurfaceWidth(surfaceScreen, viewport);
545
+ const measureKey = [surfaceScreen, surfaceWidth, locale.active].join(':');
546
+ const measured = cardMeasure.key === measureKey;
547
+ const naturalHeight = measured ? cardMeasure.height : 320;
548
+ const anchor = {
549
+ x: petGeometry.x,
550
+ y: petGeometry.y,
551
+ width: petGeometry.petWidth,
552
+ height: petGeometry.petHeight,
553
+ };
554
+ const placementKey = [
555
+ measureKey,
556
+ viewport.width,
557
+ viewport.height,
558
+ viewport.left,
559
+ viewport.top,
560
+ (dragPosition ?? savedView.petPosition)?.x,
561
+ (dragPosition ?? savedView.petPosition)?.y,
562
+ ].join(':');
563
+ const previousPlacement =
564
+ panelPlacement.current?.key === placementKey ? panelPlacement.current.panel : null;
565
+ const floatingPanel = placePetSurface(anchor, viewport, {
566
+ width: surfaceWidth,
567
+ height: naturalHeight,
568
+ previous: previousPlacement,
569
+ });
570
+ if (!petMode || !petOpen) panelPlacement.current = null;
571
+ else if (measured) panelPlacement.current = { key: placementKey, panel: floatingPanel };
572
+ const menuWidth = petSurfaceWidth('menu', viewport);
573
+ const menuLayout = placePetSurface(anchor, viewport, {
574
+ width: menuWidth,
575
+ height: menuMeasure.width === menuWidth ? menuMeasure.height : 260,
576
+ kind: 'menu',
577
+ point: ui.menu,
578
+ });
579
+ const alertRoom = Math.max(
580
+ anchor.x - viewport.left - 24,
581
+ viewport.left + viewport.width - anchor.x - anchor.width - 24,
582
+ );
583
+ const waterAlertLayout = placePetSurface(anchor, viewport, {
584
+ width: Math.min(140, Math.max(110, alertRoom)),
585
+ height: 34,
586
+ kind: 'menu',
587
+ obstacles: [...(petOpen ? [floatingPanel] : []), ...(ui.menu ? [menuLayout] : [])],
588
+ });
589
+ const toastLayout = placePetSurface(anchor, viewport, {
590
+ width: Math.min(224, viewport.width - 24),
591
+ height: toastHeight,
592
+ kind: 'menu',
593
+ });
594
+ const collapsed =
595
+ !petMode &&
596
+ !!safeState &&
597
+ !editing &&
598
+ !waterEditing &&
599
+ view.mini &&
600
+ !error &&
601
+ !state?.error;
303
602
  const seconds = safeState
304
603
  ? safeState.status === 'before'
305
604
  ? safeState.startRemaining
@@ -318,10 +617,23 @@ window.__ModuleLoader__.load({
318
617
  );
319
618
  const money = (value) => (view.masked ? '••••' : formatMoney.format(value));
320
619
  const moneyText = safeState ? money(safeState.earned) : '—';
321
- const cardStyle = Object.assign(
322
- { '--owc-alpha': String(view.opacity) },
323
- view.position ? { left: view.position.x, top: view.position.y, right: 'auto' } : {},
620
+ const classicPosition = clampPosition(
621
+ view.position ?? {
622
+ x: viewport.left + viewport.width - classicSize.width - 24,
623
+ y: viewport.top + 96,
624
+ },
625
+ viewport,
626
+ classicSize,
324
627
  );
628
+ const cardStyle = {
629
+ '--owc-alpha': String(view.opacity),
630
+ '--owc-viewport-height': Math.max(1, viewport.height - 24) + 'px',
631
+ left: classicPosition.x,
632
+ top: classicPosition.y,
633
+ right: 'auto',
634
+ maxWidth: Math.max(1, viewport.width - 24),
635
+ maxHeight: Math.max(1, viewport.height - 24),
636
+ };
325
637
 
326
638
  function safeCalculate(value) {
327
639
  try {
@@ -339,20 +651,20 @@ window.__ModuleLoader__.load({
339
651
 
340
652
  const clamp = React.useCallback(
341
653
  (position) =>
342
- clampPosition(
343
- position,
344
- { width: window.innerWidth, height: window.innerHeight },
345
- {
346
- width: box.current?.offsetWidth || 300,
347
- height: box.current?.offsetHeight || 200,
348
- },
349
- ),
654
+ clampPosition(position, petViewport(window), {
655
+ width: box.current?.offsetWidth || 300,
656
+ height: box.current?.offsetHeight || 200,
657
+ }),
350
658
  [],
351
659
  );
352
660
 
353
661
  React.useEffect(() => {
354
662
  try {
355
- const saved = localStorage.getItem(viewKey);
663
+ const saved =
664
+ localStorage.getItem(viewKey) ??
665
+ localStorage.getItem(previousViewKey) ??
666
+ localStorage.getItem(olderViewKey) ??
667
+ localStorage.getItem(oldestViewKey);
356
668
  const legacy = !saved ? JSON.parse(localStorage.getItem(legacyKey) || 'null') : null;
357
669
  setView(
358
670
  normalizeView(
@@ -378,11 +690,310 @@ window.__ModuleLoader__.load({
378
690
  React.useEffect(() => {
379
691
  if (!ready) return;
380
692
  try {
381
- localStorage.setItem(viewKey, JSON.stringify(normalizeView(view)));
693
+ localStorage.setItem(viewKey, JSON.stringify(normalizeView(savedView)));
382
694
  } catch {
383
695
  setStorageError(true);
384
696
  }
385
- }, [ready, view]);
697
+ }, [ready, savedView]);
698
+
699
+ React.useEffect(() => {
700
+ let active = true;
701
+ let running = 0;
702
+ let failed = false;
703
+ let store;
704
+ let channel;
705
+ try {
706
+ store = createHydrationStore(window.indexedDB);
707
+ } catch {
708
+ store = createHydrationStore(null);
709
+ }
710
+ try {
711
+ channel = new BroadcastChannel('dsh.offwork-clock.hydration.v1');
712
+ } 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
+ const update = async (action = { type: 'tick' }, automatic = false) => {
723
+ if (!active || (automatic && (running || failed))) return;
724
+ running++;
725
+ try {
726
+ if (action.type === 'retry') {
727
+ store.close();
728
+ try {
729
+ store = createHydrationStore(window.indexedDB);
730
+ } catch {
731
+ store = createHydrationStore(null);
732
+ }
733
+ }
734
+ let changed = false;
735
+ let recorded = false;
736
+ let quietChanged = false;
737
+ let usedPermit = null;
738
+ const result = await store.update(
739
+ (current) => {
740
+ const time = Date.now();
741
+ const context = contextAt(time);
742
+ 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
+ );
755
+ recorded =
756
+ ['drink', 'manual-drink'].includes(action.type) &&
757
+ next.shift?.count >
758
+ (current.shift?.id === next.shift?.id ? (current.shift?.count ?? 0) : 0);
759
+ quietChanged =
760
+ ['quiet', 'end-quiet'].includes(action.type) &&
761
+ next.shift?.quietUntil !== current.shift?.quietUntil;
762
+ if (automatic) {
763
+ const allow = !document.hidden && !waterInputs.current.editing;
764
+ next = transitionHydration(
765
+ next,
766
+ context,
767
+ { type: allow ? 'claim' : 'release' },
768
+ time,
769
+ waterOwner.current,
770
+ );
771
+ }
772
+ if (
773
+ permit &&
774
+ context &&
775
+ permit.schedule === context.schedule &&
776
+ next.shift?.schedule === context.schedule
777
+ )
778
+ usedPermit = permit;
779
+ changed = JSON.stringify(next) !== JSON.stringify(current);
780
+ return next;
781
+ },
782
+ { reset: action.type === 'reset' },
783
+ );
784
+ if (!active) return;
785
+ if (
786
+ action.type === 'reset' ||
787
+ (usedPermit && waterScheduleChange.current === usedPermit)
788
+ )
789
+ waterScheduleChange.current = null;
790
+ failed = false;
791
+ setWater(result);
792
+ setWaterReady(true);
793
+ setWaterError(false);
794
+ if (changed || action.type === 'reset') {
795
+ try {
796
+ channel?.postMessage('changed');
797
+ } catch {}
798
+ }
799
+ return { state: result, recorded, quietChanged };
800
+ } catch (waterFailure) {
801
+ console.warn(
802
+ 'Water reminder storage failed:',
803
+ waterFailure?.name,
804
+ waterFailure?.message,
805
+ );
806
+ if (active) {
807
+ failed = true;
808
+ setWaterError(true);
809
+ setWaterReady(true);
810
+ }
811
+ } finally {
812
+ running--;
813
+ }
814
+ };
815
+ const tick = () => {
816
+ void update({ type: 'tick' }, true);
817
+ };
818
+ const visibility = () => {
819
+ setWaterVisible(!document.hidden);
820
+ setNow(Date.now());
821
+ if (document.hidden) void update({ type: 'release' });
822
+ else tick();
823
+ };
824
+ const pageHide = () => {
825
+ setWaterVisible(false);
826
+ void update({ type: 'release' });
827
+ };
828
+ waterRuntime.current = { update, tick };
829
+ if (channel)
830
+ channel.onmessage = () => {
831
+ void update({ type: 'sync' });
832
+ };
833
+ const timer = setInterval(tick, 2000);
834
+ document.addEventListener('visibilitychange', visibility);
835
+ window.addEventListener('pageshow', visibility);
836
+ window.addEventListener('pagehide', pageHide);
837
+ window.addEventListener('focus', tick);
838
+ tick();
839
+ return () => {
840
+ active = false;
841
+ waterRuntime.current = null;
842
+ clearInterval(timer);
843
+ document.removeEventListener('visibilitychange', visibility);
844
+ window.removeEventListener('pageshow', visibility);
845
+ window.removeEventListener('pagehide', pageHide);
846
+ window.removeEventListener('focus', tick);
847
+ channel?.close();
848
+ void store
849
+ .update((current) => {
850
+ if (current.shift?.claim?.owner === waterOwner.current)
851
+ current.shift.claim = null;
852
+ return current;
853
+ })
854
+ .catch(() => {})
855
+ .finally(() => store.close());
856
+ };
857
+ }, []);
858
+
859
+ React.useEffect(() => {
860
+ waterRuntime.current?.tick();
861
+ }, [settings, editing, waterEditing, error]);
862
+
863
+ React.useEffect(() => {
864
+ if (!waterNotice) return;
865
+ const timer = setTimeout(() => setWaterNotice(false), 5000);
866
+ return () => clearTimeout(timer);
867
+ }, [waterNotice]);
868
+
869
+ React.useEffect(() => {
870
+ if (!waterQuickOpen || !collapsed) return;
871
+ const closeOutside = (event) => {
872
+ if (!box.current?.contains(event.target)) setWaterQuickOpen(false);
873
+ };
874
+ document.addEventListener('pointerdown', closeOutside);
875
+ return () => document.removeEventListener('pointerdown', closeOutside);
876
+ }, [waterQuickOpen, collapsed]);
877
+
878
+ function toggleWaterQuick(event) {
879
+ waterQuickTrigger.current = event.currentTarget;
880
+ setWaterQuickOpen((open) => !open);
881
+ }
882
+
883
+ function closeWaterQuick() {
884
+ setWaterQuickOpen(false);
885
+ (
886
+ (waterQuickTrigger.current?.isConnected ? waterQuickTrigger.current : null) ||
887
+ box.current?.querySelector('.owc-pill-water') ||
888
+ box.current?.querySelector('.owc-companion-trigger') ||
889
+ box.current?.querySelector('.owc-grip')
890
+ )?.focus();
891
+ }
892
+
893
+ function openWaterEditor(event) {
894
+ navigateScreen('water', event);
895
+ }
896
+
897
+ function closeWaterEditor() {
898
+ if (waterPending.current) return;
899
+ performNavigation(ui.back);
900
+ }
901
+
902
+ async function saveWaterSettings(event) {
903
+ event.preventDefault();
904
+ const interval = Number(waterInterval);
905
+ if (!Number.isInteger(interval) || interval < 5 || interval > 240) {
906
+ setWaterInvalid(true);
907
+ return;
908
+ }
909
+ const result = await waterAction({
910
+ type: 'preferences',
911
+ ...(waterEnabledDraft !== waterPreferencesBase.current?.enabled
912
+ ? { enabled: waterEnabledDraft }
913
+ : {}),
914
+ ...(interval !== waterPreferencesBase.current?.interval ? { interval } : {}),
915
+ });
916
+ if (result) performNavigation(ui.back);
917
+ return result;
918
+ }
919
+
920
+ async function waterAction(action) {
921
+ if (waterPending.current) return;
922
+ waterPending.current = true;
923
+ setWaterBusy(true);
924
+ setWaterNotice(false);
925
+ const actionFocus = document.activeElement;
926
+ const hadFocus = box.current?.querySelector('.owc-water-due')?.contains(actionFocus);
927
+ try {
928
+ const outcome = await waterRuntime.current?.update(action);
929
+ const result = outcome?.state;
930
+ if (result) {
931
+ setWaterConfirm(null);
932
+ setNow(Date.now());
933
+ if (action.type === 'preferences')
934
+ setWaterNotice({ key: 'waterChanged', kind: 'notice' });
935
+ if (action.type === 'drink' && outcome.recorded)
936
+ setWaterNotice({ key: 'waterRecorded', kind: 'drink', token: result.revision });
937
+ if (action.type === 'manual-drink') {
938
+ setWaterNotice({
939
+ key: result.shift?.recentActions.includes(action.actionId)
940
+ ? 'waterRecorded'
941
+ : 'waterActionStale',
942
+ kind: outcome.recorded ? 'drink' : 'notice',
943
+ token: result.revision,
944
+ });
945
+ manualAttempt.current = null;
946
+ }
947
+ if (action.type === 'quiet')
948
+ setWaterNotice({
949
+ key:
950
+ outcome.quietChanged && result.shift?.quietUntil > Date.now()
951
+ ? 'waterQuietStarted'
952
+ : 'waterActionStale',
953
+ kind: 'notice',
954
+ });
955
+ if (action.type === 'end-quiet')
956
+ setWaterNotice({
957
+ key: outcome.quietChanged ? 'waterQuietEnded' : 'waterActionStale',
958
+ kind: 'notice',
959
+ });
960
+ if (action.type === 'reset') {
961
+ manualAttempt.current = null;
962
+ setWaterInterval('60');
963
+ setWaterEnabledDraft(false);
964
+ setWaterInvalid(false);
965
+ }
966
+ if (
967
+ hadFocus ||
968
+ ['manual-drink', 'end-quiet', 'clear-count', 'reset'].includes(action.type)
969
+ ) {
970
+ requestAnimationFrame(() => {
971
+ const active = document.activeElement;
972
+ if (active !== actionFocus && active?.tagName !== 'BODY') return;
973
+ const target = hadFocus
974
+ ? petMode
975
+ ? '.owc-pet-handle'
976
+ : '.owc-grip,.owc-title'
977
+ : action.type === 'manual-drink'
978
+ ? petMode
979
+ ? '.owc-pet-water-row .owc-primary'
980
+ : '.owc-water-actions .owc-primary'
981
+ : action.type === 'end-quiet'
982
+ ? petMode
983
+ ? '.owc-pet-quiet-row button'
984
+ : '.owc-water-quiet summary'
985
+ : '.owc-water-form details > summary';
986
+ box.current?.querySelector(target)?.focus();
987
+ });
988
+ }
989
+ waterRuntime.current?.tick();
990
+ }
991
+ return result;
992
+ } finally {
993
+ waterPending.current = false;
994
+ setWaterBusy(false);
995
+ }
996
+ }
386
997
 
387
998
  React.useEffect(() => {
388
999
  if (sessionId) reload();
@@ -411,27 +1022,61 @@ window.__ModuleLoader__.load({
411
1022
  };
412
1023
  }, []);
413
1024
 
414
- React.useEffect(() => {
415
- const resize = () =>
416
- setView((current) =>
417
- current.position ? { ...current, position: clamp(current.position) } : current,
1025
+ React.useLayoutEffect(() => {
1026
+ const resize = () => {
1027
+ const rect = box.current?.getBoundingClientRect();
1028
+ const nextViewport = petViewport(window);
1029
+ setViewport((current) =>
1030
+ Object.keys(nextViewport).every((key) => current[key] === nextViewport[key])
1031
+ ? current
1032
+ : nextViewport,
418
1033
  );
1034
+ if (!rect || petMode) return;
1035
+ const width = box.current.offsetWidth,
1036
+ height = box.current.offsetHeight;
1037
+ setClassicSize((current) =>
1038
+ current.width === width && current.height === height ? current : { width, height },
1039
+ );
1040
+ };
419
1041
  const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(resize) : null;
420
1042
  if (box.current) ro?.observe(box.current);
1043
+ resize();
421
1044
  window.addEventListener('resize', resize);
1045
+ window.visualViewport?.addEventListener('resize', resize);
1046
+ window.visualViewport?.addEventListener('scroll', resize);
422
1047
  return () => {
423
1048
  ro?.disconnect();
424
1049
  window.removeEventListener('resize', resize);
1050
+ window.visualViewport?.removeEventListener('resize', resize);
1051
+ window.visualViewport?.removeEventListener('scroll', resize);
425
1052
  };
426
- }, [clamp]);
1053
+ }, [clamp, petMode]);
427
1054
 
428
1055
  React.useEffect(() => {
429
- if (!editing) {
430
- focusReturn.current?.focus?.();
1056
+ if (ui.pending) {
1057
+ box.current?.querySelector('[data-keep-editing]')?.focus();
1058
+ return;
1059
+ }
1060
+ const wasOpen = editorWasOpen.current;
1061
+ const modeChanged = previousPetMode.current !== petMode;
1062
+ previousPetMode.current = petMode;
1063
+ editorWasOpen.current = editing || waterEditing;
1064
+ if (editing && modeChanged) {
1065
+ box.current?.querySelector('input[name=companion]')?.focus();
431
1066
  return;
432
1067
  }
433
- box.current?.querySelector('form input,form select')?.focus?.();
434
- }, [editing]);
1068
+ if (!editing && !waterEditing) {
1069
+ if (!wasOpen) return;
1070
+ if (focusReturn.current?.isConnected) focusReturn.current.focus?.();
1071
+ else if (petMode) box.current?.querySelector('.owc-pet-handle')?.focus?.();
1072
+ else if (waterQuickOpen)
1073
+ box.current?.querySelector('[data-water-quick] .owc-text')?.focus?.();
1074
+ else box.current?.querySelector('.owc-grip,.owc-title')?.focus?.();
1075
+ focusReturn.current = null;
1076
+ return;
1077
+ }
1078
+ box.current?.querySelector('form input,form select,.owc-appearance input')?.focus?.();
1079
+ }, [editing, waterEditing, petMode, ui.screen, ui.pending]);
435
1080
 
436
1081
  React.useEffect(
437
1082
  () => () => {
@@ -457,12 +1102,22 @@ window.__ModuleLoader__.load({
457
1102
  setBusy(true);
458
1103
  setError('');
459
1104
  try {
1105
+ const baseSchedule = (await waterRuntime.current?.update({ type: 'sync' }))?.state
1106
+ ?.shift?.schedule;
1107
+ if (token !== generation.current) return;
460
1108
  const next = await request('get');
461
1109
  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
+ };
462
1116
  setData(next);
463
1117
  if (!next.configured) {
464
1118
  setDraft({ ...next.settings });
465
- setEditing(true);
1119
+ dispatchUI({ type: 'navigate', screen: 'schedule', discard: true });
1120
+ salaryBase.current = next.settings;
466
1121
  }
467
1122
  if (explicit) setNotice(t('synced'));
468
1123
  } catch (e) {
@@ -480,12 +1135,24 @@ window.__ModuleLoader__.load({
480
1135
  setError('');
481
1136
  const token = ++generation.current;
482
1137
  try {
483
- const next = await request('set', changedSettings(settings, draft));
1138
+ const baseSchedule = (await waterRuntime.current?.update({ type: 'sync' }))?.state
1139
+ ?.shift?.schedule;
1140
+ if (token !== generation.current) return;
1141
+ const next = await request(
1142
+ 'set',
1143
+ changedSettings(salaryBase.current ?? settings, draft),
1144
+ );
484
1145
  if (token === generation.current) {
1146
+ waterScheduleChange.current = {
1147
+ baseSchedule,
1148
+ schedule: hydrationContext(next.settings, calculate(next.settings, Date.now()))
1149
+ .schedule,
1150
+ };
485
1151
  setData(next);
486
- setEditing(false);
1152
+ dispatchUI({ type: 'navigate', screen: ui.back, discard: true });
487
1153
  setDraft(null);
488
1154
  setNotice(t('saved'));
1155
+ return next;
489
1156
  }
490
1157
  } catch (e) {
491
1158
  if (token === generation.current) setError(errorText(e));
@@ -497,15 +1164,22 @@ window.__ModuleLoader__.load({
497
1164
 
498
1165
  function commitPosition(position) {
499
1166
  const next = clamp(position);
500
- setPreference({ position: next });
1167
+ setPreference(petMode ? { petPosition: next } : { position: next });
501
1168
  lastPosition.current = next;
502
1169
  }
503
1170
  function dragDown(e) {
504
1171
  if (e.button !== 0) return;
1172
+ petDragged.current = false;
505
1173
  if (
506
- e.target.closest?.('button,input,select,label,a,textarea,summary,[contenteditable]')
1174
+ petMode
1175
+ ? !e.target.closest?.('.owc-pet-handle')
1176
+ : e.target.closest?.(
1177
+ 'button,input,select,label,a,textarea,summary,[contenteditable]',
1178
+ )
507
1179
  )
508
1180
  return;
1181
+ const capture = petMode ? e.target.closest('.owc-pet-handle') : e.currentTarget;
1182
+ if (petMode && ui.menu) dispatchUI({ type: 'dismiss-menu' });
509
1183
  const rect = box.current.getBoundingClientRect();
510
1184
  dragState.current = {
511
1185
  dx: e.clientX - rect.left,
@@ -513,17 +1187,23 @@ window.__ModuleLoader__.load({
513
1187
  x0: e.clientX,
514
1188
  y0: e.clientY,
515
1189
  moved: false,
1190
+ capture,
516
1191
  };
517
1192
  try {
518
- e.currentTarget.setPointerCapture?.(e.pointerId);
1193
+ capture.setPointerCapture?.(e.pointerId);
519
1194
  } catch {}
520
1195
  }
521
1196
  function dragMove(e) {
522
1197
  const d = dragState.current;
523
1198
  if (!d) return;
524
- if (!d.moved && Math.abs(e.clientX - d.x0) + Math.abs(e.clientY - d.y0) < 4) return;
1199
+ if (!d.moved && Math.hypot(e.clientX - d.x0, e.clientY - d.y0) < 6) return;
525
1200
  d.moved = true;
1201
+ petDragged.current = true;
526
1202
  const next = clamp({ x: e.clientX - d.dx, y: e.clientY - d.dy });
1203
+ if (petMode) {
1204
+ setDragPosition(next);
1205
+ petMotion.current?.drag(e.clientX - d.x0);
1206
+ }
527
1207
  lastPosition.current = next;
528
1208
  if (box.current) {
529
1209
  box.current.style.left = next.x + 'px';
@@ -535,16 +1215,20 @@ window.__ModuleLoader__.load({
535
1215
  const d = dragState.current;
536
1216
  if (!d) return;
537
1217
  dragState.current = null;
1218
+ if (petMode && d.moved) petMotion.current?.release(e.type !== 'pointerup');
1219
+ if (e.type === 'pointercancel' || e.type === 'lostpointercapture')
1220
+ petDragged.current = false;
538
1221
  if (d.moved && lastPosition.current) commitPosition(lastPosition.current);
1222
+ setDragPosition(null);
539
1223
  try {
540
- if (e.currentTarget.hasPointerCapture?.(e.pointerId))
541
- e.currentTarget.releasePointerCapture(e.pointerId);
1224
+ if (d.capture?.hasPointerCapture?.(e.pointerId))
1225
+ d.capture.releasePointerCapture(e.pointerId);
542
1226
  } catch {}
543
1227
  }
544
1228
  function gripKeys(e) {
545
1229
  if (e.key === 'Home') {
546
1230
  e.preventDefault();
547
- setPreference({ position: null });
1231
+ setPreference(petMode ? { petPosition: null } : { position: null });
548
1232
  return;
549
1233
  }
550
1234
  if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return;
@@ -556,18 +1240,131 @@ window.__ModuleLoader__.load({
556
1240
  });
557
1241
  }
558
1242
 
559
- function openEditor(e) {
560
- focusReturn.current = e.currentTarget;
561
- setDraft({ ...settings });
562
- setEditing(true);
563
- setError('');
564
- setNotice('');
1243
+ function openAppearanceContext(event) {
1244
+ if (
1245
+ !box.current?.contains(event.target) ||
1246
+ event.target.closest?.(
1247
+ 'textarea,select,input:not([type=checkbox]):not([type=radio]):not([type=range]),[contenteditable=true],[contenteditable=""]',
1248
+ )
1249
+ )
1250
+ return false;
1251
+ event.preventDefault();
1252
+ event.stopPropagation();
1253
+ petDragged.current = false;
1254
+ if (ui.pending) return true;
1255
+ if (!petMode) {
1256
+ if (!busy && !waterBusy) navigateScreen('appearance', event);
1257
+ return true;
1258
+ }
1259
+ const trigger =
1260
+ event.target.closest('button,.owc-title,[data-pet-page-title]') ||
1261
+ event.target
1262
+ .closest('.owc-pet-panel')
1263
+ ?.querySelector('[data-pet-page-title],.owc-head button') ||
1264
+ box.current.querySelector('.owc-pet-handle');
1265
+ menuTrigger.current = menuBox.current?.contains(trigger)
1266
+ ? menuTrigger.current?.isConnected
1267
+ ? menuTrigger.current
1268
+ : box.current?.querySelector('.owc-pet-handle')
1269
+ : trigger;
1270
+ const rect = trigger?.getBoundingClientRect();
1271
+ const point =
1272
+ event.type === 'keydown'
1273
+ ? { x: rect?.right ?? petGeometry.x, y: rect?.top ?? petGeometry.y }
1274
+ : { x: event.clientX, y: event.clientY };
1275
+ dispatchUI({ type: 'menu', position: point });
1276
+ return true;
1277
+ }
1278
+ function appearanceShortcut(event) {
1279
+ if (event.key === 'ContextMenu' || (event.shiftKey && event.key === 'F10'))
1280
+ return openAppearanceContext(event);
1281
+ return false;
1282
+ }
1283
+ function performNavigation(screen, root = false) {
1284
+ if (root) panelPlacement.current = null;
1285
+ if (screen !== ui.screen) {
1286
+ if (screen === 'water') {
1287
+ waterPreferencesBase.current = { enabled: water.enabled, interval: water.interval };
1288
+ setWaterInterval(String(water.interval));
1289
+ setWaterEnabledDraft(water.enabled);
1290
+ setWaterInvalid(false);
1291
+ setWaterConfirm(null);
1292
+ }
1293
+ if (screen === 'schedule') {
1294
+ salaryBase.current = settings;
1295
+ setDraft({ ...settings });
1296
+ setError('');
1297
+ }
1298
+ if (screen === 'appearance') {
1299
+ appearanceBase.current = appearanceValues(savedView);
1300
+ appearanceWasPet.current = !!petMode;
1301
+ setAppearanceDraft(appearanceValues(savedView));
1302
+ }
1303
+ if (ui.screen === 'schedule') {
1304
+ setDraft(null);
1305
+ setError('');
1306
+ }
1307
+ if (ui.screen === 'appearance') setAppearanceDraft(null);
1308
+ }
1309
+ dispatchUI({ type: 'navigate', screen, discard: true, root });
1310
+ setWaterConfirm(null);
1311
+ }
1312
+ function navigateScreen(screen, event) {
1313
+ if (!data?.configured && screen !== 'schedule') return;
1314
+ if (ui.pending || busy || waterBusy || requestBusy.current || waterPending.current)
1315
+ return;
1316
+ const root = event?.currentTarget?.getAttribute?.('role') === 'menuitem';
1317
+ if (event?.currentTarget) focusReturn.current = event.currentTarget;
1318
+ if (screen !== ui.screen && draftChanged()) {
1319
+ dispatchUI({ type: 'navigate', screen, dirty: true, root });
1320
+ return;
1321
+ }
1322
+ performNavigation(screen, root);
1323
+ }
1324
+ function draftChanged() {
1325
+ if (waterEditing && waterPreferencesBase.current)
1326
+ return (
1327
+ waterEnabledDraft !== waterPreferencesBase.current.enabled ||
1328
+ Number(waterInterval) !== waterPreferencesBase.current.interval
1329
+ );
1330
+ if (ui.screen === 'schedule' && draft && salaryBase.current)
1331
+ return Object.keys(changedSettings(salaryBase.current, draft)).length > 0;
1332
+ if (appearanceOnly && appearanceDraft && appearanceBase.current)
1333
+ return (
1334
+ Object.keys(changedSettings(appearanceBase.current, appearanceDraft)).length > 0
1335
+ );
1336
+ return false;
1337
+ }
1338
+ function applyAppearance() {
1339
+ panelPlacement.current = null;
1340
+ if (appearanceDraft)
1341
+ setPreference(changedSettings(appearanceBase.current, appearanceDraft));
1342
+ performNavigation(ui.back);
1343
+ return true;
1344
+ }
1345
+ async function saveAndContinue() {
1346
+ if (!ui.pending || busy || waterBusy) return;
1347
+ const destination = ui.pending.screen;
1348
+ const result = waterEditing
1349
+ ? await saveWaterSettings({ preventDefault() {} })
1350
+ : appearanceOnly
1351
+ ? applyAppearance()
1352
+ : await save({ preventDefault() {} });
1353
+ if (result) performNavigation(destination, ui.pending.root);
1354
+ else dispatchUI({ type: 'stay' });
1355
+ }
1356
+ function updateAppearance(patch) {
1357
+ setAppearanceDraft((current) => ({ ...current, ...patch }));
1358
+ }
1359
+ function openAppearance(event) {
1360
+ navigateScreen('appearance', event);
1361
+ }
1362
+ function openEditor(event) {
1363
+ navigateScreen('schedule', event);
565
1364
  }
566
1365
  function closeEditor() {
567
- if (!data?.configured) return;
568
- setEditing(false);
569
- setDraft(null);
570
- setError('');
1366
+ if ((!data?.configured && !appearanceOnly) || busy) return;
1367
+ performNavigation(ui.back);
571
1368
  }
572
1369
  function update(key, value) {
573
1370
  setDraft((current) => ({ ...current, [key]: value }));
@@ -644,136 +1441,756 @@ window.__ModuleLoader__.load({
644
1441
  t(view.masked ? 'show' : 'hide'),
645
1442
  );
646
1443
 
647
- const strip = () =>
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;
1448
+ const waterPrompt =
1449
+ waterReady &&
1450
+ !waterError &&
1451
+ !editing &&
1452
+ !waterEditing &&
1453
+ waterVisible &&
1454
+ !document.hidden &&
1455
+ waterState.phase === 'due' &&
1456
+ waterClaim?.owner === waterOwner.current &&
1457
+ waterClaim.sequence === water.shift.sequence &&
1458
+ waterClaim.until > Date.now() &&
1459
+ waterClaim.until <= Date.now() + HYDRATION_LEASE_MS;
1460
+ const reactionSurface = [
1461
+ collapsed,
1462
+ editing,
1463
+ waterEditing,
1464
+ view.companion,
1465
+ view.companionMotion,
1466
+ waterVisible,
1467
+ ].join(':');
1468
+ if (reactionState.current?.token !== waterNotice?.token) {
1469
+ reactionState.current = {
1470
+ token: waterNotice?.token,
1471
+ surface: reactionSurface,
1472
+ stopped: ['quiet', 'paused', 'break'].includes(waterState.phase),
1473
+ };
1474
+ } else if (reactionState.current && reactionState.current.surface !== reactionSurface) {
1475
+ reactionState.current.stopped = true;
1476
+ }
1477
+ const companion = companionPresentation({
1478
+ workStatus: safeState?.status,
1479
+ phase: waterState.phase,
1480
+ ownsPrompt: waterPrompt,
1481
+ ready: waterReady && !!safeState,
1482
+ failed: waterError || !!error,
1483
+ feedback: waterNotice || null,
1484
+ visible: view.companion,
1485
+ motion: view.companionMotion && !reactionState.current?.stopped,
1486
+ pageVisible: waterVisible && !document.hidden && !editing && !waterEditing,
1487
+ });
1488
+ React.useLayoutEffect(() => {
1489
+ if (!petMode) return;
1490
+ const artwork = box.current?.querySelector('.owc-pet-handle > .owc-pet-art');
1491
+ if (!artwork) return;
1492
+ const base = createPetMotion(artwork, window, { enabled: false });
1493
+ const controller = createPetVideoMotion(
1494
+ artwork,
1495
+ window,
1496
+ base,
1497
+ PET_CLIPS.models[stageModel],
1498
+ PET_CLIPS.probe,
1499
+ );
1500
+ petMotion.current = controller;
1501
+ return () => {
1502
+ controller.destroy();
1503
+ petMotion.current = null;
1504
+ };
1505
+ }, [petMode, stageModel]);
1506
+ React.useLayoutEffect(() => {
1507
+ petMotion.current?.configure({
1508
+ enabled:
1509
+ savedView.companionMotion &&
1510
+ waterVisible &&
1511
+ waterReady &&
1512
+ !waterError &&
1513
+ !error &&
1514
+ !editing &&
1515
+ !waterEditing &&
1516
+ !ui.menu,
1517
+ resting: ['quiet', 'rest', 'offline'].includes(companion.pose),
1518
+ });
1519
+ }, [
1520
+ petMode,
1521
+ stageModel,
1522
+ savedView.companionMotion,
1523
+ waterVisible,
1524
+ waterReady,
1525
+ waterError,
1526
+ error,
1527
+ editing,
1528
+ waterEditing,
1529
+ !!ui.menu,
1530
+ companion.pose,
1531
+ ]);
1532
+ React.useLayoutEffect(() => {
1533
+ const token = waterNotice?.token;
1534
+ if (petReactionToken.current === token) return;
1535
+ petReactionToken.current = token;
1536
+ if (petMode && companion.animate) petMotion.current?.play('celebrate');
1537
+ }, [petMode, waterNotice?.token, companion.animate]);
1538
+ const companionCard = (compact = false) =>
1539
+ companion.visible &&
648
1540
  h(
649
1541
  'div',
650
- { className: 'owc-pill' },
1542
+ { className: 'owc-companion', 'data-companion': companion.pose },
1543
+ !compact && companionArtwork(h, companion, 52, waterNotice?.token),
651
1544
  h(
652
1545
  'div',
653
- { className: 'owc-pill-top' },
654
- iconSvg('alarm', 16),
1546
+ { className: 'owc-companion-copy' },
655
1547
  h(
656
- 'span',
657
- { className: 'owc-grip', tabIndex: 0, title: t('drag'), onKeyDown: gripKeys },
658
- safeState.status !== 'done' &&
659
- h('span', { className: 'owc-pill-status' }, t(safeState.status)),
660
- h('span', { className: 'owc-pill-time' }, duration),
1548
+ 'div',
1549
+ { className: 'owc-companion-name' },
1550
+ t('companionName'),
1551
+ h('span', null, t('companionLabel')),
1552
+ ),
1553
+ companion.message &&
1554
+ h(
1555
+ 'p',
1556
+ {
1557
+ key: companion.announce
1558
+ ? 'feedback:' + (waterNotice?.token ?? companion.message)
1559
+ : 'ambient',
1560
+ },
1561
+ t(companion.message),
1562
+ ),
1563
+ ),
1564
+ );
1565
+ const waterSummary = () => {
1566
+ if (!waterReady) return t('waterLoading');
1567
+ 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
+ const labels = {
1576
+ off: 'waterOff',
1577
+ waiting: 'waterWaiting',
1578
+ break: 'waterBreak',
1579
+ paused: 'waterPaused',
1580
+ finished: 'waterFinished',
1581
+ due: waterPrompt ? 'waterDue' : 'waterOther',
1582
+ };
1583
+ if (waterState.phase === 'quiet')
1584
+ return t('waterQuiet') + ' · ' + formatDuration(waterState.quietRemaining / 1000);
1585
+ return waterState.phase === 'countdown'
1586
+ ? t('waterNext') + ' · ' + formatDuration(waterState.remaining / 1000)
1587
+ : t(labels[waterState.phase]);
1588
+ };
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;
1596
+ const recordWater = (event) => {
1597
+ if (event.detail > 1) return;
1598
+ if (!manualAttempt.current)
1599
+ manualAttempt.current = {
1600
+ type: 'manual-drink',
1601
+ shiftId: waterContext.id,
1602
+ actionId: window.crypto?.randomUUID?.() || String(Math.random()) + Date.now(),
1603
+ ...(waterState.phase === 'due' ? { eventId: waterState.eventId } : {}),
1604
+ };
1605
+ void waterAction(manualAttempt.current);
1606
+ };
1607
+ const waterControls = () =>
1608
+ h(
1609
+ 'div',
1610
+ { className: 'owc-water-actions' },
1611
+ !waterPrompt &&
1612
+ h(
1613
+ 'button',
1614
+ {
1615
+ type: 'button',
1616
+ className: 'owc-primary',
1617
+ disabled: !waterCanRecord || waterBusy,
1618
+ onClick: recordWater,
1619
+ },
1620
+ t('waterManual'),
661
1621
  ),
1622
+ waterState.phase === 'quiet' &&
662
1623
  h(
663
1624
  'button',
664
1625
  {
665
1626
  type: 'button',
666
- className: 'owc-icon',
667
- style: { marginLeft: 'auto' },
668
- title: t('expand'),
669
- 'aria-label': t('expand'),
670
- onClick: () => setPreference({ mini: false }),
1627
+ disabled: waterBusy || waterError,
1628
+ onClick: () => waterAction({ type: 'end-quiet' }),
671
1629
  },
672
- iconSvg('chevronUp', 14),
1630
+ t('waterQuietEnd'),
673
1631
  ),
674
- ),
1632
+ waterCanQuiet &&
1633
+ h(
1634
+ 'details',
1635
+ { className: 'owc-water-quiet' },
1636
+ h(
1637
+ 'summary',
1638
+ null,
1639
+ t(waterState.phase === 'quiet' ? 'waterQuietAdjust' : 'waterQuiet'),
1640
+ ),
1641
+ h(
1642
+ 'div',
1643
+ { className: 'owc-water-actions' },
1644
+ [30, 60].map((minutes) =>
1645
+ h(
1646
+ 'button',
1647
+ {
1648
+ key: minutes,
1649
+ type: 'button',
1650
+ disabled: waterBusy,
1651
+ onClick: (event) => {
1652
+ const menu = event.currentTarget.closest('details');
1653
+ menu.open = false;
1654
+ menu.querySelector('summary')?.focus();
1655
+ void waterAction({ type: 'quiet', minutes });
1656
+ },
1657
+ },
1658
+ t(minutes === 30 ? 'waterQuiet30' : 'waterQuiet60'),
1659
+ ),
1660
+ ),
1661
+ ),
1662
+ waterState.phase !== 'quiet' &&
1663
+ h('p', { className: 'owc-hint' }, t('waterQuietHint')),
1664
+ ),
1665
+ water.enabled &&
1666
+ h(
1667
+ 'button',
1668
+ {
1669
+ 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' }),
1676
+ },
1677
+ t(water.shift?.paused ? 'waterResume' : 'waterPause'),
1678
+ ),
1679
+ waterError &&
1680
+ h(
1681
+ 'button',
1682
+ {
1683
+ type: 'button',
1684
+ disabled: waterBusy,
1685
+ onClick: () => waterAction({ type: 'retry' }),
1686
+ },
1687
+ t('waterRetry'),
1688
+ ),
1689
+ );
1690
+
1691
+ const waterOverview = (quick = false) =>
1692
+ h(
1693
+ 'div',
1694
+ {
1695
+ className: 'owc-water owc-water-overview',
1696
+ 'data-water-overview': true,
1697
+ 'data-water-quick': quick || undefined,
1698
+ role: quick ? 'region' : undefined,
1699
+ 'aria-label': quick ? t('waterQuick') : undefined,
1700
+ onKeyDown: quick
1701
+ ? (event) => {
1702
+ if (event.key === 'Escape') {
1703
+ event.preventDefault();
1704
+ event.stopPropagation();
1705
+ closeWaterQuick();
1706
+ }
1707
+ }
1708
+ : undefined,
1709
+ },
675
1710
  h(
676
1711
  'div',
677
- { className: 'owc-pill-bottom' },
1712
+ { className: 'owc-water-head' },
1713
+ h('strong', null, iconSvg('water', 14), t('waterTitle')),
1714
+ quick &&
1715
+ h(
1716
+ 'button',
1717
+ { type: 'button', className: 'owc-text', onClick: openAppearance },
1718
+ t('appearanceGroup'),
1719
+ ),
678
1720
  h(
679
- 'span',
1721
+ 'button',
680
1722
  {
681
- className: 'owc-pill-money',
682
- 'aria-label': view.masked ? t('masked') : moneyText,
1723
+ type: 'button',
1724
+ className: 'owc-text',
1725
+ style: { marginLeft: 'auto' },
1726
+ 'aria-label': t('waterEdit'),
1727
+ disabled: !waterReady || !data?.configured,
1728
+ onClick: openWaterEditor,
683
1729
  },
684
- moneyText,
1730
+ t('settings'),
685
1731
  ),
686
- h(
687
- 'div',
688
- { className: 'owc-pill-actions' },
1732
+ quick &&
689
1733
  h(
690
1734
  'button',
691
1735
  {
692
1736
  type: 'button',
693
1737
  className: 'owc-icon',
694
- title: t(view.masked ? 'show' : 'hide'),
695
- 'aria-label': t(view.masked ? 'show' : 'hide'),
696
- onClick: () => setPreference({ masked: !view.masked }),
1738
+ 'aria-label': t('waterQuickClose'),
1739
+ title: t('waterQuickClose'),
1740
+ onClick: closeWaterQuick,
697
1741
  },
698
- iconSvg(view.masked ? 'eyeOff' : 'eye', 14),
1742
+ iconSvg('close', 14),
699
1743
  ),
1744
+ ),
1745
+ quick && companionCard(true),
1746
+ !waterPrompt &&
1747
+ h(
1748
+ 'p',
1749
+ { className: 'owc-water-status', 'data-water-status': true },
1750
+ waterSummary(),
1751
+ ),
1752
+ h(
1753
+ 'div',
1754
+ { className: 'owc-water-meta' },
1755
+ h(
1756
+ 'span',
1757
+ { 'data-water-effective': water.interval },
1758
+ t('waterEvery') + ' ' + water.interval + ' ' + t('waterMinutes'),
700
1759
  ),
1760
+ h('span', { 'data-water-count': true }, t('waterCount') + ': ' + waterState.count),
701
1761
  ),
1762
+ waterControls(),
1763
+ !['working', 'lunch'].includes(waterContext?.status) &&
1764
+ h('p', { className: 'owc-hint' }, t('waterRecordHint')),
1765
+ waterState.phase === 'quiet' &&
1766
+ h('p', { className: 'owc-hint' }, t('waterQuietHint')),
1767
+ waterNotice && !view.companion && h('p', null, t(waterNotice.key)),
702
1768
  );
703
1769
 
704
- const panelHead = () =>
1770
+ const waterSettings = () =>
705
1771
  h(
706
- 'div',
707
- { className: 'owc-head' },
708
- h(
709
- 'span',
710
- {
711
- className: 'owc-title',
712
- tabIndex: 0,
713
- title: t('drag'),
714
- onKeyDown: gripKeys,
1772
+ 'form',
1773
+ {
1774
+ className: 'owc-water owc-water-form',
1775
+ 'data-water-settings': true,
1776
+ onSubmit: saveWaterSettings,
1777
+ noValidate: true,
1778
+ onKeyDown: (event) => {
1779
+ if (event.key === 'Escape') {
1780
+ event.preventDefault();
1781
+ navigateScreen(ui.back);
1782
+ }
715
1783
  },
716
- editing ? t(data?.configured ? 'settings' : 'setup') : t('title'),
1784
+ },
1785
+ h(
1786
+ 'label',
1787
+ { className: 'owc-water-toggle' },
1788
+ h('span', null, t('waterEnable')),
1789
+ h('input', {
1790
+ type: 'checkbox',
1791
+ checked: waterEnabledDraft,
1792
+ disabled: !waterReady || waterError || waterBusy || !data?.configured,
1793
+ onChange: (event) => {
1794
+ setWaterEnabledDraft(event.target.checked);
1795
+ setWaterNotice(false);
1796
+ },
1797
+ }),
717
1798
  ),
718
- editing
719
- ? data?.configured &&
720
- h(
721
- 'button',
722
- {
723
- type: 'button',
724
- className: 'owc-icon',
725
- title: t('cancel'),
726
- 'aria-label': t('cancel'),
727
- onClick: closeEditor,
728
- disabled: busy,
729
- },
730
- iconSvg('close'),
731
- )
732
- : h(
733
- React.Fragment,
734
- null,
735
- h(
736
- 'button',
737
- {
738
- type: 'button',
739
- className: 'owc-icon',
740
- title: t('settings'),
741
- 'aria-label': t('settings'),
742
- onClick: openEditor,
743
- disabled: busy,
744
- },
745
- iconSvg('sliders'),
746
- ),
1799
+ h(
1800
+ 'fieldset',
1801
+ { disabled: !waterReady || waterError || waterBusy || !data?.configured },
1802
+ h('legend', { className: 'owc-muted' }, t('waterPresets')),
1803
+ h(
1804
+ 'div',
1805
+ { className: 'owc-water-presets' },
1806
+ [15, 30, 45, 60, 90, 120].map((minutes) =>
747
1807
  h(
748
1808
  'button',
749
1809
  {
750
1810
  type: 'button',
751
- className: 'owc-icon',
752
- title: t('collapse'),
753
- 'aria-label': t('collapse'),
754
- onClick: () => setPreference({ mini: true }),
1811
+ key: minutes,
1812
+ 'aria-pressed': Number(waterInterval) === minutes,
1813
+ onClick: () => {
1814
+ setWaterInterval(String(minutes));
1815
+ setWaterInvalid(false);
1816
+ },
755
1817
  },
756
- iconSvg('chevronDown'),
1818
+ minutes + ' ' + t('waterMinutes'),
757
1819
  ),
758
1820
  ),
759
- );
760
-
761
- const panelBody = () => {
762
- if (!safeState)
763
- return h(
764
- 'div',
765
- { className: 'owc-main' },
766
- h(
767
- 'div',
768
- { className: 'owc-status' },
769
- h('span', { className: 'owc-dot' }),
770
- t('loading'),
771
1821
  ),
772
- !sessionId && h('p', { className: 'owc-hint' }, t('noSession')),
773
1822
  h(
774
- 'div',
775
- { className: 'owc-row-actions' },
776
- 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
+ ),
1842
+ 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' },
1855
+ h(
1856
+ 'button',
1857
+ { type: 'button', disabled: waterBusy, onClick: closeWaterEditor },
1858
+ t('cancel'),
1859
+ ),
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 &&
1871
+ h(
1872
+ 'button',
1873
+ {
1874
+ type: 'button',
1875
+ disabled: waterBusy,
1876
+ onClick: () => waterAction({ type: 'retry' }),
1877
+ },
1878
+ t('waterRetry'),
1879
+ ),
1880
+ h(
1881
+ 'details',
1882
+ null,
1883
+ h('summary', null, t('waterManage')),
1884
+ h('p', null, t('waterLocal')),
1885
+ h(
1886
+ 'div',
1887
+ { className: 'owc-water-actions' },
1888
+ h(
1889
+ 'button',
1890
+ {
1891
+ type: 'button',
1892
+ disabled: waterError || waterBusy || !waterState.count,
1893
+ onClick: () => setWaterConfirm('clear-count'),
1894
+ },
1895
+ t('waterClear'),
1896
+ ),
1897
+ h(
1898
+ 'button',
1899
+ {
1900
+ type: 'button',
1901
+ disabled: waterBusy || !waterReady,
1902
+ onClick: () => setWaterConfirm('reset'),
1903
+ },
1904
+ t('waterReset'),
1905
+ ),
1906
+ ),
1907
+ waterConfirm &&
1908
+ h(
1909
+ 'div',
1910
+ { className: 'owc-water-confirm' },
1911
+ h('p', null, t(waterConfirm === 'reset' ? 'waterResetHint' : 'waterClearHint')),
1912
+ h(
1913
+ 'div',
1914
+ { className: 'owc-water-actions' },
1915
+ h(
1916
+ 'button',
1917
+ {
1918
+ type: 'button',
1919
+ disabled: waterBusy,
1920
+ onClick: () => waterAction({ type: waterConfirm }),
1921
+ },
1922
+ t('waterConfirm'),
1923
+ ),
1924
+ h(
1925
+ 'button',
1926
+ { type: 'button', onClick: () => setWaterConfirm(null) },
1927
+ t('cancel'),
1928
+ ),
1929
+ ),
1930
+ ),
1931
+ ),
1932
+ );
1933
+ const waterReminder = () =>
1934
+ waterPrompt &&
1935
+ h(
1936
+ 'div',
1937
+ { className: 'owc-water owc-water-due', 'data-water-event': waterState.eventId },
1938
+ h('p', null, iconSvg('water', 14), ' ' + t('waterDue')),
1939
+ h(
1940
+ 'div',
1941
+ { className: 'owc-water-actions' },
1942
+ h(
1943
+ 'button',
1944
+ {
1945
+ type: 'button',
1946
+ disabled: waterBusy,
1947
+ onClick: () => waterAction({ type: 'drink', eventId: waterState.eventId }),
1948
+ },
1949
+ t('waterDrink'),
1950
+ ),
1951
+ h(
1952
+ 'button',
1953
+ {
1954
+ type: 'button',
1955
+ disabled: waterBusy,
1956
+ onClick: () => waterAction({ type: 'snooze', eventId: waterState.eventId }),
1957
+ },
1958
+ t('waterSnooze'),
1959
+ ),
1960
+ ),
1961
+ );
1962
+
1963
+ const strip = () =>
1964
+ h(
1965
+ 'div',
1966
+ { className: 'owc-pill' },
1967
+ h(
1968
+ 'div',
1969
+ { className: 'owc-pill-top' },
1970
+ companion.visible
1971
+ ? h(
1972
+ 'button',
1973
+ {
1974
+ type: 'button',
1975
+ className: 'owc-companion-trigger',
1976
+ 'aria-label': t('companionOpen'),
1977
+ 'aria-expanded': waterQuickOpen,
1978
+ title:
1979
+ t('companionName') +
1980
+ (companion.message ? ' · ' + t(companion.message) : ''),
1981
+ onClick: toggleWaterQuick,
1982
+ onKeyDown: (event) => {
1983
+ if (event.key === 'Escape' && waterQuickOpen) {
1984
+ event.preventDefault();
1985
+ closeWaterQuick();
1986
+ }
1987
+ },
1988
+ },
1989
+ companionArtwork(h, companion, 36, waterNotice?.token),
1990
+ )
1991
+ : iconSvg('alarm', 16),
1992
+ h(
1993
+ 'span',
1994
+ { className: 'owc-grip', tabIndex: 0, title: t('drag'), onKeyDown: gripKeys },
1995
+ safeState.status !== 'done' &&
1996
+ h('span', { className: 'owc-pill-status' }, t(safeState.status)),
1997
+ h('span', { className: 'owc-pill-time' }, duration),
1998
+ ),
1999
+ h(
2000
+ 'button',
2001
+ {
2002
+ type: 'button',
2003
+ className: 'owc-icon',
2004
+ style: { marginLeft: 'auto' },
2005
+ title: t('expand'),
2006
+ 'aria-label': t('expand'),
2007
+ onClick: () => {
2008
+ setWaterQuickOpen(false);
2009
+ setPreference({ mini: false });
2010
+ },
2011
+ },
2012
+ iconSvg('chevronUp', 14),
2013
+ ),
2014
+ ),
2015
+ waterPrompt
2016
+ ? waterReminder()
2017
+ : h(
2018
+ 'div',
2019
+ { className: 'owc-pill-bottom' },
2020
+ h(
2021
+ 'div',
2022
+ { className: 'owc-pill-salary' },
2023
+ h(
2024
+ 'span',
2025
+ {
2026
+ className: 'owc-pill-money',
2027
+ 'aria-label': view.masked ? t('masked') : moneyText,
2028
+ },
2029
+ moneyText,
2030
+ ),
2031
+ h(
2032
+ 'button',
2033
+ {
2034
+ type: 'button',
2035
+ className: 'owc-icon',
2036
+ title: t(view.masked ? 'show' : 'hide'),
2037
+ 'aria-label': t(view.masked ? 'show' : 'hide'),
2038
+ onClick: () => setPreference({ masked: !view.masked }),
2039
+ },
2040
+ iconSvg(view.masked ? 'eyeOff' : 'eye', 13),
2041
+ ),
2042
+ ),
2043
+ (water.enabled || waterError) &&
2044
+ h(
2045
+ 'button',
2046
+ {
2047
+ type: 'button',
2048
+ className: 'owc-pill-water',
2049
+ onClick: toggleWaterQuick,
2050
+ 'aria-label': t('waterQuick'),
2051
+ 'aria-expanded': waterQuickOpen,
2052
+ onKeyDown: (event) => {
2053
+ if (event.key === 'Escape' && waterQuickOpen) {
2054
+ event.preventDefault();
2055
+ closeWaterQuick();
2056
+ }
2057
+ },
2058
+ title: waterSummary(),
2059
+ },
2060
+ iconSvg('water', 13),
2061
+ waterState.phase === 'quiet' && !waterError
2062
+ ? t('waterQuiet') +
2063
+ ' ' +
2064
+ formatDuration(waterState.quietRemaining / 1000).replace(/^00:/, '')
2065
+ : waterState.phase === 'countdown' && !waterError
2066
+ ? formatDuration(waterState.remaining / 1000).replace(/^00:/, '')
2067
+ : t(
2068
+ waterError
2069
+ ? 'waterTitle'
2070
+ : {
2071
+ paused: 'waterPaused',
2072
+ break: 'lunch',
2073
+ waiting: 'waterWaiting',
2074
+ finished: 'waterFinished',
2075
+ due: 'waterDue',
2076
+ off: 'waterOff',
2077
+ }[waterState.phase],
2078
+ ),
2079
+ ),
2080
+ ),
2081
+ );
2082
+
2083
+ const panelHead = () =>
2084
+ h(
2085
+ 'div',
2086
+ { className: 'owc-head' },
2087
+ h(
2088
+ 'span',
2089
+ {
2090
+ className: 'owc-title',
2091
+ tabIndex: 0,
2092
+ title: t('drag'),
2093
+ onKeyDown: gripKeys,
2094
+ },
2095
+ waterEditing
2096
+ ? t('waterTitle')
2097
+ : editing
2098
+ ? t(
2099
+ appearanceOnly
2100
+ ? 'appearanceGroup'
2101
+ : data?.configured
2102
+ ? 'settings'
2103
+ : 'setup',
2104
+ )
2105
+ : petMode
2106
+ ? t('companionName')
2107
+ : t('title'),
2108
+ ),
2109
+ !editing &&
2110
+ !waterEditing &&
2111
+ data?.configured &&
2112
+ h(
2113
+ 'button',
2114
+ {
2115
+ type: 'button',
2116
+ className: 'owc-text',
2117
+ onClick: openAppearance,
2118
+ },
2119
+ t('appearanceGroup'),
2120
+ ),
2121
+ waterEditing
2122
+ ? h(
2123
+ 'button',
2124
+ {
2125
+ type: 'button',
2126
+ className: 'owc-icon',
2127
+ title: t('waterBack'),
2128
+ 'aria-label': t('waterBack'),
2129
+ onClick: closeWaterEditor,
2130
+ disabled: waterBusy,
2131
+ },
2132
+ iconSvg('close'),
2133
+ )
2134
+ : editing
2135
+ ? data?.configured &&
2136
+ h(
2137
+ 'button',
2138
+ {
2139
+ type: 'button',
2140
+ className: 'owc-icon',
2141
+ title: t(appearanceOnly ? 'waterBack' : 'cancel'),
2142
+ 'aria-label': t(appearanceOnly ? 'waterBack' : 'cancel'),
2143
+ onClick: closeEditor,
2144
+ disabled: busy,
2145
+ },
2146
+ iconSvg('close'),
2147
+ )
2148
+ : h(
2149
+ React.Fragment,
2150
+ null,
2151
+ h(
2152
+ 'button',
2153
+ {
2154
+ type: 'button',
2155
+ className: 'owc-icon',
2156
+ title: t('settings'),
2157
+ 'aria-label': t('settings'),
2158
+ onClick: openEditor,
2159
+ disabled: busy,
2160
+ },
2161
+ iconSvg('sliders'),
2162
+ ),
2163
+ h(
2164
+ 'button',
2165
+ {
2166
+ type: 'button',
2167
+ className: 'owc-icon',
2168
+ title: t(petMode ? 'petClose' : 'collapse'),
2169
+ 'aria-label': t(petMode ? 'petClose' : 'collapse'),
2170
+ onClick: () =>
2171
+ petMode ? closePetPanel() : setPreference({ mini: true }),
2172
+ },
2173
+ iconSvg('chevronDown'),
2174
+ ),
2175
+ ),
2176
+ );
2177
+
2178
+ const panelBody = (section = 'all') => {
2179
+ if (!safeState)
2180
+ return h(
2181
+ 'div',
2182
+ { className: 'owc-main' },
2183
+ h(
2184
+ 'div',
2185
+ { className: 'owc-status' },
2186
+ h('span', { className: 'owc-dot' }),
2187
+ t('loading'),
2188
+ ),
2189
+ !sessionId && h('p', { className: 'owc-hint' }, t('noSession')),
2190
+ h(
2191
+ 'div',
2192
+ { className: 'owc-row-actions' },
2193
+ h(
777
2194
  'button',
778
2195
  {
779
2196
  type: 'button',
@@ -790,48 +2207,58 @@ window.__ModuleLoader__.load({
790
2207
  return h(
791
2208
  'div',
792
2209
  { className: 'owc-main' },
793
- status &&
2210
+ section !== 'earnings' &&
2211
+ status &&
794
2212
  h('div', { className: 'owc-status' }, h('span', { className: 'owc-dot' }), status),
795
- h('div', { className: 'owc-time', 'aria-label': status + ' ' + duration }, duration),
796
- h(
797
- 'div',
798
- {
799
- className: 'owc-progress',
800
- role: 'progressbar',
801
- 'aria-label': t('progress'),
802
- 'aria-valuenow': Math.floor(safeState.progress * 100),
803
- 'aria-valuemin': 0,
804
- 'aria-valuemax': 100,
805
- },
806
- h('div', { style: { width: safeState.progress * 100 + '%' } }),
807
- ),
808
- h(
809
- 'div',
810
- { className: 'owc-earn-row' },
811
- h('span', { className: 'owc-muted' }, t('earned')),
812
- salaryToggle(),
813
- ),
814
- h(
815
- 'div',
816
- { className: 'owc-money', 'aria-label': view.masked ? t('masked') : moneyText },
817
- moneyText,
818
- ),
819
- h(
820
- 'div',
821
- { className: 'owc-stats' },
2213
+ section !== 'earnings' &&
822
2214
  h(
823
2215
  'div',
824
- null,
825
- h('span', null, t('daily')),
826
- h('strong', null, money(safeState.daily)),
2216
+ { className: 'owc-time', 'aria-label': status + ' ' + duration },
2217
+ duration,
827
2218
  ),
2219
+ section !== 'earnings' &&
828
2220
  h(
829
2221
  'div',
830
- null,
831
- h('span', null, t('hourly')),
832
- h('strong', null, money(safeState.hourly)),
2222
+ {
2223
+ className: 'owc-progress',
2224
+ role: 'progressbar',
2225
+ 'aria-label': t('progress'),
2226
+ 'aria-valuenow': Math.floor(safeState.progress * 100),
2227
+ 'aria-valuemin': 0,
2228
+ 'aria-valuemax': 100,
2229
+ },
2230
+ h('div', { style: { width: safeState.progress * 100 + '%' } }),
2231
+ ),
2232
+ section !== 'clock' &&
2233
+ h(
2234
+ 'div',
2235
+ { className: 'owc-earn-row' },
2236
+ h('span', { className: 'owc-muted' }, t('earned')),
2237
+ salaryToggle(),
2238
+ ),
2239
+ section !== 'clock' &&
2240
+ h(
2241
+ 'div',
2242
+ { className: 'owc-money', 'aria-label': view.masked ? t('masked') : moneyText },
2243
+ moneyText,
2244
+ ),
2245
+ section !== 'clock' &&
2246
+ h(
2247
+ 'div',
2248
+ { className: 'owc-stats' },
2249
+ h(
2250
+ 'div',
2251
+ null,
2252
+ h('span', null, t('daily')),
2253
+ h('strong', null, money(safeState.daily)),
2254
+ ),
2255
+ h(
2256
+ 'div',
2257
+ null,
2258
+ h('span', null, t('hourly')),
2259
+ h('strong', null, money(safeState.hourly)),
2260
+ ),
833
2261
  ),
834
- ),
835
2262
  h(
836
2263
  'div',
837
2264
  { className: 'owc-meta' },
@@ -867,20 +2294,140 @@ window.__ModuleLoader__.load({
867
2294
  iconSvg('refresh', 12),
868
2295
  t('refresh'),
869
2296
  ),
2297
+ petMode &&
2298
+ h(
2299
+ 'button',
2300
+ { type: 'button', className: 'owc-text', onClick: openEditor },
2301
+ t('petWorkSettings'),
2302
+ ),
2303
+ !petMode &&
2304
+ h(
2305
+ 'button',
2306
+ {
2307
+ type: 'button',
2308
+ className: 'owc-text',
2309
+ onClick: () =>
2310
+ setPreference(petMode ? { petPosition: null } : { position: null }),
2311
+ },
2312
+ t('reset'),
2313
+ ),
2314
+ ),
2315
+ );
2316
+ };
2317
+
2318
+ const appearanceFields = () =>
2319
+ h(
2320
+ 'fieldset',
2321
+ { className: 'owc-setting-group' },
2322
+ h('legend', null, t('appearanceGroup')),
2323
+ h(
2324
+ 'label',
2325
+ { className: 'owc-check' },
2326
+ h('input', {
2327
+ type: 'checkbox',
2328
+ name: 'companion',
2329
+ checked: view.companion,
2330
+ onChange: (event) => updateAppearance({ companion: event.target.checked }),
2331
+ }),
2332
+ t('petMode'),
2333
+ ),
2334
+ h(
2335
+ 'label',
2336
+ { className: 'owc-check' },
2337
+ h('input', {
2338
+ type: 'checkbox',
2339
+ checked: view.companionMotion,
2340
+ disabled: !view.companion,
2341
+ onChange: (event) => updateAppearance({ companionMotion: event.target.checked }),
2342
+ }),
2343
+ t('companionMotion'),
2344
+ ),
2345
+ label(
2346
+ t('petCharacter'),
870
2347
  h(
871
- 'button',
2348
+ 'select',
872
2349
  {
873
- type: 'button',
874
- className: 'owc-text',
875
- onClick: () => setPreference({ position: null }),
2350
+ 'aria-label': t('petCharacter'),
2351
+ value: view.petModel,
2352
+ disabled: !view.companion,
2353
+ onChange: (event) => updateAppearance({ petModel: event.target.value }),
876
2354
  },
877
- t('reset'),
2355
+ ...Object.values(PET_MODELS).map((model) =>
2356
+ h('option', { key: model.id, value: model.id }, t(model.label)),
2357
+ ),
2358
+ ),
2359
+ ),
2360
+ label(
2361
+ t('petSize'),
2362
+ h('input', {
2363
+ type: 'range',
2364
+ min: '80',
2365
+ max: '180',
2366
+ step: '4',
2367
+ value: view.petSize,
2368
+ 'aria-label': t('petSize'),
2369
+ disabled: !view.companion,
2370
+ onChange: (event) => updateAppearance({ petSize: event.target.valueAsNumber }),
2371
+ }),
2372
+ ),
2373
+ h('p', { className: 'owc-hint' }, t('petAppearanceHint')),
2374
+ label(
2375
+ t('opacity'),
2376
+ h(
2377
+ React.Fragment,
2378
+ null,
2379
+ h('input', {
2380
+ className: 'owc-range',
2381
+ type: 'range',
2382
+ min: '30',
2383
+ max: '100',
2384
+ step: '1',
2385
+ value: String(Math.round(view.opacity * 100)),
2386
+ disabled: busy,
2387
+ 'aria-label': t('opacity'),
2388
+ onChange: (event) =>
2389
+ updateAppearance({ opacity: event.target.valueAsNumber / 100 }),
2390
+ }),
2391
+ h(
2392
+ 'span',
2393
+ { className: 'owc-hint', style: { margin: 0 } },
2394
+ Math.round(view.opacity * 100) + '% · ' + t('opacityHint'),
2395
+ ),
878
2396
  ),
879
2397
  ),
880
2398
  );
881
- };
882
-
883
2399
  const editor = () => {
2400
+ if (appearanceOnly)
2401
+ return h(
2402
+ 'div',
2403
+ { className: 'owc-form owc-appearance', 'data-appearance-settings': true },
2404
+ h('p', { className: 'owc-hint' }, t('petAppearancePreview')),
2405
+ h(
2406
+ 'div',
2407
+ { 'data-appearance-preview': true, className: 'owc-appearance-preview' },
2408
+ view.companion
2409
+ ? petArtwork(
2410
+ h,
2411
+ companion,
2412
+ view.petSize * (petModel(view.petModel).renderer === 'svg' ? 1 : 0.65),
2413
+ '',
2414
+ view.petModel,
2415
+ PET_SPRITES,
2416
+ )
2417
+ : h('p', null, t('petClassicPreview')),
2418
+ ),
2419
+ appearanceFields(),
2420
+ h(
2421
+ 'div',
2422
+ { className: 'owc-save' },
2423
+ h('button', { type: 'button', onClick: closeEditor }, t('cancel')),
2424
+ h(
2425
+ 'button',
2426
+ { type: 'button', className: 'owc-primary', onClick: applyAppearance },
2427
+ t('petApplyAppearance'),
2428
+ ),
2429
+ ),
2430
+ );
884
2431
  if (!workingDraft) return null;
885
2432
  const payDaily = workingDraft.payMode === 'daily';
886
2433
  const salaryKey = payDaily ? 'dailySalary' : 'monthlySalary';
@@ -932,7 +2479,7 @@ window.__ModuleLoader__.load({
932
2479
  onKeyDown: (e) => {
933
2480
  if (e.key === 'Escape') {
934
2481
  e.preventDefault();
935
- closeEditor();
2482
+ navigateScreen(ui.back);
936
2483
  }
937
2484
  },
938
2485
  },
@@ -1048,34 +2595,6 @@ window.__ModuleLoader__.load({
1048
2595
  ),
1049
2596
  ),
1050
2597
  ),
1051
- h(
1052
- 'fieldset',
1053
- { className: 'owc-setting-group' },
1054
- h('legend', null, t('appearanceGroup')),
1055
- label(
1056
- t('opacity'),
1057
- h(
1058
- React.Fragment,
1059
- null,
1060
- h('input', {
1061
- className: 'owc-range',
1062
- type: 'range',
1063
- min: '30',
1064
- max: '100',
1065
- step: '1',
1066
- value: String(Math.round(view.opacity * 100)),
1067
- disabled: busy,
1068
- 'aria-label': t('opacity'),
1069
- onChange: (e) => setPreference({ opacity: e.target.valueAsNumber / 100 }),
1070
- }),
1071
- h(
1072
- 'span',
1073
- { className: 'owc-hint', style: { margin: 0 } },
1074
- Math.round(view.opacity * 100) + '% · ' + t('opacityHint'),
1075
- ),
1076
- ),
1077
- ),
1078
- ),
1079
2598
  preview &&
1080
2599
  !preview.error &&
1081
2600
  h(
@@ -1133,14 +2652,677 @@ window.__ModuleLoader__.load({
1133
2652
  storageError && h('p', { className: 'owc-note' }, t('storage')),
1134
2653
  );
1135
2654
 
2655
+ function closePetPanel() {
2656
+ navigateScreen(null);
2657
+ }
2658
+ function interactWithPet(event) {
2659
+ if (petDragged.current && event.detail !== 0) {
2660
+ petDragged.current = false;
2661
+ return;
2662
+ }
2663
+ petDragged.current = false;
2664
+ if (!ui.pending) petMotion.current?.play('greet');
2665
+ }
2666
+ function closePetMenu(restore = true) {
2667
+ dispatchUI({ type: 'dismiss-menu' });
2668
+ if (restore)
2669
+ (menuTrigger.current?.isConnected
2670
+ ? menuTrigger.current
2671
+ : box.current?.querySelector('.owc-pet-handle')
2672
+ )?.focus();
2673
+ }
2674
+ const liveFeedback = () =>
2675
+ h(
2676
+ 'span',
2677
+ { className: 'owc-sr-only', role: 'status', 'aria-atomic': true },
2678
+ h(
2679
+ 'span',
2680
+ {
2681
+ key: waterPrompt
2682
+ ? waterState.eventId
2683
+ : (waterNotice?.token ?? waterNotice?.key ?? 'idle'),
2684
+ },
2685
+ waterPrompt ? t('waterDue') : waterNotice ? t(waterNotice.key) : '',
2686
+ ),
2687
+ );
2688
+ const petShowing = petOpen;
2689
+ const petFloating = petShowing || waterPrompt;
2690
+ React.useLayoutEffect(() => {
2691
+ if (!petMode || !petFloating || !cardContent.current) return;
2692
+ const content = cardContent.current,
2693
+ header = petPanel.current?.querySelector('.owc-head');
2694
+ let frame = 0;
2695
+ const measure = () => {
2696
+ const height = Math.ceil(
2697
+ content.getBoundingClientRect().height +
2698
+ (header?.getBoundingClientRect().height ?? 0) +
2699
+ 2,
2700
+ );
2701
+ setCardMeasure((current) =>
2702
+ current.key === measureKey && Math.abs(current.height - height) < 1
2703
+ ? current
2704
+ : { key: measureKey, height },
2705
+ );
2706
+ };
2707
+ const observer =
2708
+ typeof ResizeObserver === 'function'
2709
+ ? new ResizeObserver(() => {
2710
+ cancelAnimationFrame(frame);
2711
+ frame = requestAnimationFrame(measure);
2712
+ })
2713
+ : null;
2714
+ observer?.observe(content);
2715
+ if (header) observer?.observe(header);
2716
+ measure();
2717
+ return () => {
2718
+ observer?.disconnect();
2719
+ cancelAnimationFrame(frame);
2720
+ };
2721
+ }, [petMode, petFloating, measureKey, ui.pending]);
2722
+ React.useLayoutEffect(() => {
2723
+ if (!petMode || !ui.menu || !menuContent.current) return;
2724
+ const content = menuContent.current;
2725
+ let frame = 0;
2726
+ const measure = () => {
2727
+ const height = Math.ceil(content.getBoundingClientRect().height + 2);
2728
+ setMenuMeasure((current) =>
2729
+ current.width === menuWidth && Math.abs(current.height - height) < 1
2730
+ ? current
2731
+ : { width: menuWidth, height },
2732
+ );
2733
+ };
2734
+ const observer =
2735
+ typeof ResizeObserver === 'function'
2736
+ ? new ResizeObserver(() => {
2737
+ cancelAnimationFrame(frame);
2738
+ frame = requestAnimationFrame(measure);
2739
+ })
2740
+ : null;
2741
+ observer?.observe(content);
2742
+ measure();
2743
+ return () => {
2744
+ observer?.disconnect();
2745
+ cancelAnimationFrame(frame);
2746
+ };
2747
+ }, [petMode, !!ui.menu, menuWidth]);
2748
+ React.useLayoutEffect(() => {
2749
+ if (!petMode || petShowing || waterPrompt || !waterNotice || !toastContent.current)
2750
+ return;
2751
+ const content = toastContent.current;
2752
+ let frame = 0;
2753
+ const measure = () => {
2754
+ const height = Math.ceil(content.getBoundingClientRect().height + 20);
2755
+ setToastHeight((current) => (current === height ? current : height));
2756
+ };
2757
+ const observer =
2758
+ typeof ResizeObserver === 'function'
2759
+ ? new ResizeObserver(() => {
2760
+ cancelAnimationFrame(frame);
2761
+ frame = requestAnimationFrame(measure);
2762
+ })
2763
+ : null;
2764
+ observer?.observe(content);
2765
+ measure();
2766
+ return () => {
2767
+ observer?.disconnect();
2768
+ cancelAnimationFrame(frame);
2769
+ };
2770
+ }, [petMode, petShowing, waterPrompt, waterNotice, viewport.width]);
2771
+ React.useEffect(() => {
2772
+ if (!ui.menu) return;
2773
+ const blur = () => dispatchUI({ type: 'dismiss-menu' });
2774
+ window.addEventListener('blur', blur);
2775
+ return () => window.removeEventListener('blur', blur);
2776
+ }, [ui.menu]);
2777
+ React.useEffect(() => {
2778
+ if (!petMode || (!petShowing && !ui.menu)) return;
2779
+ const outside = (event) => {
2780
+ if (ui.menu && !menuBox.current?.contains(event.target)) {
2781
+ dispatchUI({ type: 'dismiss-menu' });
2782
+ return;
2783
+ }
2784
+ if (box.current?.contains(event.target)) return;
2785
+ if (ui.menu) dispatchUI({ type: 'dismiss-menu' });
2786
+ else if (!editing && !waterEditing && !waterBusy)
2787
+ dispatchUI({ type: 'navigate', screen: null });
2788
+ };
2789
+ document.addEventListener('pointerdown', outside);
2790
+ return () => document.removeEventListener('pointerdown', outside);
2791
+ }, [petMode, petShowing, ui.menu, editing, waterEditing, waterBusy]);
2792
+ React.useEffect(() => {
2793
+ if (ui.menu) menuBox.current?.querySelector('[role=menuitem]:not(:disabled)')?.focus();
2794
+ }, [ui.menu]);
2795
+ React.useEffect(() => {
2796
+ if (!petMode || editing || waterEditing || ui.menu) return;
2797
+ if (ui.pending) {
2798
+ box.current?.querySelector('[data-keep-editing]')?.focus();
2799
+ return;
2800
+ }
2801
+ if (ui.screen === null) box.current?.querySelector('.owc-pet-handle')?.focus();
2802
+ else petPanel.current?.querySelector('[data-pet-page-title]')?.focus();
2803
+ }, [ui.screen]);
2804
+
2805
+ React.useEffect(() => {
2806
+ if (ui.pending && (busy || waterBusy))
2807
+ box.current?.querySelector('[data-nav-confirm]')?.focus();
2808
+ }, [ui.pending, busy, waterBusy]);
2809
+ const draftGuard = () =>
2810
+ h(
2811
+ 'div',
2812
+ {
2813
+ className: 'owc-pet-unsaved',
2814
+ 'data-nav-confirm': true,
2815
+ tabIndex: -1,
2816
+ onKeyDown: (event) => {
2817
+ if (event.key !== 'Tab') return;
2818
+ const buttons = [
2819
+ ...event.currentTarget.querySelectorAll('button:not(:disabled)'),
2820
+ ];
2821
+ if (!buttons.length) {
2822
+ event.preventDefault();
2823
+ return;
2824
+ }
2825
+ if (event.shiftKey && document.activeElement === buttons[0]) {
2826
+ event.preventDefault();
2827
+ buttons.at(-1).focus();
2828
+ } else if (!event.shiftKey && document.activeElement === buttons.at(-1)) {
2829
+ event.preventDefault();
2830
+ buttons[0].focus();
2831
+ }
2832
+ },
2833
+ role: 'alertdialog',
2834
+ 'aria-label': t('petUnsaved'),
2835
+ },
2836
+ h('h3', null, t('petUnsaved')),
2837
+ h('p', null, t('petUnsavedHint')),
2838
+ h(
2839
+ 'button',
2840
+ {
2841
+ type: 'button',
2842
+ className: 'owc-primary',
2843
+ disabled: busy || waterBusy,
2844
+ onClick: saveAndContinue,
2845
+ },
2846
+ t('petSaveContinue'),
2847
+ ),
2848
+ h(
2849
+ 'button',
2850
+ {
2851
+ type: 'button',
2852
+ disabled: busy || waterBusy,
2853
+ onClick: () => performNavigation(ui.pending.screen, ui.pending.root),
2854
+ },
2855
+ t('petDiscard'),
2856
+ ),
2857
+ h(
2858
+ 'button',
2859
+ {
2860
+ type: 'button',
2861
+ 'data-keep-editing': true,
2862
+ disabled: busy || waterBusy,
2863
+ onClick: () => dispatchUI({ type: 'stay' }),
2864
+ },
2865
+ t('petKeepEditing'),
2866
+ ),
2867
+ );
2868
+ const petPageTitle = {
2869
+ home: 'petWaterCard',
2870
+ income: 'petCountdownCard',
2871
+ earnings: 'petEarningsCard',
2872
+ water: 'petWaterSettings',
2873
+ appearance: 'petAppearance',
2874
+ schedule: 'petWorkSettings',
2875
+ };
2876
+ const petHeader = () =>
2877
+ h(
2878
+ 'div',
2879
+ { className: 'owc-head' },
2880
+ PET_EDITOR_SCREENS.includes(ui.screen) &&
2881
+ ui.back !== null &&
2882
+ h(
2883
+ 'button',
2884
+ {
2885
+ type: 'button',
2886
+ className: 'owc-text',
2887
+ disabled: busy || waterBusy || !!ui.pending,
2888
+ onClick: () => navigateScreen(ui.back),
2889
+ 'aria-label': t('petBack'),
2890
+ },
2891
+ '← ',
2892
+ t('petBack'),
2893
+ ),
2894
+ h(
2895
+ 'strong',
2896
+ { 'data-pet-page-title': true, tabIndex: -1, style: { flex: 1, fontSize: 12 } },
2897
+ t(petPageTitle[ui.screen] || 'waterTitle'),
2898
+ ),
2899
+ h(
2900
+ 'button',
2901
+ {
2902
+ type: 'button',
2903
+ className: 'owc-icon',
2904
+ 'aria-label': t('petCloseAll'),
2905
+ disabled: busy || waterBusy || !!ui.pending,
2906
+ onClick: closePetPanel,
2907
+ },
2908
+ iconSvg('close', 14),
2909
+ ),
2910
+ );
2911
+ const petHome = () =>
2912
+ h(
2913
+ 'div',
2914
+ { className: 'owc-pet-home', 'data-pet-home': true },
2915
+
2916
+ h(
2917
+ 'div',
2918
+ {
2919
+ className: 'owc-pet-water-card' + (waterPrompt ? ' owc-water-due' : ''),
2920
+ 'data-water-event': waterPrompt ? waterState.eventId : undefined,
2921
+ },
2922
+ 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(),
2930
+ ),
2931
+ h(
2932
+ 'div',
2933
+ { className: 'owc-pet-water-row' },
2934
+ h(
2935
+ 'button',
2936
+ {
2937
+ type: 'button',
2938
+ className: 'owc-primary',
2939
+ disabled: !waterCanRecord || waterBusy,
2940
+ onClick: recordWater,
2941
+ },
2942
+ t('petRecord'),
2943
+ ),
2944
+ h(
2945
+ 'span',
2946
+ { 'data-water-count': true },
2947
+ t('waterCount') + ': ' + waterState.count,
2948
+ ),
2949
+ ),
2950
+ ),
2951
+ h(
2952
+ 'div',
2953
+ { className: 'owc-pet-quiet-row' },
2954
+ [30, 60].map((minutes) =>
2955
+ h(
2956
+ 'button',
2957
+ {
2958
+ type: 'button',
2959
+ key: minutes,
2960
+ disabled: !waterCanQuiet || waterBusy,
2961
+ onClick: () => waterAction({ type: 'quiet', minutes }),
2962
+ },
2963
+ t(minutes === 30 ? 'petQuiet30' : 'petQuiet60'),
2964
+ ),
2965
+ ),
2966
+ ),
2967
+ h(
2968
+ 'div',
2969
+ { className: 'owc-pet-quiet-status' },
2970
+ waterPrompt
2971
+ ? h(
2972
+ 'button',
2973
+ {
2974
+ type: 'button',
2975
+ disabled: waterBusy,
2976
+ onClick: () => waterAction({ type: 'snooze', eventId: waterState.eventId }),
2977
+ },
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'),
3009
+ ),
3010
+ waterError &&
3011
+ h(
3012
+ 'button',
3013
+ {
3014
+ type: 'button',
3015
+ disabled: waterBusy,
3016
+ onClick: () => waterAction({ type: 'retry' }),
3017
+ },
3018
+ t('waterRetry'),
3019
+ ),
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
+ );
3041
+ const contextMenu = () =>
3042
+ ui.menu &&
3043
+ h(
3044
+ 'div',
3045
+ {
3046
+ ref: menuBox,
3047
+ className: 'owc-pet-context',
3048
+ role: 'menu',
3049
+ 'aria-label': t('petMenu'),
3050
+ style: {
3051
+ left: menuLayout.left,
3052
+ top: menuLayout.top,
3053
+ width: menuLayout.width,
3054
+ maxHeight: menuLayout.height,
3055
+ },
3056
+ onBlur: (event) => {
3057
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget))
3058
+ dispatchUI({ type: 'dismiss-menu' });
3059
+ },
3060
+ onKeyDown: (event) => {
3061
+ if (event.key === 'Escape') {
3062
+ event.preventDefault();
3063
+ event.stopPropagation();
3064
+ closePetMenu();
3065
+ return;
3066
+ }
3067
+ const items = [
3068
+ ...event.currentTarget.querySelectorAll('[role=menuitem]:not(:disabled)'),
3069
+ ];
3070
+ const index = items.indexOf(document.activeElement);
3071
+ const next =
3072
+ event.key === 'ArrowDown'
3073
+ ? (index + 1) % items.length
3074
+ : event.key === 'ArrowUp'
3075
+ ? (index + items.length - 1) % items.length
3076
+ : event.key === 'Home'
3077
+ ? 0
3078
+ : event.key === 'End'
3079
+ ? items.length - 1
3080
+ : -1;
3081
+ if (next >= 0) {
3082
+ event.preventDefault();
3083
+ event.stopPropagation();
3084
+ items[next]?.focus();
3085
+ }
3086
+ },
3087
+ },
3088
+ h(
3089
+ 'div',
3090
+ { ref: menuContent, className: 'owc-menu-natural' },
3091
+ [
3092
+ ['income', 'petCountdownCard'],
3093
+ ['home', 'petWaterCard'],
3094
+ ['earnings', 'petEarningsCard'],
3095
+ ['appearance', 'petAppearance'],
3096
+ ['schedule', 'petWorkSettings'],
3097
+ ].map(([screen, label]) =>
3098
+ h(
3099
+ 'button',
3100
+ {
3101
+ key: screen,
3102
+ type: 'button',
3103
+ role: 'menuitem',
3104
+ tabIndex: -1,
3105
+ disabled: busy || waterBusy,
3106
+ onClick: (event) => navigateScreen(screen, event),
3107
+ },
3108
+ t(label),
3109
+ ),
3110
+ ),
3111
+
3112
+ h('hr', null),
3113
+ h(
3114
+ 'button',
3115
+ {
3116
+ type: 'button',
3117
+ role: 'menuitem',
3118
+ tabIndex: -1,
3119
+ onClick: () => {
3120
+ setPreference({ petPosition: null });
3121
+ closePetMenu();
3122
+ },
3123
+ },
3124
+ t('reset'),
3125
+ ),
3126
+ ),
3127
+ );
3128
+
3129
+ if (petMode)
3130
+ return h(
3131
+ 'section',
3132
+ {
3133
+ ref: box,
3134
+ className: 'owc-v2 owc-pet-shell',
3135
+ 'data-version': '1.1.1',
3136
+ 'data-pet-model': stageModel,
3137
+ 'data-status': safeState.status,
3138
+ 'aria-label': t('companionLabel'),
3139
+ style: {
3140
+ '--owc-alpha': String(view.opacity),
3141
+ left: petGeometry.x,
3142
+ top: petGeometry.y,
3143
+ right: 'auto',
3144
+ width: petGeometry.petWidth,
3145
+ height: petGeometry.petHeight,
3146
+ },
3147
+ onPointerDown: dragDown,
3148
+ onPointerMove: dragMove,
3149
+ onPointerUp: dragEnd,
3150
+ onPointerCancel: dragEnd,
3151
+ onLostPointerCapture: dragEnd,
3152
+ onContextMenuCapture: openAppearanceContext,
3153
+ onKeyDown: (event) => {
3154
+ if (appearanceShortcut(event)) return;
3155
+ if (event.key !== 'Escape' || event.defaultPrevented) return;
3156
+ event.preventDefault();
3157
+ event.stopPropagation();
3158
+ if (ui.menu) closePetMenu();
3159
+ else if (busy || waterBusy) return;
3160
+ else if (ui.pending) dispatchUI({ type: 'stay' });
3161
+ else if (editing || waterEditing) navigateScreen(ui.back);
3162
+ else if (petShowing) closePetPanel();
3163
+ else if (waterPrompt)
3164
+ void waterAction({ type: 'snooze', eventId: waterState.eventId });
3165
+ },
3166
+ },
3167
+ h('style', null, STYLE),
3168
+ liveFeedback(),
3169
+ h(
3170
+ 'div',
3171
+ { className: 'owc-pet-stage' },
3172
+ h(
3173
+ 'button',
3174
+ {
3175
+ type: 'button',
3176
+ className: 'owc-pet-handle',
3177
+ 'aria-label': t('petGreet'),
3178
+ title: t('companionName') + ' · ' + t('petHint'),
3179
+ onClick: interactWithPet,
3180
+ onPointerEnter: (event) => {
3181
+ if (event.pointerType === 'mouse' && !dragState.current)
3182
+ petMotion.current?.play('look');
3183
+ },
3184
+ onKeyDown: gripKeys,
3185
+ },
3186
+ petArtwork(
3187
+ h,
3188
+ companion,
3189
+ petGeometry.artworkSize,
3190
+ waterNotice?.token,
3191
+ stageModel,
3192
+ PET_SPRITES,
3193
+ true,
3194
+ ),
3195
+ ),
3196
+ h(
3197
+ 'div',
3198
+ {
3199
+ className: 'owc-pet-clock',
3200
+ role: 'timer',
3201
+ 'aria-label': t('petClock'),
3202
+ },
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),
3206
+ ),
3207
+ ),
3208
+ waterPrompt &&
3209
+ petShowing &&
3210
+ ui.screen !== 'home' &&
3211
+ h(
3212
+ 'div',
3213
+ {
3214
+ className: 'owc-pet-water-alert owc-water-due',
3215
+ 'data-water-event': waterState.eventId,
3216
+ role: 'note',
3217
+ 'aria-label': t('petWaterAlert'),
3218
+ title: t('petWaterAlert'),
3219
+ style: {
3220
+ left: waterAlertLayout.left,
3221
+ top: waterAlertLayout.top,
3222
+ width: waterAlertLayout.width,
3223
+ height: waterAlertLayout.height,
3224
+ },
3225
+ },
3226
+ iconSvg('water', 13),
3227
+ t('petWaterBadge'),
3228
+ ),
3229
+ contextMenu(),
3230
+ !petShowing &&
3231
+ !waterPrompt &&
3232
+ waterNotice &&
3233
+ h(
3234
+ 'div',
3235
+ {
3236
+ className: 'owc-pet-toast',
3237
+ 'aria-hidden': true,
3238
+ style: {
3239
+ left: toastLayout.left,
3240
+ top: toastLayout.top,
3241
+ width: toastLayout.width,
3242
+ maxHeight: toastLayout.height,
3243
+ },
3244
+ },
3245
+ h('div', { ref: toastContent }, t(waterNotice.key)),
3246
+ ),
3247
+ petFloating &&
3248
+ h(
3249
+ 'div',
3250
+ {
3251
+ ref: petPanel,
3252
+ className: 'owc-pet-panel owc-pet-workspace',
3253
+ 'data-pet-panel': petShowing ? 'open' : 'bubble',
3254
+ 'data-pet-screen': ui.screen || 'reminder',
3255
+ 'data-side': floatingPanel.side,
3256
+ role: 'region',
3257
+ 'aria-label': t(petPageTitle[ui.screen] || 'waterTitle'),
3258
+ style: {
3259
+ left: floatingPanel.left,
3260
+ top: floatingPanel.top,
3261
+ width: floatingPanel.width,
3262
+ height: floatingPanel.height,
3263
+ maxHeight: floatingPanel.maxHeight,
3264
+ },
3265
+ },
3266
+ h('div', { className: 'owc-glass', 'aria-hidden': true }),
3267
+ petShowing
3268
+ ? petHeader()
3269
+ : h(
3270
+ 'div',
3271
+ { className: 'owc-head' },
3272
+ h('strong', { style: { flex: 1, fontSize: 12 } }, t('waterTitle')),
3273
+ h(
3274
+ 'button',
3275
+ {
3276
+ type: 'button',
3277
+ className: 'owc-icon',
3278
+ 'aria-label': t('waterSnooze'),
3279
+ disabled: waterBusy,
3280
+ onClick: () =>
3281
+ waterAction({ type: 'snooze', eventId: waterState.eventId }),
3282
+ },
3283
+ iconSvg('close', 14),
3284
+ ),
3285
+ ),
3286
+ h(
3287
+ 'div',
3288
+ { className: 'owc-pet-scroll' },
3289
+ h(
3290
+ 'div',
3291
+ { ref: cardContent, className: 'owc-pet-natural' },
3292
+ petShowing
3293
+ ? h(
3294
+ React.Fragment,
3295
+ null,
3296
+ ui.pending
3297
+ ? draftGuard()
3298
+ : waterEditing
3299
+ ? waterSettings()
3300
+ : editing
3301
+ ? editor()
3302
+ : petDetails
3303
+ ? panelBody('clock')
3304
+ : ui.screen === 'earnings'
3305
+ ? panelBody('earnings')
3306
+ : petHome(),
3307
+ notices(),
3308
+ )
3309
+ : waterReminder(),
3310
+ ),
3311
+ ),
3312
+ ),
3313
+ );
3314
+
1136
3315
  return h(
1137
3316
  'section',
1138
3317
  {
1139
3318
  ref: box,
1140
3319
  className:
1141
- 'owc-v2' + (collapsed ? ' owc-compact' : '') + (editing ? ' owc-editing' : ''),
3320
+ 'owc-v2' +
3321
+ (collapsed ? ' owc-compact' : '') +
3322
+ (collapsed && waterQuickOpen ? ' owc-quick' : '') +
3323
+ (editing || waterEditing ? ' owc-editing' : ''),
1142
3324
  'aria-label': t('title'),
1143
- 'data-version': '1.0.1',
3325
+ 'data-version': '1.1.1',
1144
3326
  'data-status': safeState?.status || 'setup',
1145
3327
  style: cardStyle,
1146
3328
  onPointerDown: dragDown,
@@ -1148,15 +3330,68 @@ window.__ModuleLoader__.load({
1148
3330
  onPointerUp: dragEnd,
1149
3331
  onPointerCancel: dragEnd,
1150
3332
  onLostPointerCapture: dragEnd,
3333
+ onContextMenuCapture: openAppearanceContext,
3334
+ onKeyDown: (event) => {
3335
+ if (
3336
+ appearanceShortcut(event) ||
3337
+ event.key !== 'Escape' ||
3338
+ event.defaultPrevented ||
3339
+ busy ||
3340
+ waterBusy
3341
+ )
3342
+ return;
3343
+ event.preventDefault();
3344
+ if (ui.pending) dispatchUI({ type: 'stay' });
3345
+ else if (editing || waterEditing) navigateScreen(ui.back);
3346
+ else if (waterQuickOpen) closeWaterQuick();
3347
+ else setPreference({ mini: true });
3348
+ },
1151
3349
  },
1152
3350
  h('style', null, STYLE),
1153
3351
  h('div', { className: 'owc-glass', 'aria-hidden': 'true' }),
1154
3352
  h(
1155
3353
  'div',
1156
3354
  { className: 'owc-content' },
3355
+ h(
3356
+ 'span',
3357
+ { className: 'owc-sr-only', role: 'status', 'aria-atomic': true },
3358
+ h(
3359
+ 'span',
3360
+ {
3361
+ key: waterPrompt
3362
+ ? waterState.eventId
3363
+ : (waterNotice?.token ?? waterNotice?.key ?? 'idle'),
3364
+ },
3365
+ waterPrompt ? t('waterDue') : waterNotice ? t(waterNotice.key) : '',
3366
+ ),
3367
+ ),
1157
3368
  collapsed
1158
- ? strip()
1159
- : h(React.Fragment, null, panelHead(), editing ? editor() : panelBody(), notices()),
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(),
1160
3395
  ),
1161
3396
  );
1162
3397
  }