@c2-markdown-ui/vue 0.0.0-stage → 0.4.3

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.
Files changed (61) hide show
  1. package/README.md +112 -2
  2. package/dist/MarkdownUI.vue.d.ts +15 -0
  3. package/dist/favicon.ico +0 -0
  4. package/dist/index.css +1 -0
  5. package/dist/index.d.ts +23 -0
  6. package/dist/index.js +10843 -0
  7. package/dist/index.umd.cjs +53 -0
  8. package/dist/widgets/Button.vue.d.ts +19 -0
  9. package/dist/widgets/ButtonGroup.vue.d.ts +13 -0
  10. package/dist/widgets/Cascader.vue.d.ts +48 -0
  11. package/dist/widgets/Chart.vue.d.ts +24 -0
  12. package/dist/widgets/DatePicker.vue.d.ts +42 -0
  13. package/dist/widgets/Form.vue.d.ts +59 -0
  14. package/dist/widgets/Incomplete.vue.d.ts +2 -0
  15. package/dist/widgets/MultipleChoiceQuestion.vue.d.ts +20 -0
  16. package/dist/widgets/NumberInput.vue.d.ts +37 -0
  17. package/dist/widgets/Quiz.vue.d.ts +39 -0
  18. package/dist/widgets/RadioGroup.vue.d.ts +43 -0
  19. package/dist/widgets/Select.vue.d.ts +27 -0
  20. package/dist/widgets/SelectMulti.vue.d.ts +38 -0
  21. package/dist/widgets/ShortAnswerQuestion.vue.d.ts +22 -0
  22. package/dist/widgets/Slider.vue.d.ts +21 -0
  23. package/dist/widgets/Switch.vue.d.ts +28 -0
  24. package/dist/widgets/Text.vue.d.ts +22 -0
  25. package/dist/widgets/TextInput.vue.d.ts +32 -0
  26. package/dist/widgets/TreeSelect.vue.d.ts +109 -0
  27. package/dist/widgets/Widget.vue.d.ts +8 -0
  28. package/dist/widgets/chartRemoteData.d.ts +17 -0
  29. package/dist/widgets/index.d.ts +20 -0
  30. package/dist/widgets/remoteChoices.d.ts +22 -0
  31. package/dist/widgets/treeSelectTypes.d.ts +43 -0
  32. package/package.json +46 -4
  33. package/src/App.vue +11 -0
  34. package/src/lib/MarkdownUI.vue +105 -0
  35. package/src/lib/index.ts +28 -0
  36. package/src/lib/widgets/Button.vue +47 -0
  37. package/src/lib/widgets/ButtonGroup.vue +43 -0
  38. package/src/lib/widgets/Cascader.vue +475 -0
  39. package/src/lib/widgets/Chart.vue +287 -0
  40. package/src/lib/widgets/DatePicker.vue +659 -0
  41. package/src/lib/widgets/Form.vue +479 -0
  42. package/src/lib/widgets/Incomplete.vue +56 -0
  43. package/src/lib/widgets/MultipleChoiceQuestion.vue +73 -0
  44. package/src/lib/widgets/NumberInput.vue +179 -0
  45. package/src/lib/widgets/Quiz.vue +205 -0
  46. package/src/lib/widgets/RadioGroup.vue +237 -0
  47. package/src/lib/widgets/Select.vue +279 -0
  48. package/src/lib/widgets/SelectMulti.vue +145 -0
  49. package/src/lib/widgets/ShortAnswerQuestion.vue +79 -0
  50. package/src/lib/widgets/Slider.vue +61 -0
  51. package/src/lib/widgets/Switch.vue +68 -0
  52. package/src/lib/widgets/Text.vue +59 -0
  53. package/src/lib/widgets/TextInput.vue +83 -0
  54. package/src/lib/widgets/TreeSelect.vue +872 -0
  55. package/src/lib/widgets/Widget.vue +185 -0
  56. package/src/lib/widgets/chartRemoteData.ts +84 -0
  57. package/src/lib/widgets/index.ts +20 -0
  58. package/src/lib/widgets/remoteChoices.ts +64 -0
  59. package/src/lib/widgets/treeSelectTypes.ts +49 -0
  60. package/src/lib/widgets.css +1754 -0
  61. package/src/main.ts +4 -0
