@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.
@@ -6,15 +6,13 @@
6
6
 
7
7
  import {
8
8
  h,
9
+ Teleport,
9
10
  ref, computed,
10
11
  onMounted, onBeforeUnmount
11
12
  } from 'vue'
12
13
 
13
14
  import { useColorize } from 'q-colorize-mixin'
14
15
 
15
- import { dom } from 'quasar'
16
- const { offset } = dom
17
-
18
16
  import './QZoom.sass'
19
17
 
20
18
  // @
@@ -34,7 +32,12 @@ export default {
34
32
  initialScaleText: { type: Number, default: 100, validator: v => v >= 50 && v <= 500 },
35
33
  noCenter: Boolean,
36
34
  noWheelScale: Boolean,
37
- noEscClose: Boolean
35
+ noEscClose: Boolean,
36
+ showCloseButton: Boolean,
37
+ closeButtonLabel: {
38
+ type: String,
39
+ default: 'Close zoom'
40
+ }
38
41
  },
39
42
 
40
43
  setup (props, { emit, slots }) {
@@ -183,13 +186,29 @@ export default {
183
186
  }
184
187
  }
185
188
 
189
+ const onOverlayClick = (e) => {
190
+ if (isZoomed.value) {
191
+ hide()
192
+
193
+ e.preventDefault()
194
+ }
195
+ }
196
+
197
+ const stopEvent = (e) => {
198
+ e.stopPropagation()
199
+ }
200
+
186
201
  const getPosition = () => {
187
- const position = offset(vComponent.value)
202
+ const rect = vComponent.value.getBoundingClientRect()
203
+ const position = {
204
+ left: rect.left,
205
+ top: rect.top
206
+ }
188
207
 
189
208
  position.left = position.left + 'px'
190
209
  position.top = position.top + 'px'
191
- position.width = vComponent.value.clientWidth + 'px'
192
- position.height = vComponent.value.clientHeight + 'px'
210
+ position.width = rect.width + 'px'
211
+ position.height = rect.height + 'px'
193
212
 
194
213
  return position
195
214
  }
@@ -245,7 +264,34 @@ export default {
245
264
  fontSize: props.scaleText && !props.scale && `${scaleTextValue.value}%`
246
265
  }
247
266
  }), [
248
- slot && slot({ zoomed: isZoomed.value })
267
+ h('div', {
268
+ class: 'q-zoom__content-inner',
269
+ onClick: stopEvent
270
+ }, [
271
+ slot && slot({ zoomed: isZoomed.value })
272
+ ])
273
+ ])
274
+ }
275
+
276
+ const __renderCloseButton = () => {
277
+ if (!props.showCloseButton) {
278
+ return null
279
+ }
280
+
281
+ return h('button', {
282
+ type: 'button',
283
+ class: 'q-zoom__close-button',
284
+ 'aria-label': props.closeButtonLabel,
285
+ title: props.closeButtonLabel,
286
+ onClick: (e) => {
287
+ stopEvent(e)
288
+ hide()
289
+ }
290
+ }, [
291
+ h('span', {
292
+ class: 'q-zoom__close-icon',
293
+ 'aria-hidden': 'true'
294
+ }, '×')
249
295
  ])
250
296
  }
251
297
 
@@ -256,13 +302,18 @@ export default {
256
302
 
257
303
  return h('div', Colorize.setBackgroundColor(bgColor.value, {
258
304
  class: 'q-zoom__overlay' +
259
- (props.manual ? '' : ' q-zoom__zoom-out')
305
+ (props.manual ? '' : ' q-zoom__zoom-out'),
306
+ onClick: onOverlayClick
260
307
  }), [
308
+ __renderCloseButton(),
261
309
  __renderOverlayContent()
262
310
  ])
263
311
  }
264
312
 
