dsh-offwork 1.1.1 → 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +48 -26
  3. package/ROADMAP.md +70 -0
  4. package/client.js +5066 -1099
  5. package/client.source.js +1988 -541
  6. package/companion-app.js +1431 -0
  7. package/companion-image.js +59 -0
  8. package/companion-state.js +424 -0
  9. package/companion.js +6 -9
  10. package/countdown.js +83 -0
  11. package/docs/RELEASE-1.2.2.md +20 -0
  12. package/hydration-store.js +55 -16
  13. package/hydration.js +231 -250
  14. package/icon.svg +28 -1
  15. package/index.js +59 -6
  16. package/locale/en.json +1 -1
  17. package/locale/zh.json +1 -1
  18. package/media-manifest.js +29 -0
  19. package/media-server.js +123 -0
  20. package/package.json +13 -3
  21. package/pet-interactions.js +121 -17
  22. package/pet-motion.js +2 -1
  23. package/pet-reminder.js +20 -0
  24. package/pet-surfaces.js +130 -6
  25. package/pet-video.js +221 -72
  26. package/pet-view.js +71 -4
  27. package/settings-api.js +126 -0
  28. package/tests/classic-overview.test.mjs +77 -0
  29. package/tests/companion-state.test.mjs +267 -0
  30. package/tests/companion.test.mjs +12 -12
  31. package/tests/countdown.test.mjs +115 -0
  32. package/tests/host.test.mjs +26 -13
  33. package/tests/hydration-extras.test.mjs +168 -204
  34. package/tests/hydration-store.test.mjs +199 -213
  35. package/tests/hydration.test.mjs +116 -186
  36. package/tests/media-server.test.mjs +179 -0
  37. package/tests/pet-interactions.test.mjs +19 -0
  38. package/tests/pet-reminder.test.mjs +71 -0
  39. package/tests/pet-video.test.mjs +171 -5
  40. package/tests/settings-api.test.mjs +130 -0
  41. package/tests/ui.test.mjs +4 -1
  42. package/tools/build.mjs +45 -16
  43. package/tools/check-package.mjs +12 -0
  44. package/ui-utils.js +44 -0
