dsh-offwork 1.0.1 → 1.1.0

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.
@@ -0,0 +1,138 @@
1
+ // Ephemeral UI navigation only. Business data stays in its existing stores.
2
+ export const PET_EDITOR_SCREENS = ['water', 'schedule', 'appearance'];
3
+ export const initialPetUI = { screen: null, back: null, menu: null, pending: null };
4
+ export function petUIReducer(state, action) {
5
+ switch (action.type) {
6
+ case 'menu':
7
+ return { ...state, menu: action.position };
8
+ case 'dismiss-menu':
9
+ return { ...state, menu: null };
10
+ case 'stay':
11
+ return { ...state, pending: null };
12
+ case 'navigate': {
13
+ if (![null, 'home', 'income', 'earnings', ...PET_EDITOR_SCREENS].includes(action.screen))
14
+ return state;
15
+ if (state.screen === action.screen)
16
+ return { ...state, back: action.root ? null : state.back, menu: null, pending: null };
17
+ if (action.dirty && !action.discard)
18
+ return {
19
+ ...state,
20
+ menu: null,
21
+ pending: { screen: action.screen, ...(action.root ? { root: true } : {}) },
22
+ };
23
+ const back = action.root
24
+ ? null
25
+ : PET_EDITOR_SCREENS.includes(action.screen)
26
+ ? PET_EDITOR_SCREENS.includes(state.screen)
27
+ ? state.back
28
+ : state.screen
29
+ : state.back;
30
+ return { screen: action.screen, back, menu: null, pending: null };
31
+ }
32
+ default:
33
+ return state;
34
+ }
35
+ }
36
+ export function appearanceValues(view) {
37
+ return {
38
+ companion: view.companion,
39
+ companionMotion: view.companionMotion,
40
+ petSize: view.petSize,
41
+ petModel: view.petModel,
42
+ opacity: view.opacity,
43
+ };
44
+ }
45
+ export const PET_INTERACTION_MESSAGES = {
46
+ zh: {
47
+ petHome: '喝水',
48
+ petWaterCard: '喝水',
49
+ petCountdownCard: '下班倒计时',
50
+ petEarningsCard: '收入',
51
+ petWaterAlert: '查看喝水提醒',
52
+ petPausedHint: '本班已暂停,可手动恢复。',
53
+ petMenu: '宠物菜单',
54
+ petBack: '返回',
55
+ petCloseAll: '关闭面板',
56
+ petWorkSettings: '作息与薪资',
57
+ petWaterSettings: '提醒设置',
58
+ petAppearance: '外观',
59
+ petRecord: '喝了一杯',
60
+ petQuiet30: '免打扰 30 分钟',
61
+ petQuiet60: '免打扰 1 小时',
62
+ petUnsaved: '有未保存的修改',
63
+ petUnsavedHint: '保存后继续,或放弃这次修改。',
64
+ petSaveContinue: '保存并继续',
65
+ petDiscard: '放弃修改',
66
+ petKeepEditing: '继续编辑',
67
+ petApplyAppearance: '应用外观',
68
+ petAppearanceHint: '大小和透明度可实时预览。应用后保存;取消会还原。',
69
+ petQuietActive: '喝水提醒已静音',
70
+ petHomeHint: '喝水、歇一会儿,都在这里。',
71
+ petQuietHint: '按自然时间,到期自动恢复。',
72
+ petResume: '恢复本班提醒',
73
+ petDismissReminder: '稍后处理',
74
+ },
75
+ en: {
76
+ petHome: 'Water',
77
+ petWaterCard: 'Water',
78
+ petCountdownCard: 'Shift countdown',
79
+ petEarningsCard: 'Earnings',
80
+ petWaterAlert: 'View water reminder',
81
+ petPausedHint: 'Paused for this shift. Resume when ready.',
82
+ petMenu: 'Pet menu',
83
+ petBack: 'Back',
84
+ petCloseAll: 'Close panel',
85
+ petWorkSettings: 'Schedule and pay',
86
+ petWaterSettings: 'Reminders',
87
+ petAppearance: 'Appearance',
88
+ petRecord: 'Had a glass',
89
+ petQuiet30: 'Quiet for 30 minutes',
90
+ petQuiet60: 'Quiet for 1 hour',
91
+ petUnsaved: 'Unsaved changes',
92
+ petUnsavedHint: 'Save to continue, or discard these changes.',
93
+ petSaveContinue: 'Save and continue',
94
+ petDiscard: 'Discard changes',
95
+ petKeepEditing: 'Keep editing',
96
+ petApplyAppearance: 'Apply appearance',
97
+ petAppearanceHint:
98
+ 'Preview size and opacity now. Apply to save; Cancel restores the previous appearance.',
99
+ petQuietActive: 'Water reminders are muted',
100
+ petHomeHint: 'A drink or a little break, right here.',
101
+ petQuietHint: 'Uses clock time and resumes automatically.',
102
+ petResume: 'Resume reminders',
103
+ petDismissReminder: 'Handle later',
104
+ },
105
+ };
106
+ export const PET_INTERACTION_STYLE = `
107
+ .owc-pet-workspace .owc-glass{opacity:var(--owc-alpha)}
108
+ .owc-pet-water-card.owc-water-due{border-radius:14px}
109
+ .owc-pet-water-alert{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}
110
+ .owc-pet-panel.owc-pet-workspace{display:flex;flex-direction:column;overflow:hidden}
111
+ .owc-pet-workspace>.owc-head{position:relative;flex:none;min-height:46px;padding:8px 12px;box-sizing:border-box}
112
+ .owc-pet-workspace>.owc-pet-scroll{position:relative;min-height:0;flex:1;overflow:auto;overscroll-behavior:contain;scrollbar-width:none}
113
+ .owc-pet-scroll::-webkit-scrollbar{display:none}
114
+ .owc-pet-scroll .owc-save{position:sticky;bottom:0;background:var(--dsw-alias-bg-overlay);padding:10px 0;margin-bottom:0;z-index:2}
115
+ .owc-pet-scroll .owc-appearance legend{display:none}
116
+ .owc-pet-home{padding:14px}
117
+ .owc-pet-home-clock{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;color:var(--dsw-alias-label-secondary);margin-bottom:12px}
118
+ .owc-pet-home-clock strong{font-size:24px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}
119
+ .owc-pet-water-card{padding:12px;border:1px solid color-mix(in srgb,var(--owc-accent) 16%,transparent);border-radius:14px;background:color-mix(in srgb,var(--owc-accent) 4%,transparent)}
120
+ .owc-pet-water-card .owc-water-status{font-size:12px;font-variant-numeric:tabular-nums;height:34px;display:flex;align-items:center;overflow:hidden;margin:0 0 9px}
121
+ .owc-pet-water-row{display:flex;align-items:center;justify-content:space-between;font-size:11px;gap:10px}
122
+ .owc-pet-water-row .owc-primary{flex:1;min-height:40px}
123
+ .owc-pet-quiet-row{display:flex;gap:6px;margin-top:12px}
124
+ .owc-pet-quiet-row button{flex:1;border:1px solid color-mix(in srgb,var(--owc-accent) 18%,transparent);font-size:11px;min-height:34px}
125
+ .owc-pet-quiet-status{min-height:32px;display:flex;align-items:center;justify-content:space-between;font-size:10.5px;gap:5px;color:var(--dsw-alias-label-secondary)}
126
+ .owc-pet-feedback{height:32px;overflow:hidden;display:flex;align-items:center;margin:6px 0 4px;font-size:11px;line-height:1.5;color:var(--owc-accent)}
127
+ .owc-pet-home-links{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:4px}
128
+ .owc-pet-home-links button{font-size:11px;min-height:32px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 4%,transparent)}
129
+ .owc-pet-context{position:fixed;z-index:3;padding:6px;border:1px solid color-mix(in srgb,var(--owc-accent) 18%,transparent);background:var(--dsw-alias-bg-overlay);border-radius:12px;box-shadow:0 10px 28px #132c4626;box-sizing:border-box;overflow:auto;cursor:default;touch-action:manipulation}
130
+ .owc-pet-context button{display:flex;width:100%;justify-content:flex-start;min-height:34px;font-size:12px;padding:7px 10px}
131
+ .owc-pet-context button:focus-visible{outline-offset:-2px}
132
+ .owc-pet-context hr{border:0;border-top:1px solid color-mix(in srgb,var(--owc-accent) 12%,transparent);margin:5px 3px}
133
+ .owc-pet-unsaved{padding:20px 16px;line-height:1.6}
134
+ .owc-pet-unsaved h3{font-size:15px;margin:0 0 8px}
135
+ .owc-pet-unsaved p{font-size:12px;color:var(--dsw-alias-label-secondary)}
136
+ .owc-pet-unsaved button{width:100%;margin-top:8px;min-height:36px}
137
+ .owc-pet-toast{position:fixed;width:208px;padding:9px 12px;box-sizing:border-box;border-radius:12px;background:var(--dsw-alias-bg-overlay);border:1px solid color-mix(in srgb,var(--owc-accent) 18%,transparent);font-size:11px;line-height:1.5;box-shadow:0 5px 16px #18354b18;pointer-events:none}
138
+ `;
@@ -0,0 +1,194 @@
1
+ // Shared layout for our own pet cards and context menu.
2
+ export function petViewport(host) {
3
+ const visual = host.visualViewport;
4
+ return {
5
+ width: Math.max(1, Math.min(host.innerWidth, visual?.width ?? host.innerWidth)),
6
+ height: Math.max(1, Math.min(host.innerHeight, visual?.height ?? host.innerHeight)),
7
+ left: Math.max(0, visual?.offsetLeft ?? 0),
8
+ top: Math.max(0, visual?.offsetTop ?? 0),
9
+ };
10
+ }
11
+ export function petSurfaceWidth(screen, viewport) {
12
+ const ideal =
13
+ {
14
+ income: 320,
15
+ earnings: 320,
16
+ home: 336,
17
+ water: 360,
18
+ appearance: 360,
19
+ schedule: 400,
20
+ menu: 224,
21
+ reminder: 304,
22
+ }[screen] ?? 336;
23
+ return Math.max(1, Math.min(ideal, viewport.width - 24));
24
+ }
25
+ export function placePetSurface(
26
+ anchor,
27
+ viewport,
28
+ { width, height, kind = 'card', previous = null, point = null, obstacles = [] },
29
+ ) {
30
+ const edge = Math.min(
31
+ 12,
32
+ Math.max(0, (viewport.width - 1) / 2),
33
+ Math.max(0, (viewport.height - 1) / 2),
34
+ ),
35
+ gap = 12;
36
+ const left = (viewport.left ?? 0) + edge,
37
+ top = (viewport.top ?? 0) + edge;
38
+ const right = (viewport.left ?? 0) + viewport.width - edge;
39
+ const bottom = (viewport.top ?? 0) + viewport.height - edge;
40
+ const w = Math.max(1, Math.min(width, right - left));
41
+ const natural = Math.max(1, height),
42
+ available = Math.max(1, bottom - top);
43
+ const full = Math.min(natural, available);
44
+ const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
45
+ const candidates = [];
46
+ const add = (x, y, h, side) => {
47
+ h = Math.max(1, Math.min(h, available));
48
+ x = clamp(x, left, right - w);
49
+ y = clamp(y, top, bottom - h);
50
+ const intersection = (rect) =>
51
+ Math.max(0, Math.min(x + w, rect.x + rect.width) - Math.max(x, rect.x)) *
52
+ Math.max(0, Math.min(y + h, rect.y + rect.height) - Math.max(y, rect.y));
53
+ const overlap =
54
+ intersection(anchor) +
55
+ obstacles.reduce(
56
+ (sum, rect) =>
57
+ sum + intersection({ x: rect.left, y: rect.top, width: rect.width, height: rect.height }),
58
+ 0,
59
+ );
60
+ const distance = previous ? Math.abs(previous.left - x) + Math.abs(previous.top - y) : 0;
61
+ candidates.push({
62
+ left: x,
63
+ top: y,
64
+ width: w,
65
+ height: h,
66
+ maxHeight: h,
67
+ side: overlap ? 'overlay' : side,
68
+ overlap,
69
+ clipped: natural - h,
70
+ distance,
71
+ order: candidates.length,
72
+ });
73
+ };
74
+ if (previous && Math.abs(previous.width - w) < 1)
75
+ add(previous.left, previous.top, full, previous.side);
76
+ if (kind === 'menu' && point) {
77
+ add(point.x + 8, point.y + 8, full, 'context-right');
78
+ add(point.x - w - 8, point.y + 8, full, 'context-left');
79
+ add(point.x + 8, point.y - full - 8, full, 'context-above-right');
80
+ add(point.x - w - 8, point.y - full - 8, full, 'context-above-left');
81
+ }
82
+ const preferredY = kind === 'menu' && point ? point.y + 6 : anchor.y + anchor.height - full;
83
+ add(anchor.x - w - gap, preferredY, full, 'left');
84
+ add(anchor.x + anchor.width + gap, preferredY, full, 'right');
85
+ const centered = anchor.x + anchor.width / 2 - w / 2;
86
+ add(centered, anchor.y - gap - full, full, 'above');
87
+ add(centered, anchor.y + anchor.height + gap, full, 'below');
88
+ const above = anchor.y - gap - top,
89
+ below = bottom - anchor.y - anchor.height - gap;
90
+ const minimum = Math.min(full, kind === 'menu' ? 160 : 240);
91
+ if (above >= minimum && above < full) add(centered, top, above, 'above');
92
+ if (below >= minimum && below < full)
93
+ add(centered, anchor.y + anchor.height + gap, below, 'below');
94
+ candidates.sort(
95
+ (a, b) =>
96
+ Number(a.overlap > 0) - Number(b.overlap > 0) ||
97
+ a.overlap - b.overlap ||
98
+ a.clipped - b.clipped ||
99
+ a.distance - b.distance ||
100
+ a.order - b.order,
101
+ );
102
+ const { overlap, clipped, distance, order, ...result } = candidates[0];
103
+ return result;
104
+ }
105
+ export const PET_SURFACE_MESSAGES = {
106
+ zh: {
107
+ petAppearancePreview: '角色预览',
108
+ petWaterBadge: '该喝水啦',
109
+ petClassicPreview: '应用后使用计时卡片',
110
+ petSurfaceHint: '内容较长时可用滚轮或触控滚动',
111
+ },
112
+ en: {
113
+ petAppearancePreview: 'Character preview',
114
+ petWaterBadge: 'Time for water',
115
+ petClassicPreview: 'Apply to use the clock card',
116
+ petSurfaceHint: 'Scroll with the wheel or touch when content is longer',
117
+ },
118
+ };
119
+ export const PET_SURFACE_STYLE = `
120
+ .owc-v2{--owc-accent:#2563eb;--owc-on-accent:#fff;--owc-surface:var(--dsw-alias-bg-overlay,#fff);--owc-text:var(--dsw-alias-label-primary,#172033);--owc-muted:var(--dsw-alias-label-secondary,#526074);--owc-border:color-mix(in srgb,var(--owc-text) 13%,transparent);--owc-subtle:color-mix(in srgb,var(--owc-text) 4%,var(--owc-surface));--owc-hover:color-mix(in srgb,var(--owc-accent) 8%,var(--owc-surface));--owc-radius:16px;--owc-control-radius:10px;--owc-shadow:0 10px 32px color-mix(in srgb,var(--owc-text) 13%,transparent);--owc-control-height:36px;color:var(--owc-text);line-height:1.5}
121
+ .owc-v2 .owc-content{max-height:var(--owc-viewport-height,calc(100dvh - 24px))}
122
+ .owc-v2:not(.owc-pet-shell) .owc-head{position:sticky;top:0;z-index:2;background:var(--owc-surface)}
123
+ .owc-v2:not(.owc-pet-shell) .owc-save{position:sticky;bottom:0;z-index:2;background:var(--owc-surface);padding:12px 0}
124
+ .owc-v2 .owc-pet-toast{overflow-wrap:anywhere;overflow:auto;scrollbar-width:none}
125
+ .owc-v2 button.owc-pet-water-alert{border-color:var(--owc-accent)!important;color:var(--owc-accent)}
126
+ .owc-v2 .owc-glass{background:var(--owc-surface);border:1px solid var(--owc-border);box-shadow:var(--owc-shadow);opacity:var(--owc-alpha,.94)}
127
+ .owc-v2 .owc-pet-workspace{border-radius:var(--owc-radius);container-type:inline-size}
128
+ .owc-v2 .owc-head{margin:0;padding:10px 16px;gap:8px;min-height:52px;border-bottom:1px solid var(--owc-border);box-sizing:border-box}
129
+ .owc-v2 [data-pet-page-title]{font-size:14px!important;line-height:1.4;min-width:0;overflow-wrap:anywhere;font-weight:600}
130
+ .owc-v2 [data-pet-page-title]:focus{outline:none}
131
+ .owc-v2 :is(.owc-pet-workspace,.owc-pet-context) button{min-height:var(--owc-control-height);padding:7px 10px;font-size:13px;line-height:1.4;border-radius:var(--owc-control-radius);box-sizing:border-box}
132
+ .owc-v2 :is(.owc-pet-workspace,.owc-pet-context) button:hover:not(:disabled){background:var(--owc-hover)}
133
+ .owc-v2 button.owc-primary{min-height:40px;background:var(--owc-accent);color:var(--owc-on-accent);border-radius:var(--owc-control-radius);font-size:13px}
134
+ .owc-v2 button.owc-primary:hover:not(:disabled){background:color-mix(in srgb,var(--owc-accent) 88%,#000)}
135
+ .owc-v2 :is(button,input,select,summary):focus-visible{outline:2px solid var(--owc-accent);outline-offset:2px}
136
+ .owc-v2 .owc-icon{flex:none;min-width:36px;padding:7px}
137
+ .owc-v2 .owc-main,.owc-v2 .owc-form,.owc-v2 .owc-water-form,.owc-v2 .owc-pet-home{padding:16px;min-width:0;box-sizing:border-box}
138
+ .owc-v2 .owc-main .owc-time{font-size:clamp(28px,10cqw,40px);font-weight:500;letter-spacing:-.5px;overflow-wrap:anywhere}
139
+ .owc-v2 .owc-money{font-size:clamp(24px,9cqw,34px);overflow-wrap:anywhere;line-height:1.35}
140
+ .owc-v2 :is(.owc-hint,.owc-muted,.owc-meta span,.owc-field>span,.owc-stats span,.owc-status,.owc-error,.owc-note){font-size:12px;line-height:1.55;color:var(--owc-muted)}
141
+ .owc-v2 .owc-error{color:var(--dsw-alias-state-error-primary,#dc2626)}
142
+ .owc-v2 .owc-meta span{white-space:normal;overflow:visible;text-overflow:clip;overflow-wrap:anywhere}
143
+ .owc-v2 .owc-row-actions{flex-wrap:wrap;gap:4px;margin:12px 0 0}
144
+ .owc-v2 .owc-stats{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;border-color:var(--owc-border)}
145
+ .owc-v2 .owc-stats>div{min-width:0;background:var(--owc-subtle);border:1px solid var(--owc-border);border-radius:12px;padding:12px}
146
+ .owc-v2 .owc-stats strong{font-size:14px;overflow-wrap:anywhere}
147
+ .owc-v2 .owc-setting-group{padding:14px 12px 4px;background:var(--owc-subtle);border-color:var(--owc-border);border-radius:12px}
148
+ .owc-v2 .owc-setting-group legend{font-size:13px}
149
+ .owc-v2 .owc-field{margin-bottom:12px;min-width:0}
150
+ .owc-v2 .owc-row2{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
151
+ .owc-v2 :is(input[type=text],input[type=time],input[type=number],select){max-width:100%;min-width:0;min-height:var(--owc-control-height);font-size:13px;border-color:var(--owc-border);border-radius:var(--owc-control-radius);background:var(--owc-surface);color:var(--owc-text)}
152
+ .owc-v2 :is(input[type=checkbox],input[type=radio],input[type=range]){accent-color:var(--owc-accent)}
153
+ .owc-v2 .owc-check{min-height:30px;font-size:13px;gap:9px}
154
+ .owc-v2 .owc-private{flex-wrap:wrap;border-color:var(--owc-border);background:var(--owc-surface);border-radius:var(--owc-control-radius)}
155
+ .owc-v2 .owc-days{gap:4px}
156
+ .owc-v2 .owc-days button{min-width:0;padding:5px 2px;font-size:12px}
157
+ .owc-v2 .owc-seg button{min-width:0}
158
+ .owc-v2 .owc-water-presets{gap:8px}
159
+ .owc-v2 .owc-water-presets button{font-size:12px;padding:7px 4px;border-color:var(--owc-border);white-space:normal}
160
+ .owc-v2 .owc-water-form p{font-size:12px;line-height:1.55;overflow-wrap:anywhere}
161
+ .owc-v2 .owc-water-card,.owc-v2 .owc-pet-water-card{padding:14px;border-color:var(--owc-border);background:var(--owc-subtle);border-radius:12px}
162
+ .owc-v2 .owc-pet-water-card .owc-water-status{height:auto;min-height:40px;overflow:visible;font-size:13px;line-height:1.5;overflow-wrap:anywhere;margin-bottom:10px}
163
+ .owc-v2 .owc-pet-water-row{font-size:12px;flex-wrap:wrap}
164
+ .owc-v2 .owc-pet-water-row .owc-primary{min-width:120px}
165
+ .owc-v2 .owc-pet-quiet-row{gap:8px;margin-top:14px}
166
+ .owc-v2 .owc-pet-quiet-row button{min-width:0;font-size:12px;white-space:normal;border-color:var(--owc-border)}
167
+ .owc-v2 .owc-pet-quiet-status{min-height:38px;height:auto;flex-wrap:wrap;font-size:12px;line-height:1.5;color:var(--owc-muted)}
168
+ .owc-v2 .owc-pet-feedback{height:auto;min-height:36px;overflow:visible;font-size:12px;line-height:1.5;margin:8px 0;overflow-wrap:anywhere}
169
+ .owc-v2 .owc-pet-home-links{gap:8px}
170
+ .owc-v2 .owc-pet-home-links button{font-size:12px;background:var(--owc-subtle);border:1px solid var(--owc-border)}
171
+ .owc-v2 .owc-pet-scroll{scrollbar-width:none;scroll-padding-block:12px 64px}
172
+ .owc-v2 .owc-pet-natural{display:flow-root;min-width:0;width:100%}
173
+ .owc-v2 .owc-pet-natural>.owc-form:last-child,.owc-v2 .owc-pet-natural>.owc-water-form:last-child{padding-bottom:0}
174
+ .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
+ .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}
177
+ .owc-v2 .owc-appearance-preview .owc-pet-art{flex:none;animation:none}
178
+ .owc-v2 .owc-appearance-preview .owc-pet-art *{animation:none!important}
179
+ .owc-v2 .owc-appearance-preview p{font-size:13px;color:var(--owc-muted);padding:16px;text-align:center}
180
+ .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
+ .owc-v2 .owc-pet-context::-webkit-scrollbar{display:none}
182
+ .owc-v2 .owc-menu-natural{padding:6px;display:flow-root;box-sizing:border-box;min-width:0}
183
+ .owc-v2 .owc-pet-context button{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;justify-content:flex-start;font-size:13px}
184
+ .owc-v2 .owc-pet-context hr{border-color:var(--owc-border);margin:6px 4px}
185
+ .owc-v2 .owc-pet-toast,.owc-v2 button.owc-pet-clock,.owc-v2 button.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
+ .owc-v2 .owc-pet-toast{border-radius:12px;font-size:12px}
187
+ .owc-v2 .owc-pet-unsaved{padding:20px 16px}
188
+ .owc-v2 .owc-pet-unsaved p{font-size:13px;overflow-wrap:anywhere}
189
+ @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
+ @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)}}
192
+ @media (prefers-contrast:more){.owc-v2{--owc-border:var(--owc-text)}.owc-v2 .owc-glass{opacity:1!important}}
193
+ @media (prefers-reduced-transparency:reduce){.owc-v2 .owc-glass{opacity:1!important;backdrop-filter:none;-webkit-backdrop-filter:none}}
194
+ `;
package/pet-view.js ADDED
@@ -0,0 +1,272 @@
1
+ // Independent Harness pet presentation. No third-party pet code or assets.
2
+ export const PET_MODEL = Object.freeze({ id: 'xiaoxie-original', renderer: 'svg', license: 'MIT' });
3
+
4
+ export const PET_MODELS = Object.freeze({
5
+ 'whale-dress': {
6
+ id: 'whale-dress',
7
+ renderer: 'image',
8
+ widthScale: 1.3125,
9
+ heightScale: 2.1,
10
+ label: 'petWhaleDress',
11
+ },
12
+ 'whale-ornate': {
13
+ id: 'whale-ornate',
14
+ renderer: 'image',
15
+ widthScale: 1.3125,
16
+ heightScale: 2.1,
17
+ label: 'petWhaleOrnate',
18
+ },
19
+ 'xiaoxie-original': { ...PET_MODEL, widthScale: 1, heightScale: 1.1, label: 'petDroplet' },
20
+ });
21
+ export function petModel(id) {
22
+ return Object.hasOwn(PET_MODELS, id) ? PET_MODELS[id] : PET_MODELS['whale-dress'];
23
+ }
24
+
25
+ export function petLayout(
26
+ position,
27
+ viewport,
28
+ size = 132,
29
+ contentHeight = 420,
30
+ modelId = 'xiaoxie-original',
31
+ ) {
32
+ const model = petModel(modelId);
33
+ const originX = viewport.left ?? 0,
34
+ originY = viewport.top ?? 0;
35
+ const edge = 12,
36
+ gap = 12;
37
+ const width = Math.max(1, viewport.width),
38
+ height = Math.max(1, viewport.height);
39
+ const artworkSize = Math.max(
40
+ 1,
41
+ Math.min(
42
+ size,
43
+ Math.max(1, (width - edge * 2) / model.widthScale),
44
+ Math.max(1, Math.floor((height - edge * 2 - 30) / model.heightScale)),
45
+ ),
46
+ );
47
+ const petWidth = Math.min(
48
+ Math.max(1, width - edge * 2),
49
+ Math.max(148, artworkSize * model.widthScale),
50
+ );
51
+ const petHeight = Math.ceil(artworkSize * model.heightScale) + 30;
52
+ const bound = (n, max) => Math.max(Math.min(edge, max), Math.min(n, max));
53
+ const x = bound(
54
+ Number.isFinite(position?.x) ? position.x - originX : width - petWidth - 32,
55
+ Math.max(0, width - petWidth - edge),
56
+ );
57
+ const y = bound(
58
+ Number.isFinite(position?.y) ? position.y - originY : height - petHeight - 36,
59
+ Math.max(0, height - petHeight - edge),
60
+ );
61
+ const panelWidth = Math.min(296, Math.max(1, width - edge * 2));
62
+ let side =
63
+ x >= panelWidth + gap + edge
64
+ ? 'left'
65
+ : width - x - petWidth >= panelWidth + gap + edge
66
+ ? 'right'
67
+ : 'stack';
68
+ let maxHeight = Math.max(60, height - edge * 2);
69
+ let left = side === 'left' ? x - gap - panelWidth : x + petWidth + gap;
70
+ let top;
71
+ if (side === 'stack') {
72
+ left = Math.max(edge, Math.min(x + petWidth - panelWidth, width - panelWidth - edge));
73
+ const above = y - gap - edge;
74
+ const below = height - y - petHeight - gap - edge;
75
+ if (Math.max(above, below) < 48) {
76
+ side = 'overlay';
77
+ maxHeight = Math.max(1, height - edge * 2);
78
+ top = Math.min(edge, height / 2);
79
+ } else {
80
+ side = above >= below ? 'above' : 'below';
81
+ maxHeight = Math.max(48, side === 'above' ? above : below);
82
+ top = side === 'above' ? y - gap - Math.min(contentHeight, maxHeight) : y + petHeight + gap;
83
+ }
84
+ } else {
85
+ top = Math.max(
86
+ edge,
87
+ Math.min(y + petHeight - contentHeight, height - Math.min(contentHeight, maxHeight) - edge),
88
+ );
89
+ }
90
+ return {
91
+ x: x + originX,
92
+ y: y + originY,
93
+ petWidth,
94
+ artworkSize,
95
+ petHeight,
96
+ panel: {
97
+ left: left + originX,
98
+ top: Math.max(0, top) + originY,
99
+ width: panelWidth,
100
+ maxHeight,
101
+ side,
102
+ },
103
+ };
104
+ }
105
+
106
+ // Original full-body Xiaoxie: droplet body, small feet and a scarf.
107
+ export function petArtwork(
108
+ h,
109
+ presentation,
110
+ size,
111
+ reaction = '',
112
+ modelId = 'xiaoxie-original',
113
+ sprites = {},
114
+ ) {
115
+ const { pose, animate } = presentation;
116
+ const model = petModel(modelId);
117
+ if (model.renderer === 'image' && sprites[model.id])
118
+ return h('img', {
119
+ key: model.id + ':' + pose + ':' + reaction,
120
+ src: sprites[model.id],
121
+ alt: '',
122
+ 'aria-hidden': true,
123
+ draggable: false,
124
+ width: size * model.widthScale,
125
+ height: size * model.heightScale,
126
+ className: 'owc-pet-art owc-pet-portrait',
127
+ 'data-pose': pose,
128
+ 'data-animate': animate,
129
+ });
130
+ const asleep = ['quiet', 'rest'].includes(pose);
131
+ const path = (d, props = {}) => h('path', { d, ...props });
132
+ return h(
133
+ 'svg',
134
+ {
135
+ key: pose + ':' + reaction,
136
+ viewBox: '0 0 160 176',
137
+ width: size,
138
+ height: size * 1.1,
139
+ className: 'owc-pet-art',
140
+ 'data-pose': pose,
141
+ 'data-animate': animate,
142
+ 'aria-hidden': true,
143
+ focusable: false,
144
+ },
145
+ h('ellipse', { cx: 80, cy: 160, rx: 42, ry: 6, fill: '#183b65', opacity: 0.13 }),
146
+ h(
147
+ 'g',
148
+ {
149
+ className: 'owc-pet-body',
150
+ stroke: '#315b82',
151
+ strokeWidth: 2.2,
152
+ strokeLinecap: 'round',
153
+ strokeLinejoin: 'round',
154
+ },
155
+ h('ellipse', { cx: 58, cy: 148, rx: 12, ry: 8, fill: '#92cce2' }),
156
+ h('ellipse', { cx: 102, cy: 148, rx: 12, ry: 8, fill: '#92cce2' }),
157
+ path(
158
+ pose === 'happy' ? 'M36 86Q14 91 15 65Q28 65 43 76Z' : 'M36 87Q15 87 19 109Q31 113 44 99Z',
159
+ { fill: '#a4dcea' },
160
+ ),
161
+ path(
162
+ pose === 'happy'
163
+ ? 'M124 86Q146 91 145 65Q132 65 117 76Z'
164
+ : 'M124 87Q145 87 141 109Q129 113 116 99Z',
165
+ { fill: '#a4dcea' },
166
+ ),
167
+ path(
168
+ 'M80 12C67 34 38 47 34 81C29 117 46 149 80 149C114 149 131 117 126 81C122 47 93 34 80 12Z',
169
+ { fill: '#c7eaf3' },
170
+ ),
171
+ path('M64 37Q48 53 44 69', { fill: 'none', stroke: '#fff', strokeWidth: 6, opacity: 0.8 }),
172
+ path('M46 113Q80 126 114 113L109 128Q79 137 51 127Z', { fill: '#fff1bc', stroke: '#b49454' }),
173
+ path('M102 123L113 145L100 147L92 127Z', { fill: '#ffe39b', stroke: '#b49454' }),
174
+ h('ellipse', { cx: 80, cy: 111, rx: 8, ry: 5, fill: '#e5f6fa', stroke: 'none' }),
175
+ asleep
176
+ ? path('M51 79Q59 85 67 79M93 79Q101 85 109 79', {
177
+ fill: 'none',
178
+ stroke: '#24425d',
179
+ strokeWidth: 3,
180
+ })
181
+ : h(
182
+ 'g',
183
+ { fill: '#24425d', stroke: 'none' },
184
+ h('ellipse', { cx: 59, cy: 78, rx: 3.5, ry: 5 }),
185
+ h('ellipse', { cx: 101, cy: 78, rx: 3.5, ry: 5 }),
186
+ ),
187
+ path(
188
+ pose === 'happy'
189
+ ? 'M69 91Q80 108 91 91Z'
190
+ : pose === 'offline'
191
+ ? 'M74 95H86'
192
+ : 'M72 93Q80 101 88 93',
193
+ { fill: pose === 'happy' ? '#f19baf' : 'none', stroke: '#24425d', strokeWidth: 2.5 },
194
+ ),
195
+ h(
196
+ 'g',
197
+ { fill: '#f0a2af', stroke: 'none', opacity: 0.65 },
198
+ h('ellipse', { cx: 46, cy: 91, rx: 8, ry: 4 }),
199
+ h('ellipse', { cx: 114, cy: 91, rx: 8, ry: 4 }),
200
+ ),
201
+ pose === 'drink' &&
202
+ h(
203
+ 'g',
204
+ { fill: '#fff', stroke: '#44728f' },
205
+ path('M113 103H139L135 133H117Z'),
206
+ path('M140 108H146Q154 121 138 122', { fill: 'none' }),
207
+ path('M119 115H134', { stroke: '#6dc9e2', strokeWidth: 5 }),
208
+ ),
209
+ pose === 'quiet' &&
210
+ path('M122 32H135L122 44H135', { fill: 'none', stroke: '#67899c', strokeWidth: 3 }),
211
+ ),
212
+ );
213
+ }
214
+
215
+ export const PET_MESSAGES = {
216
+ zh: {
217
+ petOpen: '打开宠物面板',
218
+ petCharacter: '角色形象',
219
+ petWhaleDress: '鲸蓝少女 · 长裙',
220
+ petWhaleOrnate: '鲸蓝少女 · 礼裙',
221
+ petDroplet: '小歇 · 水滴',
222
+ petClose: '收起宠物面板',
223
+ petQuick: '日常陪伴',
224
+ petDetails: '下班与收入',
225
+ petSize: '宠物大小',
226
+ petHint: '左键喝水 · 右键菜单 · 拖动移动',
227
+ petMode: '宠物模式',
228
+ petClock: '下班倒计时',
229
+ },
230
+ en: {
231
+ petOpen: 'Open pet panel',
232
+ petCharacter: 'Character',
233
+ petWhaleDress: 'Blue whale · Long dress',
234
+ petWhaleOrnate: 'Blue whale · Ornate dress',
235
+ petDroplet: 'Xiaoxie · Droplet',
236
+ petClose: 'Close pet panel',
237
+ petQuick: 'Daily companion',
238
+ petDetails: 'Clock and earnings',
239
+ petSize: 'Pet size',
240
+ petHint: 'Click for water · right-click for menu · drag to move',
241
+ petMode: 'Pet mode',
242
+ petClock: 'Shift countdown',
243
+ },
244
+ };
245
+
246
+ export const PET_STYLE = `
247
+ .owc-v2.owc-pet-shell{border-radius:0;touch-action:none;cursor:default;overflow:visible}
248
+ .owc-pet-stage{display:flex;flex-direction:column;align-items:center;position:relative}
249
+ .owc-v2 button.owc-pet-handle{padding:0;background:none!important;border-radius:50%;min-height:0;display:block;cursor:grab;touch-action:none;line-height:0}
250
+ .owc-v2 button.owc-pet-handle:active{cursor:grabbing}
251
+ .owc-pet-art{overflow:visible;pointer-events:none;filter:drop-shadow(0 3px 5px #32557418)}
252
+ .owc-pet-portrait{display:block;object-fit:contain;transform-origin:50% 90%}
253
+ .owc-pet-portrait[data-animate=true]{animation:owc-portrait-thanks .65s ease-in-out 2}
254
+ @keyframes owc-portrait-thanks{0%,100%{transform:translateY(0)}45%{transform:translateY(-3px) rotate(-1deg)}75%{transform:rotate(1deg)}}
255
+ .owc-pet-art[data-pose=offline]{opacity:.65}
256
+ .owc-pet-art[data-animate=true] .owc-pet-body{transform-origin:80px 150px;animation:owc-pet-thanks .65s ease-in-out 2}
257
+ @keyframes owc-pet-thanks{0%,100%{transform:translateY(0)}45%{transform:translateY(-4px) rotate(-3deg)}75%{transform:rotate(2deg)}}
258
+ .owc-v2 button.owc-pet-clock{padding:4px 10px;min-height:26px;gap:5px;white-space:nowrap;border-radius:999px;font-size:11px;background:var(--dsw-alias-bg-overlay);border:1px solid color-mix(in srgb,var(--owc-accent) 25%,transparent);box-shadow:0 3px 12px #142e4812;font-variant-numeric:tabular-nums;max-width:100%;box-sizing:border-box;overflow:hidden}
259
+ .owc-pet-clock-label{min-width:0;overflow:hidden;text-overflow:ellipsis}
260
+ .owc-pet-clock strong{flex:none}
261
+ .owc-pet-panel{position:fixed;border-radius:20px;cursor:default;touch-action:manipulation;max-width:calc(100vw - 24px)}
262
+ .owc-pet-panel>.owc-content{max-height:inherit}
263
+ .owc-pet-panel .owc-glass{opacity:max(var(--owc-alpha),.94)}
264
+ .owc-pet-tabs{display:flex;gap:5px;padding:8px 14px 0}
265
+ .owc-pet-tabs button{flex:1;font-size:11px}
266
+ .owc-pet-tabs button[aria-pressed=true]{color:var(--owc-accent);background:color-mix(in srgb,var(--owc-accent) 10%,transparent)}
267
+ .owc-pet-time{padding:12px 17px 0;display:flex;align-items:center;justify-content:space-between;gap:8px;color:var(--dsw-alias-label-secondary);font-size:11px}
268
+ .owc-pet-time strong{font-size:21px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}
269
+ .owc-pet-speech{padding:12px 16px;font-size:12px;line-height:1.65}
270
+ .owc-pet-speech strong{display:block;font-size:11px;color:var(--dsw-alias-label-secondary);margin-bottom:4px}
271
+ @media(prefers-reduced-motion:reduce){.owc-pet-art,.owc-pet-art *{animation:none!important;transition:none!important}}
272
+ `;