265
- return () => h('div', {
313
+ return () => {
314
+ const overlay = __renderOverlay()
315
+
316
+ return h('div', {
266
317
  class: 'q-zoom' +
267
318
  (props.manual ? '' : ' q-zoom__zoom-in'),
268
319
 
@@ -270,9 +321,12 @@ export default {
270
321
  onWheel: wheelEvent,
271
322
 
272
323
  ref: vComponent
273
- }, [
274
- slots.default && slots.default({ zoomed: isZoomed.value }),
275
- __renderOverlay()
276
- ])
324
+ }, [
325
+ slots.default && slots.default({ zoomed: isZoomed.value }),
326
+ overlay
327
+ ? h(Teleport, { to: 'body' }, overlay)
328
+ : null
329
+ ])
330
+ }
277
331
  }
278
332
  }
@@ -22,19 +22,54 @@ $box-shadow: 1px 1px 7px 1px rgba(0,0,0,.2) !default
22
22
  padding: 0
23
23
  margin: 0
24
24
  z-index: 6000
25
+ overflow: hidden
25
26
 
26
27
  &__content
27
- position: relative
28
- display: block
28
+ position: absolute
29
+ display: flex
30
+ align-items: center
31
+ justify-content: center
29
32
  transition: all .5s cubic-bezier(.2,0,.2,1)
30
33
  text-align: center
31
- vertical-align: middle
32
34
  width: 100%
33
35
  height: 0
34
36
  max-width: 100%
35
37
  max-height: 100%
36
38
  overflow: hidden
37
39
 
40
+ & > *
41
+ max-width: 100%
42
+ max-height: 100%
43
+
44
+ &__content-inner
45
+ display: inline-flex
46
+ align-items: center
47
+ justify-content: center
48
+ max-width: 100%
49
+ max-height: 100%
50
+
51
+ &__close-button
52
+ position: absolute
53
+ top: 24px
54
+ right: 24px
55
+ width: 56px
56
+ height: 56px
57
+ display: inline-flex
58
+ align-items: center
59
+ justify-content: center
60
+ border: 0
61
+ border-radius: 999px
62
+ background: rgba(255,255,255,.12)
63
+ color: #fff
64
+ cursor: pointer
65
+ z-index: 6001
66
+ box-shadow: $box-shadow
67
+ backdrop-filter: blur(8px)
68
+
69
+ &__close-icon
70
+ font-size: 34px
71
+ line-height: 1
72
+
38
73
  &__no-center
39
74
  text-align: unset
40
75
  vertical-align: unset
@@ -14,6 +14,7 @@ const ALERT_MESSAGE_TYPES = new Set([
14
14
 
15
15
  const QUICK_LINKS_MARKER_PREFIX = '@@DOCSECTOR_QUICK_LINKS_'
16
16
  const EXPANDABLE_MARKER_PREFIX = '@@DOCSECTOR_EXPANDABLE_'
17
+ const FILE_MARKER_PREFIX = '@@DOCSECTOR_FILE_'
17
18
  const CODE_SEGMENT_MARKER_PREFIX = '@@DOCSECTOR_CODE_SEGMENT_'
18
19
  const MATH_KATEX_OPTIONS = {
19
20
  throwOnError: false,
@@ -190,6 +191,62 @@ const extractExpandableBlocks = (source = '') => {
190
191
  }
191
192
  }
192
193
 
194
+ const getFileTitleFromSrc = (src = '') => {
195
+ const normalized = String(src)
196
+ .split('#')[0]
197
+ .split('?')[0]
198
+ const rawSegment = normalized
199
+ .split('/')
200
+ .filter(Boolean)
201
+ .pop() || ''
202
+
203
+ if (!rawSegment) {
204
+ return 'Download file'
205
+ }
206
+
207
+ try {
208
+ return decodeURIComponent(rawSegment)
209
+ } catch {
210
+ return rawSegment
211
+ }
212
+ }
213
+
214
+ const extractFileBlocks = (source = '') => {
215
+ const map = new Map()
216
+ let index = 0
217
+
218
+ const replaceBlock = (match, rawAttrs, rawCaption = '') => {
219
+ const attrs = parseCustomTagAttributes(rawAttrs)
220
+ const src = decodeHtmlEntities(attrs.src || attrs.href || '').trim()
221
+
222
+ if (!src) {
223
+ return match
224
+ }
225
+
226
+ const marker = `${FILE_MARKER_PREFIX}${index}@@`
227
+ index++
228
+
229
+ map.set(marker, {
230
+ src,
231
+ title: decodeHtmlEntities(attrs.title || attrs.name || '').trim(),
232
+ size: decodeHtmlEntities(attrs.size || '').trim(),
233
+ caption: String(rawCaption).trim()
234
+ })
235
+
236
+ return `\n${marker}\n`
237
+ }
238
+
239
+ const replacedSelfClosing = String(source).replace(/<d-file\b([^>]*)\/\s*>/gi, (match, rawAttrs) => {
240
+ return replaceBlock(match, rawAttrs)
241
+ })
242
+ const replaced = replacedSelfClosing.replace(/<d-file\b([^>]*)>([\s\S]*?)<\/d-file>/gi, replaceBlock)
243
+
244
+ return {
245
+ source: replaced,
246
+ fileMap: map
247
+ }
248
+ }
249
+
193
250
  const parseFenceAttributes = (raw = '') => {
194
251
  const parsed = {}
195
252
  const pattern = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s;]+))/g
