dsh-offwork 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +58 -39
- package/client.js +1001 -299
- package/client.source.js +896 -252
- package/clock.js +81 -34
- package/index.js +142 -66
- package/locale/en.json +6 -1
- package/locale/zh.json +6 -1
- package/package.json +18 -5
- package/tests/clock.test.mjs +76 -33
- package/tests/host.test.mjs +76 -27
- package/tests/ui.test.mjs +77 -20
- package/tools/build.mjs +22 -7
- package/tools/check-package.mjs +96 -0
- package/ui-utils.js +21 -10
package/client.js
CHANGED
|
@@ -12,37 +12,48 @@ function calculate(settings, timestamp = Date.now()) {
|
|
|
12
12
|
if (!Number.isFinite(now)) throw new RangeError('Invalid timestamp');
|
|
13
13
|
const DAY = 86400000;
|
|
14
14
|
const formatter = new Intl.DateTimeFormat('en-CA', {
|
|
15
|
-
timeZone: settings.timezone,
|
|
16
|
-
|
|
15
|
+
timeZone: settings.timezone,
|
|
16
|
+
year: 'numeric',
|
|
17
|
+
month: '2-digit',
|
|
18
|
+
day: '2-digit',
|
|
19
|
+
hour: '2-digit',
|
|
20
|
+
minute: '2-digit',
|
|
21
|
+
second: '2-digit',
|
|
22
|
+
hourCycle: 'h23',
|
|
17
23
|
});
|
|
18
|
-
const civil = time => {
|
|
19
|
-
const p = Object.fromEntries(formatter.formatToParts(time).map(p => [p.type, p.value]));
|
|
24
|
+
const civil = (time) => {
|
|
25
|
+
const p = Object.fromEntries(formatter.formatToParts(time).map((p) => [p.type, p.value]));
|
|
20
26
|
return Date.UTC(+p.year, +p.month - 1, +p.day, +p.hour, +p.minute, +p.second);
|
|
21
27
|
};
|
|
22
28
|
// Convert a timezone-local civil time to an absolute instant. At DST overlaps use
|
|
23
29
|
// the earlier occurrence; at gaps move forward by the gap (Temporal-compatible).
|
|
24
|
-
const instant = local => {
|
|
25
|
-
const offsets = [
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const instant = (local) => {
|
|
31
|
+
const offsets = [
|
|
32
|
+
...new Set([-DAY, 0, DAY].map((delta) => civil(local + delta) - (local + delta))),
|
|
33
|
+
];
|
|
34
|
+
const candidates = offsets.map((offset) => local - offset);
|
|
35
|
+
const exact = candidates.filter((time) => civil(time) === local);
|
|
28
36
|
if (exact.length) return Math.min(...exact);
|
|
29
|
-
const after = candidates.filter(time => civil(time) >= local);
|
|
37
|
+
const after = candidates.filter((time) => civil(time) >= local);
|
|
30
38
|
return Math.min(...(after.length ? after : candidates));
|
|
31
39
|
};
|
|
32
|
-
const minute = text => {
|
|
40
|
+
const minute = (text) => {
|
|
33
41
|
if (typeof text !== 'string' || !/^(?:[01]\d|2[0-3]):[0-5]\d$/.test(text)) {
|
|
34
42
|
throw new RangeError('Shift times must be HH:mm');
|
|
35
43
|
}
|
|
36
44
|
const [h, m] = text.split(':').map(Number);
|
|
37
45
|
return h * 60 + m;
|
|
38
46
|
};
|
|
39
|
-
const start = minute(settings.start),
|
|
47
|
+
const start = minute(settings.start),
|
|
48
|
+
end = minute(settings.end);
|
|
40
49
|
if (start === end) throw new RangeError('Shift must not be empty');
|
|
41
50
|
const overnight = end < start;
|
|
42
51
|
const duration = (end - start + 1440) % 1440;
|
|
43
|
-
let breakOffset = 0,
|
|
52
|
+
let breakOffset = 0,
|
|
53
|
+
breakDuration = 0;
|
|
44
54
|
if (settings.breakStart != null || settings.breakEnd != null) {
|
|
45
|
-
const bs = minute(settings.breakStart),
|
|
55
|
+
const bs = minute(settings.breakStart),
|
|
56
|
+
be = minute(settings.breakEnd);
|
|
46
57
|
if (bs !== be) {
|
|
47
58
|
breakOffset = (bs - start + 1440) % 1440;
|
|
48
59
|
breakDuration = (be - bs + 1440) % 1440;
|
|
@@ -54,7 +65,7 @@ function calculate(settings, timestamp = Date.now()) {
|
|
|
54
65
|
const localNow = civil(now);
|
|
55
66
|
const today = Math.floor(localNow / DAY) * DAY;
|
|
56
67
|
const minuteNow = (localNow - today) / 60000;
|
|
57
|
-
const workday = day => settings.weekdays.includes(new Date(day).getUTCDay());
|
|
68
|
+
const workday = (day) => settings.weekdays.includes(new Date(day).getUTCDay());
|
|
58
69
|
let shiftDay = today;
|
|
59
70
|
if (overnight && minuteNow < start) {
|
|
60
71
|
// Retain yesterday's finished work shift until the next shift starts. If
|
|
@@ -65,47 +76,83 @@ function calculate(settings, timestamp = Date.now()) {
|
|
|
65
76
|
const shiftStart = instant(shiftDay + start * 60000);
|
|
66
77
|
const shiftEnd = instant(shiftDay + (start + duration) * 60000);
|
|
67
78
|
const breakStart = breakDuration ? instant(shiftDay + (start + breakOffset) * 60000) : null;
|
|
68
|
-
const breakEnd = breakDuration
|
|
79
|
+
const breakEnd = breakDuration
|
|
80
|
+
? instant(shiftDay + (start + breakOffset + breakDuration) * 60000)
|
|
81
|
+
: null;
|
|
69
82
|
const breakMs = breakDuration ? breakEnd - breakStart : 0;
|
|
70
83
|
const totalMs = shiftEnd - shiftStart - breakMs;
|
|
71
84
|
if (totalMs <= 0 || breakMs < 0) throw new RangeError('Shift has no paid duration');
|
|
72
85
|
const clampedNow = Math.max(shiftStart, Math.min(now, shiftEnd));
|
|
73
|
-
const elapsedMs = isWorkday
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
const elapsedMs = isWorkday
|
|
87
|
+
? Math.max(
|
|
88
|
+
0,
|
|
89
|
+
Math.min(
|
|
90
|
+
totalMs,
|
|
91
|
+
clampedNow -
|
|
92
|
+
shiftStart -
|
|
93
|
+
(breakDuration ? Math.max(0, Math.min(clampedNow, breakEnd) - breakStart) : 0),
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
: 0;
|
|
97
|
+
const status = !isWorkday
|
|
98
|
+
? 'rest'
|
|
99
|
+
: now < shiftStart
|
|
100
|
+
? 'before'
|
|
101
|
+
: now >= shiftEnd
|
|
102
|
+
? 'done'
|
|
103
|
+
: breakDuration && now >= breakStart && now < breakEnd
|
|
104
|
+
? 'lunch'
|
|
105
|
+
: 'working';
|
|
77
106
|
// Decimal inputs become exact rational numbers before truncation. Never use
|
|
78
107
|
// floor((salary / days * progress) * 100): binary rounding loses whole cents.
|
|
79
|
-
const rational = number => {
|
|
80
|
-
if (typeof number !== 'number' || !Number.isFinite(number) || number < 0)
|
|
108
|
+
const rational = (number) => {
|
|
109
|
+
if (typeof number !== 'number' || !Number.isFinite(number) || number < 0)
|
|
110
|
+
throw new RangeError('Invalid salary');
|
|
81
111
|
const [mantissa, exponent = '0'] = String(number).toLowerCase().split('e');
|
|
82
112
|
const [integer, fraction = ''] = mantissa.split('.');
|
|
83
113
|
const scale = fraction.length - Number(exponent);
|
|
84
|
-
return scale >= 0
|
|
114
|
+
return scale >= 0
|
|
115
|
+
? [BigInt(integer + fraction), 10n ** BigInt(scale)]
|
|
85
116
|
: [BigInt(integer + fraction) * 10n ** BigInt(-scale), 1n];
|
|
86
117
|
};
|
|
87
118
|
const payMode = settings.payMode === 'daily' ? 'daily' : 'monthly';
|
|
88
|
-
let [numerator, denominator] = rational(
|
|
119
|
+
let [numerator, denominator] = rational(
|
|
120
|
+
payMode === 'daily' ? settings.dailySalary : settings.monthlySalary,
|
|
121
|
+
);
|
|
89
122
|
if (payMode === 'monthly') {
|
|
90
123
|
const [daysN, daysD] = rational(settings.workdaysPerMonth);
|
|
91
124
|
if (daysN === 0n) throw new RangeError('Workdays per month must be positive');
|
|
92
125
|
numerator *= daysD;
|
|
93
126
|
denominator *= daysN;
|
|
94
127
|
}
|
|
95
|
-
const dailyCents = Number(numerator * 100n / denominator);
|
|
96
|
-
const earnedCents = Math.min(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
128
|
+
const dailyCents = Number((numerator * 100n) / denominator);
|
|
129
|
+
const earnedCents = Math.min(
|
|
130
|
+
dailyCents,
|
|
131
|
+
Number((numerator * 100n * BigInt(Math.floor(elapsedMs))) / (denominator * BigInt(totalMs))),
|
|
132
|
+
);
|
|
133
|
+
const hourlyCents = Number((numerator * 100n * 3600000n) / (denominator * BigInt(totalMs)));
|
|
134
|
+
const dateString = (day) => new Date(day).toISOString().slice(0, 10);
|
|
100
135
|
return {
|
|
101
|
-
status,
|
|
136
|
+
status,
|
|
137
|
+
remaining: isWorkday ? Math.max(0, Math.ceil((shiftEnd - now) / 1000)) : 0,
|
|
102
138
|
startRemaining: status === 'before' ? Math.max(0, Math.ceil((shiftStart - now) / 1000)) : 0,
|
|
103
139
|
progress: elapsedMs / totalMs,
|
|
104
|
-
earned: earnedCents / 100,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
earned: earnedCents / 100,
|
|
141
|
+
daily: dailyCents / 100,
|
|
142
|
+
hourly: hourlyCents / 100,
|
|
143
|
+
date: dateString(today),
|
|
144
|
+
shiftDate: dateString(shiftDay),
|
|
145
|
+
earnedCents,
|
|
146
|
+
dailyCents,
|
|
147
|
+
payMode,
|
|
148
|
+
isWorkday,
|
|
149
|
+
overnight,
|
|
150
|
+
shiftStart,
|
|
151
|
+
shiftEnd,
|
|
152
|
+
breakStart,
|
|
153
|
+
breakEnd,
|
|
154
|
+
elapsed: elapsedMs / 1000,
|
|
155
|
+
total: totalMs / 1000,
|
|
109
156
|
};
|
|
110
157
|
}
|
|
111
158
|
|
|
@@ -113,12 +160,14 @@ function calculate(settings, timestamp = Date.now()) {
|
|
|
113
160
|
function formatDuration(seconds) {
|
|
114
161
|
if (!Number.isFinite(seconds) || seconds < 0) return '--:--:--';
|
|
115
162
|
const n = Math.ceil(seconds);
|
|
116
|
-
return [Math.floor(n / 3600), Math.floor(n % 3600 / 60), n % 60]
|
|
117
|
-
.map(part => String(part).padStart(2, '0'))
|
|
163
|
+
return [Math.floor(n / 3600), Math.floor((n % 3600) / 60), n % 60]
|
|
164
|
+
.map((part) => String(part).padStart(2, '0'))
|
|
165
|
+
.join(':');
|
|
118
166
|
}
|
|
119
167
|
|
|
120
168
|
function clampPosition(position, viewport, size) {
|
|
121
|
-
const width = Math.max(0, viewport.width),
|
|
169
|
+
const width = Math.max(0, viewport.width),
|
|
170
|
+
height = Math.max(0, viewport.height);
|
|
122
171
|
const edge = Math.min(12, width / 2, height / 2);
|
|
123
172
|
const maxX = Math.max(edge, width - size.width - edge);
|
|
124
173
|
const maxY = Math.max(edge, height - size.height - edge);
|
|
@@ -139,8 +188,10 @@ function normalizeView(value) {
|
|
|
139
188
|
mini: value?.mini !== false,
|
|
140
189
|
masked: value?.masked !== false,
|
|
141
190
|
opacity: clampOpacity(value?.opacity),
|
|
142
|
-
position:
|
|
143
|
-
|
|
191
|
+
position:
|
|
192
|
+
Number.isFinite(value?.position?.x) && Number.isFinite(value?.position?.y)
|
|
193
|
+
? { x: value.position.x, y: value.position.y }
|
|
194
|
+
: null,
|
|
144
195
|
};
|
|
145
196
|
}
|
|
146
197
|
|
|
@@ -149,48 +200,146 @@ function decodeCommand(rpc) {
|
|
|
149
200
|
const result = rpc.value?.result;
|
|
150
201
|
if (!result || typeof result.text !== 'string') throw new Error('Command unavailable');
|
|
151
202
|
let data;
|
|
152
|
-
try {
|
|
153
|
-
|
|
154
|
-
|
|
203
|
+
try {
|
|
204
|
+
data = JSON.parse(result.text);
|
|
205
|
+
} catch {
|
|
206
|
+
throw new Error('Invalid command response');
|
|
207
|
+
}
|
|
208
|
+
if (result.kind !== 'success' || data?.ok !== true)
|
|
209
|
+
throw new Error(data?.error || 'Request failed');
|
|
155
210
|
if (!data.settings || typeof data.settings !== 'object') throw new Error('Missing settings');
|
|
156
211
|
return data;
|
|
157
212
|
}
|
|
158
213
|
|
|
159
214
|
function changedSettings(original, draft) {
|
|
160
|
-
return Object.fromEntries(
|
|
161
|
-
.
|
|
215
|
+
return Object.fromEntries(
|
|
216
|
+
Object.entries(draft).filter(
|
|
217
|
+
([key, value]) => JSON.stringify(value) !== JSON.stringify(original[key]),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
162
220
|
}
|
|
163
221
|
|
|
164
222
|
const messages = {
|
|
165
223
|
zh: {
|
|
166
|
-
title:'下班计时器',
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
224
|
+
title: '下班计时器',
|
|
225
|
+
working: '距离下班',
|
|
226
|
+
before: '距离开工',
|
|
227
|
+
lunch: '午休中',
|
|
228
|
+
done: '本班已完成',
|
|
229
|
+
rest: '今天休息',
|
|
230
|
+
earned: '本班已赚 · 税前',
|
|
231
|
+
daily: '日薪',
|
|
232
|
+
hourly: '有效时薪',
|
|
233
|
+
progress: '计薪进度',
|
|
234
|
+
settings: '设置',
|
|
235
|
+
collapse: '收起为小条',
|
|
236
|
+
expand: '展开',
|
|
237
|
+
hide: '隐藏工资',
|
|
238
|
+
show: '显示工资',
|
|
239
|
+
save: '保存',
|
|
240
|
+
cancel: '取消',
|
|
241
|
+
monthlySalary: '税前月薪(元)',
|
|
242
|
+
dailySalary: '税前日薪(元)',
|
|
243
|
+
workdaysPerMonth: '月计薪天数',
|
|
244
|
+
start: '上班时间',
|
|
245
|
+
end: '下班时间',
|
|
246
|
+
breakStart: '休息开始',
|
|
247
|
+
breakEnd: '休息结束',
|
|
248
|
+
timezone: '时区',
|
|
249
|
+
weekdays: '班次开始日',
|
|
250
|
+
payMode: '计薪方式',
|
|
251
|
+
monthly: '月薪',
|
|
252
|
+
dailyMode: '日薪',
|
|
253
|
+
loading: '正在读取设置…',
|
|
254
|
+
retry: '重新读取',
|
|
255
|
+
noSession: '请先打开或创建一个会话。',
|
|
256
|
+
error: '操作失败,请重试。',
|
|
257
|
+
saved: '设置已保存',
|
|
258
|
+
days: '日一二三四五六',
|
|
259
|
+
privacy: '配置保存在 Harness 所在机器;读写结果进入所选会话记录。',
|
|
260
|
+
drag: '按住可拖动;方向键微调,Home 复位',
|
|
261
|
+
setup: '先设好你的作息',
|
|
262
|
+
setupHint: '示例数值不是你的收入,保存后才开始计时。',
|
|
263
|
+
breakEnabled: '扣除无薪休息',
|
|
264
|
+
noBreak: '不扣休息',
|
|
265
|
+
nextDay: '次日',
|
|
266
|
+
preview: '折算',
|
|
267
|
+
salaryGroup: '薪资',
|
|
268
|
+
scheduleGroup: '作息',
|
|
269
|
+
appearanceGroup: '外观',
|
|
270
|
+
advanced: '高级设置',
|
|
271
|
+
sessionHint: '仅用于记录配置读写操作,不是账本,不影响工资计算。',
|
|
272
|
+
session: '操作记录会话',
|
|
273
|
+
masked: '工资已隐藏',
|
|
274
|
+
reset: '复位位置',
|
|
275
|
+
synced: '已读取最新配置',
|
|
276
|
+
refresh: '读取最新配置',
|
|
277
|
+
busy: '保存中…',
|
|
278
|
+
storage: '浏览器禁止本地存储;位置与透明度本次有效。',
|
|
279
|
+
shiftDate: '班次',
|
|
280
|
+
opacity: '背景透明度',
|
|
281
|
+
opacityHint: '只影响背景,不影响文字。',
|
|
179
282
|
},
|
|
180
283
|
en: {
|
|
181
|
-
title:'Offwork Clock',
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
284
|
+
title: 'Offwork Clock',
|
|
285
|
+
working: 'Until clock-out',
|
|
286
|
+
before: 'Until work starts',
|
|
287
|
+
lunch: 'On break',
|
|
288
|
+
done: 'Shift complete',
|
|
289
|
+
rest: 'Day off',
|
|
290
|
+
earned: 'Earned this shift · gross',
|
|
291
|
+
daily: 'Shift pay',
|
|
292
|
+
hourly: 'Paid-hour rate',
|
|
293
|
+
progress: 'Paid progress',
|
|
294
|
+
settings: 'Settings',
|
|
295
|
+
collapse: 'Minimize to strip',
|
|
296
|
+
expand: 'Expand',
|
|
297
|
+
hide: 'Hide salary',
|
|
298
|
+
show: 'Show salary',
|
|
299
|
+
save: 'Save',
|
|
300
|
+
cancel: 'Cancel',
|
|
301
|
+
monthlySalary: 'Monthly gross (CNY)',
|
|
302
|
+
dailySalary: 'Daily gross (CNY)',
|
|
303
|
+
workdaysPerMonth: 'Paid days / month',
|
|
304
|
+
start: 'Shift starts',
|
|
305
|
+
end: 'Shift ends',
|
|
306
|
+
breakStart: 'Break starts',
|
|
307
|
+
breakEnd: 'Break ends',
|
|
308
|
+
timezone: 'Time zone',
|
|
309
|
+
weekdays: 'Shift starting days',
|
|
310
|
+
payMode: 'Pay basis',
|
|
311
|
+
monthly: 'Monthly',
|
|
312
|
+
dailyMode: 'Daily',
|
|
313
|
+
loading: 'Loading settings…',
|
|
314
|
+
retry: 'Reload',
|
|
315
|
+
noSession: 'Open or create a session first.',
|
|
316
|
+
error: 'Operation failed. Please retry.',
|
|
317
|
+
saved: 'Settings saved',
|
|
318
|
+
days: 'SMTWTFS',
|
|
319
|
+
privacy: 'Settings are saved on the Harness host; results enter the selected session log.',
|
|
320
|
+
drag: 'Drag to move; arrow keys to adjust; Home resets',
|
|
321
|
+
setup: 'Set up your schedule',
|
|
322
|
+
setupHint: 'These are example values, not your income. Save to start.',
|
|
323
|
+
breakEnabled: 'Deduct unpaid break',
|
|
324
|
+
noBreak: 'no break',
|
|
325
|
+
nextDay: 'next day',
|
|
326
|
+
preview: 'Preview',
|
|
327
|
+
salaryGroup: 'Salary',
|
|
328
|
+
scheduleGroup: 'Schedule',
|
|
329
|
+
appearanceGroup: 'Appearance',
|
|
330
|
+
advanced: 'Advanced',
|
|
331
|
+
sessionHint:
|
|
332
|
+
'Logs configuration operations only; not a ledger and does not affect pay calculations.',
|
|
333
|
+
session: 'Action log session',
|
|
334
|
+
masked: 'Salary hidden',
|
|
335
|
+
reset: 'Reset position',
|
|
336
|
+
synced: 'Latest settings loaded',
|
|
337
|
+
refresh: 'Reload latest settings',
|
|
338
|
+
busy: 'Saving…',
|
|
339
|
+
storage: 'Local storage is blocked; position and opacity last only for this visit.',
|
|
340
|
+
shiftDate: 'Shift',
|
|
341
|
+
opacity: 'Surface opacity',
|
|
342
|
+
opacityHint: 'Affects the background only.',
|
|
194
343
|
},
|
|
195
344
|
};
|
|
196
345
|
const STYLE = `
|
|
@@ -280,31 +429,65 @@ function changedSettings(original, draft) {
|
|
|
280
429
|
@media(max-width:420px){.owc-pill{min-width:136px;padding:8px 8px 7px 12px}.owc-pill-status{display:none}.owc-pill-time{font-size:22px}.owc-main,.owc-form{padding-left:14px;padding-right:14px}.owc-time{font-size:31px}}
|
|
281
430
|
`;
|
|
282
431
|
const ICONS = {
|
|
283
|
-
eye: [
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
432
|
+
eye: [
|
|
433
|
+
['path', { d: 'M1.7 8S4.3 3.9 8 3.9 14.3 8 14.3 8 11.7 12.1 8 12.1 1.7 8 1.7 8Z' }],
|
|
434
|
+
['circle', { cx: 8, cy: 8, r: 2.05 }],
|
|
435
|
+
],
|
|
436
|
+
eyeOff: [
|
|
437
|
+
['path', { d: 'M1.7 8S4.3 3.9 8 3.9 14.3 8 14.3 8 11.7 12.1 8 12.1 1.7 8 1.7 8Z' }],
|
|
438
|
+
['circle', { cx: 8, cy: 8, r: 2.05 }],
|
|
439
|
+
['path', { d: 'M3 3l10 10' }],
|
|
440
|
+
],
|
|
441
|
+
sliders: [
|
|
442
|
+
['path', { d: 'M2.2 4.6h3.4M9.2 4.6h4.6M2.2 11.4h5.4M11.4 11.4h2.4' }],
|
|
443
|
+
['circle', { cx: 7.3, cy: 4.6, r: 1.55 }],
|
|
444
|
+
['circle', { cx: 9.4, cy: 11.4, r: 1.55 }],
|
|
445
|
+
],
|
|
446
|
+
alarm: [
|
|
447
|
+
['circle', { cx: 8, cy: 9, r: 4.5 }],
|
|
448
|
+
['path', { d: 'M8 6.5V9l1.7 1M2 4l2-2M12 2l2 2M5 13l-1 2M11 13l1 2' }],
|
|
449
|
+
],
|
|
450
|
+
chevronUp: [['path', { d: 'M3.7 9.8L8 5.5l4.3 4.3' }]],
|
|
451
|
+
chevronDown: [['path', { d: 'M3.7 6.2L8 10.5l4.3-4.3' }]],
|
|
452
|
+
close: [['path', { d: 'M4.3 4.3l7.4 7.4M11.7 4.3l-7.4 7.4' }]],
|
|
453
|
+
refresh: [
|
|
454
|
+
['path', { d: 'M13.1 8a5.1 5.1 0 1 1-1.5-3.6' }],
|
|
455
|
+
['path', { d: 'M13.1 3v3.2H9.9' }],
|
|
456
|
+
],
|
|
291
457
|
};
|
|
292
|
-
const iconSvg = (name, size = 15) =>
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
458
|
+
const iconSvg = (name, size = 15) =>
|
|
459
|
+
h(
|
|
460
|
+
'svg',
|
|
461
|
+
{
|
|
462
|
+
width: size,
|
|
463
|
+
height: size,
|
|
464
|
+
viewBox: '0 0 16 16',
|
|
465
|
+
fill: 'none',
|
|
466
|
+
stroke: 'currentColor',
|
|
467
|
+
strokeWidth: 1.55,
|
|
468
|
+
strokeLinecap: 'round',
|
|
469
|
+
strokeLinejoin: 'round',
|
|
470
|
+
'aria-hidden': 'true',
|
|
471
|
+
focusable: 'false',
|
|
472
|
+
},
|
|
473
|
+
ICONS[name].map(([tag, attrs], index) => h(tag, { key: index, ...attrs })),
|
|
474
|
+
);
|
|
296
475
|
|
|
297
476
|
return {
|
|
298
477
|
inject: ['slots', 'locale', 'remote', 'remote.commands'],
|
|
299
478
|
apply(ctx) {
|
|
300
|
-
for (const [lang, dict] of Object.entries(messages))
|
|
479
|
+
for (const [lang, dict] of Object.entries(messages))
|
|
480
|
+
ctx.effect(() => ctx.locale.register('offwork-clock', lang, dict));
|
|
301
481
|
const viewKey = 'dsh.offwork-clock.view.v4';
|
|
302
482
|
const legacyKey = 'dsh.offwork-clock.view.v3';
|
|
303
483
|
|
|
304
484
|
function Clock(props) {
|
|
305
|
-
const locale = React.useSyncExternalStore(
|
|
485
|
+
const locale = React.useSyncExternalStore(
|
|
486
|
+
(fn) => ctx.locale.subscribe(fn),
|
|
487
|
+
() => ctx.locale.getSnapshot(),
|
|
488
|
+
);
|
|
306
489
|
const t = ctx.locale.bind('offwork-clock');
|
|
307
|
-
const sessions = props.useSessions(s => s);
|
|
490
|
+
const sessions = props.useSessions((s) => s);
|
|
308
491
|
const sessionIds = Array.isArray(sessions?.ids) ? sessions.ids : [];
|
|
309
492
|
const [sessionChoice, setSessionChoice] = React.useState('');
|
|
310
493
|
const sessionId = sessionIds.includes(sessionChoice) ? sessionChoice : sessionIds[0];
|
|
@@ -331,37 +514,76 @@ function changedSettings(original, draft) {
|
|
|
331
514
|
const preview = workingDraft ? safeCalculate(workingDraft) : null;
|
|
332
515
|
const safeState = state && !state.error ? state : null;
|
|
333
516
|
const collapsed = !!safeState && !editing && view.mini && !error && !state?.error;
|
|
334
|
-
const seconds = safeState
|
|
517
|
+
const seconds = safeState
|
|
518
|
+
? safeState.status === 'before'
|
|
519
|
+
? safeState.startRemaining
|
|
520
|
+
: safeState.remaining
|
|
521
|
+
: NaN;
|
|
335
522
|
const duration = formatDuration(seconds);
|
|
336
|
-
const formatMoney = React.useMemo(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
523
|
+
const formatMoney = React.useMemo(
|
|
524
|
+
() =>
|
|
525
|
+
new Intl.NumberFormat(locale.active, {
|
|
526
|
+
style: 'currency',
|
|
527
|
+
currency: 'CNY',
|
|
528
|
+
minimumFractionDigits: 2,
|
|
529
|
+
maximumFractionDigits: 2,
|
|
530
|
+
}),
|
|
531
|
+
[locale.active],
|
|
532
|
+
);
|
|
533
|
+
const money = (value) => (view.masked ? '••••' : formatMoney.format(value));
|
|
340
534
|
const moneyText = safeState ? money(safeState.earned) : '—';
|
|
341
|
-
const cardStyle = Object.assign(
|
|
342
|
-
|
|
535
|
+
const cardStyle = Object.assign(
|
|
536
|
+
{ '--owc-alpha': String(view.opacity) },
|
|
537
|
+
view.position ? { left: view.position.x, top: view.position.y, right: 'auto' } : {},
|
|
538
|
+
);
|
|
343
539
|
|
|
344
540
|
function safeCalculate(value) {
|
|
345
|
-
try {
|
|
346
|
-
|
|
541
|
+
try {
|
|
542
|
+
return calculate(value, now);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
return { error: e instanceof Error ? e.message : t('error') };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function errorText(e) {
|
|
548
|
+
return e instanceof Error ? e.message : t('error');
|
|
549
|
+
}
|
|
550
|
+
function setPreference(patch) {
|
|
551
|
+
setView((current) => ({ ...current, ...patch }));
|
|
347
552
|
}
|
|
348
|
-
function errorText(e) { return e instanceof Error ? e.message : t('error'); }
|
|
349
|
-
function setPreference(patch) { setView(current => ({ ...current, ...patch })); }
|
|
350
553
|
|
|
351
|
-
const clamp = React.useCallback(
|
|
352
|
-
|
|
353
|
-
|
|
554
|
+
const clamp = React.useCallback(
|
|
555
|
+
(position) =>
|
|
556
|
+
clampPosition(
|
|
557
|
+
position,
|
|
558
|
+
{ width: window.innerWidth, height: window.innerHeight },
|
|
559
|
+
{
|
|
560
|
+
width: box.current?.offsetWidth || 300,
|
|
561
|
+
height: box.current?.offsetHeight || 200,
|
|
562
|
+
},
|
|
563
|
+
),
|
|
564
|
+
[],
|
|
565
|
+
);
|
|
354
566
|
|
|
355
567
|
React.useEffect(() => {
|
|
356
568
|
try {
|
|
357
569
|
const saved = localStorage.getItem(viewKey);
|
|
358
570
|
const legacy = !saved ? JSON.parse(localStorage.getItem(legacyKey) || 'null') : null;
|
|
359
|
-
setView(
|
|
360
|
-
|
|
571
|
+
setView(
|
|
572
|
+
normalizeView(
|
|
573
|
+
saved
|
|
574
|
+
? JSON.parse(saved)
|
|
575
|
+
: { position: legacy?.position ?? null, masked: legacy?.masked ?? true },
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
} catch {
|
|
579
|
+
setStorageError(true);
|
|
580
|
+
}
|
|
361
581
|
setReady(true);
|
|
362
|
-
const sync = e => {
|
|
582
|
+
const sync = (e) => {
|
|
363
583
|
if (e.key !== viewKey) return;
|
|
364
|
-
try {
|
|
584
|
+
try {
|
|
585
|
+
setView(normalizeView(JSON.parse(e.newValue || 'null')));
|
|
586
|
+
} catch {}
|
|
365
587
|
};
|
|
366
588
|
window.addEventListener('storage', sync);
|
|
367
589
|
return () => window.removeEventListener('storage', sync);
|
|
@@ -369,72 +591,122 @@ function changedSettings(original, draft) {
|
|
|
369
591
|
|
|
370
592
|
React.useEffect(() => {
|
|
371
593
|
if (!ready) return;
|
|
372
|
-
try {
|
|
373
|
-
|
|
594
|
+
try {
|
|
595
|
+
localStorage.setItem(viewKey, JSON.stringify(normalizeView(view)));
|
|
596
|
+
} catch {
|
|
597
|
+
setStorageError(true);
|
|
598
|
+
}
|
|
374
599
|
}, [ready, view]);
|
|
375
600
|
|
|
376
601
|
React.useEffect(() => {
|
|
377
602
|
if (sessionId) reload();
|
|
378
603
|
else setError(t('noSession'));
|
|
379
|
-
return () => {
|
|
604
|
+
return () => {
|
|
605
|
+
generation.current++;
|
|
606
|
+
};
|
|
380
607
|
}, [sessionId]);
|
|
381
608
|
|
|
382
609
|
React.useEffect(() => {
|
|
383
610
|
let timer;
|
|
384
|
-
const tick = () => {
|
|
385
|
-
|
|
611
|
+
const tick = () => {
|
|
612
|
+
setNow(Date.now());
|
|
613
|
+
timer = setTimeout(tick, 1000 - (Date.now() % 1000));
|
|
614
|
+
};
|
|
615
|
+
const visibility = () => {
|
|
616
|
+
clearTimeout(timer);
|
|
617
|
+
setNow(Date.now());
|
|
618
|
+
if (!document.hidden) tick();
|
|
619
|
+
};
|
|
386
620
|
if (!document.hidden) tick();
|
|
387
621
|
document.addEventListener('visibilitychange', visibility);
|
|
388
|
-
return () => {
|
|
622
|
+
return () => {
|
|
623
|
+
clearTimeout(timer);
|
|
624
|
+
document.removeEventListener('visibilitychange', visibility);
|
|
625
|
+
};
|
|
389
626
|
}, []);
|
|
390
627
|
|
|
391
628
|
React.useEffect(() => {
|
|
392
|
-
const resize = () =>
|
|
629
|
+
const resize = () =>
|
|
630
|
+
setView((current) =>
|
|
631
|
+
current.position ? { ...current, position: clamp(current.position) } : current,
|
|
632
|
+
);
|
|
393
633
|
const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(resize) : null;
|
|
394
634
|
if (box.current) ro?.observe(box.current);
|
|
395
635
|
window.addEventListener('resize', resize);
|
|
396
|
-
return () => {
|
|
636
|
+
return () => {
|
|
637
|
+
ro?.disconnect();
|
|
638
|
+
window.removeEventListener('resize', resize);
|
|
639
|
+
};
|
|
397
640
|
}, [clamp]);
|
|
398
641
|
|
|
399
642
|
React.useEffect(() => {
|
|
400
|
-
if (!editing) {
|
|
643
|
+
if (!editing) {
|
|
644
|
+
focusReturn.current?.focus?.();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
401
647
|
box.current?.querySelector('form input,form select')?.focus?.();
|
|
402
648
|
}, [editing]);
|
|
403
649
|
|
|
404
|
-
React.useEffect(
|
|
650
|
+
React.useEffect(
|
|
651
|
+
() => () => {
|
|
652
|
+
generation.current++;
|
|
653
|
+
dragState.current = null;
|
|
654
|
+
},
|
|
655
|
+
[],
|
|
656
|
+
);
|
|
405
657
|
|
|
406
658
|
async function request(action, patch) {
|
|
407
659
|
if (!sessionId) throw new Error(t('noSession'));
|
|
408
|
-
const rpc = await ctx.remote.commands.execute(
|
|
660
|
+
const rpc = await ctx.remote.commands.execute(
|
|
661
|
+
sessionId,
|
|
662
|
+
'/offwork-clock ' + action + (patch ? ' ' + JSON.stringify(patch) : ''),
|
|
663
|
+
[],
|
|
664
|
+
);
|
|
409
665
|
return decodeCommand(rpc);
|
|
410
666
|
}
|
|
411
667
|
|
|
412
668
|
async function reload(explicit = false) {
|
|
413
669
|
if (requestBusy.current) return;
|
|
414
670
|
const token = ++generation.current;
|
|
415
|
-
setBusy(true);
|
|
671
|
+
setBusy(true);
|
|
672
|
+
setError('');
|
|
416
673
|
try {
|
|
417
674
|
const next = await request('get');
|
|
418
675
|
if (token !== generation.current) return;
|
|
419
676
|
setData(next);
|
|
420
|
-
if (!next.configured) {
|
|
677
|
+
if (!next.configured) {
|
|
678
|
+
setDraft({ ...next.settings });
|
|
679
|
+
setEditing(true);
|
|
680
|
+
}
|
|
421
681
|
if (explicit) setNotice(t('synced'));
|
|
422
|
-
} catch (e) {
|
|
423
|
-
|
|
682
|
+
} catch (e) {
|
|
683
|
+
if (token === generation.current) setError(errorText(e));
|
|
684
|
+
} finally {
|
|
685
|
+
if (token === generation.current) setBusy(false);
|
|
686
|
+
}
|
|
424
687
|
}
|
|
425
688
|
|
|
426
689
|
async function save(event) {
|
|
427
690
|
event.preventDefault();
|
|
428
691
|
if (requestBusy.current) return;
|
|
429
|
-
requestBusy.current = true;
|
|
692
|
+
requestBusy.current = true;
|
|
693
|
+
setBusy(true);
|
|
694
|
+
setError('');
|
|
430
695
|
const token = ++generation.current;
|
|
431
696
|
try {
|
|
432
697
|
const next = await request('set', changedSettings(settings, draft));
|
|
433
698
|
if (token === generation.current) {
|
|
434
|
-
setData(next);
|
|
699
|
+
setData(next);
|
|
700
|
+
setEditing(false);
|
|
701
|
+
setDraft(null);
|
|
702
|
+
setNotice(t('saved'));
|
|
435
703
|
}
|
|
436
|
-
} catch (e) {
|
|
437
|
-
|
|
704
|
+
} catch (e) {
|
|
705
|
+
if (token === generation.current) setError(errorText(e));
|
|
706
|
+
} finally {
|
|
707
|
+
requestBusy.current = false;
|
|
708
|
+
if (token === generation.current) setBusy(false);
|
|
709
|
+
}
|
|
438
710
|
}
|
|
439
711
|
|
|
440
712
|
function commitPosition(position) {
|
|
@@ -444,10 +716,21 @@ function changedSettings(original, draft) {
|
|
|
444
716
|
}
|
|
445
717
|
function dragDown(e) {
|
|
446
718
|
if (e.button !== 0) return;
|
|
447
|
-
if (
|
|
719
|
+
if (
|
|
720
|
+
e.target.closest?.('button,input,select,label,a,textarea,summary,[contenteditable]')
|
|
721
|
+
)
|
|
722
|
+
return;
|
|
448
723
|
const rect = box.current.getBoundingClientRect();
|
|
449
|
-
dragState.current = {
|
|
450
|
-
|
|
724
|
+
dragState.current = {
|
|
725
|
+
dx: e.clientX - rect.left,
|
|
726
|
+
dy: e.clientY - rect.top,
|
|
727
|
+
x0: e.clientX,
|
|
728
|
+
y0: e.clientY,
|
|
729
|
+
moved: false,
|
|
730
|
+
};
|
|
731
|
+
try {
|
|
732
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
733
|
+
} catch {}
|
|
451
734
|
}
|
|
452
735
|
function dragMove(e) {
|
|
453
736
|
const d = dragState.current;
|
|
@@ -467,10 +750,17 @@ function changedSettings(original, draft) {
|
|
|
467
750
|
if (!d) return;
|
|
468
751
|
dragState.current = null;
|
|
469
752
|
if (d.moved && lastPosition.current) commitPosition(lastPosition.current);
|
|
470
|
-
try {
|
|
753
|
+
try {
|
|
754
|
+
if (e.currentTarget.hasPointerCapture?.(e.pointerId))
|
|
755
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
756
|
+
} catch {}
|
|
471
757
|
}
|
|
472
758
|
function gripKeys(e) {
|
|
473
|
-
if (e.key === 'Home') {
|
|
759
|
+
if (e.key === 'Home') {
|
|
760
|
+
e.preventDefault();
|
|
761
|
+
setPreference({ position: null });
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
474
764
|
if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return;
|
|
475
765
|
e.preventDefault();
|
|
476
766
|
const rect = box.current.getBoundingClientRect();
|
|
@@ -484,114 +774,324 @@ function changedSettings(original, draft) {
|
|
|
484
774
|
focusReturn.current = e.currentTarget;
|
|
485
775
|
setDraft({ ...settings });
|
|
486
776
|
setEditing(true);
|
|
487
|
-
setError('');
|
|
777
|
+
setError('');
|
|
778
|
+
setNotice('');
|
|
488
779
|
}
|
|
489
780
|
function closeEditor() {
|
|
490
781
|
if (!data?.configured) return;
|
|
491
|
-
setEditing(false);
|
|
782
|
+
setEditing(false);
|
|
783
|
+
setDraft(null);
|
|
784
|
+
setError('');
|
|
785
|
+
}
|
|
786
|
+
function update(key, value) {
|
|
787
|
+
setDraft((current) => ({ ...current, [key]: value }));
|
|
492
788
|
}
|
|
493
|
-
function update(key, value) { setDraft(current => ({ ...current, [key]: value })); }
|
|
494
789
|
function toggleBreak(e) {
|
|
495
|
-
const point = text => +text.slice(0, 2) * 60 + +text.slice(3);
|
|
496
|
-
const label =
|
|
497
|
-
|
|
498
|
-
|
|
790
|
+
const point = (text) => +text.slice(0, 2) * 60 + +text.slice(3);
|
|
791
|
+
const label = (minutes) =>
|
|
792
|
+
String(Math.floor(minutes / 60)).padStart(2, '0') +
|
|
793
|
+
':' +
|
|
794
|
+
String(minutes % 60).padStart(2, '0');
|
|
795
|
+
setDraft((current) => {
|
|
796
|
+
if (!e.target.checked)
|
|
797
|
+
return { ...current, breakStart: current.start, breakEnd: current.start };
|
|
499
798
|
const start = point(current.start);
|
|
500
799
|
const span = (point(current.end) - start + 1440) % 1440;
|
|
501
|
-
return {
|
|
800
|
+
return {
|
|
801
|
+
...current,
|
|
802
|
+
breakStart: current.start,
|
|
803
|
+
breakEnd: label((start + Math.max(1, Math.min(60, Math.floor(span / 2)))) % 1440),
|
|
804
|
+
};
|
|
502
805
|
});
|
|
503
806
|
}
|
|
504
807
|
|
|
505
|
-
const label = (text, control) =>
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
808
|
+
const label = (text, control) =>
|
|
809
|
+
h('label', { className: 'owc-field' }, h('span', null, text), control);
|
|
810
|
+
const numberInput = (key) =>
|
|
811
|
+
h('input', {
|
|
812
|
+
name: key,
|
|
813
|
+
type: 'number',
|
|
814
|
+
step: 'any',
|
|
815
|
+
min: key === 'workdaysPerMonth' ? '0.01' : '0',
|
|
816
|
+
value: workingDraft?.[key] ?? '',
|
|
817
|
+
disabled: busy,
|
|
818
|
+
onChange: (e) => update(key, e.target.value === '' ? '' : e.target.valueAsNumber),
|
|
819
|
+
});
|
|
820
|
+
const timeInput = (key) =>
|
|
821
|
+
h('input', {
|
|
822
|
+
name: key,
|
|
823
|
+
type: 'time',
|
|
824
|
+
required: true,
|
|
825
|
+
value: workingDraft?.[key] ?? '',
|
|
826
|
+
disabled: busy,
|
|
827
|
+
onChange: (e) => update(key, e.target.value),
|
|
828
|
+
});
|
|
829
|
+
const sessionSelect = () =>
|
|
830
|
+
h(
|
|
831
|
+
'select',
|
|
832
|
+
{
|
|
833
|
+
value: sessionId || '',
|
|
834
|
+
disabled: busy,
|
|
835
|
+
onChange: (e) => setSessionChoice(e.target.value),
|
|
836
|
+
},
|
|
837
|
+
sessionIds.length
|
|
838
|
+
? sessionIds.map((id) =>
|
|
839
|
+
h(
|
|
840
|
+
'option',
|
|
841
|
+
{ key: id, value: id },
|
|
842
|
+
sessions?.byId?.[id]?.title || String(id).slice(0, 12),
|
|
843
|
+
),
|
|
844
|
+
)
|
|
845
|
+
: h('option', { value: '' }, t('noSession')),
|
|
846
|
+
);
|
|
847
|
+
const salaryToggle = () =>
|
|
848
|
+
h(
|
|
849
|
+
'button',
|
|
850
|
+
{
|
|
851
|
+
type: 'button',
|
|
852
|
+
className: 'owc-text',
|
|
853
|
+
'aria-pressed': view.masked,
|
|
854
|
+
title: t(view.masked ? 'show' : 'hide'),
|
|
855
|
+
onClick: () => setPreference({ masked: !view.masked }),
|
|
856
|
+
},
|
|
857
|
+
iconSvg(view.masked ? 'eyeOff' : 'eye', 13),
|
|
858
|
+
t(view.masked ? 'show' : 'hide'),
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
const strip = () =>
|
|
862
|
+
h(
|
|
863
|
+
'div',
|
|
864
|
+
{ className: 'owc-pill' },
|
|
865
|
+
h(
|
|
866
|
+
'div',
|
|
867
|
+
{ className: 'owc-pill-top' },
|
|
868
|
+
iconSvg('alarm', 16),
|
|
869
|
+
h(
|
|
870
|
+
'span',
|
|
871
|
+
{ className: 'owc-grip', tabIndex: 0, title: t('drag'), onKeyDown: gripKeys },
|
|
872
|
+
safeState.status !== 'done' &&
|
|
873
|
+
h('span', { className: 'owc-pill-status' }, t(safeState.status)),
|
|
874
|
+
h('span', { className: 'owc-pill-time' }, duration),
|
|
875
|
+
),
|
|
876
|
+
h(
|
|
877
|
+
'button',
|
|
878
|
+
{
|
|
879
|
+
type: 'button',
|
|
880
|
+
className: 'owc-icon',
|
|
881
|
+
style: { marginLeft: 'auto' },
|
|
882
|
+
title: t('expand'),
|
|
883
|
+
'aria-label': t('expand'),
|
|
884
|
+
onClick: () => setPreference({ mini: false }),
|
|
885
|
+
},
|
|
886
|
+
iconSvg('chevronUp', 14),
|
|
887
|
+
),
|
|
888
|
+
),
|
|
889
|
+
h(
|
|
890
|
+
'div',
|
|
891
|
+
{ className: 'owc-pill-bottom' },
|
|
892
|
+
h(
|
|
893
|
+
'span',
|
|
894
|
+
{
|
|
895
|
+
className: 'owc-pill-money',
|
|
896
|
+
'aria-label': view.masked ? t('masked') : moneyText,
|
|
897
|
+
},
|
|
898
|
+
moneyText,
|
|
899
|
+
),
|
|
900
|
+
h(
|
|
901
|
+
'div',
|
|
902
|
+
{ className: 'owc-pill-actions' },
|
|
903
|
+
h(
|
|
904
|
+
'button',
|
|
905
|
+
{
|
|
906
|
+
type: 'button',
|
|
907
|
+
className: 'owc-icon',
|
|
908
|
+
title: t(view.masked ? 'show' : 'hide'),
|
|
909
|
+
'aria-label': t(view.masked ? 'show' : 'hide'),
|
|
910
|
+
onClick: () => setPreference({ masked: !view.masked }),
|
|
911
|
+
},
|
|
912
|
+
iconSvg(view.masked ? 'eyeOff' : 'eye', 14),
|
|
913
|
+
),
|
|
914
|
+
),
|
|
915
|
+
),
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
const panelHead = () =>
|
|
919
|
+
h(
|
|
920
|
+
'div',
|
|
921
|
+
{ className: 'owc-head' },
|
|
922
|
+
h(
|
|
923
|
+
'span',
|
|
924
|
+
{
|
|
925
|
+
className: 'owc-title',
|
|
926
|
+
tabIndex: 0,
|
|
927
|
+
title: t('drag'),
|
|
928
|
+
onKeyDown: gripKeys,
|
|
929
|
+
},
|
|
930
|
+
editing ? t(data?.configured ? 'settings' : 'setup') : t('title'),
|
|
931
|
+
),
|
|
932
|
+
editing
|
|
933
|
+
? data?.configured &&
|
|
934
|
+
h(
|
|
935
|
+
'button',
|
|
936
|
+
{
|
|
937
|
+
type: 'button',
|
|
938
|
+
className: 'owc-icon',
|
|
939
|
+
title: t('cancel'),
|
|
940
|
+
'aria-label': t('cancel'),
|
|
941
|
+
onClick: closeEditor,
|
|
942
|
+
disabled: busy,
|
|
943
|
+
},
|
|
944
|
+
iconSvg('close'),
|
|
945
|
+
)
|
|
946
|
+
: h(
|
|
947
|
+
React.Fragment,
|
|
948
|
+
null,
|
|
949
|
+
h(
|
|
950
|
+
'button',
|
|
951
|
+
{
|
|
952
|
+
type: 'button',
|
|
953
|
+
className: 'owc-icon',
|
|
954
|
+
title: t('settings'),
|
|
955
|
+
'aria-label': t('settings'),
|
|
956
|
+
onClick: openEditor,
|
|
957
|
+
disabled: busy,
|
|
958
|
+
},
|
|
959
|
+
iconSvg('sliders'),
|
|
960
|
+
),
|
|
961
|
+
h(
|
|
962
|
+
'button',
|
|
963
|
+
{
|
|
964
|
+
type: 'button',
|
|
965
|
+
className: 'owc-icon',
|
|
966
|
+
title: t('collapse'),
|
|
967
|
+
'aria-label': t('collapse'),
|
|
968
|
+
onClick: () => setPreference({ mini: true }),
|
|
969
|
+
},
|
|
970
|
+
iconSvg('chevronDown'),
|
|
971
|
+
),
|
|
972
|
+
),
|
|
973
|
+
);
|
|
565
974
|
|
|
566
975
|
const panelBody = () => {
|
|
567
|
-
if (!safeState)
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
h(
|
|
572
|
-
|
|
976
|
+
if (!safeState)
|
|
977
|
+
return h(
|
|
978
|
+
'div',
|
|
979
|
+
{ className: 'owc-main' },
|
|
980
|
+
h(
|
|
981
|
+
'div',
|
|
982
|
+
{ className: 'owc-status' },
|
|
983
|
+
h('span', { className: 'owc-dot' }),
|
|
984
|
+
t('loading'),
|
|
985
|
+
),
|
|
986
|
+
!sessionId && h('p', { className: 'owc-hint' }, t('noSession')),
|
|
987
|
+
h(
|
|
988
|
+
'div',
|
|
989
|
+
{ className: 'owc-row-actions' },
|
|
990
|
+
h(
|
|
991
|
+
'button',
|
|
992
|
+
{
|
|
993
|
+
type: 'button',
|
|
994
|
+
className: 'owc-text',
|
|
995
|
+
onClick: () => reload(true),
|
|
996
|
+
disabled: busy || !sessionId,
|
|
997
|
+
},
|
|
998
|
+
iconSvg('refresh', 12),
|
|
999
|
+
t('retry'),
|
|
1000
|
+
),
|
|
1001
|
+
),
|
|
1002
|
+
);
|
|
573
1003
|
const status = safeState.status === 'done' ? '' : t(safeState.status);
|
|
574
|
-
return h(
|
|
575
|
-
|
|
1004
|
+
return h(
|
|
1005
|
+
'div',
|
|
1006
|
+
{ className: 'owc-main' },
|
|
1007
|
+
status &&
|
|
1008
|
+
h('div', { className: 'owc-status' }, h('span', { className: 'owc-dot' }), status),
|
|
576
1009
|
h('div', { className: 'owc-time', 'aria-label': status + ' ' + duration }, duration),
|
|
577
|
-
h(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
h('
|
|
593
|
-
|
|
594
|
-
|
|
1010
|
+
h(
|
|
1011
|
+
'div',
|
|
1012
|
+
{
|
|
1013
|
+
className: 'owc-progress',
|
|
1014
|
+
role: 'progressbar',
|
|
1015
|
+
'aria-label': t('progress'),
|
|
1016
|
+
'aria-valuenow': Math.floor(safeState.progress * 100),
|
|
1017
|
+
'aria-valuemin': 0,
|
|
1018
|
+
'aria-valuemax': 100,
|
|
1019
|
+
},
|
|
1020
|
+
h('div', { style: { width: safeState.progress * 100 + '%' } }),
|
|
1021
|
+
),
|
|
1022
|
+
h(
|
|
1023
|
+
'div',
|
|
1024
|
+
{ className: 'owc-earn-row' },
|
|
1025
|
+
h('span', { className: 'owc-muted' }, t('earned')),
|
|
1026
|
+
salaryToggle(),
|
|
1027
|
+
),
|
|
1028
|
+
h(
|
|
1029
|
+
'div',
|
|
1030
|
+
{ className: 'owc-money', 'aria-label': view.masked ? t('masked') : moneyText },
|
|
1031
|
+
moneyText,
|
|
1032
|
+
),
|
|
1033
|
+
h(
|
|
1034
|
+
'div',
|
|
1035
|
+
{ className: 'owc-stats' },
|
|
1036
|
+
h(
|
|
1037
|
+
'div',
|
|
1038
|
+
null,
|
|
1039
|
+
h('span', null, t('daily')),
|
|
1040
|
+
h('strong', null, money(safeState.daily)),
|
|
1041
|
+
),
|
|
1042
|
+
h(
|
|
1043
|
+
'div',
|
|
1044
|
+
null,
|
|
1045
|
+
h('span', null, t('hourly')),
|
|
1046
|
+
h('strong', null, money(safeState.hourly)),
|
|
1047
|
+
),
|
|
1048
|
+
),
|
|
1049
|
+
h(
|
|
1050
|
+
'div',
|
|
1051
|
+
{ className: 'owc-meta' },
|
|
1052
|
+
h(
|
|
1053
|
+
'span',
|
|
1054
|
+
null,
|
|
1055
|
+
settings.start +
|
|
1056
|
+
' — ' +
|
|
1057
|
+
settings.end +
|
|
1058
|
+
(settings.end < settings.start ? ' +1' : '') +
|
|
1059
|
+
' · ' +
|
|
1060
|
+
(settings.breakStart === settings.breakEnd
|
|
1061
|
+
? t('noBreak')
|
|
1062
|
+
: settings.breakStart + '–' + settings.breakEnd),
|
|
1063
|
+
),
|
|
1064
|
+
h(
|
|
1065
|
+
'span',
|
|
1066
|
+
null,
|
|
1067
|
+
t('shiftDate') + ' ' + safeState.shiftDate + ' · ' + settings.timezone,
|
|
1068
|
+
),
|
|
1069
|
+
),
|
|
1070
|
+
h(
|
|
1071
|
+
'div',
|
|
1072
|
+
{ className: 'owc-row-actions' },
|
|
1073
|
+
h(
|
|
1074
|
+
'button',
|
|
1075
|
+
{
|
|
1076
|
+
type: 'button',
|
|
1077
|
+
className: 'owc-text',
|
|
1078
|
+
onClick: () => reload(true),
|
|
1079
|
+
disabled: busy,
|
|
1080
|
+
},
|
|
1081
|
+
iconSvg('refresh', 12),
|
|
1082
|
+
t('refresh'),
|
|
1083
|
+
),
|
|
1084
|
+
h(
|
|
1085
|
+
'button',
|
|
1086
|
+
{
|
|
1087
|
+
type: 'button',
|
|
1088
|
+
className: 'owc-text',
|
|
1089
|
+
onClick: () => setPreference({ position: null }),
|
|
1090
|
+
},
|
|
1091
|
+
t('reset'),
|
|
1092
|
+
),
|
|
1093
|
+
),
|
|
1094
|
+
);
|
|
595
1095
|
};
|
|
596
1096
|
|
|
597
1097
|
const editor = () => {
|
|
@@ -599,91 +1099,293 @@ function changedSettings(original, draft) {
|
|
|
599
1099
|
const payDaily = workingDraft.payMode === 'daily';
|
|
600
1100
|
const salaryKey = payDaily ? 'dailySalary' : 'monthlySalary';
|
|
601
1101
|
const salaryControl = view.masked
|
|
602
|
-
? h(
|
|
603
|
-
|
|
1102
|
+
? h(
|
|
1103
|
+
'div',
|
|
1104
|
+
{ className: 'owc-private' },
|
|
1105
|
+
h('span', null, '••••'),
|
|
1106
|
+
h(
|
|
1107
|
+
'button',
|
|
1108
|
+
{
|
|
1109
|
+
type: 'button',
|
|
1110
|
+
className: 'owc-text',
|
|
1111
|
+
'aria-label': t('show'),
|
|
1112
|
+
onClick: () => setPreference({ masked: false }),
|
|
1113
|
+
},
|
|
1114
|
+
t('show'),
|
|
1115
|
+
),
|
|
1116
|
+
)
|
|
604
1117
|
: numberInput(salaryKey);
|
|
605
1118
|
const breakOn = workingDraft.breakStart !== workingDraft.breakEnd;
|
|
606
|
-
const commonZones = [
|
|
1119
|
+
const commonZones = [
|
|
1120
|
+
'Asia/Shanghai',
|
|
1121
|
+
'Asia/Hong_Kong',
|
|
1122
|
+
'Asia/Taipei',
|
|
1123
|
+
'Asia/Singapore',
|
|
1124
|
+
'Asia/Tokyo',
|
|
1125
|
+
'Asia/Seoul',
|
|
1126
|
+
'Asia/Kolkata',
|
|
1127
|
+
'Europe/London',
|
|
1128
|
+
'Europe/Paris',
|
|
1129
|
+
'America/New_York',
|
|
1130
|
+
'America/Los_Angeles',
|
|
1131
|
+
'Australia/Sydney',
|
|
1132
|
+
'UTC',
|
|
1133
|
+
];
|
|
607
1134
|
let allZones = [];
|
|
608
|
-
try {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
h('
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1135
|
+
try {
|
|
1136
|
+
allZones = Intl.supportedValuesOf('timeZone');
|
|
1137
|
+
} catch {}
|
|
1138
|
+
const zones = [
|
|
1139
|
+
...new Set([workingDraft.timezone, ...commonZones, ...allZones].filter(Boolean)),
|
|
1140
|
+
];
|
|
1141
|
+
return h(
|
|
1142
|
+
'form',
|
|
1143
|
+
{
|
|
1144
|
+
className: 'owc-form',
|
|
1145
|
+
onSubmit: save,
|
|
1146
|
+
onKeyDown: (e) => {
|
|
1147
|
+
if (e.key === 'Escape') {
|
|
1148
|
+
e.preventDefault();
|
|
1149
|
+
closeEditor();
|
|
1150
|
+
}
|
|
1151
|
+
},
|
|
1152
|
+
},
|
|
1153
|
+
!data?.configured && h('p', { className: 'owc-hint' }, t('setupHint')),
|
|
1154
|
+
h(
|
|
1155
|
+
'fieldset',
|
|
1156
|
+
{ className: 'owc-setting-group' },
|
|
1157
|
+
h('legend', null, t('salaryGroup')),
|
|
1158
|
+
label(
|
|
1159
|
+
t('payMode'),
|
|
1160
|
+
h(
|
|
1161
|
+
'div',
|
|
1162
|
+
{ className: 'owc-seg' },
|
|
1163
|
+
h(
|
|
1164
|
+
'button',
|
|
1165
|
+
{
|
|
1166
|
+
type: 'button',
|
|
1167
|
+
disabled: busy,
|
|
1168
|
+
'aria-pressed': !payDaily,
|
|
1169
|
+
'aria-label': t('monthly'),
|
|
1170
|
+
onClick: () => update('payMode', 'monthly'),
|
|
1171
|
+
},
|
|
1172
|
+
t('monthly'),
|
|
1173
|
+
),
|
|
1174
|
+
h(
|
|
1175
|
+
'button',
|
|
1176
|
+
{
|
|
1177
|
+
type: 'button',
|
|
1178
|
+
disabled: busy,
|
|
1179
|
+
'aria-pressed': payDaily,
|
|
1180
|
+
'aria-label': t('dailyMode'),
|
|
1181
|
+
onClick: () => update('payMode', 'daily'),
|
|
1182
|
+
},
|
|
1183
|
+
t('dailyMode'),
|
|
1184
|
+
),
|
|
1185
|
+
),
|
|
1186
|
+
),
|
|
1187
|
+
h(
|
|
1188
|
+
'div',
|
|
1189
|
+
{ className: 'owc-row2' },
|
|
1190
|
+
label(t(salaryKey), salaryControl),
|
|
1191
|
+
!payDaily && label(t('workdaysPerMonth'), numberInput('workdaysPerMonth')),
|
|
1192
|
+
),
|
|
1193
|
+
),
|
|
1194
|
+
h(
|
|
1195
|
+
'fieldset',
|
|
1196
|
+
{ className: 'owc-setting-group' },
|
|
1197
|
+
h('legend', null, t('scheduleGroup')),
|
|
1198
|
+
h(
|
|
1199
|
+
'div',
|
|
1200
|
+
{ className: 'owc-row2' },
|
|
1201
|
+
label(t('start'), timeInput('start')),
|
|
1202
|
+
label(t('end'), timeInput('end')),
|
|
1203
|
+
),
|
|
1204
|
+
workingDraft.end < workingDraft.start &&
|
|
1205
|
+
h('p', { className: 'owc-hint' }, t('end') + ' · ' + t('nextDay')),
|
|
1206
|
+
h(
|
|
1207
|
+
'label',
|
|
1208
|
+
{ className: 'owc-check' },
|
|
1209
|
+
h('input', {
|
|
1210
|
+
type: 'checkbox',
|
|
1211
|
+
checked: breakOn,
|
|
1212
|
+
disabled: busy,
|
|
1213
|
+
onChange: toggleBreak,
|
|
1214
|
+
}),
|
|
1215
|
+
t('breakEnabled'),
|
|
1216
|
+
),
|
|
1217
|
+
breakOn &&
|
|
1218
|
+
h(
|
|
1219
|
+
'div',
|
|
1220
|
+
{ className: 'owc-row2' },
|
|
1221
|
+
label(t('breakStart'), timeInput('breakStart')),
|
|
1222
|
+
label(t('breakEnd'), timeInput('breakEnd')),
|
|
1223
|
+
),
|
|
1224
|
+
label(
|
|
1225
|
+
t('weekdays'),
|
|
1226
|
+
h(
|
|
1227
|
+
'div',
|
|
1228
|
+
{ className: 'owc-days' },
|
|
1229
|
+
[0, 1, 2, 3, 4, 5, 6].map((day) =>
|
|
1230
|
+
h(
|
|
1231
|
+
'button',
|
|
1232
|
+
{
|
|
1233
|
+
key: day,
|
|
1234
|
+
type: 'button',
|
|
1235
|
+
disabled: busy,
|
|
1236
|
+
'aria-pressed': workingDraft.weekdays.includes(day),
|
|
1237
|
+
'aria-label': t('weekdays') + ' ' + t('days')[day],
|
|
1238
|
+
onClick: () =>
|
|
1239
|
+
update(
|
|
1240
|
+
'weekdays',
|
|
1241
|
+
workingDraft.weekdays.includes(day)
|
|
1242
|
+
? workingDraft.weekdays.filter((d) => d !== day)
|
|
1243
|
+
: [...workingDraft.weekdays, day].sort((a, b) => a - b),
|
|
1244
|
+
),
|
|
1245
|
+
},
|
|
1246
|
+
t('days')[day],
|
|
1247
|
+
),
|
|
1248
|
+
),
|
|
1249
|
+
),
|
|
1250
|
+
),
|
|
1251
|
+
label(
|
|
1252
|
+
t('timezone'),
|
|
1253
|
+
h(
|
|
1254
|
+
'select',
|
|
1255
|
+
{
|
|
1256
|
+
name: 'timezone',
|
|
1257
|
+
value: workingDraft.timezone ?? '',
|
|
1258
|
+
disabled: busy,
|
|
1259
|
+
onChange: (e) => update('timezone', e.target.value),
|
|
1260
|
+
},
|
|
1261
|
+
zones.map((zone) => h('option', { key: zone, value: zone }, zone)),
|
|
1262
|
+
),
|
|
1263
|
+
),
|
|
1264
|
+
),
|
|
1265
|
+
h(
|
|
1266
|
+
'fieldset',
|
|
1267
|
+
{ className: 'owc-setting-group' },
|
|
1268
|
+
h('legend', null, t('appearanceGroup')),
|
|
1269
|
+
label(
|
|
1270
|
+
t('opacity'),
|
|
1271
|
+
h(
|
|
1272
|
+
React.Fragment,
|
|
1273
|
+
null,
|
|
1274
|
+
h('input', {
|
|
1275
|
+
className: 'owc-range',
|
|
1276
|
+
type: 'range',
|
|
1277
|
+
min: '30',
|
|
1278
|
+
max: '100',
|
|
1279
|
+
step: '1',
|
|
1280
|
+
value: String(Math.round(view.opacity * 100)),
|
|
1281
|
+
disabled: busy,
|
|
1282
|
+
'aria-label': t('opacity'),
|
|
1283
|
+
onChange: (e) => setPreference({ opacity: e.target.valueAsNumber / 100 }),
|
|
1284
|
+
}),
|
|
1285
|
+
h(
|
|
1286
|
+
'span',
|
|
1287
|
+
{ className: 'owc-hint', style: { margin: 0 } },
|
|
1288
|
+
Math.round(view.opacity * 100) + '% · ' + t('opacityHint'),
|
|
1289
|
+
),
|
|
1290
|
+
),
|
|
1291
|
+
),
|
|
1292
|
+
),
|
|
1293
|
+
preview &&
|
|
1294
|
+
!preview.error &&
|
|
1295
|
+
h(
|
|
1296
|
+
'div',
|
|
1297
|
+
{ className: 'owc-preview' },
|
|
1298
|
+
t('preview') +
|
|
1299
|
+
' · ' +
|
|
1300
|
+
t('daily') +
|
|
1301
|
+
' ' +
|
|
1302
|
+
money(preview.daily) +
|
|
1303
|
+
' · ' +
|
|
1304
|
+
t('hourly') +
|
|
1305
|
+
' ' +
|
|
1306
|
+
money(preview.hourly),
|
|
1307
|
+
),
|
|
1308
|
+
preview?.error &&
|
|
1309
|
+
h(
|
|
1310
|
+
'p',
|
|
1311
|
+
{ className: 'owc-error', role: 'alert', style: { padding: 0 } },
|
|
1312
|
+
preview.error,
|
|
1313
|
+
),
|
|
1314
|
+
h(
|
|
1315
|
+
'details',
|
|
1316
|
+
{ className: 'owc-advanced' },
|
|
1317
|
+
h('summary', null, t('advanced')),
|
|
1318
|
+
h('p', { className: 'owc-hint' }, t('sessionHint')),
|
|
1319
|
+
label(t('session'), sessionSelect()),
|
|
1320
|
+
),
|
|
1321
|
+
h('p', { className: 'owc-hint' }, t('privacy')),
|
|
1322
|
+
h(
|
|
1323
|
+
'div',
|
|
1324
|
+
{ className: 'owc-save' },
|
|
1325
|
+
data?.configured &&
|
|
1326
|
+
h(
|
|
1327
|
+
'button',
|
|
1328
|
+
{ type: 'button', className: 'owc-text', onClick: closeEditor, disabled: busy },
|
|
1329
|
+
t('cancel'),
|
|
1330
|
+
),
|
|
1331
|
+
h(
|
|
1332
|
+
'button',
|
|
1333
|
+
{ type: 'submit', className: 'owc-primary', disabled: busy || !!preview?.error },
|
|
1334
|
+
t(busy ? 'busy' : 'save'),
|
|
1335
|
+
),
|
|
1336
|
+
),
|
|
1337
|
+
);
|
|
658
1338
|
};
|
|
659
1339
|
|
|
660
|
-
const notices = () =>
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1340
|
+
const notices = () =>
|
|
1341
|
+
h(
|
|
1342
|
+
React.Fragment,
|
|
1343
|
+
null,
|
|
1344
|
+
error && h('p', { className: 'owc-error', role: 'alert' }, error),
|
|
1345
|
+
state?.error && h('p', { className: 'owc-error', role: 'alert' }, state.error),
|
|
1346
|
+
notice && h('p', { className: 'owc-note', role: 'status' }, notice),
|
|
1347
|
+
storageError && h('p', { className: 'owc-note' }, t('storage')),
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
return h(
|
|
1351
|
+
'section',
|
|
1352
|
+
{
|
|
1353
|
+
ref: box,
|
|
1354
|
+
className:
|
|
1355
|
+
'owc-v2' + (collapsed ? ' owc-compact' : '') + (editing ? ' owc-editing' : ''),
|
|
1356
|
+
'aria-label': t('title'),
|
|
1357
|
+
'data-version': '1.0.1',
|
|
1358
|
+
'data-status': safeState?.status || 'setup',
|
|
1359
|
+
style: cardStyle,
|
|
1360
|
+
onPointerDown: dragDown,
|
|
1361
|
+
onPointerMove: dragMove,
|
|
1362
|
+
onPointerUp: dragEnd,
|
|
1363
|
+
onPointerCancel: dragEnd,
|
|
1364
|
+
onLostPointerCapture: dragEnd,
|
|
1365
|
+
},
|
|
1366
|
+
h('style', null, STYLE),
|
|
1367
|
+
h('div', { className: 'owc-glass', 'aria-hidden': 'true' }),
|
|
1368
|
+
h(
|
|
1369
|
+
'div',
|
|
1370
|
+
{ className: 'owc-content' },
|
|
1371
|
+
collapsed
|
|
1372
|
+
? strip()
|
|
1373
|
+
: h(React.Fragment, null, panelHead(), editing ? editor() : panelBody(), notices()),
|
|
1374
|
+
),
|
|
1375
|
+
);
|
|
681
1376
|
}
|
|
682
1377
|
|
|
683
|
-
ctx.slots.inject('shell.overlay', () =>
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
1378
|
+
ctx.slots.inject('shell.overlay', () =>
|
|
1379
|
+
ctx.slots.register(
|
|
1380
|
+
{
|
|
1381
|
+
name: 'shell.overlay',
|
|
1382
|
+
id: 'offwork-clock',
|
|
1383
|
+
order: 900,
|
|
1384
|
+
label: () => ctx.locale.bind('offwork-clock')('title'),
|
|
1385
|
+
},
|
|
1386
|
+
Clock,
|
|
1387
|
+
),
|
|
1388
|
+
);
|
|
687
1389
|
},
|
|
688
1390
|
};
|
|
689
1391
|
},
|