dsh-offwork 1.1.2 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.source.js CHANGED
@@ -83,7 +83,7 @@ window.__ModuleLoader__.load({
83
83
  error: '操作失败,请重试。',
84
84
  saved: '设置已保存',
85
85
  days: '日一二三四五六',
86
- privacy: '配置保存在 Harness 所在机器;读写结果进入所选会话记录。',
86
+ privacy: '配置保存在 Harness 所在机器;界面直接保存,不写入会话。代理访问默认关闭。',
87
87
  drag: '按住可拖动;方向键微调,Home 复位',
88
88
  setup: '先设好你的作息',
89
89
  setupHint: '示例数值不是你的收入,保存后才开始计时。',
@@ -240,7 +240,8 @@ window.__ModuleLoader__.load({
240
240
  error: 'Operation failed. Please retry.',
241
241
  saved: 'Settings saved',
242
242
  days: 'SMTWTFS',
243
- privacy: 'Settings are saved on the Harness host; results enter the selected session log.',
243
+ privacy:
244
+ 'Settings stay on the Harness host. UI saves directly without conversation logs; agent access is off by default.',
244
245
  drag: 'Drag to move; arrow keys to adjust; Home resets',
245
246
  setup: 'Set up your schedule',
246
247
  setupHint: 'These are example values, not your income. Save to start.',
@@ -330,7 +331,7 @@ window.__ModuleLoader__.load({
330
331
  };
331
332
  const STYLE =
332
333
  `
333
- .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}
334
+ .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:var(--owc-font-size,13px)/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif;cursor:grab;touch-action:manipulation;user-select:none;-webkit-user-select:none}
334
335
  .owc-v2:active{cursor:grabbing}
335
336
  .owc-v2.owc-compact{touch-action:none;border-radius:18px}
336
337
  .owc-v2.owc-compact.owc-quick{width:296px;touch-action:manipulation}
@@ -492,7 +493,19 @@ window.__ModuleLoader__.load({
492
493
  ['path', { d: 'M13.1 3v3.2H9.9' }],
493
494
  ],
494
495
  };
495
- const LOCAL_SCREENS = [null, 'appearance', 'smoking', 'water-countdown', 'water', 'home'];
496
+ const LOCAL_SCREENS = [
497
+ null,
498
+ 'appearance',
499
+ 'smoking',
500
+ 'water-countdown',
501
+ 'water',
502
+ 'home',
503
+ 'income',
504
+ 'wrap',
505
+ 'preferences',
506
+ 'data',
507
+ 'reminders',
508
+ ];
496
509
  const iconSvg = (name, size = 15) =>
497
510
  h(
498
511
  'svg',
@@ -658,6 +671,7 @@ window.__ModuleLoader__.load({
658
671
  } catch {
659
672
  /* Polling also synchronizes. */
660
673
  }
674
+ return changedElsewhere ? null : next;
661
675
  } catch {
662
676
  if (alive) setSmokingError('timerStorageError');
663
677
  } finally {
@@ -724,28 +738,83 @@ window.__ModuleLoader__.load({
724
738
  const state = settings ? safeCalculate(settings) : null;
725
739
  const preview = workingDraft ? safeCalculate(workingDraft) : null;
726
740
  const safeState = state && !state.error ? state : null;
741
+ const features = useCompanionFeatures(React, {
742
+ language: locale.active,
743
+ screen: ui.screen,
744
+ menuOpen: !!ui.menu,
745
+ navigate: (screen, event) => navigateScreen(screen, event),
746
+ enableCharacter: () => updateAppearance({ companion: true }),
747
+ configured: !!data?.configured,
748
+ shift: data?.configured ? safeState : null,
749
+ timezone: settings?.timezone,
750
+ smoking,
751
+ smokingReady: smokingReady && !smokingError,
752
+ water,
753
+ waterReady: waterReady && !waterError,
754
+ waterOwner: waterOwner.current,
755
+ smokingLabel: t('smokingCountdown'),
756
+ waterLabel: t('waterDue'),
757
+ wrapLabel: t('companionDone'),
758
+ editing: editing || waterEditing || ui.screen === 'wrap' || !!ui.pending,
759
+ sessionIds,
760
+ sessionTitles: Object.fromEntries(
761
+ sessionIds.map((id) => [id, sessions?.byId?.[id]?.title]),
762
+ ),
763
+ runningSessions: sessionIds.filter((id) => sessions?.byId?.[id]?.running === true),
764
+ view: savedView,
765
+ setView: (patch) => setPreference(patch),
766
+ openLegacy: (screen) => navigateScreen(screen),
767
+ reload,
768
+ refreshLegacy: () => {
769
+ waterRuntime.current?.tick();
770
+ smokingRuntime.current?.run({ type: 'retry' });
771
+ },
772
+ restartSmoking: () => smokingAction({ type: 'restart' }),
773
+ drink: () => waterAction({ type: 'manual-drink', actionId: crypto.randomUUID() }),
774
+ react: () => petMotion.current?.play('greet'),
775
+ });
776
+ const displayPreferences =
777
+ appearanceOnly && features.previewPreferences
778
+ ? features.previewPreferences
779
+ : features.state.preferences;
780
+ const featureStyle = {
781
+ ...(COMPANION_THEMES[displayPreferences.theme]
782
+ ? { '--owc-accent': COMPANION_THEMES[displayPreferences.theme] }
783
+ : {}),
784
+ '--owc-font-size': 13 * displayPreferences.cardScale + 'px',
785
+ };
727
786
  const petMode = ready && (appearanceOnly ? appearanceWasPet.current : view.companion);
728
- const petClockStatus = !sessionId
729
- ? 'petNeedsSession'
730
- : error || state?.error
787
+ const petClockStatus =
788
+ error || state?.error
731
789
  ? 'petLoadFailed'
732
790
  : !data
733
791
  ? 'loading'
734
792
  : !data.configured
735
793
  ? 'petNeedsSetup'
736
794
  : safeState?.status || 'loading';
737
- const stageModel = appearanceOnly ? savedView.petModel : view.petModel;
795
+ const stageModel = features.customImage
796
+ ? 'custom-image'
797
+ : appearanceOnly
798
+ ? savedView.petModel
799
+ : view.petModel;
738
800
  const petGeometry = petLayout(
739
801
  dragPosition ?? view.petPosition,
740
802
  viewport,
741
803
  appearanceOnly ? savedView.petSize : view.petSize,
742
804
  420,
743
805
  stageModel,
744
- 72,
806
+ (window.matchMedia?.('(pointer:coarse)').matches ? 144 : 108) +
807
+ (features.activeTimer ? 30 : 0),
745
808
  196,
746
809
  );
747
810
  const surfaceScreen = ui.screen || 'reminder';
748
- const surfaceWidth = petSurfaceWidth(surfaceScreen, viewport);
811
+ const surfaceWidth = Math.max(
812
+ 1,
813
+ Math.min(
814
+ petSurfaceWidth(surfaceScreen, viewport) * displayPreferences.cardScale,
815
+ viewport.width - 24,
816
+ ),
817
+ );
749
818
  const measureKey = [surfaceScreen, surfaceWidth, locale.active].join(':');
750
819
  const measured = cardMeasure.key === measureKey;
751
820
  const naturalHeight = measured ? cardMeasure.height : 320;
@@ -797,7 +866,7 @@ window.__ModuleLoader__.load({
797
866
  });
798
867
  const collapsed = !petMode && ui.screen === null && view.mini;
799
868
  const classicClock = classicClockPresentation({
800
- hasSession: !!sessionId,
869
+ hasSession: true,
801
870
  loaded: !!data,
802
871
  configured: !!data?.configured,
803
872
  failed: !!error || !!state?.error,
@@ -831,9 +900,16 @@ window.__ModuleLoader__.load({
831
900
  classicSize,
832
901
  );
833
902
  const cardStyle = {
903
+ ...featureStyle,
834
904
  width: ui.screen
835
- ? petSurfaceWidth(ui.screen, viewport)
836
- : Math.min(collapsed ? 224 : 320, Math.max(1, viewport.width - 24)),
905
+ ? Math.min(
906
+ petSurfaceWidth(ui.screen, viewport) * displayPreferences.cardScale,
907
+ viewport.width - 24,
908
+ )
909
+ : Math.min(
910
+ (collapsed ? 224 : 320) * displayPreferences.cardScale,
911
+ Math.max(1, viewport.width - 24),
912
+ ),
837
913
  '--owc-alpha': String(view.opacity),
838
914
  '--owc-viewport-height': Math.max(1, viewport.height - 24) + 'px',
839
915
  left: classicPosition.x,
@@ -1102,6 +1178,13 @@ window.__ModuleLoader__.load({
1102
1178
  const outcome = await waterRuntime.current?.update(action);
1103
1179
  const result = outcome?.state;
1104
1180
  if (result) {
1181
+ if (outcome.recorded)
1182
+ void features.record({
1183
+ id: 'water-record:' + result.revision,
1184
+ type: 'water',
1185
+ at: Date.now(),
1186
+ label: 'Water',
1187
+ });
1105
1188
  setWaterConfirm(null);
1106
1189
  setNow(Date.now());
1107
1190
  if (action.type === 'preferences')
@@ -1166,12 +1249,11 @@ window.__ModuleLoader__.load({
1166
1249
  }
1167
1250
 
1168
1251
  React.useEffect(() => {
1169
- if (sessionId) reload();
1170
- else setError(t('noSession'));
1252
+ reload();
1171
1253
  return () => {
1172
1254
  generation.current++;
1173
1255
  };
1174
- }, [sessionId]);
1256
+ }, []);
1175
1257
 
1176
1258
  React.useEffect(() => {
1177
1259
  let timer;
@@ -1257,13 +1339,19 @@ window.__ModuleLoader__.load({
1257
1339
  );
1258
1340
 
1259
1341
  async function request(action, patch) {
1260
- if (!sessionId) throw new Error(t('noSession'));
1261
- const rpc = await ctx.remote.commands.execute(
1262
- sessionId,
1263
- '/offwork-clock ' + action + (patch ? ' ' + JSON.stringify(patch) : ''),
1264
- [],
1265
- );
1266
- return decodeCommand(rpc);
1342
+ const response = await fetch(new URL('offwork-clock/settings', document.baseURI), {
1343
+ method: action === 'get' ? 'GET' : 'POST',
1344
+ credentials: 'same-origin',
1345
+ cache: 'no-store',
1346
+ headers: {
1347
+ 'X-Offwork-Client': '1',
1348
+ ...(action === 'get' ? {} : { 'Content-Type': 'application/json' }),
1349
+ },
1350
+ ...(action === 'get' ? {} : { body: JSON.stringify({ action, settings: patch }) }),
1351
+ });
1352
+ const result = await response.json();
1353
+ if (!response.ok || !result.ok) throw new Error(result.error || t('error'));
1354
+ return result;
1267
1355
  }
1268
1356
 
1269
1357
  async function reload(explicit = false) {
@@ -1435,6 +1523,7 @@ window.__ModuleLoader__.load({
1435
1523
  function performNavigation(screen, root = false) {
1436
1524
  if (root) panelPlacement.current = null;
1437
1525
  if (screen !== ui.screen) {
1526
+ features.discardDrafts();
1438
1527
  if (screen === 'water') {
1439
1528
  waterPreferencesBase.current = { enabled: water.enabled, interval: water.interval };
1440
1529
  setWaterInterval(String(water.interval));
@@ -1448,6 +1537,7 @@ window.__ModuleLoader__.load({
1448
1537
  setError('');
1449
1538
  }
1450
1539
  if (screen === 'appearance') {
1540
+ features.beginAppearance();
1451
1541
  appearanceBase.current = appearanceValues(savedView);
1452
1542
  appearanceWasPet.current = !!petMode;
1453
1543
  setAppearanceDraft(appearanceValues(savedView));
@@ -1462,6 +1552,7 @@ window.__ModuleLoader__.load({
1462
1552
  setWaterConfirm(null);
1463
1553
  }
1464
1554
  function navigateScreen(screen, event) {
1555
+ if (screen === 'water-countdown') screen = 'home';
1465
1556
  if (
1466
1557
  screen !== null &&
1467
1558
  !LOCAL_SCREENS.includes(screen) &&
@@ -1470,6 +1561,7 @@ window.__ModuleLoader__.load({
1470
1561
  return;
1471
1562
  if (
1472
1563
  ui.pending ||
1564
+ features.busy ||
1473
1565
  waterBusy ||
1474
1566
  requestBusy.current ||
1475
1567
  waterPending.current ||
@@ -1485,6 +1577,7 @@ window.__ModuleLoader__.load({
1485
1577
  performNavigation(screen, root);
1486
1578
  }
1487
1579
  function draftChanged() {
1580
+ if (features.dirty) return true;
1488
1581
  if (waterEditing && waterPreferencesBase.current)
1489
1582
  return (
1490
1583
  waterEnabledDraft !== waterPreferencesBase.current.enabled ||
@@ -1498,7 +1591,8 @@ window.__ModuleLoader__.load({
1498
1591
  );
1499
1592
  return false;
1500
1593
  }
1501
- function applyAppearance() {
1594
+ async function applyAppearance() {
1595
+ if (!(await features.applyAppearance())) return false;
1502
1596
  panelPlacement.current = null;
1503
1597
  if (appearanceDraft)
1504
1598
  setPreference(changedSettings(appearanceBase.current, appearanceDraft));
@@ -1510,13 +1604,16 @@ window.__ModuleLoader__.load({
1510
1604
  const destination = ui.pending.screen;
1511
1605
  const result = waterEditing
1512
1606
  ? await saveWaterSettings({ preventDefault() {} })
1513
- : appearanceOnly
1514
- ? applyAppearance()
1515
- : await save({ preventDefault() {} });
1607
+ : ui.screen === 'wrap'
1608
+ ? await features.saveNotes(false)
1609
+ : appearanceOnly
1610
+ ? await applyAppearance()
1611
+ : await save({ preventDefault() {} });
1516
1612
  if (result) performNavigation(destination, ui.pending.root);
1517
1613
  else dispatchUI({ type: 'stay' });
1518
1614
  }
1519
1615
  function updateAppearance(patch) {
1616
+ if (patch.petModel) features.useBuiltin();
1520
1617
  setAppearanceDraft((current) => ({ ...current, ...patch }));
1521
1618
  }
1522
1619
  function openAppearance(event) {
@@ -1607,6 +1704,7 @@ window.__ModuleLoader__.load({
1607
1704
  const waterState = hydrationView(water, now);
1608
1705
  const waterClaim = water.claim;
1609
1706
  const waterPrompt =
1707
+ !features.ready &&
1610
1708
  waterReady &&
1611
1709
  !waterError &&
1612
1710
  !editing &&
@@ -1835,7 +1933,7 @@ window.__ModuleLoader__.load({
1835
1933
  break: 'waterBreak',
1836
1934
  paused: 'waterPaused',
1837
1935
  finished: 'waterFinished',
1838
- due: waterPrompt ? 'waterDue' : 'waterOther',
1936
+ due: 'waterDue',
1839
1937
  };
1840
1938
  if (waterState.phase === 'quiet')
1841
1939
  return t('waterQuiet') + ' · ' + formatDuration(waterState.quietRemaining / 1000);
@@ -1846,6 +1944,8 @@ window.__ModuleLoader__.load({
1846
1944
  const waterCountdownText = () => {
1847
1945
  if (waterError) return t('timerUnavailable');
1848
1946
  if (!waterReady) return t('timerLoading');
1947
+ if (waterState.phase === 'due' && features.waterSnoozedUntil > now)
1948
+ return formatDuration(Math.ceil((features.waterSnoozedUntil - now) / 1000));
1849
1949
  if (['countdown', 'due', 'break'].includes(waterState.phase))
1850
1950
  return formatDuration(Math.ceil(waterState.remaining / 1000));
1851
1951
  if (waterState.phase === 'quiet')
@@ -1864,25 +1964,34 @@ window.__ModuleLoader__.load({
1864
1964
  ? t(smokingError ? 'timerUnavailable' : 'timerLoading')
1865
1965
  : smokingState.phase === 'idle'
1866
1966
  ? t('timerIdle')
1867
- : formatDuration(Math.ceil(smokingState.remaining / 1000));
1967
+ : smokingState.phase === 'due' && features.smokingSnoozedUntil > now
1968
+ ? formatDuration(Math.ceil((features.smokingSnoozedUntil - now) / 1000))
1969
+ : formatDuration(Math.ceil(smokingState.remaining / 1000));
1868
1970
  const timerRow = (kind, value, due, title) => {
1869
- const phase = kind === 'water' ? waterState.phase : smokingState.phase;
1971
+ let phase = kind === 'water' ? waterState.phase : smokingState.phase;
1972
+ if (
1973
+ phase === 'due' &&
1974
+ (kind === 'water' ? features.waterSnoozedUntil : features.smokingSnoozedUntil) > now
1975
+ )
1976
+ phase = 'snoozed';
1870
1977
  const extra = {
1978
+ snoozed: 'timerSnoozed',
1871
1979
  paused: 'timerPaused',
1872
1980
  break: 'timerBreak',
1873
1981
  quiet: 'timerQuiet',
1874
1982
  due: 'timerDue',
1875
1983
  }[phase];
1876
1984
  return h(
1877
- 'div',
1985
+ 'button',
1878
1986
  {
1987
+ type: 'button',
1988
+ onClick: (event) => navigateScreen(kind === 'water' ? 'home' : 'smoking', event),
1879
1989
  className: 'owc-pet-timer-row',
1880
1990
  'data-countdown': kind,
1881
- 'data-countdown-due': due,
1991
+ 'data-countdown-due': due && phase !== 'snoozed',
1882
1992
  'data-countdown-phase': phase,
1883
- role: 'timer',
1884
1993
  'aria-label': t(kind === 'water' ? 'waterCountdown' : 'smokingCountdown'),
1885
- title,
1994
+ title: phase === 'snoozed' ? t('timerSnoozed') + ' · ' + value : title,
1886
1995
  },
1887
1996
  h(
1888
1997
  'span',
@@ -1913,11 +2022,13 @@ window.__ModuleLoader__.load({
1913
2022
  { className: 'owc-detail-kicker', 'data-water-status': true },
1914
2023
  t(
1915
2024
  ticking
1916
- ? waterState.phase === 'quiet'
1917
- ? 'waterQuiet'
1918
- : waterState.phase === 'due'
1919
- ? 'waterDue'
1920
- : 'waterNext'
2025
+ ? features.waterSnoozedUntil > now
2026
+ ? 'timerSnoozed'
2027
+ : waterState.phase === 'quiet'
2028
+ ? 'waterQuiet'
2029
+ : waterState.phase === 'due'
2030
+ ? 'waterDue'
2031
+ : 'waterNext'
1921
2032
  : 'waterTitle',
1922
2033
  ),
1923
2034
  ),
@@ -1995,14 +2106,16 @@ window.__ModuleLoader__.load({
1995
2106
  { role: 'status' },
1996
2107
  smokingUnavailable
1997
2108
  ? t(smokingError ? 'timerUnavailable' : 'timerLoading')
1998
- : t(
1999
- {
2000
- idle: 'timerIdle',
2001
- running: 'timerRunning',
2002
- paused: 'timerPaused',
2003
- due: 'timerDue',
2004
- }[smokingState.phase],
2005
- ),
2109
+ : smokingState.phase === 'due' && features.smokingSnoozedUntil > now
2110
+ ? t('timerSnoozed')
2111
+ : t(
2112
+ {
2113
+ idle: 'timerIdle',
2114
+ running: 'timerRunning',
2115
+ paused: 'timerPaused',
2116
+ due: 'timerDue',
2117
+ }[smokingState.phase],
2118
+ ),
2006
2119
  ),
2007
2120
  h(
2008
2121
  'label',
@@ -2069,7 +2182,8 @@ window.__ModuleLoader__.load({
2069
2182
  t('timerStop'),
2070
2183
  ),
2071
2184
  ),
2072
- h('p', { className: 'owc-hint' }, t('detailNextInterval')),
2185
+ smokingState.phase !== 'idle' &&
2186
+ h('p', { className: 'owc-hint' }, t('detailNextInterval')),
2073
2187
  h(
2074
2188
  'details',
2075
2189
  { className: 'owc-detail-more' },
@@ -2525,11 +2639,12 @@ window.__ModuleLoader__.load({
2525
2639
  h('span', { className: 'owc-classic-mini-label' }, t(caption)),
2526
2640
  countdown
2527
2641
  ? h(
2528
- 'strong',
2642
+ 'button',
2529
2643
  {
2644
+ type: 'button',
2645
+ onClick: (event) => navigateScreen('income', event),
2530
2646
  className: 'owc-classic-mini-time',
2531
- role: 'timer',
2532
- 'aria-label': t(classicClock.title),
2647
+ 'aria-label': t('petCountdownCard'),
2533
2648
  },
2534
2649
  formatDuration(classicClock.seconds),
2535
2650
  )
@@ -2662,7 +2777,7 @@ window.__ModuleLoader__.load({
2662
2777
  h(
2663
2778
  'strong',
2664
2779
  { className: 'owc-title', tabIndex: 0, title: t('drag'), onKeyDown: gripKeys },
2665
- t(ui.screen ? petPageTitle[ui.screen] || 'classicOverview' : 'classicOverview'),
2780
+ t(ui.screen ? pageTitle(ui.screen) : 'classicOverview'),
2666
2781
  ),
2667
2782
  ['home', 'water-countdown'].includes(ui.screen) &&
2668
2783
  h(
@@ -2691,6 +2806,18 @@ window.__ModuleLoader__.load({
2691
2806
  },
2692
2807
  iconSvg('water'),
2693
2808
  ),
2809
+ ui.screen &&
2810
+ h(
2811
+ 'button',
2812
+ {
2813
+ type: 'button',
2814
+ className: 'owc-icon',
2815
+ 'data-panel-more': true,
2816
+ 'aria-label': t('quickMore'),
2817
+ onClick: openQuickMenu,
2818
+ },
2819
+ '⋯',
2820
+ ),
2694
2821
  !ui.screen &&
2695
2822
  h(
2696
2823
  React.Fragment,
@@ -2792,7 +2919,7 @@ window.__ModuleLoader__.load({
2792
2919
  'button',
2793
2920
  {
2794
2921
  type: 'button',
2795
- disabled: hostBusy || !sessionId,
2922
+ disabled: hostBusy,
2796
2923
  onClick: () => reload(true),
2797
2924
  },
2798
2925
  iconSvg('refresh', 12),
@@ -3128,7 +3255,6 @@ window.__ModuleLoader__.load({
3128
3255
  h('span', { className: 'owc-dot' }),
3129
3256
  t('loading'),
3130
3257
  ),
3131
- !sessionId && h('p', { className: 'owc-hint' }, t('noSession')),
3132
3258
  h(
3133
3259
  'div',
3134
3260
  { className: 'owc-row-actions' },
@@ -3138,7 +3264,7 @@ window.__ModuleLoader__.load({
3138
3264
  type: 'button',
3139
3265
  className: 'owc-text',
3140
3266
  onClick: () => reload(true),
3141
- disabled: hostBusy || !sessionId,
3267
+ disabled: hostBusy,
3142
3268
  },
3143
3269
  iconSvg('refresh', 12),
3144
3270
  t('retry'),
@@ -3244,38 +3370,6 @@ window.__ModuleLoader__.load({
3244
3370
  ),
3245
3371
  ),
3246
3372
  ),
3247
- h(
3248
- 'div',
3249
- { className: 'owc-row-actions' },
3250
- h(
3251
- 'button',
3252
- {
3253
- type: 'button',
3254
- className: 'owc-text',
3255
- onClick: () => reload(true),
3256
- disabled: hostBusy,
3257
- },
3258
- iconSvg('refresh', 12),
3259
- t('refresh'),
3260
- ),
3261
- petMode &&
3262
- h(
3263
- 'button',
3264
- { type: 'button', className: 'owc-text', onClick: openEditor },
3265
- t('petWorkSettings'),
3266
- ),
3267
- !petMode &&
3268
- h(
3269
- 'button',
3270
- {
3271
- type: 'button',
3272
- className: 'owc-text',
3273
- onClick: () =>
3274
- setPreference(petMode ? { petPosition: null } : { position: null }),
3275
- },
3276
- t('reset'),
3277
- ),
3278
- ),
3279
3373
  );
3280
3374
  };
3281
3375
 
@@ -3295,66 +3389,85 @@ window.__ModuleLoader__.load({
3295
3389
  }),
3296
3390
  t('petMode'),
3297
3391
  ),
3298
- h(
3299
- 'label',
3300
- { className: 'owc-check' },
3301
- h('input', {
3302
- type: 'checkbox',
3303
- checked: view.companionMotion,
3304
- disabled: !view.companion,
3305
- onChange: (event) => updateAppearance({ companionMotion: event.target.checked }),
3306
- }),
3307
- t('companionMotion'),
3308
- ),
3309
3392
  label(
3310
3393
  t('petCharacter'),
3311
3394
  h(
3312
3395
  'select',
3313
3396
  {
3314
3397
  'aria-label': t('petCharacter'),
3315
- value: view.petModel,
3398
+ value: features.previewImage ? 'custom-image' : view.petModel,
3316
3399
  disabled: !view.companion,
3317
- onChange: (event) => updateAppearance({ petModel: event.target.value }),
3400
+ onChange: (event) =>
3401
+ event.target.value === 'custom-image'
3402
+ ? features.selectCustom()
3403
+ : updateAppearance({ petModel: event.target.value }),
3318
3404
  },
3405
+ features.hasCustom &&
3406
+ h(
3407
+ 'option',
3408
+ { value: 'custom-image' },
3409
+ locale.active.startsWith('en') ? 'Custom image' : '自定义图片',
3410
+ ),
3319
3411
  ...Object.values(PET_MODELS).map((model) =>
3320
3412
  h('option', { key: model.id, value: model.id }, t(model.label)),
3321
3413
  ),
3322
3414
  ),
3323
3415
  ),
3324
- label(
3325
- t('petSize'),
3326
- h('input', {
3327
- type: 'range',
3328
- min: '80',
3329
- max: '180',
3330
- step: '4',
3331
- value: view.petSize,
3332
- 'aria-label': t('petSize'),
3333
- disabled: !view.companion,
3334
- onChange: (event) => updateAppearance({ petSize: event.target.valueAsNumber }),
3335
- }),
3336
- ),
3337
- label(
3338
- t('opacity'),
3416
+ h(
3417
+ 'details',
3418
+ { className: 'owc-appearance-display' },
3339
3419
  h(
3340
- React.Fragment,
3420
+ 'summary',
3341
3421
  null,
3422
+ locale.active.startsWith('en') ? 'Display and motion' : '显示与动作',
3423
+ ),
3424
+ h(
3425
+ 'label',
3426
+ { className: 'owc-check' },
3342
3427
  h('input', {
3343
- className: 'owc-range',
3344
- type: 'range',
3345
- min: '30',
3346
- max: '100',
3347
- step: '1',
3348
- value: String(Math.round(view.opacity * 100)),
3349
- disabled: busy,
3350
- 'aria-label': t('opacity'),
3428
+ type: 'checkbox',
3429
+ checked: view.companionMotion,
3430
+ disabled: !view.companion,
3351
3431
  onChange: (event) =>
3352
- updateAppearance({ opacity: event.target.valueAsNumber / 100 }),
3432
+ updateAppearance({ companionMotion: event.target.checked }),
3353
3433
  }),
3434
+ t('companionMotion'),
3435
+ ),
3436
+ label(
3437
+ t('petSize'),
3438
+ h('input', {
3439
+ type: 'range',
3440
+ min: '80',
3441
+ max: '180',
3442
+ step: '4',
3443
+ value: view.petSize,
3444
+ 'aria-label': t('petSize'),
3445
+ disabled: !view.companion,
3446
+ onChange: (event) => updateAppearance({ petSize: event.target.valueAsNumber }),
3447
+ }),
3448
+ ),
3449
+ label(
3450
+ t('opacity'),
3354
3451
  h(
3355
- 'span',
3356
- { className: 'owc-range-value', title: t('opacityHint') },
3357
- Math.round(view.opacity * 100) + '%',
3452
+ React.Fragment,
3453
+ null,
3454
+ h('input', {
3455
+ className: 'owc-range',
3456
+ type: 'range',
3457
+ min: '30',
3458
+ max: '100',
3459
+ step: '1',
3460
+ value: String(Math.round(view.opacity * 100)),
3461
+ disabled: busy,
3462
+ 'aria-label': t('opacity'),
3463
+ onChange: (event) =>
3464
+ updateAppearance({ opacity: event.target.valueAsNumber / 100 }),
3465
+ }),
3466
+ h(
3467
+ 'span',
3468
+ { className: 'owc-range-value', title: t('opacityHint') },
3469
+ Math.round(view.opacity * 100) + '%',
3470
+ ),
3358
3471
  ),
3359
3472
  ),
3360
3473
  ),
@@ -3440,6 +3553,7 @@ window.__ModuleLoader__.load({
3440
3553
  {
3441
3554
  ref: previewBox,
3442
3555
  'data-appearance-preview': true,
3556
+ style: features.cropping ? { display: 'none' } : undefined,
3443
3557
  className: 'owc-appearance-preview',
3444
3558
  },
3445
3559
  view.companion
@@ -3448,14 +3562,15 @@ window.__ModuleLoader__.load({
3448
3562
  { pose: 'calm', animate: false },
3449
3563
  view.petSize * (petModel(view.petModel).renderer === 'svg' ? 1 : 0.65),
3450
3564
  '',
3451
- view.petModel,
3452
- PET_SPRITES,
3453
- petModel(view.petModel).renderer === 'video',
3565
+ features.previewImage ? 'custom-image' : view.petModel,
3566
+ { ...PET_SPRITES, 'custom-image': features.previewImage },
3567
+ !features.previewImage && petModel(view.petModel).renderer === 'video',
3454
3568
  )
3455
3569
  : h('p', null, t('petClassicPreview')),
3456
3570
  ),
3457
- previewActions(),
3571
+ !features.previewImage && previewActions(),
3458
3572
  appearanceFields(),
3573
+ features.render('appearance'),
3459
3574
  h(
3460
3575
  'div',
3461
3576
  { className: 'owc-save' },
@@ -3668,8 +3783,6 @@ window.__ModuleLoader__.load({
3668
3783
  { className: 'owc-advanced' },
3669
3784
  h('summary', null, t('advanced')),
3670
3785
  timezoneField(),
3671
- h('p', { className: 'owc-hint' }, t('sessionHint')),
3672
- label(t('session'), sessionSelect()),
3673
3786
  ),
3674
3787
  h('p', { className: 'owc-hint' }, t('privacy')),
3675
3788
  h(
@@ -3747,7 +3860,7 @@ window.__ModuleLoader__.load({
3747
3860
  if (scroll) scroll.scrollTop = 0;
3748
3861
  }, [ready, petMode, ui.screen]);
3749
3862
  const petShowing = petOpen;
3750
- const petFloating = petShowing || autoWaterPrompt;
3863
+ const petFloating = petShowing || autoWaterPrompt || features.alert;
3751
3864
  React.useLayoutEffect(() => {
3752
3865
  if (!petMode || !petFloating || !cardContent.current) return;
3753
3866
  const content = cardContent.current,
@@ -3836,20 +3949,38 @@ window.__ModuleLoader__.load({
3836
3949
  return () => window.removeEventListener('blur', blur);
3837
3950
  }, [ui.menu]);
3838
3951
  React.useEffect(() => {
3839
- if (!petMode || (!petShowing && !ui.menu)) return;
3952
+ if (!petShowing && !ui.menu) return;
3840
3953
  const outside = (event) => {
3954
+ if (ui.menu && event.target.closest?.('[data-quick-more],[data-panel-more]')) return;
3841
3955
  if (ui.menu && !menuBox.current?.contains(event.target)) {
3842
3956
  dispatchUI({ type: 'dismiss-menu' });
3843
3957
  return;
3844
3958
  }
3845
3959
  if (box.current?.contains(event.target)) return;
3846
3960
  if (ui.menu) dispatchUI({ type: 'dismiss-menu' });
3847
- else if (!editing && !waterEditing && !waterBusy)
3848
- dispatchUI({ type: 'navigate', screen: null });
3961
+ else if (
3962
+ petMode &&
3963
+ !editing &&
3964
+ !waterEditing &&
3965
+ !waterBusy &&
3966
+ !features.dirty &&
3967
+ !features.busy
3968
+ )
3969
+ navigateScreen(null);
3849
3970
  };
3850
3971
  document.addEventListener('pointerdown', outside);
3851
3972
  return () => document.removeEventListener('pointerdown', outside);
3852
- }, [petMode, petShowing, ui.menu, editing, waterEditing, waterBusy]);
3973
+ }, [
3974
+ petMode,
3975
+ petShowing,
3976
+ ui.menu,
3977
+ ui.screen,
3978
+ editing,
3979
+ waterEditing,
3980
+ waterBusy,
3981
+ features.dirty,
3982
+ features.busy,
3983
+ ]);
3853
3984
  React.useEffect(() => {
3854
3985
  if (!ui.menu) return;
3855
3986
  const waterEntry = menuTrigger.current?.classList.contains('owc-pet-water-alert')
@@ -3967,7 +4098,21 @@ window.__ModuleLoader__.load({
3967
4098
  water: 'petWaterSettings',
3968
4099
  appearance: 'petAppearance',
3969
4100
  schedule: 'petWorkSettings',
4101
+ wrap: 'companionWrapTitle',
4102
+ preferences: 'companionPreferencesTitle',
4103
+ data: 'companionDataTitle',
4104
+ reminders: 'companionRemindersTitle',
3970
4105
  };
4106
+ const pageTitle = (screen) =>
4107
+ screen === 'income' && safeState
4108
+ ? safeState.status === 'before'
4109
+ ? t('petCountdownBefore')
4110
+ : safeState.status === 'done'
4111
+ ? t('classicMiniFinished')
4112
+ : safeState.status === 'rest'
4113
+ ? t('classicMiniRest')
4114
+ : t('petCountdownCard')
4115
+ : t(petPageTitle[screen] || 'waterTitle');
3971
4116
  const petHeader = () =>
3972
4117
  h(
3973
4118
  'div',
@@ -3989,8 +4134,22 @@ window.__ModuleLoader__.load({
3989
4134
  h(
3990
4135
  'strong',
3991
4136
  { 'data-pet-page-title': true, tabIndex: -1, style: { flex: 1 } },
3992
- t(petPageTitle[ui.screen] || 'waterTitle'),
4137
+ pageTitle(ui.screen),
3993
4138
  ),
4139
+ ui.screen === 'income' &&
4140
+ data?.configured &&
4141
+ h(
4142
+ 'button',
4143
+ {
4144
+ type: 'button',
4145
+ className: 'owc-icon',
4146
+ 'aria-label': t('petWorkSettings'),
4147
+ title: t('petWorkSettings'),
4148
+ disabled: busy || !!ui.pending,
4149
+ onClick: openEditor,
4150
+ },
4151
+ iconSvg('sliders', 14),
4152
+ ),
3994
4153
  ['home', 'water-countdown'].includes(ui.screen) &&
3995
4154
  h(
3996
4155
  'button',
@@ -4004,6 +4163,17 @@ window.__ModuleLoader__.load({
4004
4163
  },
4005
4164
  iconSvg('sliders', 14),
4006
4165
  ),
4166
+ h(
4167
+ 'button',
4168
+ {
4169
+ type: 'button',
4170
+ className: 'owc-icon',
4171
+ 'data-panel-more': true,
4172
+ 'aria-label': t('quickMore'),
4173
+ onClick: openQuickMenu,
4174
+ },
4175
+ '⋯',
4176
+ ),
4007
4177
  h(
4008
4178
  'button',
4009
4179
  {
@@ -4025,6 +4195,23 @@ window.__ModuleLoader__.load({
4025
4195
  'data-water-event': waterPrompt ? waterState.eventId : undefined,
4026
4196
  },
4027
4197
  waterHero(),
4198
+ waterState.phase === 'due' &&
4199
+ features.waterSnoozedUntil > now &&
4200
+ h(
4201
+ 'p',
4202
+ { className: 'owc-hint' },
4203
+ t('waterSnooze') +
4204
+ ' · ' +
4205
+ formatDuration(Math.ceil((features.waterSnoozedUntil - now) / 1000)),
4206
+ ),
4207
+ features.quiet &&
4208
+ h(
4209
+ 'p',
4210
+ { className: 'owc-hint' },
4211
+ t('waterQuiet') +
4212
+ ' · ' +
4213
+ formatDuration(Math.ceil((features.state.preferences.quietUntil - now) / 1000)),
4214
+ ),
4028
4215
  h(
4029
4216
  'div',
4030
4217
  { className: 'owc-detail-primary-actions' },
@@ -4038,14 +4225,15 @@ window.__ModuleLoader__.load({
4038
4225
  },
4039
4226
  t('petRecord'),
4040
4227
  ),
4041
- waterPrompt &&
4228
+ waterState.phase === 'due' &&
4229
+ features.waterSnoozedUntil <= now &&
4042
4230
  h(
4043
4231
  'button',
4044
4232
  {
4045
4233
  type: 'button',
4046
4234
  className: 'owc-text',
4047
- disabled: waterBusy,
4048
- onClick: () => waterAction({ type: 'snooze', eventId: waterState.eventId }),
4235
+ disabled: waterBusy || features.busy || !features.waterCanSnooze,
4236
+ onClick: () => features.snoozeWater(),
4049
4237
  },
4050
4238
  t('waterSnooze'),
4051
4239
  ),
@@ -4057,11 +4245,16 @@ window.__ModuleLoader__.load({
4057
4245
  'button',
4058
4246
  {
4059
4247
  type: 'button',
4060
- className: 'owc-text',
4061
- disabled: !water.enabled || busy || waterBusy || waterError || !waterReady,
4062
- onClick: () => waterAction({ type: water.paused ? 'resume' : 'pause' }),
4248
+ className: 'owc-text owc-secondary',
4249
+ disabled: busy || waterBusy || waterError || !waterReady,
4250
+ onClick: () =>
4251
+ waterAction(
4252
+ water.enabled
4253
+ ? { type: water.paused ? 'resume' : 'pause' }
4254
+ : { type: 'preferences', enabled: true },
4255
+ ),
4063
4256
  },
4064
- t(water.paused ? 'waterResume' : 'waterPause'),
4257
+ t(!water.enabled ? 'waterEnable' : water.paused ? 'waterResume' : 'waterPause'),
4065
4258
  ),
4066
4259
  waterState.phase === 'quiet' &&
4067
4260
  h(
@@ -4075,33 +4268,34 @@ window.__ModuleLoader__.load({
4075
4268
  t('waterQuietEnd'),
4076
4269
  ),
4077
4270
  ),
4078
- h(
4079
- 'details',
4080
- { className: 'owc-detail-more' },
4081
- h('summary', null, t('waterQuiet')),
4271
+ water.enabled &&
4082
4272
  h(
4083
- 'div',
4084
- { className: 'owc-detail-options' },
4085
- [30, 60].map((minutes) =>
4086
- h(
4087
- 'button',
4088
- {
4089
- type: 'button',
4090
- key: minutes,
4091
- disabled: !waterCanQuiet || waterBusy,
4092
- onClick: (event) => {
4093
- const menu = event.currentTarget.closest('details');
4094
- menu.open = false;
4095
- menu.querySelector('summary')?.focus();
4096
- void waterAction({ type: 'quiet', minutes });
4273
+ 'details',
4274
+ { className: 'owc-detail-more' },
4275
+ h('summary', null, t('waterQuiet')),
4276
+ h(
4277
+ 'div',
4278
+ { className: 'owc-detail-options' },
4279
+ [30, 60].map((minutes) =>
4280
+ h(
4281
+ 'button',
4282
+ {
4283
+ type: 'button',
4284
+ key: minutes,
4285
+ disabled: !waterCanQuiet || waterBusy,
4286
+ onClick: (event) => {
4287
+ const menu = event.currentTarget.closest('details');
4288
+ menu.open = false;
4289
+ menu.querySelector('summary')?.focus();
4290
+ void waterAction({ type: 'quiet', minutes });
4291
+ },
4097
4292
  },
4098
- },
4099
- t(minutes === 30 ? 'petQuiet30' : 'petQuiet60'),
4293
+ t(minutes === 30 ? 'petQuiet30' : 'petQuiet60'),
4294
+ ),
4100
4295
  ),
4101
4296
  ),
4297
+ h('p', { className: 'owc-hint' }, t('petQuietHint')),
4102
4298
  ),
4103
- h('p', { className: 'owc-hint' }, t('petQuietHint')),
4104
- ),
4105
4299
  waterError &&
4106
4300
  h('p', { className: 'owc-error', role: 'alert' }, t('waterUnavailable')),
4107
4301
  waterError &&
@@ -4115,6 +4309,115 @@ window.__ModuleLoader__.load({
4115
4309
  t('waterRetry'),
4116
4310
  ),
4117
4311
  );
4312
+ function openQuickMenu(event) {
4313
+ if (ui.pending || busy || waterBusy || features.busy) return;
4314
+ if (ui.menu) {
4315
+ closePetMenu();
4316
+ return;
4317
+ }
4318
+ menuTrigger.current = event.currentTarget;
4319
+ const rect = event.currentTarget.getBoundingClientRect();
4320
+ dispatchUI({ type: 'menu', position: { x: rect.right, y: rect.top } });
4321
+ }
4322
+ const activeCustomTimer = () =>
4323
+ features.activeTimer &&
4324
+ h(
4325
+ 'button',
4326
+ {
4327
+ type: 'button',
4328
+ className: 'owc-inline-timer',
4329
+ 'data-active-custom-timer': true,
4330
+ onClick: (event) => navigateScreen('smoking', event),
4331
+ title: features.activeTimer.name,
4332
+ },
4333
+ iconSvg('alarm', 12),
4334
+ h('span', null, features.activeTimer.name),
4335
+ h(
4336
+ 'strong',
4337
+ null,
4338
+ (features.activeTimer.phase === 'paused' ? 'Ⅱ ' : '') +
4339
+ formatDuration(Math.ceil(features.activeTimer.remaining / 1000)).replace(
4340
+ /^00:/,
4341
+ '',
4342
+ ),
4343
+ ),
4344
+ features.activeTimer.count > 1 ? ' +' + (features.activeTimer.count - 1) : null,
4345
+ );
4346
+ const quickActions = () =>
4347
+ h(
4348
+ 'div',
4349
+ { className: 'owc-quick-actions' },
4350
+ waterState.phase === 'due' &&
4351
+ h(
4352
+ 'button',
4353
+ {
4354
+ type: 'button',
4355
+ 'data-quick-drink': true,
4356
+ disabled: !waterCanRecord || waterBusy,
4357
+ onClick: recordWater,
4358
+ },
4359
+ iconSvg('water', 13),
4360
+ waterNotice?.kind === 'drink' ? t('waterRecorded') : t('petRecord'),
4361
+ ),
4362
+ features.pendingCount > 0 &&
4363
+ h(
4364
+ 'button',
4365
+ {
4366
+ type: 'button',
4367
+ 'aria-label': t('companionRemindersTitle'),
4368
+ onClick: (event) => navigateScreen('reminders', event),
4369
+ },
4370
+ iconSvg('alarm', 13),
4371
+ String(features.pendingCount),
4372
+ ),
4373
+ h(
4374
+ 'button',
4375
+ {
4376
+ type: 'button',
4377
+ className: 'owc-quick-more',
4378
+ 'data-quick-more': true,
4379
+ 'aria-label': t('quickMore'),
4380
+ 'aria-expanded': !!ui.menu,
4381
+ onClick: openQuickMenu,
4382
+ },
4383
+ '⋯',
4384
+ ),
4385
+ );
4386
+ const contentForScreen = () => {
4387
+ if (ui.pending) return draftGuard();
4388
+ if (waterEditing) return waterSettings();
4389
+ if (editing) return editor();
4390
+ if (ui.screen === 'income')
4391
+ return h(
4392
+ React.Fragment,
4393
+ null,
4394
+ panelBody('clock'),
4395
+ features.render('wrap-entry'),
4396
+ h(
4397
+ 'div',
4398
+ { className: 'owc-extra' },
4399
+ h(
4400
+ 'button',
4401
+ {
4402
+ type: 'button',
4403
+ className: 'owc-link-row',
4404
+ onClick: (event) => navigateScreen('earnings', event),
4405
+ disabled: !data?.configured,
4406
+ },
4407
+ t('petEarningsCard'),
4408
+ ),
4409
+ ),
4410
+ );
4411
+ if (ui.screen === 'smoking')
4412
+ return h(React.Fragment, null, smokingCountdownCard(), features.render('timers'));
4413
+ if (ui.screen === 'water-countdown' || ui.screen === 'home') return petHome();
4414
+ if (ui.screen === 'earnings') return panelBody('earnings');
4415
+ if (ui.screen === 'wrap') return features.render('wrap');
4416
+ if (ui.screen === 'preferences') return features.render('settings');
4417
+ if (ui.screen === 'data') return features.render('data');
4418
+ if (ui.screen === 'reminders') return features.render('reminders');
4419
+ return collapsed ? classicMini() : classicOverview();
4420
+ };
4118
4421
  const contextMenu = () =>
4119
4422
  ui.menu &&
4120
4423
  h(
@@ -4168,11 +4471,9 @@ window.__ModuleLoader__.load({
4168
4471
  [
4169
4472
  ['income', 'petCountdownCard'],
4170
4473
  ['home', 'petWaterCard'],
4171
- ['water-countdown', 'waterCountdown'],
4172
4474
  ['smoking', 'smokingCountdown'],
4173
- ['earnings', 'petEarningsCard'],
4174
4475
  ['appearance', 'petAppearance'],
4175
- ['schedule', 'petWorkSettings'],
4476
+ ['preferences', 'companionPreferencesTitle'],
4176
4477
  ].map(([screen, label]) =>
4177
4478
  h(
4178
4479
  'button',
@@ -4200,7 +4501,7 @@ window.__ModuleLoader__.load({
4200
4501
  type: 'button',
4201
4502
  role: 'menuitem',
4202
4503
  tabIndex: -1,
4203
- disabled: hostBusy || !sessionId,
4504
+ disabled: hostBusy,
4204
4505
  onClick: () => {
4205
4506
  closePetMenu();
4206
4507
  void reload(true);
@@ -4232,7 +4533,7 @@ window.__ModuleLoader__.load({
4232
4533
  {
4233
4534
  ref: box,
4234
4535
  className: 'owc-v2 owc-pet-shell',
4235
- 'data-version': '1.1.2',
4536
+ 'data-version': '1.2.2',
4236
4537
  'data-pet-model': stageModel,
4237
4538
  'data-status':
4238
4539
  error || state?.error
@@ -4244,6 +4545,7 @@ window.__ModuleLoader__.load({
4244
4545
  : 'loading',
4245
4546
  'aria-label': t('companionLabel'),
4246
4547
  style: {
4548
+ ...featureStyle,
4247
4549
  '--owc-alpha': String(view.opacity),
4248
4550
  left: petGeometry.x,
4249
4551
  top: petGeometry.y,
@@ -4267,10 +4569,11 @@ window.__ModuleLoader__.load({
4267
4569
  else if (ui.pending) dispatchUI({ type: 'stay' });
4268
4570
  else if (editing || waterEditing) navigateScreen(ui.back);
4269
4571
  else if (petShowing) closePetPanel();
4572
+ else if (features.alert) features.dismissAlert();
4270
4573
  else if (autoWaterPrompt) dismissWaterPopup();
4271
4574
  },
4272
4575
  },
4273
- h('style', null, STYLE),
4576
+ h('style', null, STYLE + COMPANION_APP_STYLE),
4274
4577
  liveFeedback(),
4275
4578
  h(
4276
4579
  'div',
@@ -4295,7 +4598,7 @@ window.__ModuleLoader__.load({
4295
4598
  petGeometry.artworkSize,
4296
4599
  waterNotice?.token,
4297
4600
  stageModel,
4298
- PET_SPRITES,
4601
+ { ...PET_SPRITES, 'custom-image': features.customImage },
4299
4602
  true,
4300
4603
  ),
4301
4604
  ),
@@ -4307,17 +4610,14 @@ window.__ModuleLoader__.load({
4307
4610
  'aria-label': t('petCountdownGroup'),
4308
4611
  },
4309
4612
  h(
4310
- 'div',
4613
+ 'button',
4311
4614
  {
4615
+ type: 'button',
4616
+ onClick: (event) =>
4617
+ navigateScreen(data?.configured ? 'income' : 'schedule', event),
4312
4618
  className: 'owc-pet-clock',
4313
- role: data?.configured && safeState ? 'timer' : 'status',
4314
- 'aria-label': t('petClock'),
4315
- title:
4316
- error || state?.error
4317
- ? t('petRetryHint')
4318
- : !sessionId
4319
- ? t('noSession')
4320
- : undefined,
4619
+ 'aria-label': t('petCountdownCard'),
4620
+ title: error || state?.error ? t('petRetryHint') : undefined,
4321
4621
  },
4322
4622
  iconSvg('alarm', 12),
4323
4623
  h(
@@ -4328,7 +4628,6 @@ window.__ModuleLoader__.load({
4328
4628
  data?.configured &&
4329
4629
  safeState &&
4330
4630
  !error &&
4331
- sessionId &&
4332
4631
  !['done', 'rest'].includes(safeState.status)
4333
4632
  ? h('strong', null, duration)
4334
4633
  : null,
@@ -4360,6 +4659,8 @@ window.__ModuleLoader__.load({
4360
4659
  )),
4361
4660
  ),
4362
4661
  ),
4662
+ activeCustomTimer(),
4663
+ quickActions(),
4363
4664
  ),
4364
4665
  ),
4365
4666
  waterPrompt &&
@@ -4414,7 +4715,10 @@ window.__ModuleLoader__.load({
4414
4715
  'data-pet-screen': ui.screen || 'reminder',
4415
4716
  'data-side': floatingPanel.side,
4416
4717
  role: 'region',
4417
- 'aria-label': t(petPageTitle[ui.screen] || 'waterTitle'),
4718
+ 'aria-label':
4719
+ features.alert && !petShowing
4720
+ ? features.alertTitle
4721
+ : t(petPageTitle[ui.screen] || 'waterTitle'),
4418
4722
  style: {
4419
4723
  left: floatingPanel.left,
4420
4724
  top: floatingPanel.top,
@@ -4429,7 +4733,11 @@ window.__ModuleLoader__.load({
4429
4733
  : h(
4430
4734
  'div',
4431
4735
  { className: 'owc-head' },
4432
- h('strong', { style: { flex: 1, fontSize: 12 } }, t('waterTitle')),
4736
+ h(
4737
+ 'strong',
4738
+ { style: { flex: 1, fontSize: 12 } },
4739
+ features.alert ? features.alertTitle : t('waterTitle'),
4740
+ ),
4433
4741
  h(
4434
4742
  'button',
4435
4743
  {
@@ -4438,7 +4746,8 @@ window.__ModuleLoader__.load({
4438
4746
  'aria-label': t('petDismissReminder'),
4439
4747
  title: t('petDismissReminder'),
4440
4748
  disabled: waterBusy,
4441
- onClick: dismissWaterPopup,
4749
+ onClick: () =>
4750
+ features.alert ? features.dismissAlert() : dismissWaterPopup(),
4442
4751
  },
4443
4752
  iconSvg('close', 14),
4444
4753
  ),
@@ -4453,24 +4762,16 @@ window.__ModuleLoader__.load({
4453
4762
  ? h(
4454
4763
  React.Fragment,
4455
4764
  null,
4456
- ui.pending
4457
- ? draftGuard()
4458
- : waterEditing
4459
- ? waterSettings()
4460
- : editing
4461
- ? editor()
4462
- : petDetails
4463
- ? panelBody('clock')
4464
- : ui.screen === 'water-countdown'
4465
- ? waterCountdownCard()
4466
- : ui.screen === 'smoking'
4467
- ? smokingCountdownCard()
4468
- : ui.screen === 'earnings'
4469
- ? panelBody('earnings')
4470
- : petHome(),
4765
+ contentForScreen(),
4471
4766
  notices(),
4767
+ features.feedback(),
4472
4768
  )
4473
- : waterReminder(),
4769
+ : h(
4770
+ React.Fragment,
4771
+ null,
4772
+ features.alert ? features.render('alert') : waterReminder(),
4773
+ features.feedback(),
4774
+ ),
4474
4775
  ),
4475
4776
  ),
4476
4777
  ),
@@ -4487,7 +4788,7 @@ window.__ModuleLoader__.load({
4487
4788
  (collapsed && waterQuickOpen ? ' owc-quick' : '') +
4488
4789
  (editing || waterEditing ? ' owc-editing' : ''),
4489
4790
  'aria-label': t('title'),
4490
- 'data-version': '1.1.2',
4791
+ 'data-version': '1.2.2',
4491
4792
  'data-status': classicClock.phase,
4492
4793
  style: cardStyle,
4493
4794
  onPointerDown: dragDown,
@@ -4512,7 +4813,7 @@ window.__ModuleLoader__.load({
4512
4813
  else setPreference({ mini: true });
4513
4814
  },
4514
4815
  },
4515
- h('style', null, STYLE),
4816
+ h('style', null, STYLE + COMPANION_APP_STYLE),
4516
4817
  h('div', { className: 'owc-glass', 'aria-hidden': 'true' }),
4517
4818
  h(
4518
4819
  'div',
@@ -4531,27 +4832,13 @@ window.__ModuleLoader__.load({
4531
4832
  ),
4532
4833
  ),
4533
4834
  !collapsed && classicHeader(),
4534
- ui.pending
4535
- ? draftGuard()
4536
- : waterEditing
4537
- ? waterSettings()
4538
- : editing
4539
- ? editor()
4540
- : ui.screen === 'water-countdown'
4541
- ? waterCountdownCard()
4542
- : ui.screen === 'smoking'
4543
- ? smokingCountdownCard()
4544
- : ui.screen === 'home'
4545
- ? petHome()
4546
- : ui.screen === 'earnings'
4547
- ? panelBody('earnings')
4548
- : ui.screen === 'income'
4549
- ? panelBody('clock')
4550
- : collapsed
4551
- ? classicMini()
4552
- : classicOverview(),
4835
+ contentForScreen(),
4553
4836
  notices(ui.screen !== null),
4554
- ui.screen === null && waterReminder(),
4837
+ features.feedback(),
4838
+ ui.screen === null && (features.alert ? features.render('alert') : waterReminder()),
4839
+ ui.screen === null && activeCustomTimer(),
4840
+ ui.screen === null && quickActions(),
4841
+ contextMenu(),
4555
4842
  ),
4556
4843
  );
4557
4844
  }