dsh-offwork 0.0.0-stage → 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.
@@ -0,0 +1,1178 @@
1
+ // Authored Client entry. Run node tools/build.mjs to inline the tested pure modules.
2
+ window.__ModuleLoader__.load({
3
+ id: 'dsh-offwork',
4
+ factory(require) {
5
+ const React = require('react');
6
+ const h = React.createElement;
7
+ /* PURE_MODULES */
8
+ const messages = {
9
+ zh: {
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: '只影响背景,不影响文字。',
68
+ },
69
+ en: {
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.',
129
+ },
130
+ };
131
+ const STYLE = `
132
+ .owc-v2{--owc-accent:color-mix(in srgb,var(--dsw-alias-brand-primary) 65%,#2563eb);pointer-events:auto;position:fixed;top:96px;right:24px;z-index:40;max-width:calc(100vw - 24px);max-height:calc(100dvh - 24px);border-radius:20px;color:var(--dsw-alias-label-primary);font:13px/1.45 -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif;cursor:grab;touch-action:manipulation;user-select:none;-webkit-user-select:none}
133
+ .owc-v2:active{cursor:grabbing}
134
+ .owc-v2.owc-compact{touch-action:none;border-radius:18px}
135
+ .owc-v2:not(.owc-compact){width:296px}
136
+ .owc-v2 :is(button,input,select,label,a){cursor:pointer;user-select:auto;-webkit-user-select:auto}
137
+ .owc-v2 :is(input,select){user-select:text;-webkit-user-select:text}
138
+ .owc-glass{position:absolute;inset:0;border-radius:inherit;pointer-events:none;opacity:var(--owc-alpha,.82);background:color-mix(in srgb,var(--dsw-alias-bg-overlay) 97%,var(--owc-accent));-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);border:1px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 18%,transparent);box-shadow:inset 0 1px 0 color-mix(in srgb,var(--dsw-alias-bg-base) 70%,transparent),0 8px 20px color-mix(in srgb,var(--dsw-alias-label-primary) 7%,transparent),0 14px 34px color-mix(in srgb,var(--owc-accent) 5%,transparent);transition:opacity .18s ease}
139
+ .owc-content{position:relative;border-radius:inherit;max-height:calc(100dvh - 24px);overflow:auto;overscroll-behavior:contain;scrollbar-width:thin}
140
+ .owc-v2 button{appearance:none;-webkit-appearance:none;border:0;margin:0;background:transparent;color:inherit;font:inherit;display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:6px;min-height:28px;border-radius:10px;transition:background .15s ease,color .15s ease}
141
+ .owc-v2 button:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 7%,transparent)}
142
+ .owc-v2 button:focus-visible,.owc-v2 :is(input,select):focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
143
+ .owc-v2 button:disabled{opacity:.45;cursor:not-allowed;background:transparent}
144
+ .owc-icon{color:var(--dsw-alias-label-secondary);min-width:28px}
145
+ .owc-text{font-size:11px;color:var(--dsw-alias-label-secondary);padding:4px 6px;min-height:24px;white-space:nowrap}
146
+ .owc-text:hover{color:var(--dsw-alias-label-primary)}
147
+ .owc-pill{display:flex;flex-direction:column;gap:8px;padding:12px 10px 9px 14px;min-width:168px}
148
+ .owc-pill-top>svg{color:var(--owc-accent);flex:none}
149
+ .owc-pill-top>.owc-icon{color:var(--dsw-alias-label-secondary)}
150
+ .owc-pill-top{display:flex;align-items:center;gap:7px;min-width:0}
151
+ .owc-grip{display:flex;align-items:baseline;gap:7px;touch-action:none;min-width:0}
152
+ .owc-pill-bottom{display:flex;align-items:center;justify-content:space-between;gap:8px;padding-top:6px;border-top:1px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 12%,transparent)}
153
+ .owc-pill-actions{display:flex;align-items:center;gap:2px}
154
+ .owc-pill-status{font-size:10px;color:var(--dsw-alias-label-secondary);white-space:nowrap;letter-spacing:.02em}
155
+ .owc-pill-time{font-size:24px;font-weight:500;letter-spacing:-.5px;font-variant-numeric:tabular-nums;white-space:nowrap;line-height:1.25}
156
+ .owc-pill-money{font-size:13px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;min-width:46px}
157
+ .owc-dot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-idle-primary);flex:none}
158
+ .owc-v2[data-status=working] .owc-dot{background:var(--dsw-alias-state-success-primary)}
159
+ .owc-v2[data-status=lunch] .owc-dot{background:var(--dsw-alias-state-warn-primary)}
160
+ .owc-v2[data-status=done] .owc-dot{background:var(--dsw-alias-brand-primary)}
161
+ .owc-v2[data-status=before] .owc-dot,.owc-v2[data-status=rest] .owc-dot{opacity:.6}
162
+ .owc-head{display:flex;align-items:center;gap:4px;padding:12px 12px 10px 18px;margin-bottom:12px;border-bottom:1px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 12%,transparent)}
163
+ .owc-title{flex:1;min-width:0;font-size:11px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
164
+ .owc-main{padding:4px 17px 12px}
165
+ .owc-status{display:flex;align-items:center;gap:7px;font-size:11px;color:var(--dsw-alias-label-secondary);margin-bottom:1px}
166
+ .owc-time{font-size:34px;font-weight:350;line-height:1.28;letter-spacing:-1.2px;font-variant-numeric:tabular-nums}
167
+ .owc-progress{height:3px;border-radius:3px;overflow:hidden;background:color-mix(in srgb,var(--dsw-alias-label-primary) 9%,transparent);margin:10px 0 16px}
168
+ .owc-progress>div{height:100%;border-radius:3px;background:var(--owc-accent);transition:width .6s linear}
169
+ .owc-earn-row{display:flex;align-items:center;justify-content:space-between;gap:8px}
170
+ .owc-money{font-size:26px;font-weight:400;letter-spacing:-.6px;font-variant-numeric:tabular-nums;margin-top:1px}
171
+ .owc-muted{font-size:10.5px;color:var(--dsw-alias-label-secondary)}
172
+ .owc-stats{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:14px;padding-top:12px;border-top:1px solid color-mix(in srgb,var(--dsw-alias-label-primary) 8%,transparent)}
173
+ .owc-stats>div{padding:10px;border-radius:12px;background:color-mix(in srgb,var(--dsw-alias-label-secondary) 5%,transparent);border:1px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 8%,transparent)}
174
+ .owc-stats span{display:block;font-size:10px;color:var(--dsw-alias-label-secondary)}
175
+ .owc-stats strong{display:block;margin-top:1px;font-size:13px;font-weight:500;font-variant-numeric:tabular-nums}
176
+ .owc-meta{margin-top:12px}
177
+ .owc-meta span{display:block;font-size:10.5px;line-height:1.7;color:var(--dsw-alias-label-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
178
+ .owc-row-actions{display:flex;gap:4px;margin-top:8px;margin-left:-6px}
179
+ .owc-form{padding:2px 17px 15px}
180
+ .owc-v2.owc-editing{width:340px}
181
+ .owc-setting-group{min-width:0;margin:0 0 16px;padding:12px 12px 2px;border:1px solid color-mix(in srgb,var(--dsw-alias-label-secondary) 15%,transparent);border-radius:14px;background:color-mix(in srgb,var(--dsw-alias-bg-base) 24%,transparent)}
182
+ .owc-setting-group legend{padding:0 6px;font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}
183
+ .owc-form :is(input[type=time],input[type=number],select){min-height:36px;border-radius:10px}
184
+ .owc-advanced{margin:0 0 12px;padding:10px 12px;border-radius:12px;background:color-mix(in srgb,var(--dsw-alias-label-secondary) 5%,transparent)}
185
+ .owc-advanced summary{cursor:pointer;font-size:12px;font-weight:500}
186
+ .owc-advanced[open] summary{margin-bottom:10px}
187
+ .owc-grip:focus-visible,.owc-advanced summary:focus-visible{outline:2px solid var(--owc-accent);outline-offset:3px}
188
+ .owc-field{display:block;margin-bottom:10px}
189
+ .owc-field>span{display:block;font-size:10.5px;color:var(--dsw-alias-label-secondary);margin-bottom:4px}
190
+ .owc-row2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
191
+ .owc-v2 :is(input[type=text],input[type=time],input[type=number],select){width:100%;box-sizing:border-box;font:inherit;font-size:12px;padding:6px 8px;border-radius:9px;border:1px solid color-mix(in srgb,var(--dsw-alias-label-primary) 13%,transparent);background:color-mix(in srgb,var(--dsw-alias-bg-base) 55%,transparent);color:inherit;min-height:30px}
192
+ .owc-v2 select option{background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-label-primary)}
193
+ .owc-seg{display:flex;gap:3px;padding:3px;border-radius:11px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 6%,transparent)}
194
+ .owc-seg button{flex:1;font-size:11.5px;min-height:26px;border-radius:8px}
195
+ .owc-seg button[aria-pressed=true]{background:color-mix(in srgb,var(--dsw-alias-bg-overlay) 92%,var(--dsw-alias-bg-base));box-shadow:0 1px 3px color-mix(in srgb,var(--dsw-alias-label-primary) 10%,transparent)}
196
+ .owc-days{display:flex;gap:3px}
197
+ .owc-days button{flex:1;min-height:26px;font-size:11px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,transparent)}
198
+ .owc-days button[aria-pressed=true]{background:color-mix(in srgb,var(--owc-accent) 12%,transparent);color:var(--owc-accent);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--owc-accent) 22%,transparent)}
199
+ .owc-check{display:flex;flex-direction:row;align-items:center;gap:7px;font-size:11.5px;margin:0 0 10px}
200
+ .owc-check input{appearance:auto;-webkit-appearance:auto;width:auto;accent-color:var(--dsw-alias-brand-primary)}
201
+ .owc-private{display:flex;align-items:center;justify-content:space-between;gap:6px;min-height:30px;padding:4px 6px 4px 10px;border-radius:9px;border:1px dashed color-mix(in srgb,var(--dsw-alias-label-primary) 16%,transparent);font-size:12px;color:var(--dsw-alias-label-secondary)}
202
+ .owc-preview{font-size:11px;color:var(--dsw-alias-label-secondary);padding:8px 10px;border-radius:10px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,transparent);margin:0 0 10px;font-variant-numeric:tabular-nums}
203
+ .owc-hint{font-size:10.5px;line-height:1.6;color:var(--dsw-alias-label-secondary);margin:0 0 10px}
204
+ .owc-range{width:100%;accent-color:var(--dsw-alias-brand-primary);margin:1px 0 2px}
205
+ .owc-save{display:flex;align-items:center;justify-content:flex-end;gap:6px;margin-top:12px}
206
+ .owc-v2 button.owc-primary{background:var(--owc-accent);color:#fff;padding:0 16px;min-height:36px;font-size:12px;font-weight:600;border-radius:12px}
207
+ .owc-v2 button.owc-primary:hover{background:color-mix(in srgb,var(--owc-accent) 88%,#000)}
208
+ .owc-v2 button.owc-primary:disabled{opacity:.5;color:#fff;background:var(--owc-accent)}
209
+ .owc-error,.owc-note{padding:0 17px 9px;margin:0;font-size:10.5px;line-height:1.6}
210
+ .owc-error{color:var(--dsw-alias-state-error-primary)}
211
+ .owc-note{color:var(--dsw-alias-label-secondary)}
212
+ @supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.owc-glass{opacity:1}}
213
+ @media(prefers-reduced-transparency:reduce),(prefers-contrast:more){.owc-glass{opacity:1;-webkit-backdrop-filter:none;backdrop-filter:none}}
214
+ @media(prefers-reduced-motion:reduce){.owc-v2 button,.owc-glass,.owc-progress>div{transition:none}}
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}}
216
+ `;
217
+ const ICONS = {
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
+ ],
243
+ };
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
+ );
261
+
262
+ return {
263
+ inject: ['slots', 'locale', 'remote', 'remote.commands'],
264
+ apply(ctx) {
265
+ for (const [lang, dict] of Object.entries(messages))
266
+ ctx.effect(() => ctx.locale.register('offwork-clock', lang, dict));
267
+ const viewKey = 'dsh.offwork-clock.view.v4';
268
+ const legacyKey = 'dsh.offwork-clock.view.v3';
269
+
270
+ function Clock(props) {
271
+ const locale = React.useSyncExternalStore(
272
+ (fn) => ctx.locale.subscribe(fn),
273
+ () => ctx.locale.getSnapshot(),
274
+ );
275
+ const t = ctx.locale.bind('offwork-clock');
276
+ const sessions = props.useSessions((s) => s);
277
+ const sessionIds = Array.isArray(sessions?.ids) ? sessions.ids : [];
278
+ const [sessionChoice, setSessionChoice] = React.useState('');
279
+ const sessionId = sessionIds.includes(sessionChoice) ? sessionChoice : sessionIds[0];
280
+ const [data, setData] = React.useState(null);
281
+ const [draft, setDraft] = React.useState(null);
282
+ const [editing, setEditing] = React.useState(false);
283
+ const [busy, setBusy] = React.useState(false);
284
+ const [error, setError] = React.useState('');
285
+ const [notice, setNotice] = React.useState('');
286
+ const [storageError, setStorageError] = React.useState(false);
287
+ const [now, setNow] = React.useState(Date.now());
288
+ const [view, setView] = React.useState(normalizeView(null));
289
+ const [ready, setReady] = React.useState(false);
290
+ const box = React.useRef(null);
291
+ const dragState = React.useRef(null);
292
+ const lastPosition = React.useRef(null);
293
+ const generation = React.useRef(0);
294
+ const requestBusy = React.useRef(false);
295
+ const focusReturn = React.useRef(null);
296
+
297
+ const settings = data?.settings;
298
+ const workingDraft = draft || settings;
299
+ const state = settings ? safeCalculate(settings) : null;
300
+ const preview = workingDraft ? safeCalculate(workingDraft) : null;
301
+ const safeState = state && !state.error ? state : null;
302
+ const collapsed = !!safeState && !editing && view.mini && !error && !state?.error;
303
+ const seconds = safeState
304
+ ? safeState.status === 'before'
305
+ ? safeState.startRemaining
306
+ : safeState.remaining
307
+ : NaN;
308
+ const duration = formatDuration(seconds);
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));
320
+ const moneyText = safeState ? money(safeState.earned) : '—';
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
+ );
325
+
326
+ function safeCalculate(value) {
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 }));
338
+ }
339
+
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
+ );
352
+
353
+ React.useEffect(() => {
354
+ try {
355
+ const saved = localStorage.getItem(viewKey);
356
+ const legacy = !saved ? JSON.parse(localStorage.getItem(legacyKey) || 'null') : null;
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
+ }
367
+ setReady(true);
368
+ const sync = (e) => {
369
+ if (e.key !== viewKey) return;
370
+ try {
371
+ setView(normalizeView(JSON.parse(e.newValue || 'null')));
372
+ } catch {}
373
+ };
374
+ window.addEventListener('storage', sync);
375
+ return () => window.removeEventListener('storage', sync);
376
+ }, []);
377
+
378
+ React.useEffect(() => {
379
+ if (!ready) return;
380
+ try {
381
+ localStorage.setItem(viewKey, JSON.stringify(normalizeView(view)));
382
+ } catch {
383
+ setStorageError(true);
384
+ }
385
+ }, [ready, view]);
386
+
387
+ React.useEffect(() => {
388
+ if (sessionId) reload();
389
+ else setError(t('noSession'));
390
+ return () => {
391
+ generation.current++;
392
+ };
393
+ }, [sessionId]);
394
+
395
+ React.useEffect(() => {
396
+ let timer;
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
+ };
406
+ if (!document.hidden) tick();
407
+ document.addEventListener('visibilitychange', visibility);
408
+ return () => {
409
+ clearTimeout(timer);
410
+ document.removeEventListener('visibilitychange', visibility);
411
+ };
412
+ }, []);
413
+
414
+ React.useEffect(() => {
415
+ const resize = () =>
416
+ setView((current) =>
417
+ current.position ? { ...current, position: clamp(current.position) } : current,
418
+ );
419
+ const ro = typeof ResizeObserver === 'function' ? new ResizeObserver(resize) : null;
420
+ if (box.current) ro?.observe(box.current);
421
+ window.addEventListener('resize', resize);
422
+ return () => {
423
+ ro?.disconnect();
424
+ window.removeEventListener('resize', resize);
425
+ };
426
+ }, [clamp]);
427
+
428
+ React.useEffect(() => {
429
+ if (!editing) {
430
+ focusReturn.current?.focus?.();
431
+ return;
432
+ }
433
+ box.current?.querySelector('form input,form select')?.focus?.();
434
+ }, [editing]);
435
+
436
+ React.useEffect(
437
+ () => () => {
438
+ generation.current++;
439
+ dragState.current = null;
440
+ },
441
+ [],
442
+ );
443
+
444
+ async function request(action, patch) {
445
+ if (!sessionId) throw new Error(t('noSession'));
446
+ const rpc = await ctx.remote.commands.execute(
447
+ sessionId,
448
+ '/offwork-clock ' + action + (patch ? ' ' + JSON.stringify(patch) : ''),
449
+ [],
450
+ );
451
+ return decodeCommand(rpc);
452
+ }
453
+
454
+ async function reload(explicit = false) {
455
+ if (requestBusy.current) return;
456
+ const token = ++generation.current;
457
+ setBusy(true);
458
+ setError('');
459
+ try {
460
+ const next = await request('get');
461
+ if (token !== generation.current) return;
462
+ setData(next);
463
+ if (!next.configured) {
464
+ setDraft({ ...next.settings });
465
+ setEditing(true);
466
+ }
467
+ if (explicit) setNotice(t('synced'));
468
+ } catch (e) {
469
+ if (token === generation.current) setError(errorText(e));
470
+ } finally {
471
+ if (token === generation.current) setBusy(false);
472
+ }
473
+ }
474
+
475
+ async function save(event) {
476
+ event.preventDefault();
477
+ if (requestBusy.current) return;
478
+ requestBusy.current = true;
479
+ setBusy(true);
480
+ setError('');
481
+ const token = ++generation.current;
482
+ try {
483
+ const next = await request('set', changedSettings(settings, draft));
484
+ if (token === generation.current) {
485
+ setData(next);
486
+ setEditing(false);
487
+ setDraft(null);
488
+ setNotice(t('saved'));
489
+ }
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
+ }
496
+ }
497
+
498
+ function commitPosition(position) {
499
+ const next = clamp(position);
500
+ setPreference({ position: next });
501
+ lastPosition.current = next;
502
+ }
503
+ function dragDown(e) {
504
+ if (e.button !== 0) return;
505
+ if (
506
+ e.target.closest?.('button,input,select,label,a,textarea,summary,[contenteditable]')
507
+ )
508
+ return;
509
+ const rect = box.current.getBoundingClientRect();
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 {}
520
+ }
521
+ function dragMove(e) {
522
+ const d = dragState.current;
523
+ if (!d) return;
524
+ if (!d.moved && Math.abs(e.clientX - d.x0) + Math.abs(e.clientY - d.y0) < 4) return;
525
+ d.moved = true;
526
+ const next = clamp({ x: e.clientX - d.dx, y: e.clientY - d.dy });
527
+ lastPosition.current = next;
528
+ if (box.current) {
529
+ box.current.style.left = next.x + 'px';
530
+ box.current.style.top = next.y + 'px';
531
+ box.current.style.right = 'auto';
532
+ }
533
+ }
534
+ function dragEnd(e) {
535
+ const d = dragState.current;
536
+ if (!d) return;
537
+ dragState.current = null;
538
+ if (d.moved && lastPosition.current) commitPosition(lastPosition.current);
539
+ try {
540
+ if (e.currentTarget.hasPointerCapture?.(e.pointerId))
541
+ e.currentTarget.releasePointerCapture(e.pointerId);
542
+ } catch {}
543
+ }
544
+ function gripKeys(e) {
545
+ if (e.key === 'Home') {
546
+ e.preventDefault();
547
+ setPreference({ position: null });
548
+ return;
549
+ }
550
+ if (!['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) return;
551
+ e.preventDefault();
552
+ const rect = box.current.getBoundingClientRect();
553
+ commitPosition({
554
+ x: rect.left + (e.key === 'ArrowRight' ? 10 : e.key === 'ArrowLeft' ? -10 : 0),
555
+ y: rect.top + (e.key === 'ArrowDown' ? 10 : e.key === 'ArrowUp' ? -10 : 0),
556
+ });
557
+ }
558
+
559
+ function openEditor(e) {
560
+ focusReturn.current = e.currentTarget;
561
+ setDraft({ ...settings });
562
+ setEditing(true);
563
+ setError('');
564
+ setNotice('');
565
+ }
566
+ function closeEditor() {
567
+ if (!data?.configured) return;
568
+ setEditing(false);
569
+ setDraft(null);
570
+ setError('');
571
+ }
572
+ function update(key, value) {
573
+ setDraft((current) => ({ ...current, [key]: value }));
574
+ }
575
+ function toggleBreak(e) {
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 };
584
+ const start = point(current.start);
585
+ const span = (point(current.end) - start + 1440) % 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
+ };
591
+ });
592
+ }
593
+
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
+ );
646
+
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
+ );
703
+
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
+ );
760
+
761
+ const panelBody = () => {
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
+ );
789
+ const status = safeState.status === 'done' ? '' : t(safeState.status);
790
+ return h(
791
+ 'div',
792
+ { className: 'owc-main' },
793
+ status &&
794
+ h('div', { className: 'owc-status' }, h('span', { className: 'owc-dot' }), status),
795
+ h('div', { className: 'owc-time', 'aria-label': status + ' ' + duration }, duration),
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
+ );
881
+ };
882
+
883
+ const editor = () => {
884
+ if (!workingDraft) return null;
885
+ const payDaily = workingDraft.payMode === 'daily';
886
+ const salaryKey = payDaily ? 'dailySalary' : 'monthlySalary';
887
+ const salaryControl = view.masked
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
+ )
903
+ : numberInput(salaryKey);
904
+ const breakOn = workingDraft.breakStart !== workingDraft.breakEnd;
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
+ ];
920
+ let allZones = [];
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
+ );
1124
+ };
1125
+
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
+ );
1135
+
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
+ );
1162
+ }
1163
+
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
+ );
1175
+ },
1176
+ };
1177
+ },
1178
+ });