@@ -0,0 +1,475 @@
1
+ <template>
2
+ <div class="selector widget-cascader" ref="rootEl">
3
+ <div v-if="label || hint || required" class="widget-label-row">
4
+ <span v-if="required" class="widget-required-mark" aria-hidden="true">*</span>
5
+ <label v-if="label">{{ label }}</label>
6
+ <span v-if="hint" class="widget-hint" :data-hint="hint">ⓘ</span>
7
+ </div>
8
+ <div class="widget-field-content">
9
+ <button
10
+ type="button"
11
+ class="cas-input"
12
+ :class="{ 'widget-input-error': !!error }"
13
+ :disabled="disabled"
14
+ :aria-expanded="open"
15
+ @click="toggleOpen"
16
+ >
17
+ <span class="cas-input-text" :class="{ 'cas-placeholder': selected.length === 0 }">
18
+ {{ loading ? '加载中…' : selected.length > 0 ? displayText(selected) : placeholder || '请选择' }}
19
+ </span>
20
+ <span
21
+ v-if="!disabled && selected.length > 0"
22
+ class="cas-clear"
23
+ role="button"
24
+ tabindex="-1"
25
+ aria-label="清空"
26
+ @click.stop="clearValue"
27
+ >×</span>
28
+ <span v-else-if="!disabled" class="cas-arrow" :class="{ 'cas-arrow-open': open }" aria-hidden="true">▾</span>
29
+ </button>
30
+ <div v-if="error" class="widget-error-text" role="alert">{{ error }}</div>
31
+
32
+ <div v-if="open" class="cas-panel">
33
+ <div v-if="levelCount === 0" class="cas-empty">{{ loading ? '加载中…' : '暂无数据' }}</div>
34
+ <template v-else>
35
+ <div v-for="level in levelCount" :key="level" class="cas-menu" role="menu">
36
+ <button
37
+ v-for="node in levelNodes(level - 1)"
38
+ :key="node.value"
39
+ type="button"
40
+ class="cas-option"
41
+ :class="{ 'cas-option-active': active[level - 1] === node.value }"
42
+ @click="pick(level - 1, node)"
43
+ >
44
+ <span class="cas-option-label">{{ node.label }}</span>
45
+ <span v-if="!node.isLeaf" class="cas-option-arrow" aria-hidden="true">›</span>
46
+ </button>
47
+ <div v-if="levelNodes(level - 1).length === 0" class="cas-empty">
48
+ {{ loadingPath && active.join('/').startsWith(loadingPath) ? '加载中…' : '暂无数据' }}
49
+ </div>
50
+ </div>
51
+ </template>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </template>
56
+
57
+ <script setup lang="ts">
58
+ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
59
+
60
+ interface CascadeNode {
61
+ value?: string | number
62
+ /** value 的别名(key:label 风格数据源) */
63
+ key?: string | number
64
+ label?: string | number
65
+ children?: CascadeNode[]
66
+ /** lazy 模式:子级是否已请求过(请求返回空数组时为真叶子,标记避免重复请求) */
67
+ loaded?: boolean
68
+ [extra: string]: any
69
+ }
70
+
71
+ interface CasNode {
72
+ /** 段 key(无 key:label 时即段文本) */
73
+ value: string
74
+ /** 段显示文案 */
75
+ label: string
76
+ children: CasNode[]
77
+ /** 是否叶子(完整路径;lazy 模式下无 children 视为待加载,非叶子) */
78
+ isLeaf: boolean
79
+ /** lazy 模式:子级是否已请求过(请求返回空数组时为真叶子,标记避免重复请求) */
80
+ loaded?: boolean
81
+ }
82
+
83
+ interface CascaderProps {
84
+ choices: string[]
85
+ /** 嵌套对象树数据源(与扁平路径 choices 兼容共存,优先于 choices) */
86
+ data?: CascadeNode[]
87
+ /** 远端数据源 URL:设置后运行时 fetch 拉取嵌套树,data/choices 作为初始/后备 */
88
+ url?: string
89
+ /** 懒加载模式(url 存在时生效):首次只拉根级,点击父级后以 url?parent=<value> 逐级请求子级 */
90
+ lazy?: boolean
91
+ label?: string
92
+ default?: string
93
+ placeholder?: string
94
+ hint?: string
95
+ required?: boolean
96
+ /** 校验失败提示文案 */
97
+ message?: string
98
+ /** 外部(form 提交时)传入的错误信息 */
99
+ error?: string
100
+ /** 是否禁用 */
101
+ disabled?: boolean
102
+ }
103
+
104
+ const props = withDefaults(defineProps<CascaderProps>(), {
105
+ data: undefined,
106
+ url: '',
107
+ lazy: false,
108
+ label: '',
109
+ default: '',
110
+ placeholder: '',
111
+ hint: '',
112
+ required: false,
113
+ message: '',
114
+ error: '',
115
+ disabled: false
116
+ })
117
+
118
+ const emit = defineEmits<{
119
+ change: [value: string]
120
+ }>()
121
+
122
+ // ---------- 路径解析与树构建 ----------
123
+ /** 解析一段文本:key:label → {value:key, label:label};无冒号则 value=label=段文本 */
124
+ function parseSeg(seg: string): { value: string; label: string } {
125
+ const idx = seg.indexOf(':')
126
+ if (idx > 0) {
127
+ const v = seg.slice(0, idx).trim()
128
+ const l = seg.slice(idx + 1).trim()
129
+ if (v && l) return { value: v, label: l }
130
+ }
131
+ return { value: seg.trim(), label: seg.trim() }
132
+ }
133
+
134
+ /** 嵌套对象树 → 内部 CasNode 树(label 缺省回退 value,children 缺省/空视为叶子)
135
+ * lazy 模式下无 children 的节点视为"待加载"(isLeaf=false),子级点击父级时请求;
136
+ * 节点上已有的 loaded 标记(lazy 子级请求返回空数组的真叶子)需保留,避免重复请求 */
137
+ function nodesToTree(nodes: CascadeNode[] | undefined, lazy = false): CasNode[] {
138
+ if (!Array.isArray(nodes)) return []
139
+ return nodes.map(n => {
140
+ const v = n.value ?? n.key
141
+ const children = n.children
142
+ const hasChildren = Array.isArray(children) && children.length > 0
143
+ const wasLoaded = n.loaded === true
144
+ return {
145
+ value: String(v ?? ''),
146
+ label: String(n.label ?? v ?? ''),
147
+ children: hasChildren ? nodesToTree(children, lazy) : [],
148
+ isLeaf: lazy ? !hasChildren && wasLoaded : !hasChildren,
149
+ loaded: hasChildren || wasLoaded
150
+ }
151
+ })
152
+ }
153
+
154
+ /**
155
+ * 兼容常见远端嵌套树响应并归一化为 CascadeNode[]:
156
+ * - [{value,label,children}](直接树数组)
157
+ * - {data|options|choices|items|tree|treeData:[...]} 包裹
158
+ */
159
+ function normalizeRemoteTree(json: any): CascadeNode[] {
160
+ let list = json
161
+ if (!Array.isArray(json)) {
162
+ list = Array.isArray(json?.data) ? json.data
163
+ : Array.isArray(json?.options) ? json.options
164
+ : Array.isArray(json?.choices) ? json.choices
165
+ : Array.isArray(json?.items) ? json.items
166
+ : Array.isArray(json?.tree) ? json.tree
167
+ : Array.isArray(json?.treeData) ? json.treeData
168
+ : []
169
+ }
170
+ const map = (arr: any[]): CascadeNode[] =>
171
+ (arr || [])
172
+ .map((it: any): CascadeNode | null => {
173
+ if (!it || typeof it !== 'object') return null
174
+ const value = it.value ?? it.key
175
+ if (value === undefined || value === null || value === '') return null
176
+ const node: CascadeNode = { value: String(value) }
177
+ const label = it.label ?? it.name ?? it.text
178
+ if (label !== undefined && label !== null) node.label = String(label)
179
+ if (Array.isArray(it.children) && it.children.length > 0) {
180
+ node.children = map(it.children)
181
+ }
182
+ return node
183
+ })
184
+ .filter((n: CascadeNode | null): n is CascadeNode => n !== null)
185
+ return map(list)
186
+ }
187
+
188
+ // ---------- 远端与静态数据源 ----------
189
+ /** 远端拉取的嵌套树(成功后覆盖本地 data/choices;lazy 模式下仅根级,子级逐步填充)
190
+ * 加载失败静默处理:回落本地 data/choices,均无数据时面板显示灰色"暂无数据" */
191
+ const remoteData = ref<CascadeNode[] | null>(null)
192
+ const loading = ref(false)
193
+ /** lazy 模式:正在加载子级的父级路径串(/ 连接),空 = 无进行中的子级请求 */
194
+ const loadingPath = ref('')
195
+
196
+ // 静态 tree:data 优先于 choices(兼容共存),远端拉取成功时以远端为准
197
+ const tree = computed<CasNode[]>(() => {
198
+ if (remoteData.value) return nodesToTree(remoteData.value, props.lazy)
199
+ if (Array.isArray(props.data) && props.data.length > 0) return nodesToTree(props.data, props.lazy)
200
+ // 回退:扁平路径数组 → 多级树(公共前缀合并)
201
+ const root: CasNode[] = []
202
+ for (const path of props.choices) {
203
+ if (!path) continue
204
+ const segs = path.split('/').map(s => s.trim()).filter(Boolean)
205
+ if (segs.length === 0) continue
206
+ let level = root
207
+ for (let i = 0; i < segs.length; i++) {
208
+ const { value, label } = parseSeg(segs[i])
209
+ let node = level.find(n => n.value === value)
210
+ if (!node) {
211
+ node = { value, label, children: [], isLeaf: i === segs.length - 1 }
212
+ level.push(node)
213
+ }
214
+ level = node.children
215
+ }
216
+ }
217
+ return root
218
+ })
219
+
220
+ /** 默认路径(value 串,/ 分隔)→ 逐级 value 数组;支持 key:label 段与 label 串回退 */
221
+ function pathToValues(path: string | undefined, t: CasNode[]): string[] {
222
+ if (!path) return []
223
+ const segs = path.split('/').map(s => s.trim()).filter(Boolean)
224
+ const values: string[] = []
225
+ let level = t
226
+ for (const seg of segs) {
227
+ const { value: segVal, label: segLabel } = parseSeg(seg)
228
+ const node = level.find(n => n.value === segVal || n.value === seg || n.label === segLabel || n.label === seg)
229
+ if (!node) return values
230
+ values.push(node.value)
231
+ level = node.children
232
+ }
233
+ return values
234
+ }
235
+
236
+ // ---------- 状态 ----------
237
+ const selected = ref<string[]>(pathToValues(props.default, tree.value))
238
+ const active = ref<string[]>([])
239
+ const open = ref(false)
240
+
241
+ const rootEl = ref<HTMLElement | null>(null)
242
+
243
+ function onDocClick(e: MouseEvent) {
244
+ if (rootEl.value && !rootEl.value.contains(e.target as Node)) open.value = false
245
+ }
246
+ onMounted(() => document.addEventListener('mousedown', onDocClick))
247
+ onBeforeUnmount(() => document.removeEventListener('mousedown', onDocClick))
248
+
249
+ // url 远程模式:非 lazy 一次拉整棵树,lazy 只拉根级(子级点击父级时按需请求),竞态用 cancelled 保护
250
+ if (props.url) {
251
+ let cancelled = false
252
+ loading.value = true
253
+ ;(async () => {
254
+ try {
255
+ const res = await fetch(props.url)
256
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
257
+ const json: any = await res.json()
258
+ if (cancelled) return
259
+ const list = normalizeRemoteTree(json)
260
+ if (list.length === 0) throw new Error('响应中没有可用选项')
261
+ remoteData.value = list
262
+ // 用户尚未选择时回填默认路径(远端加载完成后触发;lazy 模式默认路径仅能回填根级)
263
+ if (selected.value.length === 0) selected.value = pathToValues(props.default, nodesToTree(list, props.lazy))
264
+ } catch {
265
+ // 加载失败:静默回落本地 data/choices(空态显示灰色"暂无数据")
266
+ } finally {
267
+ if (!cancelled) loading.value = false
268
+ }
269
+ })()
270
+ }
271
+
272
+ /** lazy 模式:按路径在原始树中找到节点并填充其子级(返回新树,目标节点打 loaded 标记) */
273
+ function fillChildren(treeNodes: CascadeNode[], path: string[], children: CascadeNode[]): CascadeNode[] {
274
+ return treeNodes.map(node => {
275
+ const v = String(node.value ?? node.key ?? '')
276
+ if (v !== path[0]) return node
277
+ if (path.length === 1) {
278
+ // 目标节点:填充子级;空数组 = 真叶子(loaded 防止重复请求)
279
+ return { ...node, children, loaded: true }
280
+ }
281
+ return { ...node, children: fillChildren(node.children || [], path.slice(1), children) }
282
+ })
283
+ }
284
+
285
+ /** lazy 模式:请求某父级的子级(url?parent=<value>),成功后填充进树 */
286
+ async function loadChildren(node: CasNode, path: string[]) {
287
+ if (!props.url || !node || node.loaded) return
288
+ loadingPath.value = path.join('/')
289
+ try {
290
+ const sep = props.url.includes('?') ? '&' : '?'
291
+ const res = await fetch(`${props.url}${sep}parent=${encodeURIComponent(node.value)}`)
292
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
293
+ const json: any = await res.json()
294
+ const list = normalizeRemoteTree(json)
295
+ // 递归更新 remoteData(不可变更新;children 为空时该节点确认为真叶子)
296
+ if (remoteData.value) remoteData.value = fillChildren(remoteData.value, path, list)
297
+ } catch {
298
+ // 子级加载失败:静默处理,子层级面板显示灰色"暂无数据"
299
+ } finally {
300
+ loadingPath.value = ''
301
+ }
302
+ }
303
+
304
+ // ---------- 展示与交互 ----------
305
+ /** 当前激活层级对应的子节点列表(level 0 用树根) */
306
+ function levelNodes(level: number): CasNode[] {
307
+ let nodes = tree.value
308
+ for (let i = 0; i < level; i++) {
309
+ const v = active.value[i]
310
+ const node = nodes.find(n => n.value === v)
311
+ if (!node) return []
312
+ nodes = node.children
313
+ }
314
+ return nodes
315
+ }
316
+
317
+ /** 展开层级总数(含当前激活最深一层;lazy 模式下未加载的节点也占一层,展示加载态) */
318
+ const levelCount = computed(() => {
319
+ if (!open.value) return 0
320
+ let count = 1
321
+ let nodes = tree.value
322
+ for (const v of active.value) {
323
+ const node = nodes.find(n => n.value === v)
324
+ if (!node || node.isLeaf) break
325
+ nodes = node.children
326
+ count++
327
+ }
328
+ return count
329
+ })
330
+
331
+ /** 选中节点 value 数组 → 展示文案串(label 用 / 连接) */
332
+ function displayText(values: string[]): string {
333
+ let nodes = tree.value
334
+ const labels: string[] = []
335
+ for (const v of values) {
336
+ const node = nodes.find(n => n.value === v)
337
+ if (!node) break
338
+ labels.push(node.label)
339
+ nodes = node.children
340
+ }
341
+ return labels.join(' / ')
342
+ }
343
+
344
+ function toggleOpen() {
345
+ if (props.disabled) return
346
+ open.value = !open.value
347
+ if (open.value) {
348
+ // 打开时激活层级同步到已选路径
349
+ active.value = selected.value.length > 0 ? selected.value.slice(0, -1) : []
350
+ }
351
+ }
352
+
353
+ /** 点击某层选项:叶子 → 完成选择并关闭;待加载(lazy)→ 请求子级后展开 */
354
+ function pick(level: number, node: CasNode) {
355
+ if (props.disabled) return
356
+ const next = active.value.slice(0, level)
357
+ next.push(node.value)
358
+ if (node.isLeaf) {
359
+ // 叶子节点:完成选择
360
+ selected.value = next
361
+ active.value = []
362
+ open.value = false
363
+ emit('change', next.join('/'))
364
+ } else {
365
+ // 展开下一层;lazy 模式下未加载时先请求子级
366
+ active.value = next
367
+ if (props.lazy && props.url && !node.loaded) {
368
+ void loadChildren(node, next)
369
+ }
370
+ }
371
+ }
372
+
373
+ function clearValue() {
374
+ selected.value = []
375
+ active.value = []
376
+ open.value = false
377
+ emit('change', '')
378
+ }
379
+ </script>
380
+
381
+ <style scoped>
382
+ .widget-cascader {
383
+ position: relative;
384
+ }
385
+
386
+ .cas-input {
387
+ display: flex;
388
+ align-items: center;
389
+ gap: 0.375rem;
390
+ width: 100%;
391
+ padding: 0.625rem 0.75rem;
392
+ border: 1px solid #d9d9d9;
393
+ border-radius: 6px;
394
+ font-size: 0.875rem;
395
+ background-color: #ffffff;
396
+ cursor: pointer;
397
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
398
+ text-align: left;
399
+ }
400
+ .cas-input:hover { border-color: #4096ff; }
401
+ .cas-input:focus {
402
+ outline: none;
403
+ border-color: #1677ff;
404
+ box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
405
+ }
406
+ /* 禁用态:置灰 + 禁止交互 */
407
+ .cas-input:disabled {
408
+ background-color: #f5f5f5;
409
+ color: rgba(0, 0, 0, 0.25);
410
+ border-color: #d9d9d9;
411
+ cursor: not-allowed;
412
+ }
413
+ .cas-input:disabled:hover { border-color: #d9d9d9; }
414
+
415
+ .cas-input-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; min-width: 0; }
416
+ .cas-placeholder { color: rgba(0, 0, 0, 0.45); }
417
+ .cas-arrow { color: rgba(0, 0, 0, 0.45); font-size: 0.75rem; transition: transform 0.2s ease; }
418
+ .cas-arrow-open { transform: rotate(180deg); }
419
+ .cas-clear {
420
+ padding: 0 0.25rem;
421
+ color: rgba(0, 0, 0, 0.45);
422
+ font-size: 1rem;
423
+ line-height: 1;
424
+ }
425
+ .cas-clear:hover { color: rgba(0, 0, 0, 0.88); }
426
+
427
+ /* 级联面板:横向多级菜单并排 */
428
+ .cas-panel {
429
+ position: absolute;
430
+ z-index: 50;
431
+ top: calc(100% + 4px);
432
+ left: 0;
433
+ display: flex;
434
+ background: #ffffff;
435
+ border: 1px solid #d9d9d9;
436
+ border-radius: 8px;
437
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
438
+ }
439
+ .cas-menu {
440
+ width: 160px;
441
+ max-height: 240px;
442
+ overflow-y: auto;
443
+ padding: 0.375rem;
444
+ }
445
+ .cas-menu + .cas-menu {
446
+ border-left: 1px solid #f0f0f0;
447
+ }
448
+ .cas-option {
449
+ display: flex;
450
+ align-items: center;
451
+ justify-content: space-between;
452
+ gap: 0.5rem;
453
+ width: 100%;
454
+ padding: 0.375rem 0.5rem;
455
+ border: none;
456
+ border-radius: 4px;
457
+ background: transparent;
458
+ font-size: 0.875rem;
459
+ color: rgba(0, 0, 0, 0.88);
460
+ cursor: pointer;
461
+ text-align: left;
462
+ transition: background 0.15s ease;
463
+ }
464
+ .cas-option:hover { background: #f5f5f5; }
465
+ .cas-option-active { background: #e6f4ff; color: #1677ff; font-weight: 500; }
466
+ .cas-option-active:hover { background: #bae0ff; }
467
+ .cas-option-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
468
+ .cas-option-arrow { color: rgba(0, 0, 0, 0.45); font-size: 0.875rem; flex: 0 0 auto; }
469
+ .cas-empty {
470
+ padding: 0.75rem 0.5rem;
471
+ font-size: 0.8125rem;
472
+ color: rgba(0, 0, 0, 0.45);
473
+ text-align: center;
474
+ }
475
+ </style>