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