@docsector/docsector-reader 3.2.1 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/bin/docsector.js +1 -1
- package/jsconfig.json +1 -0
- package/package.json +1 -1
- package/public/files/manual/release-checklist.txt +7 -0
- package/src/components/DPageFile.vue +430 -0
- package/src/components/DPageImage.vue +80 -0
- package/src/components/DPageTokens.vue +16 -0
- package/src/components/QZoom.js +68 -14
- package/src/components/QZoom.sass +38 -3
- package/src/components/page-section-tokens.js +243 -16
- package/src/i18n/languages/en-US.hjson +7 -0
- package/src/i18n/languages/pt-BR.hjson +7 -0
- package/src/pages/guide/i18n-and-markdown.overview.en-US.md +15 -1
- package/src/pages/guide/i18n-and-markdown.overview.pt-BR.md +15 -1
- package/src/pages/manual/content/blocks/files.overview.en-US.md +27 -0
- package/src/pages/manual/content/blocks/files.overview.pt-BR.md +27 -0
- package/src/pages/manual/content/blocks/files.showcase.en-US.md +17 -0
- package/src/pages/manual/content/blocks/files.showcase.pt-BR.md +17 -0
- package/src/pages/manual/content/blocks/images.overview.en-US.md +20 -2
- package/src/pages/manual/content/blocks/images.showcase.en-US.md +10 -2
- package/src/pages/manual.index.js +29 -0
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
41
41
|
## ✨ Features
|
|
42
42
|
|
|
43
43
|
- 📝 **Markdown Rendering** — Write docs in Markdown, rendered with syntax highlighting (Prism.js)
|
|
44
|
+
- 🔽 **Nested Markdown Lists** — Ordered and unordered lists preserve sublist hierarchy across multiple indentation levels
|
|
45
|
+
- 🖼️ **Block Image Captions & Zoom** — Standalone Markdown images render as zoomable figures, and raw `figure` / `picture` markup supports separate alt text and captions
|
|
44
46
|
- 🧱 **Raw HTML in Markdown** — Renders inline and block HTML tags inside markdown sections (including homepage remote README content)
|
|
45
47
|
- 🧩 **Mermaid Diagrams** — Native support for fenced ` ```mermaid ` blocks, with automatic dark/light theme switching
|
|
46
48
|
- ➗ **Math & KaTeX** — Native support for inline `$...$` and display `$$...$$` formulas rendered with KaTeX
|
|
@@ -69,6 +71,7 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
|
|
|
69
71
|
- 🔗 **GitHub-Compatible Heading Anchors** — Markdown headings use GitHub-style slugs so standard README Table of Contents links work inside Docsector
|
|
70
72
|
- 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
|
|
71
73
|
- 📖 **Expandable Markdown Sections** — Use `<d-expandable title="...">...</d-expandable>` to collapse secondary content while keeping rich Markdown support inside the body
|
|
74
|
+
- 📎 **File Attachment Blocks** — Use `<d-file src="/files/...">...</d-file>` in Markdown to render downloadable file cards with automatic local size detection and support for external URLs
|
|
72
75
|
- 🧭 **Quick Links Custom Element** — Use `<d-quick-links>` and `<d-quick-link>` in Markdown to render rich home navigation cards
|
|
73
76
|
- 🗂️ **API Catalog Well-Known** — Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
|
|
74
77
|
- 🗃️ **Multi-Version History** — Archive older major versions under `src/pages/.old/<version>/` and expose them at prefixed routes (e.g. `/v0.x/guide/...`) while keeping the current docs at unprefixed routes
|
|
@@ -615,7 +618,7 @@ Docsector Reader works as a **rendering engine**: it provides the layout, compon
|
|
|
615
618
|
│ ├── quasar.config.js ← thin wrapper │
|
|
616
619
|
│ ├── src/pages/ ← Markdown + route defs │
|
|
617
620
|
│ ├── src/i18n/ ← language files + tags │
|
|
618
|
-
│ └── public/ ← logo, images, icons
|
|
621
|
+
│ └── public/ ← logo, images, icons, files │
|
|
619
622
|
│ │
|
|
620
623
|
│ ┌───────────────────────────────────────────────┐ │
|
|
621
624
|
│ │ @docsector/docsector-reader (engine) │ │
|
|
@@ -873,7 +876,8 @@ my-docs/
|
|
|
873
876
|
└── public/
|
|
874
877
|
├── images/logo.png # Project logo
|
|
875
878
|
├── flags/ # Locale flag images
|
|
876
|
-
|
|
879
|
+
├── icons/ # PWA icons
|
|
880
|
+
└── files/ # Downloadable attachments served as /files/...
|
|
877
881
|
```
|
|
878
882
|
|
|
879
883
|
A common manual pattern is to keep core UI references under `src/pages/manual/basic/` with user-friendly page titles and focused entry pages such as Search, Branding, Version Switcher, Edit on GitHub, Translation Progress, and Previous & Next, end-user content references under `src/pages/manual/content/blocks/`, structural docs under `src/pages/manual/content/structures/`, and legacy/internal engine-specific references under `src/pages/manual/components/`.
|
|
@@ -1016,8 +1020,24 @@ Notes:
|
|
|
1016
1020
|
Supported alert types: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`.
|
|
1017
1021
|
Regular blockquotes without `[!TYPE]` continue to work normally.
|
|
1018
1022
|
|
|
1023
|
+
### File Attachment Blocks
|
|
1024
|
+
|
|
1025
|
+
```html
|
|
1026
|
+
<d-file src="/files/manual/release-checklist.txt" title="Release checklist" size="1 KB">
|
|
1027
|
+
Download the example file bundled with the docs.
|
|
1028
|
+
</d-file>
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
Notes:
|
|
1032
|
+
|
|
1033
|
+
- Store small repo-tracked attachments in `public/files/` and link them with absolute paths such as `/files/manual/release-checklist.txt`.
|
|
1034
|
+
- `title` and `size` are optional. If `title` is omitted, the rendered card falls back to the filename from `src`.
|
|
1035
|
+
- The block body is rendered as an inline Markdown caption.
|
|
1036
|
+
- External URLs also work, so the same syntax can later point to R2 or another CDN without changing the page structure.
|
|
1037
|
+
|
|
1019
1038
|
---
|
|
1020
1039
|
|
|
1040
|
+
|
|
1021
1041
|
## 🖥️ CLI Commands
|
|
1022
1042
|
|
|
1023
1043
|
```bash
|
package/bin/docsector.js
CHANGED
package/jsconfig.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docsector/docsector-reader",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "A documentation rendering engine built with Vue 3, Quasar v2 and Vite. Transform Markdown into beautiful, navigable documentation sites.",
|
|
5
5
|
"productName": "Docsector Reader",
|
|
6
6
|
"author": "Rodrigo de Araujo Vieira",
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Docsector Reader Release Checklist
|
|
2
|
+
=================================
|
|
3
|
+
|
|
4
|
+
1. Confirm the target route and page registry entry.
|
|
5
|
+
2. Update the overview and showcase markdown pages.
|
|
6
|
+
3. Run the focused test suite for the touched parser or helper.
|
|
7
|
+
4. Run a production build and inspect the published artifact.
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
|
+
import { useQuasar } from 'quasar'
|
|
4
|
+
import { useI18n } from 'vue-i18n'
|
|
5
|
+
|
|
6
|
+
import { resolveFileIconUrl } from '../composables/useFileIcon'
|
|
7
|
+
|
|
8
|
+
const BASE_URL = import.meta.env.BASE_URL || '/'
|
|
9
|
+
|
|
10
|
+
defineOptions({
|
|
11
|
+
name: 'DPageFile'
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
src: {
|
|
16
|
+
type: String,
|
|
17
|
+
default: ''
|
|
18
|
+
},
|
|
19
|
+
title: {
|
|
20
|
+
type: String,
|
|
21
|
+
default: ''
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: ''
|
|
26
|
+
},
|
|
27
|
+
caption: {
|
|
28
|
+
type: String,
|
|
29
|
+
default: ''
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const $q = useQuasar()
|
|
34
|
+
const { t } = useI18n()
|
|
35
|
+
const resolvedSize = ref(String(props.size || '').trim())
|
|
36
|
+
|
|
37
|
+
const isExternal = computed(() => /^https?:\/\//i.test(props.src || ''))
|
|
38
|
+
const resolvedHref = computed(() => {
|
|
39
|
+
const raw = String(props.src || '').trim()
|
|
40
|
+
|
|
41
|
+
if (!raw) {
|
|
42
|
+
return ''
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (/^(?:[a-z]+:)?\/\//i.test(raw) || /^(?:mailto:|tel:|data:)/i.test(raw)) {
|
|
46
|
+
return raw
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const trimmedBase = String(BASE_URL).replace(/\/$/, '')
|
|
50
|
+
|
|
51
|
+
if (raw.startsWith('/')) {
|
|
52
|
+
return `${trimmedBase}${raw}` || raw
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const normalized = raw.replace(/^\.\//, '')
|
|
56
|
+
return `${trimmedBase}/${normalized}`.replace(/\/+/g, '/')
|
|
57
|
+
})
|
|
58
|
+
const absoluteHref = computed(() => {
|
|
59
|
+
const href = resolvedHref.value
|
|
60
|
+
|
|
61
|
+
if (!href || /^(?:[a-z]+:)?\/\//i.test(href)) {
|
|
62
|
+
return href
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof window === 'undefined') {
|
|
66
|
+
return href
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return new URL(href, window.location.origin).toString()
|
|
70
|
+
})
|
|
71
|
+
const fileName = computed(() => {
|
|
72
|
+
const normalized = String(props.src || '')
|
|
73
|
+
.split('#')[0]
|
|
74
|
+
.split('?')[0]
|
|
75
|
+
const rawSegment = normalized
|
|
76
|
+
.split('/')
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.pop() || ''
|
|
79
|
+
|
|
80
|
+
if (!rawSegment) {
|
|
81
|
+
return ''
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
return decodeURIComponent(rawSegment)
|
|
86
|
+
} catch {
|
|
87
|
+
return rawSegment
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
const displayTitle = computed(() => {
|
|
91
|
+
return props.title || fileName.value || t('page.file.defaultTitle')
|
|
92
|
+
})
|
|
93
|
+
const displayHeading = computed(() => {
|
|
94
|
+
if (props.title && fileName.value && fileName.value !== props.title) {
|
|
95
|
+
return `${props.title} (${fileName.value})`
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return displayTitle.value
|
|
99
|
+
})
|
|
100
|
+
const iconUrl = computed(() => {
|
|
101
|
+
return resolveFileIconUrl(fileName.value || displayTitle.value || props.src, {
|
|
102
|
+
preferLight: !$q.dark.isActive
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const formatFileSize = (bytes) => {
|
|
107
|
+
const value = Number(bytes)
|
|
108
|
+
|
|
109
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
110
|
+
return ''
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
114
|
+
let size = value
|
|
115
|
+
let unitIndex = 0
|
|
116
|
+
|
|
117
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
118
|
+
size /= 1024
|
|
119
|
+
unitIndex++
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const maximumFractionDigits = size >= 100 || unitIndex === 0 ? 0 : (size >= 10 ? 1 : 2)
|
|
123
|
+
return `${new Intl.NumberFormat(undefined, {
|
|
124
|
+
maximumFractionDigits,
|
|
125
|
+
minimumFractionDigits: 0
|
|
126
|
+
}).format(size)} ${units[unitIndex]}`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const readContentLength = async (href, options = {}) => {
|
|
130
|
+
const response = await fetch(href, options)
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new Error(`HTTP ${response.status}`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const contentLength = response.headers.get('content-length') || response.headers.get('Content-Length')
|
|
137
|
+
return formatFileSize(contentLength)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const hydrateSize = async () => {
|
|
141
|
+
const manualSize = String(props.size || '').trim()
|
|
142
|
+
resolvedSize.value = manualSize
|
|
143
|
+
|
|
144
|
+
if (manualSize || !absoluteHref.value || typeof window === 'undefined') {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const automaticSize = await readContentLength(absoluteHref.value, {
|
|
150
|
+
method: 'HEAD'
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
if (automaticSize) {
|
|
154
|
+
resolvedSize.value = automaticSize
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!isExternal.value) {
|
|
159
|
+
const fallbackSize = await readContentLength(absoluteHref.value)
|
|
160
|
+
if (fallbackSize) {
|
|
161
|
+
resolvedSize.value = fallbackSize
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// External URLs may block access to Content-Length via CORS.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
watch([
|
|
170
|
+
() => props.size,
|
|
171
|
+
absoluteHref
|
|
172
|
+
], () => {
|
|
173
|
+
hydrateSize()
|
|
174
|
+
}, {
|
|
175
|
+
immediate: true
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const openFile = () => {
|
|
179
|
+
const href = absoluteHref.value
|
|
180
|
+
|
|
181
|
+
if (!href || typeof window === 'undefined') {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const link = document.createElement('a')
|
|
186
|
+
|
|
187
|
+
link.href = href
|
|
188
|
+
link.target = '_blank'
|
|
189
|
+
link.rel = 'noopener noreferrer'
|
|
190
|
+
document.body.appendChild(link)
|
|
191
|
+
link.click()
|
|
192
|
+
link.remove()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const downloadFile = async () => {
|
|
196
|
+
const href = absoluteHref.value
|
|
197
|
+
|
|
198
|
+
if (!href || typeof window === 'undefined') {
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (isExternal.value) {
|
|
203
|
+
openFile()
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const response = await fetch(href)
|
|
209
|
+
|
|
210
|
+
if (!response.ok) {
|
|
211
|
+
throw new Error(`HTTP ${response.status}`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const blob = await response.blob()
|
|
215
|
+
const objectUrl = window.URL.createObjectURL(blob)
|
|
216
|
+
const link = document.createElement('a')
|
|
217
|
+
|
|
218
|
+
link.href = objectUrl
|
|
219
|
+
link.download = fileName.value || displayTitle.value || 'download'
|
|
220
|
+
link.rel = 'noopener noreferrer'
|
|
221
|
+
document.body.appendChild(link)
|
|
222
|
+
link.click()
|
|
223
|
+
link.remove()
|
|
224
|
+
|
|
225
|
+
window.setTimeout(() => {
|
|
226
|
+
window.URL.revokeObjectURL(objectUrl)
|
|
227
|
+
}, 1000)
|
|
228
|
+
} catch {
|
|
229
|
+
window.location.assign(href)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
</script>
|
|
233
|
+
|
|
234
|
+
<template>
|
|
235
|
+
<div class="d-page-file">
|
|
236
|
+
<div class="d-page-file__body">
|
|
237
|
+
<div class="d-page-file__media-column">
|
|
238
|
+
<div class="d-page-file__media" aria-hidden="true">
|
|
239
|
+
<img
|
|
240
|
+
v-if="iconUrl"
|
|
241
|
+
class="d-page-file__icon"
|
|
242
|
+
:src="iconUrl"
|
|
243
|
+
alt=""
|
|
244
|
+
loading="lazy"
|
|
245
|
+
/>
|
|
246
|
+
<q-icon
|
|
247
|
+
v-else
|
|
248
|
+
name="attach_file"
|
|
249
|
+
size="30px"
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<div v-if="resolvedSize" class="d-page-file__size">{{ resolvedSize }}</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div class="d-page-file__content">
|
|
257
|
+
<div class="d-page-file__title">{{ displayHeading }}</div>
|
|
258
|
+
|
|
259
|
+
<div
|
|
260
|
+
v-if="caption"
|
|
261
|
+
class="d-page-file__caption"
|
|
262
|
+
v-html="caption"
|
|
263
|
+
></div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="d-page-file__actions">
|
|
267
|
+
<q-btn
|
|
268
|
+
no-caps
|
|
269
|
+
unelevated
|
|
270
|
+
padding="8px 12px"
|
|
271
|
+
class="d-page-file__action-button d-page-file__action-button--primary"
|
|
272
|
+
icon="download"
|
|
273
|
+
:label="t('page.file.download')"
|
|
274
|
+
@click="downloadFile"
|
|
275
|
+
/>
|
|
276
|
+
|
|
277
|
+
<q-btn
|
|
278
|
+
no-caps
|
|
279
|
+
unelevated
|
|
280
|
+
padding="8px 12px"
|
|
281
|
+
class="d-page-file__action-button d-page-file__action-button--secondary"
|
|
282
|
+
icon="open_in_new"
|
|
283
|
+
:label="t('page.file.open')"
|
|
284
|
+
@click="openFile"
|
|
285
|
+
/>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
</template>
|
|
290
|
+
|
|
291
|
+
<style lang="sass">
|
|
292
|
+
body.body--light
|
|
293
|
+
--d-page-file-bg: linear-gradient(180deg, #faf8f2 0%, #ffffff 100%)
|
|
294
|
+
--d-page-file-border: rgba(101, 85, 41, 0.18)
|
|
295
|
+
--d-page-file-shadow: rgba(101, 85, 41, 0.08)
|
|
296
|
+
--d-page-file-media-bg: rgba(255, 255, 255, 0.82)
|
|
297
|
+
--d-page-file-media-border: rgba(101, 85, 41, 0.1)
|
|
298
|
+
--d-page-file-meta: #665f4f
|
|
299
|
+
--d-page-file-caption: #4d5563
|
|
300
|
+
--d-page-file-accent: #655529
|
|
301
|
+
--d-page-file-action-text: #4d4020
|
|
302
|
+
--d-page-file-action-border: rgba(101, 85, 41, 0.22)
|
|
303
|
+
--d-page-file-action-bg-hover: rgba(101, 85, 41, 0.06)
|
|
304
|
+
|
|
305
|
+
body.body--dark
|
|
306
|
+
--d-page-file-bg: linear-gradient(180deg, rgba(255, 248, 235, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%)
|
|
307
|
+
--d-page-file-border: rgba(255, 235, 194, 0.14)
|
|
308
|
+
--d-page-file-shadow: rgba(0, 0, 0, 0.28)
|
|
309
|
+
--d-page-file-media-bg: rgba(255, 255, 255, 0.94)
|
|
310
|
+
--d-page-file-media-border: rgba(255, 255, 255, 0.18)
|
|
311
|
+
--d-page-file-meta: rgba(255, 255, 255, 0.7)
|
|
312
|
+
--d-page-file-caption: rgba(255, 255, 255, 0.9)
|
|
313
|
+
--d-page-file-accent: #d7bc7e
|
|
314
|
+
--d-page-file-action-text: rgba(255, 255, 255, 0.92)
|
|
315
|
+
--d-page-file-action-border: rgba(255, 235, 194, 0.18)
|
|
316
|
+
--d-page-file-action-bg-hover: rgba(255, 235, 194, 0.08)
|
|
317
|
+
|
|
318
|
+
.d-page-file
|
|
319
|
+
margin: 1.5rem 0
|
|
320
|
+
border: 1px solid var(--d-page-file-border)
|
|
321
|
+
border-radius: 18px
|
|
322
|
+
background: var(--d-page-file-bg)
|
|
323
|
+
box-shadow: 0 16px 36px var(--d-page-file-shadow)
|
|
324
|
+
overflow: hidden
|
|
325
|
+
|
|
326
|
+
.d-page-file__body
|
|
327
|
+
display: flex
|
|
328
|
+
align-items: center
|
|
329
|
+
gap: 1rem
|
|
330
|
+
padding: 0.82rem 0.92rem
|
|
331
|
+
|
|
332
|
+
.d-page-file__media-column
|
|
333
|
+
display: flex
|
|
334
|
+
flex: 0 0 68px
|
|
335
|
+
flex-direction: column
|
|
336
|
+
align-items: center
|
|
337
|
+
gap: 0.16rem
|
|
338
|
+
|
|
339
|
+
.d-page-file__media
|
|
340
|
+
width: 56px
|
|
341
|
+
height: 56px
|
|
342
|
+
display: flex
|
|
343
|
+
align-items: center
|
|
344
|
+
justify-content: center
|
|
345
|
+
border-radius: 16px
|
|
346
|
+
background: var(--d-page-file-media-bg)
|
|
347
|
+
box-shadow: inset 0 0 0 1px var(--d-page-file-media-border)
|
|
348
|
+
color: var(--d-page-file-accent)
|
|
349
|
+
|
|
350
|
+
.d-page-file__size
|
|
351
|
+
width: 100%
|
|
352
|
+
text-align: center
|
|
353
|
+
color: var(--d-page-file-meta)
|
|
354
|
+
font-size: 0.8rem
|
|
355
|
+
font-weight: 700
|
|
356
|
+
line-height: 1.05
|
|
357
|
+
|
|
358
|
+
.d-page-file__icon
|
|
359
|
+
display: block
|
|
360
|
+
width: 32px
|
|
361
|
+
height: 32px
|
|
362
|
+
|
|
363
|
+
.d-page-file__content
|
|
364
|
+
min-width: 0
|
|
365
|
+
flex: 1 1 auto
|
|
366
|
+
|
|
367
|
+
.d-page-file__title
|
|
368
|
+
font-size: 1rem
|
|
369
|
+
font-weight: 700
|
|
370
|
+
line-height: 1.4
|
|
371
|
+
word-break: break-word
|
|
372
|
+
|
|
373
|
+
.d-page-file__caption
|
|
374
|
+
margin-top: 0.35rem
|
|
375
|
+
color: var(--d-page-file-caption)
|
|
376
|
+
|
|
377
|
+
> :first-child
|
|
378
|
+
margin-top: 0
|
|
379
|
+
|
|
380
|
+
> :last-child
|
|
381
|
+
margin-bottom: 0
|
|
382
|
+
|
|
383
|
+
.d-page-file__actions
|
|
384
|
+
display: flex
|
|
385
|
+
flex: 0 0 auto
|
|
386
|
+
align-items: center
|
|
387
|
+
gap: 0.6rem
|
|
388
|
+
align-self: center
|
|
389
|
+
|
|
390
|
+
.d-page-file__action-button
|
|
391
|
+
min-height: 40px
|
|
392
|
+
border-radius: 10px
|
|
393
|
+
border: 1px solid var(--d-page-file-action-border)
|
|
394
|
+
background: transparent !important
|
|
395
|
+
color: var(--d-page-file-action-text) !important
|
|
396
|
+
transition: transform 0.18s ease, background-color 0.18s ease, border-color 0.18s ease
|
|
397
|
+
|
|
398
|
+
.q-btn__content
|
|
399
|
+
gap: 0.45rem
|
|
400
|
+
font-size: 0.9rem
|
|
401
|
+
font-weight: 600
|
|
402
|
+
line-height: 1
|
|
403
|
+
|
|
404
|
+
.q-focus-helper,
|
|
405
|
+
&:before,
|
|
406
|
+
&:after
|
|
407
|
+
display: none
|
|
408
|
+
|
|
409
|
+
&:hover
|
|
410
|
+
transform: translateY(-1px)
|
|
411
|
+
background: var(--d-page-file-action-bg-hover) !important
|
|
412
|
+
|
|
413
|
+
&:focus-visible
|
|
414
|
+
outline: 2px solid var(--d-page-file-accent)
|
|
415
|
+
outline-offset: 2px
|
|
416
|
+
|
|
417
|
+
.d-page-file__action-button--primary
|
|
418
|
+
border-color: var(--d-page-file-action-border)
|
|
419
|
+
|
|
420
|
+
@media (max-width: 720px)
|
|
421
|
+
.d-page-file__body
|
|
422
|
+
flex-wrap: wrap
|
|
423
|
+
align-items: flex-start
|
|
424
|
+
|
|
425
|
+
.d-page-file__actions
|
|
426
|
+
width: 100%
|
|
427
|
+
|
|
428
|
+
.d-page-file__action-button
|
|
429
|
+
flex: 1 1 0
|
|
430
|
+
</style>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
defineOptions({
|
|
5
|
+
name: 'DPageImage'
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
const props = defineProps({
|
|
9
|
+
content: {
|
|
10
|
+
type: String,
|
|
11
|
+
default: ''
|
|
12
|
+
},
|
|
13
|
+
captionHtml: {
|
|
14
|
+
type: String,
|
|
15
|
+
default: ''
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const hasCaption = computed(() => {
|
|
20
|
+
return String(props.captionHtml || '').trim() !== ''
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<figure class="d-page-image">
|
|
26
|
+
<q-zoom
|
|
27
|
+
class="d-page-image__zoom"
|
|
28
|
+
background-color="rgba(18, 18, 20, 0.96)"
|
|
29
|
+
show-close-button
|
|
30
|
+
>
|
|
31
|
+
<div class="d-page-image__media" v-html="content"></div>
|
|
32
|
+
</q-zoom>
|
|
33
|
+
|
|
34
|
+
<figcaption v-if="hasCaption" class="d-page-image__caption" v-html="captionHtml"></figcaption>
|
|
35
|
+
</figure>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<style lang="sass" scoped>
|
|
39
|
+
.d-page-image
|
|
40
|
+
display: flex
|
|
41
|
+
flex-direction: column
|
|
42
|
+
align-items: center
|
|
43
|
+
gap: 0.75rem
|
|
44
|
+
width: 100%
|
|
45
|
+
margin: 1.75rem auto
|
|
46
|
+
text-align: center
|
|
47
|
+
|
|
48
|
+
.d-page-image__zoom
|
|
49
|
+
display: inline-block
|
|
50
|
+
width: fit-content
|
|
51
|
+
max-width: 100%
|
|
52
|
+
|
|
53
|
+
.d-page-image__media
|
|
54
|
+
display: inline-flex
|
|
55
|
+
align-items: center
|
|
56
|
+
justify-content: center
|
|
57
|
+
line-height: 0
|
|
58
|
+
max-width: 100%
|
|
59
|
+
|
|
60
|
+
:deep(img),
|
|
61
|
+
:deep(picture)
|
|
62
|
+
display: block
|
|
63
|
+
max-width: 100%
|
|
64
|
+
|
|
65
|
+
.d-page-image__caption
|
|
66
|
+
max-width: min(100%, 42rem)
|
|
67
|
+
margin: 0
|
|
68
|
+
padding: 0 1rem
|
|
69
|
+
color: inherit
|
|
70
|
+
opacity: 0.72
|
|
71
|
+
font-size: 0.92rem
|
|
72
|
+
line-height: 1.45
|
|
73
|
+
text-align: center
|
|
74
|
+
|
|
75
|
+
:deep(p)
|
|
76
|
+
margin: 0
|
|
77
|
+
|
|
78
|
+
:deep(*)
|
|
79
|
+
color: inherit
|
|
80
|
+
</style>
|
|
@@ -22,6 +22,8 @@ import DH6 from './DH6.vue'
|
|
|
22
22
|
import DPageSourceCode from './DPageSourceCode.vue'
|
|
23
23
|
import DMermaidDiagram from './DMermaidDiagram.vue'
|
|
24
24
|
import DPageBlockquote from './DPageBlockquote.vue'
|
|
25
|
+
import DPageImage from './DPageImage.vue'
|
|
26
|
+
import DPageFile from './DPageFile.vue'
|
|
25
27
|
import DQuickLinks from './DQuickLinks.vue'
|
|
26
28
|
import DPageExpandable from './DPageExpandable.vue'
|
|
27
29
|
</script>
|
|
@@ -75,6 +77,12 @@ import DPageExpandable from './DPageExpandable.vue'
|
|
|
75
77
|
v-html="token.content"
|
|
76
78
|
></div>
|
|
77
79
|
|
|
80
|
+
<d-page-image
|
|
81
|
+
v-else-if="token.tag === 'image'"
|
|
82
|
+
:content="token.content"
|
|
83
|
+
:caption-html="token.captionHtml"
|
|
84
|
+
/>
|
|
85
|
+
|
|
78
86
|
<p
|
|
79
87
|
v-else-if="token.tag === 'p'"
|
|
80
88
|
v-html="token.content"
|
|
@@ -87,6 +95,14 @@ import DPageExpandable from './DPageExpandable.vue'
|
|
|
87
95
|
<div v-html="token.content"></div>
|
|
88
96
|
</d-page-blockquote>
|
|
89
97
|
|
|
98
|
+
<d-page-file
|
|
99
|
+
v-else-if="token.tag === 'file'"
|
|
100
|
+
:src="token.src"
|
|
101
|
+
:title="token.title"
|
|
102
|
+
:size="token.size"
|
|
103
|
+
:caption="token.caption"
|
|
104
|
+
/>
|
|
105
|
+
|
|
90
106
|
<d-page-source-code
|
|
91
107
|
v-else-if="token.tag === 'code'"
|
|
92
108
|
:index="id + token.codeIndex"
|