artalk 2.2.1 → 2.2.4

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/index.html CHANGED
@@ -29,6 +29,16 @@
29
29
  body.dark-mode .to-night { background: #fff;border: 1px solid #2d3235;color: #0083ff; }
30
30
  .to-top { right: 25px; }
31
31
  </style>
32
+ <!--<link rel="stylesheet"
33
+ href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/styles/arta.min.css">
34
+ <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.5.1/highlight.min.js"></script>-->
35
+
36
+ <!-- For mobile testing -->
37
+ <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
38
+ <script>
39
+ // VConsole will be exported to `window.VConsole` by default.
40
+ var vConsole = new window.VConsole();
41
+ </script>
32
42
  </head>
33
43
  <body>
34
44
  <div class="card billboard atk-fade-in">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "artalk",
3
- "version": "2.2.1",
4
- "backendMinVersion": "2.1.1",
3
+ "version": "2.2.4",
4
+ "backendMinVersion": "2.1.2",
5
5
  "license": "LGPL-3.0",
6
6
  "description": "A Selfhosted Comment System",
7
7
  "keywords": [
package/src/api/index.ts CHANGED
@@ -119,8 +119,11 @@ export default class Api {
119
119
 
120
120
  /** 用户 · 登录状态 */
121
121
  public async loginStatus() {
122
- const data = await POST<any>(this.ctx, `${this.baseURL}/login-status`)
123
- return (data || { is_login: false }) as { is_login: boolean }
122
+ const data = await POST<any>(this.ctx, `${this.baseURL}/login-status`, {
123
+ name: this.ctx.user.data.nick,
124
+ email: this.ctx.user.data.email
125
+ })
126
+ return (data || { is_login: false, is_admin: false }) as { is_login: boolean, is_admin: boolean }
124
127
  }
125
128
 
126
129
  // ============================
@@ -164,13 +167,14 @@ export default class Api {
164
167
  }
165
168
 
166
169
  /** 页面 · 数据更新 */
167
- public async pageFetch(id: number) {
168
- const params: any = {
169
- id,
170
- }
170
+ public async pageFetch(id?: number, siteName?: string, getStatus?: boolean) {
171
+ const params: any = {}
172
+ if (id) params.id = id
173
+ if (siteName) params.site_name = siteName
174
+ if (getStatus) params.get_status = getStatus
171
175
 
172
176
  const d = await POST<any>(this.ctx, `${this.baseURL}/admin/page-fetch`, params)
173
- return (d.page as PageData)
177
+ return (d as any)
174
178
  }
175
179
 
176
180
  // ============================
package/src/artalk.ts CHANGED
@@ -9,7 +9,7 @@ import Editor from './components/editor'
9
9
  import List from './components/list'
10
10
  import SidebarLayer from './components/sidebar-layer'
11
11
 
12
- import { GetLayerWrap } from './components/layer'
12
+ import Layer, { GetLayerWrap } from './components/layer'
13
13
  import { EventPayloadMap, Handler } from '~/types/event'
14
14
  import Api from './api'
15
15
  import * as Utils from './lib/utils'
@@ -37,6 +37,7 @@ export default class Artalk {
37
37
 
38
38
  // 默认 pageKey
39
39
  if (!this.conf.pageKey) {
40
+ // @link http://bl.ocks.org/abernier/3070589
40
41
  this.conf.pageKey = `${window.location.protocol}//${window.location.host}${window.location.pathname}`
41
42
  }
42
43
 
@@ -56,6 +57,7 @@ export default class Artalk {
56
57
  // 界面初始化
57
58
  this.$root.classList.add('artalk')
58
59
  this.$root.innerHTML = ''
60
+ this.initLayer()
59
61
  this.initDarkMode()
60
62
 
61
63
  // 组件初始化
@@ -133,6 +135,13 @@ export default class Artalk {
133
135
  this.list.fetchComments(0)
134
136
  }
135
137
 
138
+ /** Layer 初始化 */
139
+ initLayer() {
140
+ // 记录页面原始 Styles
141
+ Layer.BodyOrgOverflow = document.body.style.overflow
142
+ Layer.BodyOrgPaddingRight = document.body.style.paddingRight
143
+ }
144
+
136
145
  /** 暗黑模式 · 初始化 */
137
146
  public initDarkMode() {
138
147
  const darkModeClassName = 'atk-dark-mode'
@@ -163,14 +172,15 @@ export default class Artalk {
163
172
 
164
173
  /** PV */
165
174
  public async initPV() {
175
+ const pvNum = await new Api(this.ctx).pv()
176
+ if (Number.isNaN(Number(pvNum)))
177
+ return
178
+
166
179
  if (!this.conf.pvEl || !document.querySelector(this.conf.pvEl))
167
180
  return
168
181
 
169
182
  const $pv = document.querySelector<HTMLElement>(this.conf.pvEl)!
170
183
  // $pv.innerText = '-' // 默认占位符
171
- const pvNum = await new Api(this.ctx).pv()
172
- if (Number.isNaN(Number(pvNum)))
173
- return
174
184
 
175
185
  $pv.innerText = String(pvNum)
176
186
  }
@@ -317,7 +317,7 @@ export default class Comment extends Component {
317
317
  }
318
318
 
319
319
  getIsRoot() {
320
- return this.parent === null
320
+ return this.data.rid === 0
321
321
  }
322
322
 
323
323
  getChildren() {
@@ -22,6 +22,7 @@ export default class Editor extends Component {
22
22
  public $bottom: HTMLElement
23
23
  public $plugBtnWrap: HTMLElement
24
24
  public $imgUploadBtn?: HTMLElement
25
+ public $imgUploadInput?: HTMLInputElement
25
26
  public $submitBtn: HTMLButtonElement
26
27
  public $notifyWrap: HTMLElement
27
28
 
@@ -265,12 +266,16 @@ export default class Editor extends Component {
265
266
  this.$imgUploadBtn = Utils.createElement(`<span class="atk-plug-btn">图片</span>`)
266
267
  this.$plugBtnWrap.querySelector('[data-plug-name="preview"]')!.before(this.$imgUploadBtn) // 显示在预览图标之前
267
268
 
269
+ this.$imgUploadInput = document.createElement('input')
270
+ this.$imgUploadInput.type = 'file'
271
+ this.$imgUploadInput.style.display = 'none'
272
+ this.$imgUploadInput.accept = this.allowImgExts.map(o => `.${o}`).join(',')
273
+ this.$imgUploadBtn.after(this.$imgUploadInput)
274
+
268
275
  // 按钮点击
269
276
  this.$imgUploadBtn.onclick = () => {
270
277
  // 选择图片
271
- const $input = document.createElement('input')
272
- $input.type = 'file'
273
- $input.accept = this.allowImgExts.map(o => `.${o}`).join(',')
278
+ const $input = this.$imgUploadInput!
274
279
  $input.onchange = () => {
275
280
  (async () => { // 解决阻塞 UI 问题
276
281
  if (!$input.files || $input.files.length === 0) return
@@ -501,7 +506,7 @@ export default class Editor extends Component {
501
506
 
502
507
  // 回复不同页面的评论
503
508
  if (this.replyComment !== null && this.replyComment.page_key !== this.ctx.conf.pageKey) {
504
- window.open(`${this.replyComment.page_key}#atk-comment-${nComment.id}`)
509
+ window.open(`${this.replyComment.page_url}#atk-comment-${nComment.id}`)
505
510
  }
506
511
 
507
512
  this.ctx.trigger('list-insert', nComment)
@@ -551,6 +556,8 @@ export default class Editor extends Component {
551
556
  const $travelPlace = Utils.createElement('<div></div>')
552
557
  $afterEl.after($travelPlace)
553
558
  $travelPlace.replaceWith(this.$el)
559
+
560
+ this.$el.classList.add('atk-fade-in') // 添加渐入动画
554
561
  }
555
562
 
556
563
  travelBack () {
@@ -10,8 +10,8 @@ export default class Layer extends Component {
10
10
 
11
11
  private maskClickHideEnable: boolean = true
12
12
 
13
- private bodyStyleOrgOverflow = ''
14
- private bodyStyleOrgPaddingRight = ''
13
+ public static BodyOrgOverflow: string
14
+ public static BodyOrgPaddingRight: string
15
15
 
16
16
  public afterHide?: Function
17
17
 
@@ -73,10 +73,11 @@ export default class Layer extends Component {
73
73
  this.$wrap.classList.add('atk-fade-out')
74
74
  this.$el.style.display = 'none'
75
75
 
76
+ // body style 禁止滚动解除
77
+ this.pageBodyScrollBarShow()
78
+
76
79
  this.newActionTimer(() => {
77
80
  this.$wrap.style.display = 'none'
78
- // body style 禁止滚动解除
79
- this.pageBodyScrollBarShow()
80
81
  this.checkCleanLayer()
81
82
  }, 450)
82
83
  this.newActionTimer(() => {
@@ -91,8 +92,6 @@ export default class Layer extends Component {
91
92
 
92
93
  // 页面滚动条隐藏
93
94
  pageBodyScrollBarHide() {
94
- this.bodyStyleOrgOverflow = document.body.style.overflow
95
- this.bodyStyleOrgPaddingRight = document.body.style.paddingRight
96
95
  document.body.style.overflow = 'hidden'
97
96
 
98
97
  const bpr = parseInt(window.getComputedStyle(document.body, null).getPropertyValue('padding-right'), 10)
@@ -101,8 +100,8 @@ export default class Layer extends Component {
101
100
 
102
101
  // 页面滚动条显示
103
102
  pageBodyScrollBarShow() {
104
- document.body.style.overflow = this.bodyStyleOrgOverflow
105
- document.body.style.paddingRight = this.bodyStyleOrgPaddingRight
103
+ document.body.style.overflow = Layer.BodyOrgOverflow
104
+ document.body.style.paddingRight = Layer.BodyOrgPaddingRight
106
105
  }
107
106
 
108
107
  // Timers
@@ -128,7 +127,6 @@ export default class Layer extends Component {
128
127
 
129
128
  /** 销毁 - 无动画 */
130
129
  disposeNow () {
131
- document.body.style.overflow = ''
132
130
  this.$el.remove()
133
131
  this.pageBodyScrollBarShow()
134
132
  // this.$el dispose
@@ -91,7 +91,7 @@ export default class ListLite extends Component {
91
91
  try {
92
92
  listData = await new Api(this.ctx).get(offset, this.pageSize, this.flatMode, this.paramsEditor)
93
93
  } catch (e: any) {
94
- this.onError(e.msg || String(e), offset)
94
+ this.onError(e.msg || String(e), offset, e.data)
95
95
  throw e
96
96
  } finally {
97
97
  hideLoading()
@@ -219,7 +219,7 @@ export default class ListLite extends Component {
219
219
  }
220
220
 
221
221
  /** 错误处理 */
222
- protected onError(msg: any, offset: number) {
222
+ protected onError(msg: any, offset: number, errData?: any) {
223
223
  msg = String(msg)
224
224
  console.error(msg)
225
225
 
@@ -237,9 +237,24 @@ export default class ListLite extends Component {
237
237
  $err.appendChild($retryBtn)
238
238
 
239
239
  const adminBtn = Utils.createElement('<span atk-only-admin-show> | <span style="cursor:pointer;">打开控制台</span></span>')
240
- adminBtn.onclick = () => (this.ctx.trigger('sidebar-show'))
241
- if (!this.ctx.user.data.isAdmin) adminBtn.classList.add('atk-hide')
242
240
  $err.appendChild(adminBtn)
241
+ if (!this.ctx.user.data.isAdmin) adminBtn.classList.add('atk-hide')
242
+
243
+ let sidebarView = ''
244
+
245
+ // 找不到站点错误,打开侧边栏并填入创建站点表单
246
+ if (errData?.err_no_site) {
247
+ const viewLoadParam = {
248
+ create_name: this.ctx.conf.site,
249
+ create_urls: `${window.location.protocol}//${window.location.host}`
250
+ }
251
+ // TODO 真的是飞鸽传书啊
252
+ sidebarView = `sites|${JSON.stringify(viewLoadParam)}`
253
+ }
254
+
255
+ adminBtn.onclick = () => (this.ctx.trigger('sidebar-show', {
256
+ view: sidebarView
257
+ }))
243
258
 
244
259
  Ui.setError(this.$el, $err)
245
260
  }
@@ -7,6 +7,7 @@ import SidebarHTML from './html/sidebar-layer.html?raw'
7
7
  import * as Ui from '@/lib/ui'
8
8
  import Layer from './layer'
9
9
  import Api from '../api'
10
+ import { SidebarShowPayload } from '~/types/event'
10
11
 
11
12
  export default class SidebarLayer extends Component {
12
13
  public layer?: Layer
@@ -28,7 +29,7 @@ export default class SidebarLayer extends Component {
28
29
  }
29
30
 
30
31
  // event
31
- this.ctx.on('sidebar-show', () => (this.show()))
32
+ this.ctx.on('sidebar-show', (conf) => (this.show(conf || {})))
32
33
  this.ctx.on('sidebar-hide', () => (this.hide()))
33
34
  this.ctx.on('user-changed', () => { this.firstShow = true })
34
35
  }
@@ -36,7 +37,7 @@ export default class SidebarLayer extends Component {
36
37
  private firstShow = true
37
38
 
38
39
  /** 显示 */
39
- public async show() {
40
+ public async show(conf: SidebarShowPayload) {
40
41
  this.$el.style.transform = '' // 动画清除,防止二次打开失效
41
42
 
42
43
  // 获取 Layer
@@ -59,53 +60,60 @@ export default class SidebarLayer extends Component {
59
60
  this.$el.style.transform = 'translate(0, 0)'
60
61
  }, 20)
61
62
 
62
- // 第一次加载
63
- if (this.firstShow) {
64
- // 管理员身份验证
65
- if (this.ctx.user.data.isAdmin) {
66
- const resp = await new Api(this.ctx).loginStatus()
67
- if (!resp.is_login) {
68
- await (new Promise<any>((resolve, reject) => {
69
- this.ctx.trigger('checker-admin', {
70
- onSuccess: () => {
71
- resolve(null)
72
- setTimeout(() => {
73
- this.show()
74
- }, 500)
75
- },
76
- onCancel: () => {
77
- this.layer?.hide()
78
- }
79
- })
80
- }))
81
- }
63
+ // 管理员身份验证 (若身份失效,弹出验证窗口)
64
+ ;(async () => {
65
+ const resp = await new Api(this.ctx).loginStatus()
66
+ if (resp.is_admin && !resp.is_login) {
67
+ this.layer?.hide()
68
+ this.firstShow = true
69
+
70
+ this.ctx.trigger('checker-admin', {
71
+ onSuccess: () => {
72
+ setTimeout(() => {
73
+ this.show(conf)
74
+ }, 500)
75
+ },
76
+ onCancel: () => {}
77
+ })
82
78
  }
79
+ })()
80
+
81
+ // 清空 unread
82
+ this.ctx.trigger('unread-update', { notifies: [] })
83
83
 
84
+ // 第一次加载
85
+ if (this.firstShow) {
84
86
  this.$iframeWrap.innerHTML = ''
85
87
  this.$iframe = Utils.createElement<HTMLIFrameElement>('<iframe></iframe>')
86
88
 
89
+ // 准备 Iframe 参数
87
90
  const baseURL = (import.meta.env.MODE === 'development') ? 'http://localhost:23367/'
88
91
  : Utils.getURLBasedOnApi(this.ctx, 'sidebar/')
89
- const userData = encodeURIComponent(JSON.stringify(this.ctx.user.data))
90
92
 
91
- const location = window.location
92
- this.iframeLoad(`${baseURL}`
93
- + `?pageKey=${encodeURIComponent(this.conf.pageKey)}`
94
- + `&site=${encodeURIComponent(this.conf.site || '')}`
95
- + `&user=${userData}`
96
- + `&referer=${encodeURIComponent(`${location.protocol}//${location.host}${location.pathname}`)}`
97
- + `${((this.conf.darkMode) ? `&darkMode=1` : ``)}`)
93
+ const query: any = {
94
+ pageKey: this.conf.pageKey,
95
+ site: this.conf.site || '',
96
+ user: JSON.stringify(this.ctx.user.data),
97
+ time: +new Date()
98
+ }
99
+
100
+ if (conf.view) query.view = conf.view
101
+ if (this.conf.darkMode) query.darkMode = '1'
102
+
103
+ const urlParams = new URLSearchParams(query);
104
+ this.iframeLoad(`${baseURL}?${urlParams.toString()}`)
98
105
 
99
106
  this.$iframeWrap.append(this.$iframe)
100
107
  this.firstShow = false
101
108
  } else {
102
109
  // 暗黑模式
103
- if (this.conf.darkMode && !this.$iframe!.src.match(/darkMode=1$/)) {
110
+ const isIframeSrcDarkMode = this.$iframe!.src.includes('darkMode=1')
111
+
112
+ if (this.conf.darkMode && !isIframeSrcDarkMode)
104
113
  this.iframeLoad(`${this.$iframe!.src}&darkMode=1`)
105
- }
106
- if (!this.conf.darkMode && this.$iframe!.src.match(/darkMode=1$/)) {
107
- this.iframeLoad(this.$iframe!.src.replace(/&darkMode=1$/, ''))
108
- }
114
+
115
+ if (!this.conf.darkMode && isIframeSrcDarkMode)
116
+ this.iframeLoad(this.$iframe!.src.replace('&darkMode=1', ''))
109
117
  }
110
118
  }
111
119
 
@@ -15,7 +15,7 @@ const CaptchaChecker: Checker = {
15
15
  const $iframe = Utils.createElement<HTMLIFrameElement>(`<iframe class="atk-fade-in"></iframe>`)
16
16
  $iframe.style.display = 'none'
17
17
  Ui.showLoading($iframeWrap, { transparentBg: true })
18
- $iframe.src = `${that.ctx.conf.server}/captcha/get`
18
+ $iframe.src = `${that.ctx.conf.server}/captcha/get?t=${+new Date()}`
19
19
  $iframe.onload = () => {
20
20
  $iframe.style.display = ''
21
21
  Ui.hideLoading($iframeWrap)
package/src/lib/utils.ts CHANGED
@@ -150,25 +150,34 @@ export function initMarked(ctx: Context) {
150
150
  return html.replace(/^<a /, `<a target="_blank" ${!localLink ? `rel="noreferrer noopener nofollow"` : ''} `);
151
151
  }
152
152
 
153
+ renderer.code = (block, lang) => {
154
+ // Colorize the block only if the language is known to highlight.js
155
+ const realLang = (!lang ? 'plaintext' : lang)
156
+ let colorized = block
157
+ if ((window as any).hljs) {
158
+ if (realLang && (window as any).hljs.getLanguage(realLang)) {
159
+ colorized = (window as any).hljs.highlight(realLang, block).value
160
+ }
161
+ } else {
162
+ colorized = hanabi(block)
163
+ }
164
+
165
+ return `<pre rel="${realLang}">\n`
166
+ + `<code class="hljs language-${realLang}">${colorized.replace(/&amp;/g, '&')}</code>\n`
167
+ + `</pre>`
168
+ }
169
+
153
170
  // @see https://github.com/markedjs/marked/blob/4afb228d956a415624c4e5554bb8f25d047676fe/src/Tokenizer.js#L329
154
171
  const nMarked = libMarked
155
172
  libMarked.setOptions({
156
173
  renderer,
157
- highlight: (code) => hanabi(code),
158
174
  pedantic: false,
159
175
  gfm: true,
160
176
  breaks: true,
161
177
  smartLists: true,
162
178
  smartypants: true,
163
179
  xhtml: false,
164
- sanitize: true,
165
- sanitizer: (html) => insane(html, {
166
- ...insane.defaults,
167
- allowedAttributes: {
168
- ...insane.defaults.allowedAttributes,
169
- img: ['src', 'atk-emoticon']
170
- },
171
- }),
180
+ sanitize: false,
172
181
  silent: true,
173
182
  })
174
183
 
@@ -177,7 +186,49 @@ export function initMarked(ctx: Context) {
177
186
 
178
187
  /** 解析 markdown */
179
188
  export function marked(ctx: Context, src: string): string {
180
- return ctx.markedInstance.parse(src)
189
+ // @link https://gist.github.com/lionel-rowe/bb384465ba4e4c81a9c8dada84167225
190
+ return insane(ctx.markedInstance.parse(src), {
191
+ allowedClasses: {},
192
+ allowedSchemes: ['http', 'https', 'mailto'],
193
+ allowedTags: [
194
+ 'a', 'abbr', 'article', 'b', 'blockquote', 'br', 'caption', 'code', 'del', 'details', 'div', 'em',
195
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'main', 'mark',
196
+ 'ol', 'p', 'pre', 'section', 'span', 'strike', 'strong', 'sub', 'summary', 'sup', 'table',
197
+ 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'
198
+ ],
199
+ allowedAttributes: {
200
+ '*': ['title', 'accesskey'],
201
+ a: ['href', 'name', 'target', 'aria-label'],
202
+ img: ['src', 'alt', 'title', 'atk-emoticon', 'aria-label'],
203
+ // for code highlight
204
+ code: ['class'],
205
+ span: ['class', 'style'],
206
+ },
207
+ filter: node => {
208
+ // allow hljs style
209
+ const allowed = [
210
+ [ 'code', /^hljs\W+language-(.*)$/ ],
211
+ [ 'span', /^(hljs-.*)$/ ]
212
+ ]
213
+ allowed.forEach(([ tag, reg ]) => {
214
+ if (
215
+ node.tag === tag
216
+ && !!node.attrs.class
217
+ && !(reg as RegExp).test(node.attrs.class)
218
+ ) {
219
+ delete node.attrs.class
220
+ }
221
+ })
222
+
223
+ // allow <span> set color sty
224
+ if (node.tag === 'span' && !!node.attrs.style
225
+ && !/^color:(\W+)?#[0-9a-f]{3,6};?$/i.test(node.attrs.style)) {
226
+ delete node.attrs.style
227
+ }
228
+
229
+ return true
230
+ }
231
+ })
181
232
  }
182
233
 
183
234
  /** 获取修正后的 UserAgent */
@@ -167,6 +167,7 @@
167
167
  border-left: 3px solid #eee;
168
168
  margin-bottom: 10px;
169
169
  position: relative;
170
+ margin-top: 10px;
170
171
 
171
172
  .atk-meta {
172
173
  font-size: 15px;
@@ -7,28 +7,48 @@
7
7
  }
8
8
 
9
9
  .artalk, .atk-layer-wrap {
10
+ line-height: @line-height;
11
+ color: var(--at-color-font);
12
+ font-family: @font-family;
13
+ word-wrap: break-word;
14
+ word-break: break-all;
15
+
10
16
  * {
11
17
  box-sizing: border-box;
12
- line-height: @line-height;
18
+ }
19
+
20
+ input, textarea, button, optgroup, select {
21
+ font-family: inherit;
22
+ color: inherit;
23
+ font-size: inherit;
24
+ }
25
+
26
+ @codeFont: source code pro, Consolas, Monaco, Menlo, sans-serif;
27
+
28
+ code {
29
+ font-family: @codeFont;
30
+ margin: 0 .3em;
31
+ padding: .1em .2em;
32
+ background-color: var(--at-color-bg-grey);
13
33
  color: var(--at-color-font);
14
- font-family: @font-family;
15
- word-wrap: break-word;
16
- word-break: break-all;
34
+ border-radius: @block-radius;
17
35
  }
18
36
 
19
37
  pre {
20
- padding: 10px;
38
+ margin: 10px 0 0 0;
39
+ padding: 0;
40
+ line-height: 0;
41
+ }
42
+
43
+ pre code {
44
+ * { font-family: @codeFont; }
21
45
  line-height: @line-height;
46
+ display: block;
47
+ padding: 10px 15px;
48
+ white-space: pre-wrap;
22
49
  background-color: var(--at-color-bg-grey);
23
50
  color: var(--at-color-font);
24
- padding: 10px 15px;
25
- border-radius: @block-radius;
26
51
  margin: 0;
27
- font-family: Source Code Pro,courier new,Input Mono,PT Mono,SFMono-Regular,Consolas,Monaco,Menlo,PingFang SC,Liberation Mono,Microsoft YaHei,Courier,monospace;
28
-
29
- code {
30
- white-space: pre-wrap;
31
- }
32
52
  }
33
53
 
34
54
  a {
@@ -55,6 +75,28 @@
55
75
  img {
56
76
  max-width: 100%;
57
77
  }
78
+
79
+ table {
80
+ width: 100%;
81
+ border-collapse: collapse;
82
+ border-spacing: 0;
83
+ margin-bottom: 1.5em;
84
+ font-size: .96em
85
+ }
86
+
87
+ td, th {
88
+ text-align: left;
89
+ padding: 4px 8px 4px 10px;
90
+ border: 1px solid var(--at-color-border)
91
+ }
92
+
93
+ td {
94
+ vertical-align: top
95
+ }
96
+
97
+ tr:nth-child(even) {
98
+ background-color: var(--at-color-bg-grey);
99
+ }
58
100
  }
59
101
 
60
102
  .atk-hide {
@@ -156,6 +198,9 @@
156
198
  .atk-fade-out {
157
199
  animation: atkFadeOut both .2s;
158
200
  }
201
+ .atk-rotate {
202
+ animation: atkRotate 2s linear infinite;
203
+ }
159
204
 
160
205
  @keyframes atkFadeIn {
161
206
  0% { opacity: 0 }
@@ -167,6 +212,11 @@
167
212
  0% {}
168
213
  }
169
214
 
215
+ @keyframes atkRotate {
216
+ from { transform: rotate(0deg); }
217
+ to { transform: rotate(360deg); }
218
+ }
219
+
170
220
  i.atk-icon {
171
221
  width: 20px;
172
222
  height: 20px;
@@ -175,6 +225,7 @@ i.atk-icon {
175
225
  background-repeat: no-repeat;
176
226
  mask-repeat: no-repeat;
177
227
  mask-position: center;
228
+ mask-size: contain;
178
229
  }
179
230
 
180
231
  .atk-icon-sync {
package/types/event.d.ts CHANGED
@@ -19,7 +19,7 @@ export interface EventPayloadMap {
19
19
  'list-refresh-ui': undefined // 刷新 List UI
20
20
 
21
21
  // Sidebar
22
- 'sidebar-show': undefined // 侧边栏显示
22
+ 'sidebar-show': SidebarShowPayload // 侧边栏显示
23
23
  'sidebar-hide': undefined // 侧边栏隐藏
24
24
 
25
25
  // Editor
@@ -70,6 +70,10 @@ export interface EditorReplyPayload {
70
70
  scroll?: boolean
71
71
  }
72
72
 
73
+ export interface SidebarShowPayload {
74
+ view?: 'comments'|'sites'|'pages'|'transfer'|string
75
+ }
76
+
73
77
  // ============================================
74
78
  export interface Handler<P> {
75
79
  (payload: P): void