@@ -215,6 +272,97 @@ const parseTokenAttributes = (element) => {
215
272
  return parsed
216
273
  }
217
274
 
275
+ const decodeHtmlEntities = (value = '') => {
276
+ return String(value)
277
+ .replace(/&quot;/g, '"')
278
+ .replace(/&#39;|&apos;/g, "'")
279
+ .replace(/&lt;/g, '<')
280
+ .replace(/&gt;/g, '>')
281
+ .replace(/&amp;/g, '&')
282
+ }
283
+
284
+ const escapeHtml = (value = '') => {
285
+ return String(value)
286
+ .replace(/&/g, '&amp;')
287
+ .replace(/</g, '&lt;')
288
+ .replace(/>/g, '&gt;')
289
+ .replace(/"/g, '&quot;')
290
+ .replace(/'/g, '&#39;')
291
+ }
292
+
293
+ const extractTagAttributes = (html = '', tagName = '') => {
294
+ const match = String(html).match(new RegExp(`<${tagName}\\b([^>]*)\\/?>(?![\\s\\S]*<${tagName}\\b)`, 'i'))
295
+
296
+ if (!match) {
297
+ return {}
298
+ }
299
+
300
+ return parseCustomTagAttributes(match[1])
301
+ }
302
+
303
+ const createImageTokenData = (mediaHtml = '', options = {}) => {
304
+ const {
305
+ captionHtml = '',
306
+ fallbackCaptionFromAlt = false
307
+ } = options
308
+ const attrs = extractTagAttributes(mediaHtml, 'img')
309
+ const alt = decodeHtmlEntities(attrs.alt || '')
310
+ const title = decodeHtmlEntities(attrs.title || '')
311
+
312
+ return {
313
+ tag: 'image',
314
+ content: String(mediaHtml).trim(),
315
+ alt,
316
+ title,
317
+ captionHtml: captionHtml !== ''
318
+ ? captionHtml
319
+ : (fallbackCaptionFromAlt ? escapeHtml(alt) : '')
320
+ }
321
+ }
322
+
323
+ const parseStandaloneImageToken = (content = '') => {
324
+ const trimmed = String(content).trim()
325
+
326
+ if (!trimmed.match(/^<img\b[^>]*\/?>(?:\s*)$/i)) {
327
+ return null
328
+ }
329
+
330
+ return createImageTokenData(trimmed, {
331
+ fallbackCaptionFromAlt: true
332
+ })
333
+ }
334
+
335
+ const parseFigureImageToken = (content = '') => {
336
+ const trimmed = String(content).trim()
337
+
338
+ if (!trimmed.match(/^<figure\b[\s\S]*<\/figure>$/i)) {
339
+ return null
340
+ }
341
+
342
+ const figureBody = trimmed
343
+ .replace(/^<figure\b[^>]*>/i, '')
344
+ .replace(/<\/figure>$/i, '')
345
+ .trim()
346
+ const figcaptionMatch = figureBody.match(/<figcaption\b[^>]*>([\s\S]*?)<\/figcaption>/i)
347
+ const mediaBody = figcaptionMatch
348
+ ? figureBody.replace(figcaptionMatch[0], '').trim()
349
+ : figureBody
350
+ const pictureMatch = mediaBody.match(/^<picture\b[\s\S]*?<\/picture>$/i)
351
+ const imgMatch = pictureMatch
352
+ ? pictureMatch[0].match(/<img\b[^>]*\/?>/i)
353
+ : mediaBody.match(/^<img\b[^>]*\/?>$/i)
354
+
355
+ if (!imgMatch) {
356
+ return null
357
+ }
358
+
359
+ const mediaHtml = pictureMatch ? pictureMatch[0] : imgMatch[0]
360
+
361
+ return createImageTokenData(mediaHtml, {
362
+ captionHtml: figcaptionMatch ? figcaptionMatch[1].trim() : ''
363
+ })
364
+ }
365
+
218
366
  const parseFenceLanguage = (raw = '') => {
219
367
  const cleaned = String(raw)
220
368
  .replace(/:[^;]*;/g, ' ')
@@ -378,10 +526,19 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
378
526
  })
379
527
 
380
528
  const { source: sourceWithQuickLinks, quickLinksMap } = extractQuickLinksBlocks(sourceWithExpandables)
529
+ const { source: sourceWithFiles, fileMap } = extractFileBlocks(sourceWithQuickLinks)
530
+
531
+ fileMap.forEach((data, marker) => {
532
+ fileMap.set(marker, {
533
+ ...data,
534
+ caption: restoreShieldedCodeSegments(data.caption, codeSegmentsMap)
535
+ })
536
+ })
537
+
381
538
  const markdown = createMarkdownBlockParser()
382
539
  const markdownInline = createMarkdownInlineParser()
383
540
  const markdownEnv = {}
384
- const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithQuickLinks, codeSegmentsMap), markdownEnv)
541
+ const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithFiles, codeSegmentsMap), markdownEnv)
385
542
  const tokens = []
