dsh-life-workbench 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,1232 @@
1
+ /**
2
+ * dsh-life-workbench —— 浏览器半边。
3
+ *
4
+ * 加载协议:DSH 的 web 插件以 window.__ModuleLoader__.load({ id, factory }) 形式
5
+ * 装载,factory 收到的 require 能解析宿主内置模块(react / react-dom 等)。
6
+ *
7
+ * 与动态 Cordis 插件的两处差异(已在本文件内替换):
8
+ * 1. host.call(method, args) → hostCall():走同源 HTTP,由 Host 半边的 webServer 路由应答
9
+ * 2. styles.insert(css) → insertStyles(css):直接创建 <style>,由 ctx.effect 管理清理
10
+ */
11
+
12
+ window.__ModuleLoader__.load({
13
+ id: 'dsh-life-workbench',
14
+ factory: (require) => {
15
+ var module = { exports: {} }
16
+ var exports = module.exports
17
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
18
+
19
+ const React = require('react')
20
+
21
+ const SLOT_ID = 'life-workbench'
22
+ const MAX_UPLOAD_BYTES = 8 * 1024 * 1024
23
+
24
+ /** 同源 HTTP RPC:与 Host 半边 webServer 注册的端点一一对应。 */
25
+ async function hostCall(method, args) {
26
+ const res = await fetch('/life-workbench/' + method, {
27
+ method: 'POST',
28
+ headers: { 'content-type': 'application/json' },
29
+ body: JSON.stringify(args === undefined ? {} : args),
30
+ })
31
+ if (!res.ok) throw new Error('HTTP ' + res.status)
32
+ return await res.json()
33
+ }
34
+
35
+ /** 包内样式:随插件卸载一起移除。 */
36
+ function insertStyles(css) {
37
+ const el = document.createElement('style')
38
+ el.setAttribute('data-dsh-life-workbench', '')
39
+ el.textContent = css
40
+ document.head.appendChild(el)
41
+ return () => { el.remove() }
42
+ }
43
+
44
+ const CSS = `
45
+ .lwb-root{--p-radius:18px;--p-pad:18px 20px;--p-gap:14px;--p-bg:color-mix(in srgb,var(--dsw-alias-bg-layer-1) 62%,transparent);--p-border:color-mix(in srgb,var(--dsw-alias-border-l1) 55%,transparent);--p-divider:color-mix(in srgb,var(--dsw-alias-border-l1) 55%,transparent);--p-shadow:0 14px 44px -22px rgba(0,0,0,.30),inset 0 1px 0 0 color-mix(in srgb,#fff 45%,transparent);--p-blur:blur(22px) saturate(150%);--mono-font:ui-monospace,SFMono-Regular,'SF Mono',Menlo,Consolas,monospace;display:flex;flex-direction:column;height:100%;min-height:0;background:var(--dsw-alias-bg-base);background-image:radial-gradient(120% 80% at 10% -5%,color-mix(in srgb,var(--dsw-alias-brand-primary) 16%,transparent),transparent 55%),radial-gradient(95% 70% at 95% 4%,color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent),transparent 52%),radial-gradient(80% 60% at 60% 100%,color-mix(in srgb,var(--dsw-alias-state-warn-primary) 8%,transparent),transparent 55%);color:var(--dsw-alias-label-primary);font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',system-ui,sans-serif;font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased;--ease:cubic-bezier(0.22,1,0.36,1)}
46
+ .lwb-head{display:flex;align-items:center;gap:12px;padding:13px 20px;border-bottom:1px solid var(--p-divider);flex:none;background:transparent}
47
+ .lwb-title{font-size:14px;font-weight:600;letter-spacing:-.01em;display:flex;align-items:center;gap:9px}
48
+ .lwb-dot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-brand-primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent)}
49
+ .lwb-spacer{flex:1;min-width:0}
50
+ .lwb-tabs{display:flex;gap:3px;padding:7px 20px 0;border-bottom:1px solid var(--p-divider);flex:none;background:transparent}
51
+ .lwb-tab{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:13px;padding:9px 15px;cursor:pointer;position:relative;font-family:inherit;border-radius:10px 10px 0 0;transition:color .18s var(--ease),background .18s var(--ease)}
52
+ .lwb-tab:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}
53
+ .lwb-tab-on{color:var(--dsw-alias-label-primary);font-weight:600;background:var(--dsw-alias-bg-base)}
54
+ .lwb-tab-on:after{content:'';position:absolute;left:12px;right:12px;bottom:-1px;height:2.5px;background:var(--dsw-alias-brand-primary);border-radius:3px}
55
+ .lwb-body{flex:1;min-height:0;overflow:auto;padding:18px 20px 36px}
56
+ .lwb-hero{position:relative;overflow:hidden;padding:var(--p-pad);border:1px solid var(--p-border);border-radius:var(--p-radius);background:color-mix(in srgb,var(--dsw-alias-bg-layer-1) 56%,transparent);box-shadow:var(--p-shadow);backdrop-filter:var(--p-blur);-webkit-backdrop-filter:var(--p-blur);margin-bottom:var(--p-gap)}
57
+ .lwb-hero-in{position:relative;z-index:1}
58
+ .lwb-eyebrow{font-family:var(--mono-font);font-size:10.5px;font-weight:500;letter-spacing:.16em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}
59
+ .lwb-hero-hi{font-size:25px;font-weight:650;letter-spacing:-.03em;line-height:1.15;margin-top:6px}
60
+ .lwb-hero-stats{display:flex;margin-top:18px;flex-wrap:wrap;row-gap:14px}
61
+ .lwb-stat{display:flex;flex-direction:column;gap:2px;padding-right:26px;margin-right:26px;border-right:1px solid var(--p-divider)}
62
+ .lwb-stat:last-child{border-right:0;margin-right:0;padding-right:0}
63
+ .lwb-stat-num{font-family:var(--mono-font);font-size:24px;font-weight:500;letter-spacing:-.05em;font-variant-numeric:tabular-nums;line-height:1.15}
64
+ .lwb-stat-label{font-family:var(--mono-font);font-size:10px;color:var(--dsw-alias-label-secondary);letter-spacing:.1em;text-transform:uppercase}
65
+ .lwb-sec{margin-top:22px}
66
+ .lwb-sec-head{display:flex;align-items:center;gap:12px;margin-bottom:11px}
67
+ .lwb-sec-name{font-family:var(--mono-font);font-size:10px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--dsw-alias-label-secondary);display:flex;align-items:center;gap:8px}
68
+ .lwb-sec-name:before{content:'';width:5px;height:5px;border-radius:50%;background:var(--dsw-alias-brand-primary);opacity:.85;flex:none}
69
+ .lwb-sec-line{flex:1;height:1px;background:var(--p-divider)}
70
+ .lwb-sec-count{font-family:var(--mono-font);font-size:10px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;letter-spacing:.04em}
71
+ .lwb-wide{background:var(--p-bg);border:1px solid var(--p-border);border-radius:var(--p-radius);box-shadow:var(--p-shadow);backdrop-filter:var(--p-blur);-webkit-backdrop-filter:var(--p-blur);overflow:hidden}
72
+ .lwb-wide+.lwb-wide{margin-top:var(--p-gap)}
73
+ .lwb-split{display:grid;grid-template-columns:1fr 1fr}
74
+ .lwb-pane{min-width:0;padding:var(--p-pad)}
75
+ .lwb-pane+.lwb-pane{border-left:1px solid var(--p-divider)}
76
+ .lwb-pane-full{padding:var(--p-pad)}
77
+ @media (max-width:880px){.lwb-split{grid-template-columns:1fr}.lwb-pane+.lwb-pane{border-left:0;border-top:1px solid var(--p-divider)}}
78
+ .lwb-pane-title{font-family:var(--mono-font);font-size:10px;font-weight:600;letter-spacing:.15em;text-transform:uppercase;color:var(--dsw-alias-label-secondary);margin-bottom:11px;display:flex;align-items:center;gap:8px}
79
+ .lwb-pane-title:before{content:'';width:5px;height:5px;border-radius:50%;background:var(--dsw-alias-brand-primary);opacity:.85;flex:none}
80
+ .lwb-btn{appearance:none;border:1px solid var(--p-border);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 80%,transparent);color:var(--dsw-alias-label-primary);border-radius:9px;padding:7px 14px;font-size:12.5px;cursor:pointer;font-family:inherit;white-space:nowrap;transition:background .16s var(--ease),border-color .16s var(--ease),transform .12s var(--ease)}
81
+ .lwb-btn:hover{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-overlay)}
82
+ .lwb-btn:active{transform:scale(.97)}
83
+ .lwb-btn-primary{border-color:transparent;background:var(--dsw-alias-brand-primary);color:#fff}
84
+ .lwb-btn-primary:hover{background:var(--dsw-alias-brand-primary);opacity:.9}
85
+ .lwb-btn-danger{border-color:transparent;background:var(--dsw-alias-state-error-primary);color:#fff}
86
+ .lwb-mini{font-size:11.5px;padding:5px 11px}
87
+ .lwb-err{font-family:var(--mono-font);font-size:11px;color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;padding:4px 10px}
88
+ .lwb-icon-btn{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:15px;line-height:1;padding:5px 8px;border-radius:8px;font-family:inherit;transition:background .16s var(--ease),color .16s var(--ease)}
89
+ .lwb-icon-btn:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
90
+ .lwb-list{display:flex;flex-direction:column;max-height:236px;overflow:auto}
91
+ .lwb-item{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:10px;transition:background .14s var(--ease)}
92
+ .lwb-item:hover{background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 70%,transparent)}
93
+ .lwb-item+.lwb-item{border-top:1px solid var(--p-divider)}
94
+ .lwb-item-text{flex:1;min-width:0;font-size:13px;word-break:break-word;cursor:text}
95
+ .lwb-item-done{text-decoration:line-through;color:var(--dsw-alias-label-secondary)}
96
+ .lwb-check{width:15px;height:15px;flex:none;accent-color:var(--dsw-alias-brand-primary);cursor:pointer;margin:0}
97
+ .lwb-time{font-family:var(--mono-font);font-size:11px;color:var(--dsw-alias-label-secondary);width:48px;flex:none;font-variant-numeric:tabular-nums}
98
+ .lwb-tag{font-family:var(--mono-font);font-size:9.5px;color:var(--dsw-alias-label-secondary);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 80%,transparent);border-radius:7px;padding:3px 7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:118px;letter-spacing:.04em}
99
+ .lwb-input{flex:1;min-width:0;background:color-mix(in srgb,var(--dsw-alias-bg-base) 70%,transparent);border:1px solid var(--p-border);border-radius:9px;color:inherit;font:inherit;font-size:12.5px;padding:7px 11px;outline:none;font-family:inherit;transition:border-color .16s var(--ease)}
100
+ .lwb-input:focus{border-color:var(--dsw-alias-brand-primary)}
101
+ .lwb-select{background:color-mix(in srgb,var(--dsw-alias-bg-base) 70%,transparent);border:1px solid var(--p-border);border-radius:9px;color:inherit;font:inherit;font-size:12px;padding:7px 8px;outline:none;font-family:inherit;cursor:pointer;max-width:112px}
102
+ .lwb-textarea{width:100%;min-height:64px;resize:vertical;background:color-mix(in srgb,var(--dsw-alias-bg-base) 70%,transparent);border:1px solid var(--p-border);border-radius:11px;color:inherit;font:inherit;font-size:12.5px;padding:11px 13px;outline:none;font-family:inherit;box-sizing:border-box;line-height:1.55;transition:border-color .16s var(--ease)}
103
+ .lwb-textarea:focus{border-color:var(--dsw-alias-brand-primary)}
104
+ .lwb-row{display:flex;gap:8px;align-items:center;margin-top:11px}
105
+ .lwb-bar{flex:1;height:6px;border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 80%,transparent);overflow:hidden;min-width:36px}
106
+ .lwb-bar-fill{height:100%;border-radius:999px;background:var(--dsw-alias-brand-primary);transition:width .35s var(--ease)}
107
+ .lwb-brow{display:flex;align-items:center;gap:10px;padding:7px 0}
108
+ .lwb-brow+.lwb-brow{border-top:1px solid var(--p-divider)}
109
+ .lwb-brow-name{width:74px;flex:none;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer}
110
+ .lwb-brow-name:hover{color:var(--dsw-alias-brand-primary)}
111
+ .lwb-count{font-family:var(--mono-font);font-size:10.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
112
+ .lwb-chart{display:flex;align-items:flex-end;gap:6px;height:92px;padding-top:4px}
113
+ .lwb-chart-col{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:4px;height:100%;min-width:0;cursor:pointer;border-radius:8px;transition:background .18s var(--ease)}
114
+ .lwb-chart-col:hover{background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 70%,transparent)}
115
+ .lwb-chart-col-on{background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 70%,transparent)}
116
+ .lwb-chart-num{font-family:var(--mono-font);font-size:10px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
117
+ .lwb-chart-bar{width:100%;max-width:22px;border-radius:5px;background:var(--dsw-alias-label-primary);opacity:.22;min-height:2px;transition:height .35s var(--ease),opacity .2s var(--ease),background .2s var(--ease)}
118
+ .lwb-chart-col:hover .lwb-chart-bar,.lwb-chart-col-on .lwb-chart-bar{opacity:1;background:var(--dsw-alias-brand-primary)}
119
+ .lwb-chart-bar-zero{opacity:.09}
120
+ .lwb-chart-label{font-family:var(--mono-font);font-size:9.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;font-variant-numeric:tabular-nums}
121
+ .lwb-review{margin-top:11px;padding-top:11px;border-top:1px solid var(--p-divider);max-height:154px;overflow:auto}
122
+ .lwb-review-title{font-family:var(--mono-font);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--dsw-alias-label-secondary);margin-bottom:7px}
123
+ .lwb-review-item{display:flex;align-items:center;gap:9px;padding:4px 0;font-size:12.5px}
124
+ .lwb-empty{color:var(--dsw-alias-label-secondary);font-size:12px;padding:3px 0}
125
+ .lwb-thumbs{display:grid;grid-template-columns:repeat(auto-fill,minmax(76px,1fr));gap:9px}
126
+ .lwb-thumb{width:100%;aspect-ratio:1;object-fit:cover;border-radius:11px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 80%,transparent);cursor:zoom-in;display:block;border:1px solid var(--p-border)}
127
+ .lwb-ficon{width:100%;aspect-ratio:1;display:flex;align-items:center;justify-content:center;border-radius:11px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 80%,transparent);color:var(--dsw-alias-label-secondary);border:1px solid var(--p-border)}
128
+ .lwb-drop-area{border:1px dashed var(--dsw-alias-border-l2);border-radius:11px;padding:11px;text-align:center;color:var(--dsw-alias-label-secondary);font-size:12px;cursor:pointer;transition:border-color .2s var(--ease),background .2s var(--ease),color .2s var(--ease)}
129
+ .lwb-drop-area-on{border-color:var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 70%,transparent);color:var(--dsw-alias-label-primary)}
130
+ .lwb-lightbox{position:fixed;inset:0;background:rgba(0,0,0,.78);display:flex;align-items:center;justify-content:center;z-index:9999;cursor:zoom-out;padding:32px}
131
+ .lwb-lightbox img{max-width:92vw;max-height:88vh;border-radius:12px}
132
+ .lwb-done-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(198px,1fr));gap:2px 18px}
133
+ .lwb-boards{display:flex;gap:13px;align-items:flex-start;padding:0;min-height:100%}
134
+ .lwb-col{flex:0 0 268px;max-width:268px;display:flex;flex-direction:column;background:var(--p-bg);border:1px solid var(--p-border);border-radius:var(--p-radius);padding:14px;gap:9px;max-height:100%;box-shadow:var(--p-shadow);backdrop-filter:var(--p-blur);-webkit-backdrop-filter:var(--p-blur)}
135
+ .lwb-colhead{display:flex;align-items:center;gap:7px;padding:1px 1px 9px;border-bottom:1px solid var(--p-divider)}
136
+ .lwb-badge{font-family:var(--mono-font);font-size:9.5px;padding:3px 8px;border-radius:7px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 80%,transparent);color:var(--dsw-alias-label-secondary);letter-spacing:.05em}
137
+ .lwb-confirm{display:flex;gap:6px;align-items:center}
138
+ .lwb-menu-wrap{position:relative;display:flex}
139
+ .lwb-menu{position:absolute;right:0;top:calc(100% + 6px);z-index:30;min-width:162px;padding:5px;border-radius:11px;background:var(--dsw-alias-bg-overlay);border:1px solid var(--p-border);box-shadow:0 16px 40px -18px rgba(0,0,0,.42);backdrop-filter:var(--p-blur);-webkit-backdrop-filter:var(--p-blur)}
140
+ .lwb-menu-item{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-family:inherit;font-size:12px;text-align:left;width:100%;padding:8px 10px;border-radius:8px;cursor:pointer;white-space:nowrap;transition:background .14s var(--ease)}
141
+ .lwb-menu-item:hover{background:var(--dsw-alias-bg-layer-2)}
142
+ .lwb-menu-item-danger{color:var(--dsw-alias-state-error-primary)}
143
+ .lwb-card{display:flex;flex-direction:column;padding:9px 10px;border-radius:11px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 75%,transparent);border:1px solid transparent;transition:border-color .16s var(--ease)}
144
+ .lwb-card:hover{border-color:var(--p-border)}
145
+ .lwb-card-row{display:flex;align-items:center;gap:8px}
146
+ .lwb-attach{display:flex;flex-wrap:wrap;gap:7px;align-items:center;margin-top:9px;padding-top:9px;border-top:1px solid var(--p-divider)}
147
+ .lwb-attach-thumb{width:46px;height:46px;object-fit:cover;border-radius:9px;cursor:zoom-in;display:block}
148
+ .lwb-attach-chip{font-family:var(--mono-font);font-size:10px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 80%,transparent);border-radius:8px;padding:5px 8px;max-width:122px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
149
+ .lwb-note{background:color-mix(in srgb,var(--dsw-alias-bg-layer-2) 70%,transparent);border-radius:12px;padding:12px 14px;font-size:12.5px;margin-bottom:8px;white-space:pre-wrap;word-break:break-word;position:relative;line-height:1.6}
150
+ .lwb-note-meta{font-family:var(--mono-font);font-size:10px;color:var(--dsw-alias-label-secondary);margin-top:7px}
151
+ .lwb-foot{padding:8px 20px;font-size:10px;color:var(--dsw-alias-label-secondary);border-top:1px solid var(--p-divider);flex:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono-font);letter-spacing:.02em}
152
+ .lwb-svg{display:block;flex:none}
153
+ `
154
+
155
+ exports.inject = ['slots']
156
+
157
+ exports.apply = function (ctx) {
158
+ const slots = ctx.get('slots')
159
+ if (slots === undefined) {
160
+ console.error('[life-workbench] slots 服务不可用')
161
+ return
162
+ }
163
+
164
+ ctx.effect(() => insertStyles(CSS))
165
+
166
+ const todayKey = function () {
167
+ const d = new Date()
168
+ const pad = function (n) { return String(n).length < 2 ? '0' + String(n) : String(n) }
169
+ return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
170
+ }
171
+
172
+ let snapshot = {
173
+ status: 'loading', tab: 'overview',
174
+ boards: [], files: [], notes: [], schedule: [], history: {},
175
+ today: '', path: '', error: '', notice: '', rolled: false, migrated: false, storage: '',
176
+ }
177
+ const listeners = new Set()
178
+
179
+ function publish(patch) {
180
+ snapshot = Object.assign({}, snapshot, patch)
181
+ listeners.forEach(function (listener) {
182
+ try { listener() } catch (error) { console.error('[life-workbench] listener failed', error) }
183
+ })
184
+ }
185
+
186
+ function useSnapshot() {
187
+ const pair = React.useState(snapshot)
188
+ const setValue = pair[1]
189
+ React.useEffect(function () {
190
+ const listener = function () { setValue(snapshot) }
191
+ listeners.add(listener)
192
+ setValue(snapshot)
193
+ return function () { listeners.delete(listener) }
194
+ }, [])
195
+ return pair[0]
196
+ }
197
+
198
+ let seq = 0
199
+ function nextId(prefix) { seq = seq + 1; return prefix + '-' + Date.now().toString(36) + '-' + seq }
200
+
201
+ let chain = Promise.resolve()
202
+ function persist(patch) {
203
+ publish(patch)
204
+ const payload = {
205
+ version: 5,
206
+ dailyStamp: snapshot.today || todayKey(),
207
+ style: 'glass',
208
+ boards: patch.boards !== undefined ? patch.boards : snapshot.boards,
209
+ files: patch.files !== undefined ? patch.files : snapshot.files,
210
+ links: [],
211
+ notes: patch.notes !== undefined ? patch.notes : snapshot.notes,
212
+ schedule: patch.schedule !== undefined ? patch.schedule : snapshot.schedule,
213
+ history: snapshot.history,
214
+ }
215
+ chain = chain.then(async function () {
216
+ try {
217
+ const res = await hostCall('save', { state: payload })
218
+ if (res !== null && res !== undefined && res.ok === true) publish({ error: '' })
219
+ else publish({ error: res !== null && res !== undefined && res.error ? String(res.error) : '保存失败' })
220
+ } catch (error) {
221
+ publish({ error: String(error !== null && error !== undefined && error.message ? error.message : error) })
222
+ }
223
+ })
224
+ }
225
+
226
+ async function load() {
227
+ try {
228
+ const res = await hostCall('load', {})
229
+ if (res !== null && res !== undefined && res.ok === true) {
230
+ const state = res.state !== null && res.state !== undefined ? res.state : {}
231
+ publish({
232
+ status: 'ready',
233
+ boards: Array.isArray(state.boards) ? state.boards : [],
234
+ files: Array.isArray(state.files) ? state.files : [],
235
+ notes: Array.isArray(state.notes) ? state.notes : [],
236
+ schedule: Array.isArray(state.schedule) ? state.schedule : [],
237
+ history: state.history !== null && state.history !== undefined && typeof state.history === 'object' ? state.history : {},
238
+ today: res.today ? String(res.today) : todayKey(),
239
+ path: res.path ? String(res.path) : '',
240
+ rolled: res.rolled === true,
241
+ migrated: res.migrated === true,
242
+ storage: res.storage ? String(res.storage) : '',
243
+ error: '',
244
+ })
245
+ } else {
246
+ publish({ status: 'ready', error: res !== null && res !== undefined && res.error ? String(res.error) : '加载失败' })
247
+ }
248
+ } catch (error) {
249
+ publish({ status: 'ready', error: String(error !== null && error !== undefined && error.message ? error.message : error) })
250
+ }
251
+ }
252
+
253
+ function patchBoard(boardId, mutate) {
254
+ persist({
255
+ boards: snapshot.boards.map(function (board) {
256
+ if (board === null || board === undefined || board.id !== boardId) return board
257
+ return mutate(board)
258
+ }),
259
+ })
260
+ }
261
+ function addBoard(title) { persist({ boards: snapshot.boards.concat([{ id: nextId('board'), title: title, cards: [] }]) }) }
262
+ function removeBoard(boardId) { persist({ boards: snapshot.boards.filter(function (b) { return b === null || b === undefined || b.id !== boardId }) }) }
263
+ function addCard(boardId, title) {
264
+ patchBoard(boardId, function (board) {
265
+ const cards = Array.isArray(board.cards) ? board.cards : []
266
+ return Object.assign({}, board, {
267
+ cards: cards.concat([{ id: nextId('card'), title: title, done: false, createdAt: todayKey(), doneAt: null }]),
268
+ })
269
+ })
270
+ }
271
+ function toggleCard(boardId, cardId) {
272
+ patchBoard(boardId, function (board) {
273
+ const cards = Array.isArray(board.cards) ? board.cards : []
274
+ return Object.assign({}, board, {
275
+ cards: cards.map(function (card) {
276
+ if (card === null || card === undefined || card.id !== cardId) return card
277
+ const nextDone = card.done !== true
278
+ return Object.assign({}, card, { done: nextDone, doneAt: nextDone ? todayKey() : null })
279
+ }),
280
+ })
281
+ })
282
+ }
283
+ function renameCard(boardId, cardId, title) {
284
+ patchBoard(boardId, function (board) {
285
+ const cards = Array.isArray(board.cards) ? board.cards : []
286
+ return Object.assign({}, board, {
287
+ cards: cards.map(function (card) {
288
+ if (card === null || card === undefined || card.id !== cardId) return card
289
+ return Object.assign({}, card, { title: title })
290
+ }),
291
+ })
292
+ })
293
+ }
294
+ function removeCard(boardId, cardId) {
295
+ patchBoard(boardId, function (board) {
296
+ const cards = Array.isArray(board.cards) ? board.cards : []
297
+ return Object.assign({}, board, {
298
+ cards: cards.filter(function (card) { return card === null || card === undefined || card.id !== cardId }),
299
+ })
300
+ })
301
+ persist({
302
+ files: snapshot.files.map(function (file) {
303
+ if (file === null || file === undefined || file.cardId !== cardId) return file
304
+ return Object.assign({}, file, { cardId: null, boardId: null })
305
+ }),
306
+ })
307
+ }
308
+ function removeFile(fileId) { persist({ files: snapshot.files.filter(function (f) { return f === null || f === undefined || f.id !== fileId }) }) }
309
+
310
+ function ownerOf(file, boards) {
311
+ if (file === null || file === undefined || !file.cardId) return null
312
+ for (let i = 0; i < boards.length; i += 1) {
313
+ const board = boards[i]
314
+ if (board === null || board === undefined) continue
315
+ const cards = Array.isArray(board.cards) ? board.cards : []
316
+ for (let j = 0; j < cards.length; j += 1) {
317
+ if (cards[j] !== null && cards[j] !== undefined && cards[j].id === file.cardId) {
318
+ return { boardTitle: board.title, cardTitle: cards[j].title }
319
+ }
320
+ }
321
+ }
322
+ return null
323
+ }
324
+
325
+ function addNote(text) { persist({ notes: [{ id: nextId('note'), text: text, updatedAt: todayKey() }].concat(snapshot.notes) }) }
326
+ function removeNote(noteId) { persist({ notes: snapshot.notes.filter(function (i) { return i === null || i === undefined || i.id !== noteId }) }) }
327
+ function addSlot(time, text) { persist({ schedule: snapshot.schedule.concat([{ id: nextId('slot'), time: time, text: text, done: false, date: todayKey() }]) }) }
328
+ function toggleSlot(slotId) {
329
+ persist({ schedule: snapshot.schedule.map(function (i) {
330
+ if (i === null || i === undefined || i.id !== slotId) return i
331
+ return Object.assign({}, i, { done: i.done !== true })
332
+ }) })
333
+ }
334
+ function removeSlot(slotId) { persist({ schedule: snapshot.schedule.filter(function (i) { return i === null || i === undefined || i.id !== slotId }) }) }
335
+
336
+ const HAS_FILE_READER = typeof FileReader !== 'undefined'
337
+ const HAS_TYPED = typeof ArrayBuffer !== 'undefined' && typeof Uint8Array !== 'undefined'
338
+ const READ_MODE = HAS_FILE_READER ? 'FileReader' : (HAS_TYPED ? 'ArrayBuffer' : 'unsupported')
339
+
340
+ const B64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
341
+ function bytesToBase64(bytes) {
342
+ let out = ''
343
+ for (let i = 0; i < bytes.length; i += 3) {
344
+ const b0 = bytes[i]
345
+ const has1 = i + 1 < bytes.length
346
+ const has2 = i + 2 < bytes.length
347
+ const b1 = has1 ? bytes[i + 1] : 0
348
+ const b2 = has2 ? bytes[i + 2] : 0
349
+ out += B64_CHARS.charAt(b0 >> 2)
350
+ out += B64_CHARS.charAt(((b0 & 3) << 4) | (b1 >> 4))
351
+ out += has1 ? B64_CHARS.charAt(((b1 & 15) << 2) | (b2 >> 6)) : '='
352
+ out += has2 ? B64_CHARS.charAt(b2 & 63) : '='
353
+ }
354
+ return out
355
+ }
356
+
357
+ function encodeFile(file) {
358
+ return new Promise(function (resolve, reject) {
359
+ if (HAS_FILE_READER) {
360
+ try {
361
+ const reader = new FileReader()
362
+ reader.onload = function () {
363
+ const result = String(reader.result === null || reader.result === undefined ? '' : reader.result)
364
+ const comma = result.indexOf(',')
365
+ resolve({ base64: comma >= 0 ? result.slice(comma + 1) : '', mime: file.type || '' })
366
+ }
367
+ reader.onerror = function () { reject(new Error('FileReader 读取失败')) }
368
+ reader.readAsDataURL(file)
369
+ return
370
+ } catch (error) { reject(error); return }
371
+ }
372
+ if (HAS_TYPED && typeof file.arrayBuffer === 'function') {
373
+ file.arrayBuffer().then(function (buffer) {
374
+ resolve({ base64: bytesToBase64(new Uint8Array(buffer)), mime: file.type || '' })
375
+ }).catch(reject)
376
+ return
377
+ }
378
+ reject(new Error('当前环境无法读取本地文件内容'))
379
+ })
380
+ }
381
+
382
+ function formatBytes(size) {
383
+ const value = typeof size === 'number' && size > 0 ? size : 0
384
+ if (value < 1024) return value + ' B'
385
+ if (value < 1024 * 1024) return (value / 1024).toFixed(1) + ' KB'
386
+ return (value / 1024 / 1024).toFixed(1) + ' MB'
387
+ }
388
+
389
+ async function uploadFiles(list, target) {
390
+ const incoming = []
391
+ for (let i = 0; i < list.length; i += 1) incoming.push(list[i])
392
+ if (incoming.length === 0) return
393
+ publish({ notice: '正在上传 ' + incoming.length + ' 个文件…' })
394
+ const added = []
395
+ const failed = []
396
+ for (let i = 0; i < incoming.length; i += 1) {
397
+ const file = incoming[i]
398
+ if (typeof file.size === 'number' && file.size > MAX_UPLOAD_BYTES) {
399
+ failed.push(file.name + '(超过 ' + formatBytes(MAX_UPLOAD_BYTES) + ')')
400
+ continue
401
+ }
402
+ try {
403
+ const encoded = await encodeFile(file)
404
+ const res = await hostCall('upload', {
405
+ name: file.name || '未命名', mime: encoded.mime || file.type || '',
406
+ base64: encoded.base64, size: typeof file.size === 'number' ? file.size : 0,
407
+ })
408
+ if (res !== null && res !== undefined && res.ok === true && res.file) {
409
+ added.push(Object.assign({}, res.file, {
410
+ boardId: target ? target.boardId : null, cardId: target ? target.cardId : null,
411
+ }))
412
+ } else {
413
+ failed.push(file.name + '(' + (res !== null && res !== undefined && res.error ? String(res.error) : '写入失败') + ')')
414
+ }
415
+ } catch (error) {
416
+ failed.push(file.name + '(' + String(error !== null && error !== undefined && error.message ? error.message : error) + ')')
417
+ }
418
+ }
419
+ if (added.length > 0) persist({ files: snapshot.files.concat(added) })
420
+ publish({ notice: failed.length === 0 ? ('已上传 ' + added.length + ' 个') : ('成功 ' + added.length + ' 个,失败:' + failed.join(';')) })
421
+ }
422
+
423
+ function svg(props, children) {
424
+ return React.createElement('svg', {
425
+ className: 'lwb-svg', width: props.size || 15, height: props.size || 15, viewBox: '0 0 24 24',
426
+ fill: 'none', stroke: 'currentColor', strokeWidth: props.weight || 1.6,
427
+ strokeLinecap: 'round', strokeLinejoin: 'round',
428
+ }, children)
429
+ }
430
+ function PanelIcon() {
431
+ return svg({ size: 17 },
432
+ React.createElement('rect', { key: 'a', x: 3, y: 3, width: 18, height: 18, rx: 3 }),
433
+ React.createElement('path', { key: 'b', d: 'M3 9h18' }),
434
+ React.createElement('path', { key: 'c', d: 'M9 9v12' })
435
+ )
436
+ }
437
+ function ClipIcon(props) {
438
+ return svg({ size: props.size || 15, weight: 1.5 },
439
+ React.createElement('path', { key: 'a', d: 'M17 8.5l-6.6 6.6a2.6 2.6 0 0 1-3.7-3.7l7-7a3.9 3.9 0 0 1 5.5 5.5l-7 7a5.2 5.2 0 0 1-7.3-7.3L11 3.5' })
440
+ )
441
+ }
442
+ function FileIcon(props) {
443
+ return svg({ size: props.size || 22, weight: 1.4 },
444
+ React.createElement('path', { key: 'a', d: 'M6 3.5h8L19 8.5v12H6z' }),
445
+ React.createElement('path', { key: 'b', d: 'M13.5 3.5v5h5' })
446
+ )
447
+ }
448
+
449
+ function InlineEdit(props) {
450
+ if (!props.open) return null
451
+ return React.createElement('input', {
452
+ className: 'lwb-input', value: props.draft, autoFocus: true,
453
+ onChange: function (event) { props.onDraft(event.target.value) },
454
+ onBlur: props.onClose,
455
+ onKeyDown: function (event) {
456
+ if (event.key === 'Enter') {
457
+ const text = String(props.draft).trim()
458
+ if (text !== '') props.onCommit(text)
459
+ props.onClose()
460
+ }
461
+ if (event.key === 'Escape') props.onClose()
462
+ },
463
+ })
464
+ }
465
+
466
+ function FileThumb(props) {
467
+ const file = props.file
468
+ const pair = React.useState('')
469
+ const src = pair[0]
470
+ const setSrc = pair[1]
471
+ React.useEffect(function () {
472
+ let alive = true
473
+ if (file === null || file === undefined || file.kind !== 'image') return undefined
474
+ hostCall('readFile', { id: file.id }).then(function (res) {
475
+ if (!alive) return
476
+ if (res !== null && res !== undefined && res.ok === true) {
477
+ setSrc('data:' + (file.mime || 'image/png') + ';base64,' + String(res.base64 || ''))
478
+ }
479
+ }).catch(function (error) { if (alive) console.error('[life-workbench] 读取图片失败', error) })
480
+ return function () { alive = false }
481
+ }, [file !== null && file !== undefined ? file.id : ''])
482
+
483
+ if (file !== null && file !== undefined && file.kind === 'image' && src !== '') {
484
+ return React.createElement('img', {
485
+ className: props.className || 'lwb-thumb', src: src, alt: file.name || '', title: file.name || '点击放大',
486
+ onClick: function () { if (props.onZoom) props.onZoom(src) },
487
+ })
488
+ }
489
+ return React.createElement('div', { className: 'lwb-ficon' }, React.createElement(FileIcon, { size: 22 }))
490
+ }
491
+
492
+ function UploadZone(props) {
493
+ const overPair = React.useState(false)
494
+ const over = overPair[0]
495
+ const setOver = overPair[1]
496
+ function onDrop(event) {
497
+ event.preventDefault()
498
+ setOver(false)
499
+ const dt = event.dataTransfer
500
+ if (dt !== null && dt !== undefined && dt.files) uploadFiles(dt.files, props.target || null)
501
+ }
502
+ return React.createElement('div', {
503
+ className: over ? 'lwb-drop-area lwb-drop-area-on' : 'lwb-drop-area',
504
+ onDragOver: function (event) { event.preventDefault(); setOver(true) },
505
+ onDragLeave: function () { setOver(false) },
506
+ onDrop: onDrop,
507
+ },
508
+ React.createElement('label', { style: { cursor: 'pointer', display: 'block' } },
509
+ props.label || '拖入或点击上传',
510
+ React.createElement('input', {
511
+ type: 'file', multiple: true, style: { display: 'none' },
512
+ onChange: function (event) {
513
+ const input = event.target
514
+ if (input !== null && input !== undefined && input.files) uploadFiles(input.files, props.target || null)
515
+ if (input !== null && input !== undefined) input.value = ''
516
+ },
517
+ })
518
+ )
519
+ )
520
+ }
521
+
522
+ function WideCard(props) { return React.createElement('div', { className: 'lwb-wide' }, props.children) }
523
+ function Split(props) {
524
+ return React.createElement('div', { className: 'lwb-split' },
525
+ props.panes.map(function (pane, index) {
526
+ return React.createElement('div', { className: 'lwb-pane', key: index }, pane)
527
+ })
528
+ )
529
+ }
530
+ function section(name, count, body) {
531
+ return React.createElement('div', { className: 'lwb-sec' },
532
+ React.createElement('div', { className: 'lwb-sec-head' },
533
+ React.createElement('span', { className: 'lwb-sec-name' }, name),
534
+ React.createElement('span', { className: 'lwb-sec-line' }),
535
+ count ? React.createElement('span', { className: 'lwb-sec-count' }, count) : null
536
+ ),
537
+ body
538
+ )
539
+ }
540
+
541
+ function OverviewView() {
542
+ const view = useSnapshot()
543
+ const hour = new Date().getHours()
544
+ const greet = hour < 6 ? '凌晨好' : hour < 12 ? '早上好' : hour < 14 ? '中午好' : hour < 19 ? '下午好' : '晚上好'
545
+ const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][new Date().getDay()]
546
+ const boards = view.boards
547
+
548
+ const editPair = React.useState(null)
549
+ const editing = editPair[0]
550
+ const setEditing = editPair[1]
551
+ const zoomPair = React.useState('')
552
+ const zoom = zoomPair[0]
553
+ const setZoom = zoomPair[1]
554
+ const dayPair = React.useState('')
555
+ const reviewDay = dayPair[0]
556
+ const setReviewDay = dayPair[1]
557
+ const execPair = React.useState({ time: '', text: '', boardId: '' })
558
+ const execDraft = execPair[0]
559
+ const setExecDraft = execPair[1]
560
+ const notePair = React.useState('')
561
+ const noteDraft = notePair[0]
562
+ const setNoteDraft = notePair[1]
563
+
564
+ const allCards = []
565
+ boards.forEach(function (board) {
566
+ if (board === null || board === undefined) return
567
+ const cards = Array.isArray(board.cards) ? board.cards : []
568
+ cards.forEach(function (card) {
569
+ if (card !== null && card !== undefined) allCards.push({ board: board, card: card })
570
+ })
571
+ })
572
+ const openCards = allCards.filter(function (e) { return e.card.done !== true })
573
+ const doneCards = allCards.filter(function (e) { return e.card.done === true })
574
+ const todayDone = allCards.filter(function (e) { return e.card.doneAt === view.today }).length
575
+
576
+ const execItems = []
577
+ view.schedule.forEach(function (slot) {
578
+ if (slot === null || slot === undefined) return
579
+ const date = typeof slot.date === 'string' && slot.date !== '' ? slot.date : view.today
580
+ if (date !== view.today) return
581
+ execItems.push({ kind: 'slot', id: slot.id, time: slot.time || '--:--', text: slot.text, done: slot.done === true })
582
+ })
583
+ openCards.forEach(function (entry) {
584
+ execItems.push({ kind: 'card', id: entry.card.id, boardId: entry.board.id, time: '', text: entry.card.title, done: false, boardTitle: entry.board.title })
585
+ })
586
+ execItems.sort(function (a, b) {
587
+ if (a.time !== '' && b.time !== '') return a.time.localeCompare(b.time)
588
+ if (a.time !== '') return -1
589
+ if (b.time !== '') return 1
590
+ return 0
591
+ })
592
+ const timedCount = execItems.filter(function (i) { return i.time !== '' }).length
593
+
594
+ const days = []
595
+ const now = new Date()
596
+ for (let i = 6; i >= 0; i -= 1) {
597
+ const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i)
598
+ const pad = function (n) { return String(n).length < 2 ? '0' + String(n) : String(n) }
599
+ const key = d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
600
+ let count = 0
601
+ boards.forEach(function (board) {
602
+ if (board === null || board === undefined || !Array.isArray(board.cards)) return
603
+ board.cards.forEach(function (card) {
604
+ if (card !== null && card !== undefined && card.doneAt === key) count += 1
605
+ })
606
+ })
607
+ const shot = view.history !== null && view.history !== undefined ? view.history[key] : null
608
+ if (shot !== null && shot !== undefined && typeof shot.dailyDone === 'number') count += shot.dailyDone
609
+ days.push({ key: key, label: key.slice(5), count: count })
610
+ }
611
+ let maxDay = 1
612
+ days.forEach(function (day) { if (day.count > maxDay) maxDay = day.count })
613
+ const weekTotal = days.reduce(function (sum, day) { return sum + day.count }, 0)
614
+
615
+ function reviewItems(day) {
616
+ if (day === '') return []
617
+ const items = []
618
+ const shot = view.history !== null && view.history !== undefined ? view.history[day] : null
619
+ if (shot !== null && shot !== undefined && Array.isArray(shot.items)) {
620
+ shot.items.forEach(function (item) {
621
+ if (item === null || item === undefined) return
622
+ items.push({ title: String(item.title || ''), done: item.done === true, source: '每日' })
623
+ })
624
+ }
625
+ boards.forEach(function (board) {
626
+ if (board === null || board === undefined || board.kind === 'daily' || !Array.isArray(board.cards)) return
627
+ board.cards.forEach(function (card) {
628
+ if (card !== null && card !== undefined && card.doneAt === day) {
629
+ items.push({ title: card.title, done: true, source: board.title })
630
+ }
631
+ })
632
+ })
633
+ return items
634
+ }
635
+
636
+ const firstBoardId = boards.length > 0 && boards[0] !== null && boards[0] !== undefined ? boards[0].id : ''
637
+ const targetBoardId = execDraft.boardId !== '' ? execDraft.boardId : firstBoardId
638
+
639
+ function commitExec() {
640
+ const text = String(execDraft.text).trim()
641
+ if (text === '') return
642
+ const time = String(execDraft.time).trim()
643
+ if (time !== '') addSlot(time, text)
644
+ else if (targetBoardId !== '') addCard(targetBoardId, text)
645
+ setExecDraft({ time: '', text: '', boardId: targetBoardId })
646
+ }
647
+ function commitNote() {
648
+ const text = String(noteDraft).trim()
649
+ if (text === '') return
650
+ addNote(text)
651
+ setNoteDraft('')
652
+ }
653
+
654
+ const hero = React.createElement('div', { className: 'lwb-hero' },
655
+ React.createElement('div', { className: 'lwb-hero-in' },
656
+ React.createElement('div', { className: 'lwb-eyebrow' }, week + ' · ' + (view.today || '—')),
657
+ React.createElement('div', { className: 'lwb-hero-hi' }, greet),
658
+ React.createElement('div', { className: 'lwb-hero-stats' },
659
+ React.createElement('div', { className: 'lwb-stat' },
660
+ React.createElement('span', { className: 'lwb-stat-num' }, String(todayDone).length < 2 ? '0' + todayDone : String(todayDone)),
661
+ React.createElement('span', { className: 'lwb-stat-label' }, '今日完成')
662
+ ),
663
+ React.createElement('div', { className: 'lwb-stat' },
664
+ React.createElement('span', { className: 'lwb-stat-num' }, String(execItems.length).length < 2 ? '0' + execItems.length : String(execItems.length)),
665
+ React.createElement('span', { className: 'lwb-stat-label' }, '今日待办')
666
+ ),
667
+ React.createElement('div', { className: 'lwb-stat' },
668
+ React.createElement('span', { className: 'lwb-stat-num' }, doneCards.length + '/' + allCards.length),
669
+ React.createElement('span', { className: 'lwb-stat-label' }, '累计完成')
670
+ ),
671
+ React.createElement('div', { className: 'lwb-stat' },
672
+ React.createElement('span', { className: 'lwb-stat-num' }, String(weekTotal)),
673
+ React.createElement('span', { className: 'lwb-stat-label' }, '近七天')
674
+ ),
675
+ React.createElement('div', { className: 'lwb-stat' },
676
+ React.createElement('span', { className: 'lwb-stat-num' }, String(view.files.length)),
677
+ React.createElement('span', { className: 'lwb-stat-label' }, '资料')
678
+ )
679
+ )
680
+ )
681
+ )
682
+
683
+ const trendPane = React.createElement('div', null,
684
+ React.createElement('div', { className: 'lwb-pane-title' },
685
+ '完成趋势',
686
+ React.createElement('div', { className: 'lwb-spacer' }),
687
+ React.createElement('span', { className: 'lwb-count' }, '7 天 ' + weekTotal + ' 项')
688
+ ),
689
+ React.createElement('div', { className: 'lwb-chart' },
690
+ days.map(function (day) {
691
+ const percent = Math.round((day.count / maxDay) * 100)
692
+ return React.createElement('div', {
693
+ className: 'lwb-chart-col' + (reviewDay === day.key ? ' lwb-chart-col-on' : ''),
694
+ key: day.key, title: day.key + ' · ' + day.count + ' 项(点击回顾)',
695
+ onClick: function () { setReviewDay(reviewDay === day.key ? '' : day.key) },
696
+ },
697
+ React.createElement('div', { className: 'lwb-chart-num' }, day.count > 0 ? String(day.count) : ''),
698
+ React.createElement('div', {
699
+ className: day.count > 0 ? 'lwb-chart-bar' : 'lwb-chart-bar lwb-chart-bar-zero',
700
+ style: { height: Math.max(percent, 3) + '%' },
701
+ }),
702
+ React.createElement('div', { className: 'lwb-chart-label' }, day.label)
703
+ )
704
+ })
705
+ ),
706
+ reviewDay === ''
707
+ ? null
708
+ : React.createElement('div', { className: 'lwb-review' },
709
+ React.createElement('div', { className: 'lwb-review-title' }, reviewDay + ' · ' + reviewItems(reviewDay).length + ' 项'),
710
+ reviewItems(reviewDay).length === 0
711
+ ? React.createElement('div', { className: 'lwb-empty' }, '这天没有记录')
712
+ : reviewItems(reviewDay).map(function (item, index) {
713
+ return React.createElement('div', { className: 'lwb-review-item', key: reviewDay + '-' + index },
714
+ React.createElement('span', { style: { color: item.done ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-secondary)' } }, item.done ? '✓' : '○'),
715
+ React.createElement('span', { style: { flex: 1, minWidth: 0, color: item.done ? 'inherit' : 'var(--dsw-alias-label-secondary)' } }, item.title),
716
+ React.createElement('span', { className: 'lwb-tag' }, item.source)
717
+ )
718
+ })
719
+ )
720
+ )
721
+
722
+ const progressPane = React.createElement('div', null,
723
+ React.createElement('div', { className: 'lwb-pane-title' },
724
+ '看板进度',
725
+ React.createElement('div', { className: 'lwb-spacer' }),
726
+ React.createElement('span', { className: 'lwb-count' }, doneCards.length + '/' + allCards.length)
727
+ ),
728
+ boards.length === 0
729
+ ? React.createElement('div', { className: 'lwb-empty' }, '还没有看板')
730
+ : boards.map(function (board) {
731
+ if (board === null || board === undefined) return null
732
+ const cards = Array.isArray(board.cards) ? board.cards : []
733
+ const done = cards.filter(function (c) { return c !== null && c !== undefined && c.done === true }).length
734
+ const percent = cards.length === 0 ? 0 : Math.round((done / cards.length) * 100)
735
+ return React.createElement('div', { className: 'lwb-brow', key: board.id },
736
+ React.createElement('span', { className: 'lwb-brow-name', title: '看板', onClick: function () { publish({ tab: 'boards' }) } }, board.title),
737
+ React.createElement('div', { className: 'lwb-bar' },
738
+ React.createElement('div', { className: 'lwb-bar-fill', style: { width: percent + '%' } })
739
+ ),
740
+ React.createElement('span', { className: 'lwb-count' }, percent + '%')
741
+ )
742
+ })
743
+ )
744
+
745
+ const execPane = React.createElement('div', null,
746
+ React.createElement('div', { className: 'lwb-pane-title' },
747
+ '今日执行',
748
+ React.createElement('div', { className: 'lwb-spacer' }),
749
+ React.createElement('span', { className: 'lwb-count' }, execItems.length + ' 项' + (timedCount > 0 ? ' · ' + timedCount + ' 项定时' : ''))
750
+ ),
751
+ React.createElement('div', { className: 'lwb-list' },
752
+ execItems.length === 0
753
+ ? React.createElement('div', { className: 'lwb-empty' }, '今天还没有安排')
754
+ : execItems.slice(0, 40).map(function (item) {
755
+ const isEditing = item.kind === 'card' && editing !== null && editing.id === item.id
756
+ return React.createElement('div', { className: 'lwb-item', key: item.kind + '-' + item.id },
757
+ React.createElement('input', {
758
+ type: 'checkbox', className: 'lwb-check', checked: item.done === true,
759
+ onChange: function () {
760
+ if (item.kind === 'slot') toggleSlot(item.id)
761
+ else toggleCard(item.boardId, item.id)
762
+ },
763
+ }),
764
+ item.time !== '' ? React.createElement('span', { className: 'lwb-time' }, item.time) : null,
765
+ isEditing
766
+ ? React.createElement(InlineEdit, {
767
+ open: true, value: item.text, draft: editing.value,
768
+ onDraft: function (t) { setEditing({ id: item.id, value: t }) },
769
+ onCommit: function (t) { renameCard(item.boardId, item.id, t) },
770
+ onClose: function () { setEditing(null) },
771
+ })
772
+ : React.createElement('span', {
773
+ className: item.done === true ? 'lwb-item-text lwb-item-done' : 'lwb-item-text',
774
+ title: item.kind === 'card' ? '双击编辑' : '',
775
+ onDoubleClick: item.kind === 'card'
776
+ ? function () { setEditing({ id: item.id, value: item.text }) }
777
+ : undefined,
778
+ }, item.text),
779
+ item.boardTitle ? React.createElement('span', { className: 'lwb-tag' }, item.boardTitle) : null,
780
+ React.createElement('button', {
781
+ className: 'lwb-icon-btn', title: '删除',
782
+ onClick: function () {
783
+ if (item.kind === 'slot') removeSlot(item.id)
784
+ else removeCard(item.boardId, item.id)
785
+ },
786
+ }, '\u00d7')
787
+ )
788
+ })
789
+ ),
790
+ React.createElement('div', { className: 'lwb-row' },
791
+ React.createElement('input', {
792
+ className: 'lwb-input', placeholder: '09:30', style: { maxWidth: 68, flex: 'none' },
793
+ value: execDraft.time,
794
+ onChange: function (event) { setExecDraft(Object.assign({}, execDraft, { time: event.target.value })) },
795
+ onKeyDown: function (event) { if (event.key === 'Enter') commitExec() },
796
+ }),
797
+ React.createElement('input', {
798
+ className: 'lwb-input', placeholder: '今天要做什么(填了时间就是日程)',
799
+ value: execDraft.text,
800
+ onChange: function (event) { setExecDraft(Object.assign({}, execDraft, { text: event.target.value })) },
801
+ onKeyDown: function (event) { if (event.key === 'Enter') commitExec() },
802
+ }),
803
+ execDraft.time.trim() === ''
804
+ ? React.createElement('select', {
805
+ className: 'lwb-select', value: targetBoardId,
806
+ onChange: function (event) { setExecDraft(Object.assign({}, execDraft, { boardId: event.target.value })) },
807
+ }, boards.map(function (board) {
808
+ if (board === null || board === undefined) return null
809
+ return React.createElement('option', { key: board.id, value: board.id }, board.title)
810
+ }))
811
+ : null,
812
+ React.createElement('button', { className: 'lwb-btn lwb-btn-primary', onClick: commitExec }, '添加')
813
+ )
814
+ )
815
+
816
+ const donePane = doneCards.length === 0 ? null : React.createElement('div', { className: 'lwb-pane-full' },
817
+ React.createElement('div', { className: 'lwb-pane-title' },
818
+ '最近完成',
819
+ React.createElement('div', { className: 'lwb-spacer' }),
820
+ React.createElement('span', { className: 'lwb-count' }, doneCards.length + ' 项')
821
+ ),
822
+ React.createElement('div', { className: 'lwb-done-grid' },
823
+ doneCards.slice(-18).reverse().map(function (entry) {
824
+ return React.createElement('div', { className: 'lwb-item', key: entry.card.id },
825
+ React.createElement('input', {
826
+ type: 'checkbox', className: 'lwb-check', checked: true,
827
+ onChange: function () { toggleCard(entry.board.id, entry.card.id) },
828
+ }),
829
+ React.createElement('span', { className: 'lwb-item-text lwb-item-done' }, entry.card.title),
830
+ React.createElement('span', { className: 'lwb-tag' }, entry.board.title)
831
+ )
832
+ })
833
+ )
834
+ )
835
+
836
+ const notesPane = React.createElement('div', null,
837
+ React.createElement('div', { className: 'lwb-pane-title' },
838
+ '随手记',
839
+ React.createElement('div', { className: 'lwb-spacer' }),
840
+ React.createElement('span', { className: 'lwb-count' }, view.notes.length + ' 条')
841
+ ),
842
+ React.createElement('div', { className: 'lwb-list', style: { marginBottom: 4 } },
843
+ view.notes.length === 0
844
+ ? React.createElement('div', { className: 'lwb-empty' }, '还没有便签')
845
+ : view.notes.slice(0, 12).map(function (item) {
846
+ if (item === null || item === undefined) return null
847
+ return React.createElement('div', { className: 'lwb-note', key: item.id },
848
+ React.createElement('div', { style: { paddingRight: 20 } }, item.text),
849
+ React.createElement('div', { className: 'lwb-note-meta' }, String(item.updatedAt || '')),
850
+ React.createElement('button', {
851
+ className: 'lwb-icon-btn', title: '删除',
852
+ style: { position: 'absolute', top: 7, right: 7 },
853
+ onClick: function () { removeNote(item.id) },
854
+ }, '\u00d7')
855
+ )
856
+ })
857
+ ),
858
+ React.createElement('textarea', {
859
+ className: 'lwb-textarea', placeholder: '记一条…(⌘/Ctrl+Enter 保存)',
860
+ value: noteDraft,
861
+ onChange: function (event) { setNoteDraft(event.target.value) },
862
+ onKeyDown: function (event) { if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) commitNote() },
863
+ }),
864
+ React.createElement('div', { className: 'lwb-row' },
865
+ React.createElement('div', { className: 'lwb-spacer' }),
866
+ React.createElement('button', { className: 'lwb-btn lwb-btn-primary', onClick: commitNote }, '记录')
867
+ )
868
+ )
869
+
870
+ const filesPane = (function () {
871
+ const images = view.files.filter(function (f) { return f !== null && f !== undefined && f.kind === 'image' })
872
+ return React.createElement('div', null,
873
+ React.createElement('div', { className: 'lwb-pane-title' },
874
+ '资料',
875
+ React.createElement('div', { className: 'lwb-spacer' }),
876
+ React.createElement('button', { className: 'lwb-btn lwb-mini', onClick: function () { publish({ tab: 'files' }) } }, '全部')
877
+ ),
878
+ images.length > 0
879
+ ? React.createElement('div', { className: 'lwb-thumbs', style: { marginBottom: 10 } },
880
+ images.slice(0, 8).map(function (file) {
881
+ return React.createElement(FileThumb, { key: file.id, file: file, onZoom: setZoom })
882
+ })
883
+ )
884
+ : null,
885
+ React.createElement(UploadZone, { label: '拖入或点击上传到总库' }),
886
+ view.notice !== '' ? React.createElement('div', { className: 'lwb-empty', style: { marginTop: 7 } }, view.notice) : null
887
+ )
888
+ })()
889
+
890
+ return React.createElement('div', null,
891
+ hero,
892
+ section('统计', '近 7 天 ' + weekTotal + ' 项',
893
+ React.createElement(WideCard, null,
894
+ React.createElement(Split, { panes: [trendPane, progressPane] })
895
+ )
896
+ ),
897
+ section('今日', execItems.length + ' 项',
898
+ React.createElement('div', null,
899
+ React.createElement(WideCard, null, execPane),
900
+ donePane ? React.createElement(WideCard, null, donePane) : null
901
+ )
902
+ ),
903
+ section('记录', view.notes.length + ' 条 · ' + view.files.length + ' 份',
904
+ React.createElement(WideCard, null,
905
+ React.createElement(Split, { panes: [notesPane, filesPane] })
906
+ )
907
+ ),
908
+ zoom !== ''
909
+ ? React.createElement('div', { className: 'lwb-lightbox', onClick: function () { setZoom('') } },
910
+ React.createElement('img', { src: zoom, alt: '' })
911
+ )
912
+ : null
913
+ )
914
+ }
915
+
916
+ function CardRow(props) {
917
+ const board = props.board
918
+ const card = props.card
919
+ const view = useSnapshot()
920
+ const openPair = React.useState(false)
921
+ const attachOpen = openPair[0]
922
+ const setAttachOpen = openPair[1]
923
+ const editing = props.editing
924
+ const setEditing = props.setEditing
925
+ const zoom = props.zoom
926
+
927
+ if (card === null || card === undefined) return null
928
+ const isEditing = editing !== null && editing.id === card.id
929
+ const attached = view.files.filter(function (f) { return f !== null && f !== undefined && f.cardId === card.id })
930
+
931
+ return React.createElement('div', { className: 'lwb-card' },
932
+ React.createElement('div', { className: 'lwb-card-row' },
933
+ React.createElement('input', {
934
+ type: 'checkbox', className: 'lwb-check', checked: card.done === true,
935
+ onChange: function () { toggleCard(board.id, card.id) },
936
+ }),
937
+ isEditing
938
+ ? React.createElement(InlineEdit, {
939
+ open: true, value: card.title, draft: editing.value,
940
+ onDraft: function (t) { setEditing({ id: card.id, value: t }) },
941
+ onCommit: function (t) { renameCard(board.id, card.id, t) },
942
+ onClose: function () { setEditing(null) },
943
+ })
944
+ : React.createElement('span', {
945
+ className: card.done === true ? 'lwb-item-text lwb-item-done' : 'lwb-item-text',
946
+ title: '双击编辑',
947
+ onDoubleClick: function () { setEditing({ id: card.id, value: card.title }) },
948
+ }, card.title),
949
+ React.createElement('button', {
950
+ className: 'lwb-icon-btn', style: { display: 'flex', alignItems: 'center', gap: 4 },
951
+ title: attached.length > 0 ? ('附件 ' + attached.length) : '添加附件',
952
+ onClick: function () { setAttachOpen(!attachOpen) },
953
+ },
954
+ React.createElement(ClipIcon, { size: 15 }),
955
+ attached.length > 0 ? React.createElement('span', { style: { fontFamily: 'var(--mono-font)', fontSize: 10 } }, String(attached.length)) : null
956
+ ),
957
+ React.createElement('button', { className: 'lwb-icon-btn', title: '删除', onClick: function () { removeCard(board.id, card.id) } }, '\u00d7')
958
+ ),
959
+ attachOpen
960
+ ? React.createElement('div', { className: 'lwb-attach' },
961
+ attached.length === 0
962
+ ? React.createElement('span', { className: 'lwb-empty', style: { padding: 0 } }, '还没有附件')
963
+ : attached.map(function (file) {
964
+ if (file === null || file === undefined) return null
965
+ if (file.kind === 'image') {
966
+ return React.createElement(FileThumb, { key: file.id, file: file, className: 'lwb-attach-thumb', onZoom: zoom })
967
+ }
968
+ return React.createElement('span', { className: 'lwb-attach-chip', key: file.id, title: file.name }, file.name || '文件')
969
+ }),
970
+ React.createElement(UploadZone, { label: '上传到这条', target: { boardId: board.id, cardId: card.id } })
971
+ )
972
+ : null
973
+ )
974
+ }
975
+
976
+ function BoardsView() {
977
+ const view = useSnapshot()
978
+ const boardDraftPair = React.useState('')
979
+ const boardDraft = boardDraftPair[0]
980
+ const setBoardDraft = boardDraftPair[1]
981
+ const addingPair = React.useState(false)
982
+ const adding = addingPair[0]
983
+ const setAdding = addingPair[1]
984
+ const menuPair = React.useState('')
985
+ const menu = menuPair[0]
986
+ const setMenu = menuPair[1]
987
+ const confirmPair = React.useState('')
988
+ const confirmBoard = confirmPair[0]
989
+ const setConfirmBoard = confirmPair[1]
990
+ const draftPair = React.useState({})
991
+ const drafts = draftPair[0]
992
+ const setDrafts = draftPair[1]
993
+ const editPair = React.useState(null)
994
+ const editing = editPair[0]
995
+ const setEditing = editPair[1]
996
+ const zoomPair = React.useState('')
997
+ const zoom = zoomPair[0]
998
+ const setZoom = zoomPair[1]
999
+ const boards = view.boards
1000
+
1001
+ // 菜单打开时,点击页面其它地方关闭(菜单项与触发按钮自身 stopPropagation)
1002
+ React.useEffect(function () {
1003
+ if (menu === '') return undefined
1004
+ const close = function () { setMenu('') }
1005
+ document.addEventListener('click', close)
1006
+ return function () { document.removeEventListener('click', close) }
1007
+ }, [menu])
1008
+
1009
+ function commitBoard() {
1010
+ const title = boardDraft.trim()
1011
+ if (title === '') { setAdding(false); return }
1012
+ addBoard(title)
1013
+ setBoardDraft('')
1014
+ setAdding(false)
1015
+ }
1016
+ function commitCard(boardId) {
1017
+ const text = (drafts[boardId] || '').trim()
1018
+ if (text === '') return
1019
+ addCard(boardId, text)
1020
+ setDrafts(Object.assign({}, drafts, { [boardId]: '' }))
1021
+ }
1022
+ /**
1023
+ * 切换看板的「每日事项」类型:每日看板跨天时会把当天明细归档后重置勾选。
1024
+ * 删除 kind 字段而不是置 undefined,避免它在 JSON 里留下占位。
1025
+ */
1026
+ function toggleDaily(boardId) {
1027
+ patchBoard(boardId, function (board) {
1028
+ const next = Object.assign({}, board)
1029
+ if (next.kind === 'daily') delete next.kind
1030
+ else next.kind = 'daily'
1031
+ return next
1032
+ })
1033
+ }
1034
+
1035
+ function renderBoard(board) {
1036
+ if (board === null || board === undefined) return null
1037
+ const cards = Array.isArray(board.cards) ? board.cards : []
1038
+ const doneCount = cards.filter(function (c) { return c !== null && c !== undefined && c.done === true }).length
1039
+ return React.createElement('section', { key: board.id, className: 'lwb-col' },
1040
+ React.createElement('header', { className: 'lwb-colhead' },
1041
+ React.createElement('span', { className: 'lwb-item-text', style: { fontWeight: 600, cursor: 'default' } }, board.title),
1042
+ board.kind === 'daily' ? React.createElement('span', { className: 'lwb-badge' }, 'DAILY') : null,
1043
+ React.createElement('div', { className: 'lwb-spacer' }),
1044
+ React.createElement('span', { className: 'lwb-count' }, cards.length > 0 ? doneCount + '/' + cards.length : ''),
1045
+ confirmBoard === board.id
1046
+ ? React.createElement('span', { className: 'lwb-confirm' },
1047
+ React.createElement('button', { className: 'lwb-btn lwb-btn-danger lwb-mini', onClick: function () { removeBoard(board.id); setConfirmBoard('') } }, '删除'),
1048
+ React.createElement('button', { className: 'lwb-btn lwb-mini', onClick: function () { setConfirmBoard('') } }, '取消')
1049
+ )
1050
+ : React.createElement('span', { className: 'lwb-menu-wrap' },
1051
+ React.createElement('button', {
1052
+ className: 'lwb-icon-btn', title: '看板设置',
1053
+ onClick: function (event) {
1054
+ event.stopPropagation()
1055
+ setMenu(menu === board.id ? '' : board.id)
1056
+ },
1057
+ }, '\u22ef'),
1058
+ menu === board.id
1059
+ ? React.createElement('span', { className: 'lwb-menu' },
1060
+ React.createElement('button', {
1061
+ className: 'lwb-menu-item',
1062
+ onClick: function (event) {
1063
+ event.stopPropagation()
1064
+ toggleDaily(board.id)
1065
+ setMenu('')
1066
+ },
1067
+ }, board.kind === 'daily' ? '取消「每日事项」' : '设为「每日事项」'),
1068
+ React.createElement('button', {
1069
+ className: 'lwb-menu-item lwb-menu-item-danger',
1070
+ onClick: function (event) {
1071
+ event.stopPropagation()
1072
+ setConfirmBoard(board.id)
1073
+ setMenu('')
1074
+ },
1075
+ }, '删除看板')
1076
+ )
1077
+ : null
1078
+ )
1079
+ ),
1080
+ React.createElement('div', { className: 'lwb-cards' },
1081
+ cards.map(function (card) {
1082
+ return React.createElement(CardRow, {
1083
+ key: card !== null && card !== undefined ? card.id : Math.random(),
1084
+ board: board, card: card, editing: editing, setEditing: setEditing, zoom: setZoom,
1085
+ })
1086
+ })
1087
+ ),
1088
+ React.createElement('input', {
1089
+ className: 'lwb-input', placeholder: '添加事项',
1090
+ value: drafts[board.id] || '',
1091
+ onChange: function (event) { setDrafts(Object.assign({}, drafts, { [board.id]: event.target.value })) },
1092
+ onKeyDown: function (event) { if (event.key === 'Enter') commitCard(board.id) },
1093
+ })
1094
+ )
1095
+ }
1096
+
1097
+ return React.createElement('div', null,
1098
+ React.createElement('div', { style: { display: 'flex', gap: 9, marginBottom: 15, alignItems: 'center' } },
1099
+ React.createElement('div', { className: 'lwb-spacer' }),
1100
+ adding
1101
+ ? React.createElement('span', { className: 'lwb-confirm' },
1102
+ React.createElement('input', {
1103
+ className: 'lwb-input', placeholder: '看板名称', autoFocus: true, value: boardDraft,
1104
+ style: { maxWidth: 158 },
1105
+ onChange: function (event) { setBoardDraft(event.target.value) },
1106
+ onKeyDown: function (event) {
1107
+ if (event.key === 'Enter') commitBoard()
1108
+ if (event.key === 'Escape') { setAdding(false); setBoardDraft('') }
1109
+ },
1110
+ }),
1111
+ React.createElement('button', { className: 'lwb-btn lwb-btn-primary', onClick: commitBoard }, '创建'),
1112
+ React.createElement('button', { className: 'lwb-btn', onClick: function () { setAdding(false); setBoardDraft('') } }, '取消')
1113
+ )
1114
+ : React.createElement('button', { className: 'lwb-btn', onClick: function () { setAdding(true) } }, '新增看板')
1115
+ ),
1116
+ React.createElement('div', { className: 'lwb-boards' }, boards.map(function (board) { return renderBoard(board) })),
1117
+ zoom !== ''
1118
+ ? React.createElement('div', { className: 'lwb-lightbox', onClick: function () { setZoom('') } },
1119
+ React.createElement('img', { src: zoom, alt: '' })
1120
+ )
1121
+ : null
1122
+ )
1123
+ }
1124
+
1125
+ function FilesView() {
1126
+ const view = useSnapshot()
1127
+ const zoomPair = React.useState('')
1128
+ const zoom = zoomPair[0]
1129
+ const setZoom = zoomPair[1]
1130
+ const files = view.files.slice().sort(function (a, b) {
1131
+ const left = a !== null && a !== undefined ? String(a.addedAt || '') : ''
1132
+ const right = b !== null && b !== undefined ? String(b.addedAt || '') : ''
1133
+ return right.localeCompare(left)
1134
+ })
1135
+ return React.createElement('div', { className: 'lwb-wide' },
1136
+ React.createElement('div', { className: 'lwb-pane-full' },
1137
+ React.createElement('div', { className: 'lwb-pane-title' },
1138
+ '资料总库',
1139
+ React.createElement('div', { className: 'lwb-spacer' }),
1140
+ React.createElement('span', { className: 'lwb-count' }, files.length + ' 份')
1141
+ ),
1142
+ React.createElement(UploadZone, { label: '拖入图片或文件,或点击选择' }),
1143
+ view.notice !== '' ? React.createElement('div', { className: 'lwb-empty', style: { marginTop: 9 } }, view.notice) : null,
1144
+ files.length === 0
1145
+ ? React.createElement('div', { className: 'lwb-empty', style: { marginTop: 11 } }, '还没有资料。在事项上点回形针上传,附件会直接挂到那条事项。')
1146
+ : React.createElement('div', { className: 'lwb-thumbs', style: { marginTop: 13, gridTemplateColumns: 'repeat(auto-fill,minmax(112px,1fr))' } },
1147
+ files.map(function (file) {
1148
+ if (file === null || file === undefined) return null
1149
+ const owner = ownerOf(file, view.boards)
1150
+ return React.createElement('div', { key: file.id, style: { display: 'flex', flexDirection: 'column', gap: 5 } },
1151
+ React.createElement(FileThumb, { file: file, onZoom: setZoom }),
1152
+ React.createElement('div', { className: 'lwb-attach-chip', title: file.name || '', style: { maxWidth: '100%' } }, file.name || '未命名'),
1153
+ React.createElement('div', { style: { fontSize: 10.5, color: 'var(--dsw-alias-label-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, owner ? owner.cardTitle : '未归属'),
1154
+ React.createElement('div', { className: 'lwb-row', style: { marginTop: 0 } },
1155
+ React.createElement('span', { className: 'lwb-count' }, formatBytes(file.size)),
1156
+ React.createElement('div', { className: 'lwb-spacer' }),
1157
+ React.createElement('button', { className: 'lwb-icon-btn', title: '移除', onClick: function () { removeFile(file.id) } }, '\u00d7')
1158
+ )
1159
+ )
1160
+ })
1161
+ ),
1162
+ React.createElement('div', { className: 'lwb-empty', style: { marginTop: 15 } }, '读取方式:' + READ_MODE)
1163
+ ),
1164
+ zoom !== ''
1165
+ ? React.createElement('div', { className: 'lwb-lightbox', onClick: function () { setZoom('') } },
1166
+ React.createElement('img', { src: zoom, alt: '' })
1167
+ )
1168
+ : null
1169
+ )
1170
+ }
1171
+
1172
+ function App() {
1173
+ const view = useSnapshot()
1174
+ const tabs = [
1175
+ { id: 'overview', label: '总览' },
1176
+ { id: 'boards', label: '看板' },
1177
+ { id: 'files', label: '资料' },
1178
+ ]
1179
+ const head = React.createElement('div', { className: 'lwb-head' },
1180
+ React.createElement('div', { className: 'lwb-title' },
1181
+ React.createElement('span', { className: 'lwb-dot' }),
1182
+ '个人工作台'
1183
+ ),
1184
+ React.createElement('div', { className: 'lwb-spacer' }),
1185
+ view.error !== '' ? React.createElement('div', { className: 'lwb-err', title: view.error }, 'SAVE ERROR') : null,
1186
+ React.createElement('button', { className: 'lwb-btn', onClick: function () { void load() } }, '刷新')
1187
+ )
1188
+ const tabbar = React.createElement('div', { className: 'lwb-tabs' },
1189
+ tabs.map(function (tab) {
1190
+ return React.createElement('button', {
1191
+ key: tab.id,
1192
+ className: view.tab === tab.id ? 'lwb-tab lwb-tab-on' : 'lwb-tab',
1193
+ onClick: function () { publish({ tab: tab.id }) },
1194
+ }, tab.label)
1195
+ })
1196
+ )
1197
+ let body = null
1198
+ if (view.status === 'loading') body = React.createElement('div', { className: 'lwb-empty' }, '正在载入…')
1199
+ else if (view.tab === 'boards') body = React.createElement(BoardsView, null)
1200
+ else if (view.tab === 'files') body = React.createElement(FilesView, null)
1201
+ else body = React.createElement(OverviewView, null)
1202
+
1203
+ const foot = React.createElement('div', { className: 'lwb-foot' },
1204
+ (view.path !== '' ? view.path : 'host persisted')
1205
+ + (view.storage !== '' ? ' · ' + view.storage : '')
1206
+ + (view.today !== '' ? ' · ' + view.today : '')
1207
+ )
1208
+ return React.createElement('div', { className: 'lwb-root' },
1209
+ head, tabbar,
1210
+ React.createElement('div', { className: 'lwb-body' }, body),
1211
+ foot
1212
+ )
1213
+ }
1214
+
1215
+ ctx.effect(() => slots.inject('sidebar.panellist', () => slots.register(
1216
+ { name: 'sidebar.panellist', id: SLOT_ID, order: 40, label: '工作台' },
1217
+ function () { return React.createElement(PanelIcon, null) }
1218
+ )))
1219
+
1220
+ ctx.effect(() => slots.inject('main', () => slots.register(
1221
+ { name: 'main', key: SLOT_ID },
1222
+ function () { return React.createElement(App, null) }
1223
+ )))
1224
+
1225
+ void load()
1226
+ }
1227
+
1228
+ // __ModuleLoader__ 协议要求 factory 返回插件对象;缺少这句会让 loader
1229
+ // 拿到 undefined,报 "invalid plugin ... received undefined"。
1230
+ return module.exports
1231
+ },
1232
+ })