@@ -0,0 +1,123 @@
1
+ import { open } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { createHash } from 'node:crypto';
4
+ import { MEDIA_ROUTE, MEDIA_ASSETS } from './media-manifest.js';
5
+
6
+ export function mediaRange(value, size) {
7
+ if (!value) return { start: 0, end: size - 1, status: 200 };
8
+ if (typeof value !== 'string') return null;
9
+ const match = /^bytes=(\d*)-(\d*)$/i.exec(value.trim());
10
+ if (!match || (!match[1] && !match[2])) return null;
11
+ const first = match[1] ? Number(match[1]) : null;
12
+ const last = match[2] ? Number(match[2]) : null;
13
+ if (
14
+ (first !== null && !Number.isSafeInteger(first)) ||
15
+ (last !== null && !Number.isSafeInteger(last))
16
+ )
17
+ return null;
18
+ if (first === null) {
19
+ if (last <= 0) return null;
20
+ return { start: Math.max(0, size - last), end: size - 1, status: 206 };
21
+ }
22
+ if (first >= size || (last !== null && last < first)) return null;
23
+ return { start: first, end: Math.min(last ?? size - 1, size - 1), status: 206 };
24
+ }
25
+
26
+ // Only the generated allowlist can select a file. No request path is used as a disk path.
27
+ export function createMediaHandler({
28
+ assets = MEDIA_ASSETS,
29
+ root = new URL('./', import.meta.url),
30
+ } = {}) {
31
+ const cache = new Map();
32
+ async function bytesFor(key, asset) {
33
+ if (!cache.has(key)) {
34
+ const pending = (async () => {
35
+ const file = await open(
36
+ new URL(asset.file, root),
37
+ constants.O_RDONLY | (constants.O_NOFOLLOW || 0),
38
+ );
39
+ try {
40
+ const stats = await file.stat();
41
+ if (!stats.isFile() || stats.size !== asset.size || stats.size > 2000000)
42
+ throw new Error('Invalid media size');
43
+ const bytes = Buffer.alloc(asset.size);
44
+ let offset = 0;
45
+ while (offset < bytes.length) {
46
+ const result = await file.read(bytes, offset, bytes.length - offset, offset);
47
+ if (!result.bytesRead) throw new Error('Incomplete media');
48
+ offset += result.bytesRead;
49
+ }
50
+ if (createHash('sha256').update(bytes).digest('hex') !== asset.sha256)
51
+ throw new Error('Media integrity mismatch');
52
+ return bytes;
53
+ } finally {
54
+ await file.close();
55
+ }
56
+ })();
57
+ cache.set(key, pending);
58
+ pending.catch(() => {
59
+ if (cache.get(key) === pending) cache.delete(key);
60
+ });
61
+ }
62
+ return cache.get(key);
63
+ }
64
+ return async (req, res) => {
65
+ const empty = (status, headers = {}) => {
66
+ if (res.destroyed || res.writableEnded) return;
67
+ res.writeHead(status, { 'Cache-Control': 'no-store', 'Content-Length': '0', ...headers });
68
+ res.end();
69
+ };
70
+ if (req.method !== 'GET' && req.method !== 'HEAD') return empty(405, { Allow: 'GET, HEAD' });
71
+ let path;
72
+ try {
73
+ path = new URL(req.url, 'http://localhost').pathname;
74
+ } catch {
75
+ return empty(404);
76
+ }
77
+ if (!path.startsWith(MEDIA_ROUTE + '/')) return empty(404);
78
+ const key = path.slice(MEDIA_ROUTE.length + 1);
79
+ if (!Object.hasOwn(assets, key)) return empty(404);
80
+ const asset = assets[key],
81
+ etag = '"' + asset.sha256 + '"';
82
+ const rangeHeader =
83
+ req.method === 'GET' && (!req.headers['if-range'] || req.headers['if-range'] === etag)
84
+ ? req.headers.range
85
+ : null;
86
+ const range = mediaRange(rangeHeader, asset.size);
87
+ const matches = (req.headers['if-none-match'] || '')
88
+ .split(',')
89
+ .some((value) => value.trim() === '*' || value.trim().replace(/^W\//, '') === etag);
90
+ if (!range && !matches) return empty(416, { 'Content-Range': `bytes */${asset.size}` });
91
+ try {
92
+ const bytes = await bytesFor(key, asset);
93
+ if (res.destroyed || res.writableEnded) return;
94
+ const headers = {
95
+ 'Content-Type': 'video/webm',
96
+ 'Accept-Ranges': 'bytes',
97
+ 'Cache-Control': 'public, max-age=31536000, immutable',
98
+ ETag: etag,
99
+ 'X-Content-Type-Options': 'nosniff',
100
+ 'Cross-Origin-Resource-Policy': 'same-origin',
101
+ };
102
+ if (matches) {
103
+ res.writeHead(304, headers);
104
+ res.end();
105
+ return;
106
+ }
107
+ headers['Content-Length'] = String(range.end - range.start + 1);
108
+ if (range.status === 206)
109
+ headers['Content-Range'] = `bytes ${range.start}-${range.end}/${asset.size}`;
110
+ res.writeHead(range.status, headers);
111
+ res.end(req.method === 'HEAD' ? undefined : bytes.subarray(range.start, range.end + 1));
112
+ } catch {
113
+ empty(500);
114
+ }
115
+ };
116
+ }
117
+
118
+ export function registerMediaRoute(ctx) {
119
+ ctx.inject?.(['webServer'], (webCtx) => {
120
+ const handler = createMediaHandler();
121
+ webCtx.effect(() => webCtx.webServer.register({ kind: 'prefix', path: MEDIA_ROUTE, handler }));
122
+ });
123
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-offwork",
3
- "version": "1.1.1",
4
- "description": "Harness 页面陪伴角色、下班倒计时、实时收入与喝水提醒。Character companion, clock-out countdown, earnings and hydration reminders for DeepSeek Harness.",
3
+ "version": "1.2.2",
4
+ "description": "Harness 页面角色与小卡片、下班倒计时、全天喝水提醒、独立抽烟计时和收入展示。A Harness companion with clock-out, all-day hydration and smoking timers, earnings and action previews.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "now",
@@ -23,7 +23,7 @@
23
23
  "overlay"
24
24
  ],
25
25
  "engines": {
26
- "dsh": ">=0.2.1-rc.5 <0.3.0-0"
26
+ "dsh": ">=0.2.2-rc.1 <0.3.0-0"
27
27
  },
28
28
  "exports": {
29
29
  ".": "./index.js",
@@ -34,6 +34,12 @@
34
34
  },
35
35
  "files": [
36
36
  "index.js",
37
+ "settings-api.js",
38
+ "companion-state.js",
39
+ "companion-image.js",
40
+ "companion-app.js",
41
+ "media-server.js",
42
+ "media-manifest.js",
37
43
  "client.js",
38
44
  "client.source.js",
39
45
  "clock.js",
@@ -42,10 +48,12 @@
42
48
  "pet-view.js",
43
49
  "pet-motion.js",
44
50
  "pet-video.js",
51
+ "pet-reminder.js",
45
52
  "pet-interactions.js",
46
53
  "pet-surfaces.js",
47
54
  "hydration.js",
48
55
  "hydration-store.js",
56
+ "countdown.js",
49
57
  "cordis.patch.yml",
50
58
  "locale/*.json",
51
59
  "icon.svg",
@@ -54,6 +62,8 @@
54
62
  "LICENSE",
55
63
  "README.md",
56
64
  "CHANGELOG.md",
65
+ "ROADMAP.md",
66
+ "docs/RELEASE-1.2.2.md",
57
67
  "tests/*.mjs",
58
68
  "tools/*.mjs"
59
69
  ],
@@ -1,5 +1,13 @@
1
1
  // Ephemeral UI navigation only. Business data stays in its existing stores.
2
- export const PET_EDITOR_SCREENS = ['water', 'schedule', 'appearance'];
2
+ export const PET_EDITOR_SCREENS = [
3
+ 'water',
4
+ 'schedule',
5
+ 'appearance',
6
+ 'wrap',
7
+ 'preferences',
8
+ 'data',
9
+ 'reminders',
10
+ ];
3
11
  export const initialPetUI = { screen: null, back: null, menu: null, pending: null };
4
12
  export function petUIReducer(state, action) {
5
13
  switch (action.type) {
@@ -10,7 +18,17 @@ export function petUIReducer(state, action) {
10
18
  case 'stay':
11
19
  return { ...state, pending: null };
12
20
  case 'navigate': {
13
- if (![null, 'home', 'income', 'earnings', ...PET_EDITOR_SCREENS].includes(action.screen))
21
+ if (
22
+ ![
23
+ null,
24
+ 'home',
25
+ 'income',
26
+ 'earnings',
27
+ 'water-countdown',
28
+ 'smoking',
29
+ ...PET_EDITOR_SCREENS,
30
+ ].includes(action.screen)
31
+ )
14
32
  return state;
15
33
  if (state.screen === action.screen)
16
34
  return { ...state, back: action.root ? null : state.back, menu: null, pending: null };
@@ -22,11 +40,22 @@ export function petUIReducer(state, action) {
22
40
  };
23
41
  const back = action.root
24
42
  ? null
25
- : PET_EDITOR_SCREENS.includes(action.screen)
26
- ? PET_EDITOR_SCREENS.includes(state.screen)
27
- ? state.back
28
- : state.screen
29
- : state.back;
43
+ : action.screen === state.back
44
+ ? null
45
+ : action.screen === 'data'
46
+ ? 'preferences'
47
+ : action.screen === 'wrap'
48
+ ? 'income'
49
+ : state.screen === 'preferences' &&
50
+ ['schedule', 'water', 'appearance'].includes(action.screen)
51
+ ? 'preferences'
52
+ : action.screen === 'preferences'
53
+ ? state.screen
54
+ : PET_EDITOR_SCREENS.includes(action.screen)
55
+ ? PET_EDITOR_SCREENS.includes(state.screen)
56
+ ? state.back
57
+ : state.screen
58
+ : state.back;
30
59
  return { screen: action.screen, back, menu: null, pending: null };
31
60
  }
32
61
  default:
@@ -44,12 +73,48 @@ export function appearanceValues(view) {
44
73
  }
45
74
  export const PET_INTERACTION_MESSAGES = {
46
75
  zh: {
76
+ companionPreferencesTitle: '设置',
77
+ companionDataTitle: '备份与恢复',
78
+ companionWrapTitle: '今日收尾',
79
+ companionRemindersTitle: '待处理提醒',
80
+ quickMore: '更多',
47
81
  petHome: '喝水',
48
82
  petWaterCard: '喝水',
83
+ waterCountdown: '喝水倒计时',
84
+ timerWaterDetails: '查看喝水卡片',
85
+ smokingCountdown: '抽烟倒计时',
86
+ waterTimerShort: '喝水',
87
+ smokingTimerShort: '抽烟',
88
+ timerIdle: '未开始',
89
+ timerRunning: '计时中',
90
+ timerPaused: '已暂停',
91
+ timerSnoozed: '稍后提醒',
92
+ timerDue: '已到时',
93
+ timerStart: '开始计时',
94
+ timerPause: '暂停计时',
95
+ timerResume: '继续计时',
96
+ timerRestart: '重新计时',
97
+ timerStop: '停止计时',
98
+ timerInterval: '间隔(分钟)',
99
+ timerHint:
100
+ '按自然时间计时,后台和刷新后继续。间隔自动保存,下次开始或重新计时时生效。到点后不自动开始下一轮。',
101
+ timerStorageError: '计时数据暂时无法读取或保存,请重试。',
102
+ timerChanged: '计时已在其他标签页更新,请按最新状态操作。',
103
+ timerRetry: '重试读取计时',
104
+ timerLoading: '读取中',
105
+ waterTimerHint: '按自然时间计时,午休、下班和休息日也可提醒;可随时打卡。',
106
+ timerQuiet: '免打扰',
107
+ timerOff: '未开启',
108
+ timerWaiting: '等待开启',
109
+ timerBreak: '已暂停',
110
+ timerFinished: '已结束',
111
+ timerUnavailable: '暂不可用',
49
112
  petCountdownCard: '下班倒计时',
50
- petEarningsCard: '收入',
51
- petWaterAlert: '喝水提醒已到,可从右键菜单打开喝水卡片',
52
- petPausedHint: '本班已暂停,可手动恢复。',
113
+ petCountdownBefore: '上班倒计时',
114
+ petCountdownGroup: '计时概览',
115
+ petEarningsCard: '收入明细',
116
+ petWaterAlert: '查看喝水提醒',
117
+ petPausedHint: '提醒已暂停,手动恢复后重新计时。',
53
118
  petMenu: '宠物菜单',
54
119
  petBack: '返回',
55
120
  petCloseAll: '关闭面板',
@@ -69,16 +134,53 @@ export const PET_INTERACTION_MESSAGES = {
69
134
  petQuietActive: '喝水提醒已静音',
70
135
  petHomeHint: '喝水、歇一会儿,都在这里。',
71
136
  petQuietHint: '按自然时间,到期自动恢复。',
72
- petResume: '恢复本班提醒',
73
- petDismissReminder: '稍后处理',
137
+ petResume: '恢复提醒',
138
+ petDismissReminder: '收起提醒',
74
139
  },
75
140
  en: {
141
+ companionPreferencesTitle: 'Settings',
142
+ companionDataTitle: 'Backup and restore',
143
+ companionWrapTitle: 'Wrap up today',
144
+ companionRemindersTitle: 'Pending reminders',
145
+ quickMore: 'More',
76
146
  petHome: 'Water',
77
147
  petWaterCard: 'Water',
148
+ waterCountdown: 'Water countdown',
149
+ timerWaterDetails: 'Water controls',
150
+ smokingCountdown: 'Smoking countdown',
151
+ waterTimerShort: 'Water',
152
+ smokingTimerShort: 'Smoking',
153
+ timerIdle: 'Not started',
154
+ timerRunning: 'Running',
155
+ timerPaused: 'Paused',
156
+ timerSnoozed: 'Snoozed',
157
+ timerDue: 'Time is up',
158
+ timerStart: 'Start timer',
159
+ timerPause: 'Pause timer',
160
+ timerResume: 'Resume timer',
161
+ timerRestart: 'Restart timer',
162
+ timerStop: 'Stop timer',
163
+ timerInterval: 'Interval (minutes)',
164
+ timerHint:
165
+ 'Uses clock time and continues in the background and after reload. Interval changes save automatically and apply on the next start or restart. Expiry does not start another cycle.',
166
+ timerStorageError: 'Timer data could not be loaded or saved. Please retry.',
167
+ timerChanged: 'The timer changed in another tab. Use the updated state.',
168
+ timerRetry: 'Retry loading timer',
169
+ timerLoading: 'Loading',
170
+ waterTimerHint:
171
+ 'Uses clock time, including lunch, off-hours and days off. You can record a drink at any time.',
172
+ timerQuiet: 'Quiet time',
173
+ timerOff: 'Disabled',
174
+ timerWaiting: 'Waiting',
175
+ timerBreak: 'Paused',
176
+ timerFinished: 'Finished',
177
+ timerUnavailable: 'Unavailable',
78
178
  petCountdownCard: 'Shift countdown',
79
- petEarningsCard: 'Earnings',
80
- petWaterAlert: 'Water is due; open Water from the context menu',
81
- petPausedHint: 'Paused for this shift. Resume when ready.',
179
+ petCountdownBefore: 'Until work starts',
180
+ petCountdownGroup: 'Timer overview',
181
+ petEarningsCard: 'Earnings details',
182
+ petWaterAlert: 'View water reminder',
183
+ petPausedHint: 'Reminders paused. Resume manually to start a fresh interval.',
82
184
  petMenu: 'Pet menu',
83
185
  petBack: 'Back',
84
186
  petCloseAll: 'Close panel',
@@ -100,13 +202,15 @@ export const PET_INTERACTION_MESSAGES = {
100
202
  petHomeHint: 'A drink or a little break, right here.',
101
203
  petQuietHint: 'Uses clock time and resumes automatically.',
102
204
  petResume: 'Resume reminders',
103
- petDismissReminder: 'Handle later',
205
+ petDismissReminder: 'Dismiss reminder',
104
206
  },
105
207
  };
106
208
  export const PET_INTERACTION_STYLE = `
107
209
  .owc-pet-workspace .owc-glass{opacity:var(--owc-alpha)}
108
210
  .owc-pet-water-card.owc-water-due{border-radius:14px}
109
- .owc-pet-water-alert{display:flex;align-items:center;justify-content:center;gap:6px;padding:8px 12px;border-radius:12px;cursor:default;position:fixed!important;width:140px;box-sizing:border-box;min-height:30px!important;border:1px solid var(--owc-accent)!important;background:var(--dsw-alias-bg-overlay)!important;box-shadow:0 4px 12px #18354b18;font-size:11px!important;z-index:2}
211
+ .owc-pet-water-label{min-width:0;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
212
+ .owc-pet-water-alert svg{flex:none}
213
+ .owc-pet-water-alert{display:flex;align-items:center;justify-content:center;gap:6px;padding:8px 12px;border-radius:12px;cursor:pointer;position:fixed!important;width:140px;box-sizing:border-box;min-height:44px!important;border:1px solid var(--owc-accent)!important;background:var(--dsw-alias-bg-overlay)!important;box-shadow:0 4px 12px #18354b18;font-size:11px!important;z-index:2}
110
214
  .owc-pet-panel.owc-pet-workspace{display:flex;flex-direction:column;overflow:hidden}
111
215
  .owc-pet-workspace>.owc-head{position:relative;flex:none;min-height:46px;padding:8px 12px;box-sizing:border-box}
112
216
  .owc-pet-workspace>.owc-pet-scroll{position:relative;min-height:0;flex:1;overflow:auto;overscroll-behavior:contain;scrollbar-width:none}
package/pet-motion.js CHANGED
@@ -24,6 +24,7 @@ const PET_ACTIONS = {
24
24
  export function createPetMotion(element, host, options = {}) {
25
25
  const document = host.document;
26
26
  const reduced = host.matchMedia?.('(prefers-reduced-motion: reduce)');
27
+ const ambient = options.ambient !== false;
27
28
  let enabled = options.enabled ?? true;
28
29
  let resting = options.resting ?? false;
29
30
  let animation = null;
@@ -38,7 +39,7 @@ export function createPetMotion(element, host, options = {}) {
38
39
  }
39
40
  function sync() {
40
41
  if (!allowed()) stop();
41
- element.dataset.motion = allowed() && !resting ? 'idle' : 'off';
42
+ element.dataset.motion = allowed() && !resting && ambient ? 'idle' : 'off';
42
43
  }
43
44
  function play(action) {
44
45
  if (!allowed() || !Object.hasOwn(PET_ACTIONS, action) || !element.animate) return false;
@@ -0,0 +1,20 @@
1
+ // Page-local presentation state. Never changes reminder deadlines or records.
2
+ export const initialPetReminder = Object.freeze({ eventId: null, armed: false, open: false });
3
+
4
+ export function nextPetReminder(state, { ready, eventId, phase, visible, available, ownsPrompt }) {
5
+ let next;
6
+ if (!ready || !visible || !eventId) next = initialPetReminder;
7
+ else if (['countdown', 'break', 'quiet'].includes(phase))
8
+ next = { eventId, armed: true, open: false };
9
+ else if (phase !== 'due' || !available || state.eventId !== eventId)
10
+ next = { eventId, armed: false, open: false };
11
+ else if (state.open) next = { eventId, armed: false, open: !!ownsPrompt };
12
+ else next = { eventId, armed: state.armed && !ownsPrompt, open: state.armed && !!ownsPrompt };
13
+ return state.eventId === next.eventId && state.armed === next.armed && state.open === next.open
14
+ ? state
15
+ : next;
16
+ }
17
+
18
+ export function dismissPetReminder(state) {
19
+ return state.open || state.armed ? { ...state, armed: false, open: false } : state;
20
+ }
package/pet-surfaces.js CHANGED
@@ -13,10 +13,12 @@ export function petSurfaceWidth(screen, viewport) {
13
13
  {
14
14
  income: 320,
15
15
  earnings: 320,
16
- home: 336,
16
+ home: 320,
17
+ 'water-countdown': 320,
18
+ smoking: 320,
17
19
  water: 360,
18
- appearance: 360,
19
- schedule: 400,
20
+ appearance: 340,
21
+ schedule: 360,
20
22
  menu: 224,
21
23
  reminder: 304,
22
24
  }[screen] ?? 336;
@@ -173,22 +175,144 @@ export const PET_SURFACE_STYLE = `
173
175
  .owc-v2 .owc-pet-natural>.owc-form:last-child,.owc-v2 .owc-pet-natural>.owc-water-form:last-child{padding-bottom:0}
174
176
  .owc-v2 .owc-pet-scroll .owc-save{background:var(--owc-surface);padding:12px 0;border-top:1px solid var(--owc-border);gap:8px;flex-wrap:wrap}
175
177
  .owc-v2 .owc-save button{min-width:72px}
176
- .owc-v2 .owc-appearance-preview{height:260px;display:flex;align-items:center;justify-content:center;border-radius:12px;background:var(--owc-subtle);margin-bottom:16px;overflow:hidden}
178
+ .owc-v2 .owc-appearance-preview{height:190px;display:flex;align-items:center;justify-content:center;border-radius:12px;background:var(--owc-subtle);margin-bottom:16px;overflow:hidden}
177
179
  .owc-v2 .owc-appearance-preview .owc-pet-art{flex:none;animation:none}
178
180
  .owc-v2 .owc-appearance-preview .owc-pet-art *{animation:none!important}
181
+ .owc-v2 .owc-preview-actions{display:flex;gap:6px;flex-wrap:wrap}
182
+ .owc-v2 .owc-preview-actions button{min-width:52px}
183
+ .owc-v2 .owc-preview-actions button[aria-pressed=true]{color:var(--owc-accent);background:color-mix(in srgb,var(--owc-accent) 10%,transparent)}
184
+ .owc-v2 .owc-preview-status{min-height:36px;margin:8px 0 12px}
179
185
  .owc-v2 .owc-appearance-preview p{font-size:13px;color:var(--owc-muted);padding:16px;text-align:center}
180
186
  .owc-v2 .owc-pet-context{padding:0;background:var(--owc-surface);border:1px solid var(--owc-border);border-radius:var(--owc-radius);box-shadow:var(--owc-shadow);scrollbar-width:none}
181
187
  .owc-v2 .owc-pet-context::-webkit-scrollbar{display:none}
182
188
  .owc-v2 .owc-menu-natural{padding:6px;display:flow-root;box-sizing:border-box;min-width:0}
183
189
  .owc-v2 .owc-pet-context button{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;justify-content:flex-start;font-size:13px}
184
190
  .owc-v2 .owc-pet-context hr{border-color:var(--owc-border);margin:6px 4px}
185
- .owc-v2 .owc-pet-toast,.owc-v2 .owc-pet-clock,.owc-v2 .owc-pet-water-alert{background:var(--owc-surface)!important;color:var(--owc-text);border:1px solid var(--owc-border)!important;box-shadow:var(--owc-shadow)}
191
+ .owc-v2.owc-classic{border-radius:16px;touch-action:manipulation}
192
+ .owc-classic .owc-content{max-height:calc(var(--owc-viewport-height) - 2px);overflow:auto}
193
+ .owc-v2 .owc-classic-head{position:sticky;top:0;z-index:2;min-height:46px;padding:8px 12px;background:var(--owc-surface);border-bottom:1px solid var(--owc-border);gap:6px}
194
+ .owc-classic-head .owc-title{font-size:12px;min-width:0;overflow:hidden;text-overflow:ellipsis;touch-action:none}
195
+ .owc-v2.owc-classic.owc-compact{border-radius:14px;border-color:color-mix(in srgb,var(--owc-text) 10%,transparent);box-shadow:0 3px 12px #0a101a1a}
196
+ .owc-classic.owc-compact>.owc-glass{background:var(--owc-surface);background:oklch(from var(--owc-surface) clamp(0,calc(l + .02),1) calc(c * .55) h)}
197
+ .owc-classic-mini{padding:14px;font:400 12px/18px system-ui,sans-serif}
198
+ .owc-classic-mini-top{display:flex;align-items:center;gap:6px;height:32px}
199
+ .owc-classic-mini .owc-grip{flex:1;display:flex;flex-direction:row;align-items:center;gap:8px;min-width:0;touch-action:none;cursor:grab}
200
+ .owc-classic-mini-label{flex:0 0 30px;font:500 10px/14px system-ui,sans-serif;color:var(--owc-muted);text-wrap:balance}
201
+ .owc-classic-mini-time{flex:1;min-width:0;font:500 26px/32px system-ui,sans-serif;letter-spacing:-.3px;font-variant-numeric:tabular-nums;white-space:nowrap}
202
+ .owc-v2 .owc-classic-mini-state{flex:1;min-width:0;font:500 16px/20px system-ui,sans-serif;margin:0;padding:0;min-height:0;text-align:left;overflow-wrap:anywhere}
203
+ .owc-classic-mini[data-clock-phase=done] .owc-classic-mini-state{color:color-mix(in srgb,#43896d 65%,var(--owc-text))}
204
+ .owc-v2 .owc-classic-mini .owc-icon{padding:0;min-width:24px;width:24px;height:24px;min-height:24px;flex-shrink:0;border:0;border-radius:5px;background:transparent;color:var(--owc-muted)}
205
+ .owc-classic-mini-timers{display:flex;align-items:center;gap:10px;height:22px;margin-top:12px}
206
+ .owc-v2 .owc-classic-mini-timer{display:flex;align-items:center;justify-content:flex-start;gap:4px;flex-shrink:0;min-width:0;min-height:24px;padding:0;border:0;border-radius:5px;background:transparent;font:400 12px/18px system-ui,sans-serif;color:var(--owc-muted)}
207
+ .owc-classic-mini-timer svg{flex-shrink:0}
208
+ .owc-classic-mini-timer strong{font:inherit;font-variant-numeric:tabular-nums;color:var(--owc-text);white-space:nowrap}
209
+ .owc-v2 .owc-classic-mini-timer[data-countdown-due=true],.owc-classic-mini-timer[data-countdown-due=true] strong{color:color-mix(in srgb,var(--owc-accent) 55%,var(--owc-text));background:transparent}
210
+ .owc-classic-mini-income{display:flex;align-items:center;gap:3px;margin-left:auto;min-width:24px;flex-shrink:1}
211
+ .owc-v2 .owc-classic-mini-amount{display:block;min-width:0;max-width:45px;min-height:24px;padding:0;border:0;font:400 11px/24px system-ui,sans-serif;color:var(--owc-muted);font-variant-numeric:tabular-nums;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
212
+ .owc-v2 .owc-classic-mini button:hover{background:color-mix(in srgb,var(--owc-text) 6%,transparent)}
213
+ .owc-classic-mini button:focus-visible{outline:2px solid var(--owc-accent);outline-offset:3px}
214
+ .owc-classic-overview{padding:16px;display:flex;flex-direction:column;gap:14px}
215
+ .owc-classic-kicker{display:flex;align-items:center;gap:6px;color:var(--owc-muted);font-size:11px;margin-bottom:7px}
216
+ .owc-classic-time{font-size:34px;font-weight:650;line-height:1.2;letter-spacing:.5px;font-variant-numeric:tabular-nums}
217
+ .owc-classic-state{font-size:22px;line-height:1.3;margin:0;color:var(--owc-text)}
218
+ .owc-v2 .owc-classic-hint{font-size:11px;color:var(--owc-muted);line-height:1.6;margin:7px 0 0}
219
+ .owc-classic-hero>button{margin-top:12px;min-height:38px}
220
+ .owc-classic-schedule{display:flex;justify-content:space-between;flex-wrap:wrap;gap:4px;margin-top:10px;color:var(--owc-muted);font-size:10px;font-variant-numeric:tabular-nums}
221
+ .owc-classic-timers{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px}
222
+ .owc-v2 .owc-classic-tile{display:flex;flex-direction:column;align-items:flex-start;gap:7px;padding:11px;min-width:0;border:1px solid var(--owc-border);border-radius:12px;background:var(--owc-subtle);text-align:left}
223
+ .owc-classic-tile-title{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--owc-muted)}
224
+ .owc-classic-tile strong{max-width:100%;font-size:17px;line-height:1.2;font-weight:600;font-variant-numeric:tabular-nums;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
225
+ .owc-classic-tile-hint{display:block;max-width:100%;font-size:10px;color:var(--owc-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
226
+ .owc-classic-income{display:flex;align-items:center;gap:4px;padding-top:10px;border-top:1px solid var(--owc-border)}
227
+ .owc-v2 .owc-classic-income-link{display:flex;align-items:center;justify-content:space-between;flex:1;min-width:0;gap:8px;padding:4px 0;font-size:11px;color:var(--owc-muted);min-height:32px}
228
+ .owc-classic-income-link strong{font-size:12px;font-weight:500;color:var(--owc-text);font-variant-numeric:tabular-nums}
229
+ .owc-classic-income .owc-text{font-size:10px;gap:4px;flex-shrink:0}
230
+ .owc-classic-details{border-top:1px solid var(--owc-border);padding-top:12px;font-size:11px;color:var(--owc-muted)}
231
+ .owc-classic-detail-title{display:flex;justify-content:space-between;margin-bottom:8px}
232
+ .owc-classic-details p{font-size:11px;line-height:1.5;margin:8px 0}
233
+ .owc-classic-feedback{margin:0;font-size:11px;color:var(--owc-accent)}
234
+ .owc-classic>.owc-content>.owc-note,.owc-classic>.owc-content>.owc-error{margin:8px 16px}
235
+ .owc-v2 .owc-pet-toast,.owc-v2 .owc-pet-water-alert{background:var(--owc-surface)!important;color:var(--owc-text);border:1px solid var(--owc-border)!important;box-shadow:var(--owc-shadow)}
186
236
  .owc-v2 .owc-pet-toast{border-radius:12px;font-size:12px}
187
237
  .owc-v2 .owc-pet-unsaved{padding:20px 16px}
188
238
  .owc-v2 .owc-pet-unsaved p{font-size:13px;overflow-wrap:anywhere}
239
+ .owc-detail-page{--owc-detail-surface:var(--owc-surface);--owc-control-height:38px;border-radius:14px}
240
+ @supports(color:oklch(from white l c h)){.owc-detail-page{--owc-detail-surface:oklch(from var(--owc-surface) clamp(0,calc(l + .02),1) calc(c * .55) h)}}
241
+ .owc-detail-page>.owc-glass{background:var(--owc-detail-surface);border-color:var(--owc-border)}
242
+ .owc-pet-workspace.owc-detail-page>.owc-head,.owc-classic.owc-detail-page .owc-classic-head{min-height:44px;padding:8px 16px;gap:8px;background:var(--owc-detail-surface);border-bottom:1px solid var(--owc-border)}
243
+ .owc-detail-page .owc-head .owc-icon{width:28px;height:28px;min-width:28px;min-height:28px;padding:4px}
244
+ .owc-detail-page [data-pet-page-title],.owc-classic.owc-detail-page .owc-classic-head .owc-title{font-size:14px;font-weight:550}
245
+ .owc-detail-page .owc-form,.owc-detail-page .owc-water-form,.owc-detail-page .owc-main,.owc-detail-page .owc-pet-home{padding:16px}
246
+ .owc-v2 .owc-pet-natural>.owc-form:last-child,.owc-v2 .owc-pet-natural>.owc-water-form:last-child{padding-bottom:16px}
247
+ .owc-detail-page fieldset.owc-setting-group{border:0;border-radius:0;background:transparent;padding:0;margin:0 0 18px;min-width:0}
248
+ .owc-detail-page fieldset.owc-setting-group legend{padding:0;margin-bottom:10px;font-size:12px;font-weight:550;color:var(--owc-text)}
249
+ .owc-detail-page .owc-appearance legend{display:none}
250
+ .owc-detail-page .owc-salary-legend{display:flex;align-items:center;justify-content:space-between;width:100%}
251
+ .owc-detail-page .owc-salary-legend .owc-icon{min-width:24px;min-height:24px;padding:3px}
252
+ .owc-detail-page .owc-field{position:relative;margin:0 0 12px}
253
+ .owc-detail-page .owc-field>span{font-size:12px;margin-bottom:6px}
254
+ .owc-detail-page .owc-row2{gap:12px}
255
+ .owc-detail-page :is(input[type=text],input[type=number],input[type=time],select){min-height:38px;border-radius:8px;background:color-mix(in srgb,var(--owc-text) 3%,var(--owc-detail-surface));border-color:var(--owc-border);font-size:13px}
256
+ .owc-detail-page .owc-private{box-sizing:border-box;min-height:38px;border:1px solid var(--owc-border);border-radius:8px;background:color-mix(in srgb,var(--owc-text) 3%,var(--owc-detail-surface));padding:8px 10px;font-size:14px}
257
+ .owc-detail-page .owc-seg{padding:2px;border-radius:8px;background:var(--owc-subtle)}
258
+ .owc-detail-page .owc-seg button{min-height:32px;font-size:12px;border-radius:6px}
259
+ .owc-detail-page button.owc-primary{min-height:38px;border-radius:8px;font-size:13px;font-weight:550;box-shadow:none}
260
+ .owc-v2 .owc-detail-page .owc-save,.owc-v2.owc-detail-page .owc-save{position:sticky;bottom:0;z-index:2;display:flex;justify-content:flex-end;gap:8px;padding:12px 0 0;margin:14px 0 0;border-top:1px solid var(--owc-border);background:var(--owc-detail-surface)}
261
+ .owc-detail-page .owc-save button{min-height:38px;font-size:13px;border-radius:8px;padding:8px 16px}
262
+ .owc-detail-page .owc-save button:not(.owc-primary){background:transparent;border-color:transparent;color:var(--owc-muted)}
263
+ .owc-detail-page .owc-detail-hero{margin:0 0 16px}
264
+ .owc-detail-page p.owc-detail-kicker{margin:0 0 6px;font-size:12px;color:var(--owc-muted)}
265
+ .owc-detail-value,.owc-detail-page .owc-countdown-value,.owc-detail-page .owc-time,.owc-detail-page .owc-money{font-size:28px;line-height:1.25;font-weight:500;letter-spacing:-.3px;font-variant-numeric:tabular-nums}
266
+ .owc-detail-page .owc-detail-state{font-size:20px;letter-spacing:0}
267
+ .owc-detail-meta{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:18px;margin-top:8px;font-size:11px;color:var(--owc-muted)}
268
+ .owc-detail-feedback{color:var(--owc-accent);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
269
+ .owc-detail-page .owc-detail-primary-actions{display:flex;align-items:center;gap:8px}
270
+ .owc-detail-primary-actions>.owc-primary{flex:1}
271
+ .owc-detail-page .owc-detail-secondary{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}
272
+ .owc-detail-page .owc-detail-secondary button{background:transparent;border:0;padding:6px 0;font-size:12px;color:var(--owc-muted);min-height:30px}
273
+ .owc-detail-page details.owc-detail-more,.owc-detail-page .owc-water-form>details,.owc-detail-page details.owc-advanced{margin:12px 0 0;padding:0;border:0;border-radius:0;background:transparent;font-size:12px}
274
+ .owc-detail-page details>summary{font-size:12px;color:var(--owc-muted);padding:4px 0;cursor:pointer}
275
+ .owc-detail-page details p{font-size:11px;line-height:1.6;color:var(--owc-muted)}
276
+ .owc-detail-options{display:flex;gap:8px;margin-top:10px}
277
+ .owc-detail-options button{flex:1;min-height:34px;border:1px solid var(--owc-border);border-radius:8px;font-size:12px}
278
+ .owc-detail-page .owc-pet-home.owc-water-due{background:transparent;border-radius:0}
279
+ .owc-detail-page .owc-countdown-card{gap:12px}
280
+ .owc-detail-page .owc-countdown-card .owc-detail-hero{margin-bottom:0}
281
+ .owc-detail-page .owc-countdown-actions{gap:8px}
282
+ .owc-detail-page .owc-countdown-actions>.owc-primary{flex:1 1 100%}
283
+ .owc-detail-page .owc-countdown-actions>button:not(.owc-primary){min-height:30px;background:transparent;border:0;font-size:12px;color:var(--owc-muted)}
284
+ .owc-detail-page .owc-water-toggle,.owc-detail-page .owc-appearance .owc-check{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:38px;margin:0 0 10px;padding:0;font-size:13px;font-weight:400}
285
+ .owc-detail-page .owc-appearance .owc-check input{order:2;margin-left:auto}
286
+ .owc-detail-page .owc-water-toggle input,.owc-detail-page .owc-appearance .owc-check input{appearance:none;flex:none;width:32px;height:18px;min-height:18px;padding:0;border:0;border-radius:20px;background:color-mix(in srgb,var(--owc-text) 22%,var(--owc-detail-surface));position:relative;cursor:pointer}
287
+ .owc-detail-page .owc-water-toggle input:before,.owc-detail-page .owc-appearance .owc-check input:before{content:'';position:absolute;width:14px;height:14px;top:2px;left:2px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0002}
288
+ .owc-detail-page .owc-water-toggle input:checked,.owc-detail-page .owc-appearance .owc-check input:checked{background:var(--owc-accent)}
289
+ .owc-detail-page .owc-water-toggle input:checked:before,.owc-detail-page .owc-appearance .owc-check input:checked:before{left:16px}
290
+ .owc-detail-page .owc-water-interval{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px;margin:6px 0 10px}
291
+ .owc-detail-page .owc-water-interval input{width:92px;max-width:92px;text-align:center}
292
+ .owc-detail-page .owc-water-presets{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:5px;margin:0 0 12px}
293
+ .owc-detail-page .owc-water-presets button{font-size:12px;font-weight:400;min-height:32px;padding:4px;border-radius:7px;background:transparent}
294
+ .owc-detail-page .owc-water-presets button[aria-pressed=true]{background:color-mix(in srgb,var(--owc-accent) 9%,var(--owc-detail-surface));border-color:color-mix(in srgb,var(--owc-accent) 35%,transparent);color:var(--owc-accent)}
295
+ .owc-detail-page .owc-water-form>p{font-size:11px;line-height:1.6;margin:8px 0;color:var(--owc-muted)}
296
+ .owc-detail-page p.owc-error{color:color-mix(in srgb,#dc4c64 70%,var(--owc-text))}
297
+ .owc-detail-page .owc-stats{border:0;background:transparent;padding:0;gap:20px;margin:16px 0}
298
+ .owc-detail-page .owc-stats>div{padding:0;border:0;border-radius:0;background:transparent;min-width:0}
299
+ .owc-detail-page .owc-stats strong{font-size:15px;font-weight:500}
300
+ .owc-detail-page .owc-meta{border:0;margin:8px 0 0;padding:0;font-size:11px}
301
+ .owc-detail-page .owc-row-actions{margin-top:14px;gap:12px}
302
+ .owc-detail-page .owc-row-actions button{font-size:11px;min-height:30px;padding:3px 0}
303
+ .owc-detail-page .owc-progress{height:3px;margin:14px 0 16px}
304
+ .owc-detail-page .owc-preview{background:transparent;border:0;padding:0;margin:10px 0;font-size:11px;color:var(--owc-muted)}
305
+ .owc-v2 .owc-appearance-preview{height:180px;border-radius:10px;background:color-mix(in srgb,var(--owc-text) 3%,var(--owc-detail-surface,var(--owc-surface)))}
306
+ .owc-v2 .owc-appearance-preview>.owc-pet-art{transform:scale(.7);transform-origin:50% 50%}
307
+ .owc-detail-page .owc-appearance input[type=range]{width:100%}
308
+ .owc-detail-page .owc-range-value{position:absolute;right:0;top:0;font-size:11px;color:var(--owc-muted)}
309
+ .owc-detail-page .owc-preview-tools{margin-bottom:14px}
310
+ .owc-detail-page .owc-preview-actions button{min-height:32px;font-size:12px;border-radius:7px}
311
+ .owc-detail-page .owc-preview-status{font-size:11px;line-height:1.5}
312
+ @media(pointer:coarse){.owc-detail-page{--owc-control-height:44px}.owc-detail-page .owc-head .owc-icon{width:44px;height:44px;min-width:44px;min-height:44px}.owc-detail-page button.owc-primary,.owc-detail-page .owc-save button{min-height:44px}.owc-detail-page :is(input[type=text],input[type=number],input[type=time],select){font-size:16px;min-height:44px}}
189
313
  @container (max-width:310px){.owc-v2 .owc-row2{grid-template-columns:1fr}.owc-v2 .owc-stats{gap:8px}.owc-v2 .owc-days{gap:2px}.owc-v2 .owc-days button{font-size:11px}}
190
314
  @media (pointer:coarse){.owc-v2{--owc-control-height:44px}.owc-v2 .owc-icon{min-width:44px}.owc-v2 button.owc-primary{min-height:44px}.owc-v2 :is(input[type=text],input[type=time],input[type=number],select){font-size:16px}.owc-v2 input[type=range]{min-height:36px}.owc-v2 .owc-check{min-height:44px}}
191
- @media (max-height:600px){.owc-v2 .owc-appearance-preview{height:120px}.owc-v2 .owc-appearance-preview svg{transform:scale(.55)}.owc-v2 .owc-appearance-preview img{transform:scale(.45)}}
315
+ @media (max-height:600px){.owc-v2 .owc-appearance-preview{height:120px}.owc-v2 .owc-appearance-preview>.owc-pet-art{transform:scale(.45);transform-origin:50% 50%}.owc-v2 .owc-appearance-preview>svg.owc-pet-art{transform:scale(.55)}}
192
316
  @media (prefers-contrast:more){.owc-v2{--owc-border:var(--owc-text)}.owc-v2 .owc-glass{opacity:1!important}}
193
317
  @media (prefers-reduced-transparency:reduce){.owc-v2 .owc-glass{opacity:1!important;backdrop-filter:none;-webkit-backdrop-filter:none}}
194
318
  `;