386
543
 
387
544
  let level = 0
@@ -520,7 +677,7 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
520
677
  }
521
678
 
522
679
  switch (element.type) {
523
- case 'inline':
680
+ case 'inline': {
524
681
  const anchorId = getHeadingAnchorId(markdown, tag, element, markdownEnv, parserState)
525
682
 
526
683
  if (expandableMap.has(element.content.trim())) {
@@ -549,6 +706,34 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
549
706
  break
550
707
  }
551
708
 
709
+ if (fileMap.has(element.content.trim())) {
710
+ const data = fileMap.get(element.content.trim())
711
+
712
+ tokens.push({
713
+ tag: 'file',
714
+ map: element.map,
715
+ src: data.src,
716
+ title: data.title || getFileTitleFromSrc(data.src),
717
+ size: data.size,
718
+ caption: data.caption !== ''
719
+ ? markdownInline.renderInline(data.caption, markdownEnv)
720
+ : ''
721
+ })
722
+ break
723
+ }
724
+
725
+ if (tag === 'p') {
726
+ const imageToken = parseStandaloneImageToken(element.content)
727
+
728
+ if (imageToken !== null) {
729
+ tokens.push({
730
+ ...imageToken,
731
+ map: element.map
732
+ })
733
+ break
734
+ }
735
+ }
736
+
552
737
  tokens.push({
553
738
  tag,
554
739
  map: element.map,
@@ -557,6 +742,7 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
557
742
  info: element.info
558
743
  })
559
744
  break
745
+ }
560
746
 
561
747
  case 'fence':
562
748
  pushSourceCodeToken(tokens, element, parserState)
@@ -569,36 +755,59 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
569
755
  })
570
756
  break
571
757
 
572
- case 'html_block':
758
+ case 'html_block': {
759
+ const figureImageToken = parseFigureImageToken(element.content)
760
+
761
+ if (figureImageToken !== null) {
762
+ tokens.push({
763
+ ...figureImageToken,
764
+ map: element.map
765
+ })
766
+ break
767
+ }
768
+
573
769
  tokens.push({
574
770
  tag: 'html',
575
771
  content: element.content
576
772
  })
577
773
  break
774
+ }
578
775
  }
579
- } else if (level === 1) {
776
+ } else if (level > 0) {
580
777
  const parent = tokens[tokens.length - 1]
581
778
 
582
779
  switch (element.type) {
583
780
  case 'bullet_list_open':
584
- tokens.push({
585
- tag: 'ul',
586
- content: ''
587
- })
781
+ if (level === 1) {
782
+ tokens.push({
783
+ tag: 'ul',
784
+ content: ''
785
+ })
786
+ } else {
787
+ parent.content += '<ul>'
788
+ }
588
789
  break
589
790
 
590
791
  case 'ordered_list_open':
591
- tokens.push({
592
- tag: 'ol',
593
- content: ''
594
- })
792
+ if (level === 1) {
793
+ tokens.push({
794
+ tag: 'ol',
795
+ content: ''
796
+ })
797
+ } else {
798
+ parent.content += '<ol>'
799
+ }
595
800
  break
596
801
 
597
802
  case 'table_open':
598
- tokens.push({
599
- tag: 'table',
600
- content: ''
601
- })
803
+ if (level === 1) {
804
+ tokens.push({
805
+ tag: 'table',
806
+ content: ''
807
+ })
808
+ } else {
809
+ parent.content += '<table>'
810
+ }
602
811
  break
603
812
 
604
813
  case 'list_item_open':
@@ -636,6 +845,24 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
636
845
  parent.content += '</li>'
637
846
  break
638
847
 
848
+ case 'bullet_list_close':
849
+ if (level > 1) {
850
+ parent.content += '</ul>'
851
+ }
852
+ break
853
+
854
+ case 'ordered_list_close':
855
+ if (level > 1) {
856
+ parent.content += '</ol>'
857
+ }
858
+ break
859
+
860
+ case 'table_close':
861
+ if (level > 1) {
862
+ parent.content += '</table>'
863
+ }
864
+ break
865
+
639
866
  case 'thead_close':
640
867
  parent.content += '</thead>'
641
868
  break
@@ -38,6 +38,13 @@
38
38
  copied: 'Copied!',
39
39
  viewAsMarkdown: 'View as Markdown',
40
40
  viewAsMarkdownCaption: 'View this page as plain text',
41
+ file: {
42
+ label: 'File',
43
+ defaultTitle: 'Download file',
44
+ external: 'External file',
45
+ download: 'Download',
46
+ open: 'Open'
47
+ },
41
48
  openInChatGPT: 'Open in ChatGPT',
42
49
  openInChatGPTCaption: 'Ask ChatGPT about this page',
43
50
  openInClaude: 'Open in Claude',
@@ -37,6 +37,13 @@
37
37
  copied: 'Copiado!',
38
38
  viewAsMarkdown: 'Ver como Markdown',
39
39
  viewAsMarkdownCaption: 'Ver esta página como texto simples',
40
+ file: {
41
+ label: 'Arquivo',
42
+ defaultTitle: 'Baixar arquivo',
43
+ external: 'Arquivo externo',
44
+ download: 'Baixar',
45
+ open: 'Abrir'
46
+ },
40
47
  openInChatGPT: 'Abrir no ChatGPT',
41
48
  openInChatGPTCaption: 'Pergunte ao ChatGPT sobre esta página',
42
49
  openInClaude: 'Abrir no Claude',
@@ -45,7 +45,7 @@ See the dedicated manual pages for block-by-block reference:
45
45
 
46
46
  - [Paragraphs](/manual/content/blocks/paragraphs/overview/), [Headings](/manual/content/blocks/headings/overview/), [Unordered lists](/manual/content/blocks/unordered-lists/overview/), [Ordered lists](/manual/content/blocks/ordered-lists/overview/)
47
47
  - [Hints](/manual/content/blocks/hints/overview/), [Quote](/manual/content/blocks/quotes/overview/), [Code blocks](/manual/content/blocks/code-blocks/overview/), [Mermaid diagrams](/manual/content/blocks/mermaid-diagrams/overview/)
48
- - [Images](/manual/content/blocks/images/overview/), [Math & TeX](/manual/content/blocks/math-and-tex/overview/), [Expandable](/manual/content/blocks/expandable/overview/), [Tables](/manual/content/blocks/tables/overview/), [Raw HTML](/manual/content/blocks/raw-html/overview/), and [Quick Links](/manual/content/blocks/quick-links/overview/)
48
+ - [Images](/manual/content/blocks/images/overview/), [Files](/manual/content/blocks/files/overview/), [Math & TeX](/manual/content/blocks/math-and-tex/overview/), [Expandable](/manual/content/blocks/expandable/overview/), [Tables](/manual/content/blocks/tables/overview/), [Raw HTML](/manual/content/blocks/raw-html/overview/), and [Quick Links](/manual/content/blocks/quick-links/overview/)
49
49
 
50
50
  ### Headings
51
51
 
@@ -132,6 +132,20 @@ Set `open="true"` when the block should start expanded.
132
132
 
133
133
  The expandable body supports paragraphs, lists, alerts, code blocks, Mermaid diagrams, tables, raw HTML, and quick links. Keep headings outside the expandable block in this first version, because headings inside the body are flattened to regular paragraphs to preserve the page ToC.
134
134
 
135
+ ### File Blocks
136
+
137
+ Use `<d-file>` to publish downloadable attachments from repo-tracked files or external storage without leaving Markdown:
138
+
139
+ ```html
140
+ <d-file src="/files/manual/release-checklist.txt" title="Release checklist" size="1 KB">
141
+ Download the example attachment used in this manual.
142
+ </d-file>
143
+ ```
144
+
145
+ Store repo-tracked files under `public/files/` and prefer absolute site paths such as `/files/...`.
146
+
147
+ `title` and `size` are optional. When `title` is omitted, the UI falls back to the file name from `src`. The caption body supports inline Markdown, and the same syntax also works with external URLs if you later move storage to R2 or another CDN.
148
+
135
149
  ## Adding a New Language
136
150
 
137
151
  1. Create `src/i18n/languages/xx-XX.hjson` with all UI translations
@@ -45,7 +45,7 @@ Veja também as páginas dedicadas do manual para cada bloco:
45
45
 
46
46
  - [Parágrafos](/manual/content/blocks/paragraphs/overview/), [Títulos](/manual/content/blocks/headings/overview/), [Listas não ordenadas](/manual/content/blocks/unordered-lists/overview/), [Listas ordenadas](/manual/content/blocks/ordered-lists/overview/)
47
47
  - [Hints](/manual/content/blocks/hints/overview/), [Citação](/manual/content/blocks/quotes/overview/), [Blocos de código](/manual/content/blocks/code-blocks/overview/), [Diagramas Mermaid](/manual/content/blocks/mermaid-diagrams/overview/)
48
- - [Imagens](/manual/content/blocks/images/overview/), [Math & TeX](/manual/content/blocks/math-and-tex/overview/), [Expansível](/manual/content/blocks/expandable/overview/), [Tabelas](/manual/content/blocks/tables/overview/), [HTML bruto](/manual/content/blocks/raw-html/overview/) e [Quick Links](/manual/content/blocks/quick-links/overview/)
48
+ - [Imagens](/manual/content/blocks/images/overview/), [Arquivos](/manual/content/blocks/files/overview/), [Math & TeX](/manual/content/blocks/math-and-tex/overview/), [Expansível](/manual/content/blocks/expandable/overview/), [Tabelas](/manual/content/blocks/tables/overview/), [HTML bruto](/manual/content/blocks/raw-html/overview/) e [Quick Links](/manual/content/blocks/quick-links/overview/)
49
49
 
50
50
  ### Títulos
51
51
 
@@ -132,6 +132,20 @@ Defina `open="true"` quando o bloco precisar começar aberto.
132
132
 
133
133
  O corpo do expansível suporta parágrafos, listas, alertas, blocos de código, diagramas Mermaid, tabelas, HTML bruto e quick links. Nesta primeira versão, mantenha títulos fora do bloco expansível, porque títulos dentro do corpo viram parágrafos comuns para preservar o ToC da página.
134
134
 
135
+ ### Blocos de Arquivo
136
+
137
+ Use `<d-file>` para publicar anexos baixáveis a partir de arquivos versionados no repositório ou de storage externo sem sair do Markdown:
138
+
139
+ ```html
140
+ <d-file src="/files/manual/release-checklist.txt" title="Checklist de release" size="1 KB">
141
+ Baixe o anexo de exemplo usado neste manual.
142
+ </d-file>
143
+ ```
144
+
145
+ Guarde arquivos versionados no repositório em `public/files/` e prefira caminhos absolutos do site, como `/files/...`.
146
+
147
+ `title` e `size` são opcionais. Quando `title` não é informado, a UI usa o nome do arquivo presente em `src`. O corpo do bloco funciona como legenda com Markdown inline, e a mesma sintaxe também aceita URLs externas caso você mova o storage para R2 ou outro CDN no futuro.
148
+
135
149
  ## Adicionando um Novo Idioma
136
150
 
137
151
  1. Crie `src/i18n/languages/xx-XX.hjson` com todas as traduções de UI
@@ -0,0 +1,27 @@
1
+ ## Overview
2
+
3
+ Files render a download card directly inside Markdown so a page or subpage can publish attachments without leaving the normal authoring flow.
4
+
5
+ They are useful for checklists, sample bundles, PDFs, release notes, and any other file that should be linked from the reading flow.
6
+
7
+ ## Markdown Syntax
8
+
9
+ ```html
10
+ <d-file src="/files/manual/release-checklist.txt" title="Release checklist" size="1 KB">
11
+ Download the example attachment used in this manual.
12
+ </d-file>
13
+ ```
14
+
15
+ You can also omit the caption body when the file name already provides enough context:
16
+
17
+ ```html
18
+ <d-file src="/files/manual/release-checklist.txt" size="1 KB" />
19
+ ```
20
+
21
+ ## Notes
22
+
23
+ - Store small repo-tracked attachments under `public/files/` and prefer absolute paths such as `/files/...`.
24
+ - `src` is required. `title` and `size` are optional.
25
+ - When `title` is omitted, the rendered card falls back to the file name from `src`.
26
+ - The block body is rendered as an inline Markdown caption.
27
+ - External URLs also work, so the same syntax can point to a future R2 bucket or another CDN.
@@ -0,0 +1,27 @@
1
+ ## Visão Geral
2
+
3
+ Arquivos renderizam um card de download diretamente no Markdown para que uma page ou subpage publique anexos sem sair do fluxo normal de autoria.
4
+
5
+ Eles são úteis para checklists, bundles de exemplo, PDFs, notas de release e qualquer outro arquivo que precise ficar no fluxo de leitura.
6
+
7
+ ## Sintaxe em Markdown
8
+
9
+ ```html
10
+ <d-file src="/files/manual/release-checklist.txt" title="Checklist de release" size="1 KB">
11
+ Baixe o anexo de exemplo usado neste manual.
12
+ </d-file>
13
+ ```
14
+
15
+ Você também pode omitir o corpo da legenda quando o nome do arquivo já fornece contexto suficiente:
16
+
17
+ ```html
18
+ <d-file src="/files/manual/release-checklist.txt" size="1 KB" />
19
+ ```
20
+
21
+ ## Observações
22
+
23
+ - Guarde anexos pequenos versionados no repositório em `public/files/` e prefira caminhos absolutos como `/files/...`.
24
+ - `src` é obrigatório. `title` e `size` são opcionais.
25
+ - Quando `title` não é informado, o card renderizado usa o nome do arquivo presente em `src`.
26
+ - O corpo do bloco é renderizado como legenda com Markdown inline.
27
+ - URLs externas também funcionam, então a mesma sintaxe pode apontar no futuro para um bucket R2 ou outro CDN.