@docsector/docsector-reader 4.1.0 → 4.2.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.
@@ -22,6 +22,8 @@ const term = ref(null)
22
22
  const founds = ref(false)
23
23
  const items = ref([])
24
24
  const scrolling = ref(null)
25
+ const isMenuHovered = ref(false)
26
+ const pendingScroll = ref(false)
25
27
 
26
28
  const subpage = computed(() => {
27
29
  const parent = $route.matched[0]?.path
@@ -312,41 +314,79 @@ const getMenuItemHeaderLabel = (meta) => {
312
314
  return label // String raw
313
315
  }
314
316
 
317
+ const executeScrollToActiveMenuItem = () => {
318
+ const menu = document.getElementById('menu')
319
+ if (!menu) {
320
+ return
321
+ }
322
+
323
+ const menuItemActive = (menu.getElementsByClassName('q-router-link--active'))[0]
324
+ if (!menuItemActive || typeof menuItemActive !== 'object') {
325
+ return
326
+ }
327
+
328
+ const offsetTop1 = menuItemActive.closest('.menu-list-expansion')?.offsetTop ?? 0
329
+ const offsetTop2 = menuItemActive.offsetTop
330
+
331
+ const innerHeightBy2 = window.innerHeight / 2
332
+
333
+ const searchBarHeight = 50
334
+ let expansionHeaderHeight = 0
335
+ if (offsetTop1 > 0) {
336
+ expansionHeaderHeight = 45
337
+ }
338
+ const fixedHeight = searchBarHeight + expansionHeaderHeight
339
+
340
+ const target = scroll.getScrollTarget(menuItemActive)
341
+ const offset = (offsetTop1 + offsetTop2) - innerHeightBy2 + fixedHeight
342
+ const duration = 300
343
+
344
+ if (offset > 0) {
345
+ scroll.setVerticalScrollPosition(target, offset, duration)
346
+ }
347
+ }
348
+
349
+ const flushPendingMenuScroll = () => {
350
+ if (!pendingScroll.value || isMenuHovered.value) {
351
+ return
352
+ }
353
+
354
+ if (scrolling.value) {
355
+ clearTimeout(scrolling.value)
356
+ scrolling.value = null
357
+ }
358
+
359
+ pendingScroll.value = false
360
+ executeScrollToActiveMenuItem()
361
+ }
362
+
315
363
  const scrollToActiveMenuItem = () => {
364
+ pendingScroll.value = true
365
+
316
366
  if (scrolling.value) {
317
367
  clearTimeout(scrolling.value)
368
+ scrolling.value = null
369
+ }
370
+
371
+ if (isMenuHovered.value) {
372
+ return
318
373
  }
319
374
 
320
375
  scrolling.value = setTimeout(() => {
321
- const menu = document.getElementById('menu')
322
- if (menu) {
323
- const menuItemActive = (menu.getElementsByClassName('q-router-link--active'))[0]
324
- if (menuItemActive && typeof menuItemActive === 'object') {
325
- const offsetTop1 = menuItemActive.closest('.menu-list-expansion')?.offsetTop ?? 0
326
- const offsetTop2 = menuItemActive.offsetTop
327
-
328
- const innerHeightBy2 = window.innerHeight / 2
329
-
330
- const searchBarHeight = 50
331
- let expansionHeaderHeight = 0
332
- if (offsetTop1 > 0) {
333
- expansionHeaderHeight = 45
334
- }
335
- const fixedHeight = searchBarHeight + expansionHeaderHeight
336
-
337
- const target = scroll.getScrollTarget(menuItemActive)
338
- const offset = (offsetTop1 + offsetTop2) - innerHeightBy2 + fixedHeight
339
- const duration = 300
340
-
341
- if (offset > 0) {
342
- scroll.setVerticalScrollPosition(target, offset, duration)
343
- }
344
- }
345
- }
346
376
  scrolling.value = null
377
+ flushPendingMenuScroll()
347
378
  }, 1500)
348
379
  }
349
380
 
381
+ const handleMenuMouseEnter = () => {
382
+ isMenuHovered.value = true
383
+ }
384
+
385
+ const handleMenuMouseLeave = () => {
386
+ isMenuHovered.value = false
387
+ flushPendingMenuScroll()
388
+ }
389
+
350
390
  onMounted(() => {
351
391
  scrollToActiveMenuItem()
352
392
 
@@ -361,6 +401,9 @@ onBeforeUnmount(() => {
361
401
  if (scrolling.value) {
362
402
  clearTimeout(scrolling.value)
363
403
  }
404
+
405
+ isMenuHovered.value = false
406
+ pendingScroll.value = false
364
407
  })
365
408
 
366
409
  const buildMenuItems = () => {
@@ -428,6 +471,8 @@ watch([currentBookId, activeVersionId], rebuildItems)
428
471
  </transition>
429
472
 
430
473
  <q-scroll-area id="menu"
474
+ @mouseenter="handleMenuMouseEnter"
475
+ @mouseleave="handleMenuMouseLeave"
431
476
  :visible="true"
432
477
  :class="$q.dark.isActive ? '' : 'bg-grey-2'"
433
478
  >
@@ -35,6 +35,7 @@ import DBlockTimeline from './DBlockTimeline.vue'
35
35
  import DBlockExpandable from './DBlockExpandable.vue'
36
36
  import DBlockStepper from './DBlockStepper.vue'
37
37
  import DBlockCodeExample from './DBlockCodeExample.vue'
38
+ import DBlockApi from './DBlockApi.vue'
38
39
  </script>
39
40
 
40
41
  <template>
@@ -149,6 +150,13 @@ import DBlockCodeExample from './DBlockCodeExample.vue'
149
150
  :height="token.height"
150
151
  />
151
152
 
153
+ <d-block-api
154
+ v-else-if="token.tag === 'api'"
155
+ :src="token.src"
156
+ :title="token.title"
157
+ :page-link="token.pageLink"
158
+ />
159
+
152
160
  <d-block-mermaid-diagram
153
161
  v-else-if="token.tag === 'mermaid'"
154
162
  :content="token.content"
@@ -0,0 +1,326 @@
1
+ const defaultInnerTabName = '__default'
2
+ const fallbackCategoryName = 'general'
3
+
4
+ const isPlainObject = (value) => {
5
+ return Object.prototype.toString.call(value) === '[object Object]'
6
+ }
7
+
8
+ const isSupportedTopLevelSection = (value) => {
9
+ return typeof value === 'string' || isPlainObject(value)
10
+ }
11
+
12
+ const getEntryCategories = (entry = {}) => {
13
+ const raw = String(entry?.category || '').trim()
14
+
15
+ if (raw === '') {
16
+ return [fallbackCategoryName]
17
+ }
18
+
19
+ const groups = raw
20
+ .split('|')
21
+ .map((value) => value.trim())
22
+ .filter(Boolean)
23
+
24
+ return groups.length === 0 ? [fallbackCategoryName] : groups
25
+ }
26
+
27
+ const pruneInternalEntries = (value) => {
28
+ if (Array.isArray(value)) {
29
+ return value
30
+ .map((entry) => pruneInternalEntries(entry))
31
+ .filter((entry) => entry !== undefined)
32
+ }
33
+
34
+ if (!isPlainObject(value)) {
35
+ return value
36
+ }
37
+
38
+ if (value.internal === true) {
39
+ return undefined
40
+ }
41
+
42
+ const acc = {}
43
+
44
+ Object.entries(value).forEach(([key, entryValue]) => {
45
+ if (key === 'internal') {
46
+ return
47
+ }
48
+
49
+ const nextValue = pruneInternalEntries(entryValue)
50
+
51
+ if (nextValue !== undefined) {
52
+ acc[key] = nextValue
53
+ }
54
+ })
55
+
56
+ return acc
57
+ }
58
+
59
+ const getApiSourceName = (sourceName = '') => {
60
+ const normalized = String(sourceName || '')
61
+ .split('?')[0]
62
+ .split('#')[0]
63
+ .replace(/\\/g, '/')
64
+ const fileName = normalized.split('/').filter(Boolean).pop() || ''
65
+
66
+ return fileName.replace(/\.json$/i, '') || 'API'
67
+ }
68
+
69
+ const isEmptySingleEntry = (value) => {
70
+ if (value === undefined || value === null) {
71
+ return true
72
+ }
73
+
74
+ if (typeof value === 'string') {
75
+ return value.trim() === ''
76
+ }
77
+
78
+ if (Array.isArray(value)) {
79
+ return value.length === 0
80
+ }
81
+
82
+ if (isPlainObject(value)) {
83
+ return Object.keys(value).length === 0
84
+ }
85
+
86
+ return false
87
+ }
88
+
89
+ export const normalizeApiDocsLink = (value = '') => {
90
+ const normalized = String(value || '').trim()
91
+
92
+ if (normalized === '') {
93
+ return ''
94
+ }
95
+
96
+ return normalized
97
+ .replace(/^https:\/\/v[\d]+\.quasar\.dev/i, '')
98
+ .replace(/^https:\/\/quasar\.dev/i, '')
99
+ }
100
+
101
+ export const getPropsCategories = (props = {}) => {
102
+ const acc = new Set()
103
+
104
+ Object.values(props || {}).forEach((value) => {
105
+ if (value !== undefined && value !== null) {
106
+ getEntryCategories(value).forEach((groupKey) => {
107
+ acc.add(groupKey)
108
+ })
109
+ }
110
+ })
111
+
112
+ return acc.size === 1 ? [defaultInnerTabName] : [...acc].sort()
113
+ }
114
+
115
+ export const getInnerTabs = (api = {}, tabs = []) => {
116
+ const acc = {}
117
+
118
+ tabs.forEach((tab) => {
119
+ acc[tab] = tab === 'props' && isPlainObject(api[tab])
120
+ ? getPropsCategories(api[tab])
121
+ : [defaultInnerTabName]
122
+ })
123
+
124
+ return acc
125
+ }
126
+
127
+ export const parseApi = (api = {}, tabs = [], innerTabs = {}) => {
128
+ const acc = {}
129
+
130
+ tabs.forEach((tab) => {
131
+ const apiValue = api[tab]
132
+
133
+ if (innerTabs[tab]?.length > 1 && isPlainObject(apiValue)) {
134
+ const inner = {}
135
+
136
+ innerTabs[tab].forEach((subTab) => {
137
+ inner[subTab] = {}
138
+ })
139
+
140
+ Object.entries(apiValue).forEach(([key, value]) => {
141
+ if (value === undefined || value === null) {
142
+ return
143
+ }
144
+
145
+ getEntryCategories(value).forEach((groupKey) => {
146
+ if (inner[groupKey] !== undefined) {
147
+ inner[groupKey][key] = value
148
+ }
149
+ })
150
+ })
151
+
152
+ acc[tab] = inner
153
+ return
154
+ }
155
+
156
+ acc[tab] = {
157
+ [defaultInnerTabName]: apiValue ?? {}
158
+ }
159
+ })
160
+
161
+ return acc
162
+ }
163
+
164
+ const passesFilter = (filter, name, desc) => {
165
+ const normalizedFilter = String(filter || '').trim().toLowerCase()
166
+
167
+ if (normalizedFilter === '') {
168
+ return true
169
+ }
170
+
171
+ return (
172
+ String(name || '').toLowerCase().includes(normalizedFilter) ||
173
+ String(desc || '').toLowerCase().includes(normalizedFilter)
174
+ )
175
+ }
176
+
177
+ export const getFilteredApi = (parsedApi = {}, filter = '', tabs = [], innerTabs = {}) => {
178
+ const normalizedFilter = String(filter || '').trim().toLowerCase()
179
+
180
+ if (normalizedFilter === '') {
181
+ return parsedApi
182
+ }
183
+
184
+ const acc = {}
185
+
186
+ tabs.forEach((tab) => {
187
+ if (tab === 'injection') {
188
+ const name = parsedApi?.[tab]?.[defaultInnerTabName]
189
+
190
+ acc[tab] = {
191
+ [defaultInnerTabName]: passesFilter(normalizedFilter, name, '') ? name : {}
192
+ }
193
+ return
194
+ }
195
+
196
+ if (tab === 'quasarConfOptions') {
197
+ const api = parsedApi?.[tab]?.[defaultInnerTabName] || {}
198
+ const result = {
199
+ ...api,
200
+ definition: {}
201
+ }
202
+
203
+ Object.entries(api.definition || {}).forEach(([name, entry]) => {
204
+ if (passesFilter(normalizedFilter, name, entry?.desc)) {
205
+ result.definition[name] = entry
206
+ }
207
+ })
208
+
209
+ acc[tab] = {
210
+ [defaultInnerTabName]: Object.keys(result.definition).length === 0 && !passesFilter(normalizedFilter, api.propName, '')
211
+ ? {}
212
+ : result
213
+ }
214
+ return
215
+ }
216
+
217
+ const tabApi = parsedApi?.[tab] || {}
218
+ const tabCategories = innerTabs[tab] || [defaultInnerTabName]
219
+
220
+ acc[tab] = {}
221
+
222
+ tabCategories.forEach((category) => {
223
+ const subTabs = {}
224
+ const categoryEntries = tabApi[category] || {}
225
+
226
+ Object.entries(categoryEntries).forEach(([name, entry]) => {
227
+ if (passesFilter(normalizedFilter, name, entry?.desc)) {
228
+ subTabs[name] = entry
229
+ }
230
+ })
231
+
232
+ acc[tab][category] = subTabs
233
+ })
234
+ })
235
+
236
+ return acc
237
+ }
238
+
239
+ export const getApiCount = (parsedApi = {}, tabs = [], innerTabs = {}) => {
240
+ const acc = {}
241
+
242
+ tabs.forEach((tab) => {
243
+ const tabApi = parsedApi?.[tab] || {}
244
+ const tabCategories = innerTabs[tab] || [defaultInnerTabName]
245
+
246
+ if (['value', 'arg', 'injection'].includes(tab)) {
247
+ const value = tabApi[tabCategories[0]]
248
+
249
+ acc[tab] = {
250
+ overall: isEmptySingleEntry(value) ? 0 : 1
251
+ }
252
+ return
253
+ }
254
+
255
+ if (tab === 'quasarConfOptions') {
256
+ const api = tabApi[tabCategories[0]] || {}
257
+
258
+ acc[tab] = {
259
+ overall: Object.keys(api).length === 0
260
+ ? 0
261
+ : api.definition === undefined
262
+ ? 1
263
+ : Object.keys(api.definition || {}).length
264
+ }
265
+ return
266
+ }
267
+
268
+ const nextValue = {
269
+ overall: 0,
270
+ category: {}
271
+ }
272
+
273
+ tabCategories.forEach((category) => {
274
+ const count = Object.keys(tabApi[category] || {}).length
275
+
276
+ nextValue.category[category] = count
277
+ nextValue.overall += count
278
+ })
279
+
280
+ acc[tab] = nextValue
281
+ })
282
+
283
+ return acc
284
+ }
285
+
286
+ export const createApiBlockModel = (sourceName = '', apiDocument = {}) => {
287
+ const rawDocument = isPlainObject(apiDocument) ? apiDocument : {}
288
+ const {
289
+ type: _type,
290
+ behavior: _behavior,
291
+ meta,
292
+ addedIn: _addedIn,
293
+ internal: _internalSection,
294
+ ...apiSectionsRaw
295
+ } = rawDocument
296
+ const apiSections = {}
297
+
298
+ Object.entries(apiSectionsRaw).forEach(([sectionName, sectionValue]) => {
299
+ const sanitizedValue = pruneInternalEntries(sectionValue)
300
+
301
+ if (sanitizedValue !== undefined && isSupportedTopLevelSection(sanitizedValue)) {
302
+ apiSections[sectionName] = sanitizedValue
303
+ }
304
+ })
305
+
306
+ const tabs = Object.keys(apiSections)
307
+ const innerTabs = getInnerTabs(apiSections, tabs)
308
+ const api = parseApi(apiSections, tabs, innerTabs)
309
+ const sourceLabel = getApiSourceName(sourceName)
310
+ const docsUrl = String(meta?.docsUrl || '').trim()
311
+
312
+ return {
313
+ sourceLabel,
314
+ title: `${sourceLabel} API`,
315
+ docsUrl,
316
+ docsLink: normalizeApiDocsLink(docsUrl),
317
+ tabs,
318
+ innerTabs,
319
+ api,
320
+ nothingToShow: tabs.length === 0
321
+ }
322
+ }
323
+
324
+ export {
325
+ defaultInnerTabName
326
+ }
@@ -21,6 +21,7 @@ const EXPANDABLE_MARKER_PREFIX = '@@DOCSECTOR_EXPANDABLE_'
21
21
  const FILE_MARKER_PREFIX = '@@DOCSECTOR_FILE_'
22
22
  const EMBEDDED_URL_MARKER_PREFIX = '@@DOCSECTOR_EMBEDDED_URL_'
23
23
  const CODE_EXAMPLE_MARKER_PREFIX = '@@DOCSECTOR_CODE_EXAMPLE_'
24
+ const API_BLOCK_MARKER_PREFIX = '@@DOCSECTOR_API_BLOCK_'
24
25
  const CODE_SEGMENT_MARKER_PREFIX = '@@DOCSECTOR_CODE_SEGMENT_'
25
26
  const MATH_KATEX_OPTIONS = {
26
27
  throwOnError: false,
@@ -576,6 +577,43 @@ const extractCodeExampleBlocks = (source = '') => {
576
577
  }
577
578
  }
578
579
 
580
+ const extractApiBlocks = (source = '') => {
581
+ const map = new Map()
582
+ let index = 0
583
+
584
+ const replaceBlock = (match, rawAttrs) => {
585
+ const attrs = parseCustomTagAttributes(rawAttrs)
586
+ const src = decodeHtmlEntities(attrs.src || '').trim()
587
+
588
+ if (!src) {
589
+ return match
590
+ }
591
+
592
+ const marker = `${API_BLOCK_MARKER_PREFIX}${index}@@`
593
+ index++
594
+
595
+ map.set(marker, {
596
+ src,
597
+ title: decodeHtmlEntities(attrs.title || '').trim(),
598
+ pageLink: parseBooleanAttribute(attrs['page-link'], false)
599
+ })
600
+
601
+ return `\n${marker}\n`
602
+ }
603
+
604
+ const replacedSelfClosing = String(source).replace(/<d-block-api\b([^>]*)\/\s*>/gi, (match, rawAttrs) => {
605
+ return replaceBlock(match, rawAttrs)
606
+ })
607
+ const replaced = replacedSelfClosing.replace(/<d-block-api\b([^>]*)>([\s\S]*?)<\/d-block-api>/gi, (match, rawAttrs) => {
608
+ return replaceBlock(match, rawAttrs)
609
+ })
610
+
611
+ return {
612
+ source: replaced,
613
+ apiBlockMap: map
614
+ }
615
+ }
616
+
579
617
  const parseFenceAttributes = (raw = '') => {
580
618
  const parsed = {}
581
619
  const pattern = /([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s;]+))/g
@@ -931,6 +969,7 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
931
969
  const { source: sourceWithFiles, fileMap } = extractFileBlocks(sourceWithQuickLinks)
932
970
  const { source: sourceWithEmbeddedUrls, embeddedUrlMap } = extractEmbeddedUrlBlocks(sourceWithFiles)
933
971
  const { source: sourceWithCodeExamples, codeExampleMap } = extractCodeExampleBlocks(sourceWithEmbeddedUrls)
972
+ const { source: sourceWithApiBlocks, apiBlockMap } = extractApiBlocks(sourceWithCodeExamples)
934
973
 
935
974
  fileMap.forEach((data, marker) => {
936
975
  fileMap.set(marker, {
@@ -956,7 +995,7 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
956
995
  const markdown = createMarkdownBlockParser()
957
996
  const markdownInline = createMarkdownInlineParser()
958
997
  const markdownEnv = {}
959
- const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithCodeExamples, codeSegmentsMap), markdownEnv)
998
+ const parsed = markdown.parse(restoreShieldedCodeSegments(sourceWithApiBlocks, codeSegmentsMap), markdownEnv)
960
999
  const tokens = []
961
1000
 
962
1001
  let level = 0
@@ -1234,6 +1273,19 @@ export const tokenizePageSectionSource = (source = '', options = {}) => {
1234
1273
  break
1235
1274
  }
1236
1275
 
1276
+ if (apiBlockMap.has(element.content.trim())) {
1277
+ const data = apiBlockMap.get(element.content.trim())
1278
+
1279
+ tokens.push({
1280
+ tag: 'api',
1281
+ map: element.map,
1282
+ src: data.src,
1283
+ title: data.title,
1284
+ pageLink: data.pageLink
1285
+ })
1286
+ break
1287
+ }
1288
+
1237
1289
  if (tag === 'p') {
1238
1290
  const imageToken = parseStandaloneImageToken(element.content)
1239
1291
 
@@ -0,0 +1,17 @@
1
+ const normalizeLineBreaks = (text = '') => String(text || '').replace(/\r\n/g, '\n')
2
+
3
+ export const countRenderedCodeLines = (text = '') => {
4
+ const normalized = normalizeLineBreaks(text)
5
+
6
+ if (normalized === '') {
7
+ return 0
8
+ }
9
+
10
+ const withoutTerminalBreak = normalized.replace(/\n$/, '')
11
+
12
+ if (withoutTerminalBreak === '') {
13
+ return 1
14
+ }
15
+
16
+ return withoutTerminalBreak.split('\n').length
17
+ }
@@ -0,0 +1,40 @@
1
+ ## Overview
2
+
3
+ API Reference blocks render a JSON document that follows the existing Quasar API schema directly inside Markdown.
4
+
5
+ This keeps the viewer compatible with Quasar-style API files while still allowing non-Vue APIs to reuse the same section model for props, methods, events, values, arguments, and config shapes.
6
+
7
+ The block is authored with the custom Markdown element `<d-block-api>`.
8
+
9
+ ## Markdown Syntax
10
+
11
+ ```html
12
+ <d-block-api src="/quasar-api/QSeparator.json" />
13
+
14
+ <d-block-api
15
+ src="/api/manual/http-client.json"
16
+ title="HTTP Client API"
17
+ page-link="true"
18
+ />
19
+ ```
20
+
21
+ ## Attributes
22
+
23
+ | Attribute | Purpose |
24
+ |-----------|---------|
25
+ | `src` | Same-origin JSON path to fetch in the browser |
26
+ | `title` | Optional header override shown above the API card |
27
+ | `page-link` | Shows the Docs button when the JSON has `meta.docsUrl` |
28
+
29
+ ## JSON Source Model
30
+
31
+ - The first implementation follows the same delivery model as Quasar Docs: the JSON file is served as a public asset and fetched on demand.
32
+ - No Docsector-specific schema is required. If your file already follows the Quasar API structure, it can be rendered as-is.
33
+ - Non-Vue APIs can still use the same shape by filling the sections they need, such as `props`, `methods`, `events`, `value`, `arg`, or `quasarConfOptions`.
34
+
35
+ ## Notes
36
+
37
+ - `props` are grouped into subtabs when more than one `category` is present.
38
+ - Entries marked with `internal: true` are hidden from the rendered block.
39
+ - The current version expects same-origin JSON assets so the browser can fetch them without CORS workarounds.
40
+ - If the JSON exposes `meta.docsUrl`, `page-link="true"` can surface a Docs button without changing the schema.
@@ -0,0 +1,40 @@
1
+ ## Visão geral
2
+
3
+ Os blocos de Referência de API renderizam um documento JSON que segue o schema de API já existente do Quasar diretamente dentro do Markdown.
4
+
5
+ Isso mantém o viewer compatível com arquivos de API no estilo do Quasar e ainda permite que APIs não-Vue reutilizem o mesmo modelo de seções para props, methods, events, values, arguments e estruturas de configuração.
6
+
7
+ O bloco é escrito com o elemento Markdown customizado `<d-block-api>`.
8
+
9
+ ## Sintaxe Markdown
10
+
11
+ ```html
12
+ <d-block-api src="/quasar-api/QSeparator.json" />
13
+
14
+ <d-block-api
15
+ src="/api/manual/http-client.json"
16
+ title="HTTP Client API"
17
+ page-link="true"
18
+ />
19
+ ```
20
+
21
+ ## Atributos
22
+
23
+ | Atributo | Finalidade |
24
+ |----------|------------|
25
+ | `src` | Caminho same-origin do JSON a ser buscado no navegador |
26
+ | `title` | Sobrescreve opcionalmente o título exibido acima do card |
27
+ | `page-link` | Exibe o botão Docs quando o JSON possui `meta.docsUrl` |
28
+
29
+ ## Modelo da Fonte JSON
30
+
31
+ - A primeira implementação segue o mesmo modelo de entrega do Quasar Docs: o arquivo JSON é servido como asset público e carregado sob demanda.
32
+ - Nenhum schema específico do Docsector é exigido. Se o arquivo já seguir a estrutura de API do Quasar, ele pode ser renderizado sem alterações.
33
+ - APIs não-Vue ainda podem usar a mesma forma preenchendo apenas as seções necessárias, como `props`, `methods`, `events`, `value`, `arg` ou `quasarConfOptions`.
34
+
35
+ ## Notas
36
+
37
+ - `props` são agrupadas em subtabs quando mais de uma `category` está presente.
38
+ - Entradas marcadas com `internal: true` são ocultadas do bloco renderizado.
39
+ - A versão atual espera assets JSON same-origin para que o navegador faça o fetch sem workarounds de CORS.
40
+ - Se o JSON expuser `meta.docsUrl`, `page-link="true"` pode exibir um botão Docs sem alterar o schema.
@@ -0,0 +1,33 @@
1
+ ## Showcase
2
+
3
+ ### Quasar JSON Without Refactoring
4
+
5
+ This example renders a real Quasar API JSON file copied into `public/quasar-api/`.
6
+
7
+ <d-block-api src="/quasar-api/QSeparator.json" />
8
+
9
+ ### Generic SDK JSON With the Same Schema
10
+
11
+ This example uses the same section model for a non-Vue HTTP client and also enables the optional Docs button.
12
+
13
+ <d-block-api src="/api/manual/http-client.json" title="HTTP Client API" page-link="true" />
14
+
15
+ ## Authoring Syntax
16
+
17
+ ```html
18
+ <d-block-api src="/quasar-api/QSeparator.json" />
19
+
20
+ <d-block-api
21
+ src="/api/manual/http-client.json"
22
+ title="HTTP Client API"
23
+ page-link="true"
24
+ />
25
+ ```
26
+
27
+ ## Features Visible Above
28
+
29
+ - **Quasar JSON compatibility** with a real file served from `public/quasar-api/`
30
+ - **Generic API support** without introducing a new schema
31
+ - **Local filter** across names and descriptions inside the loaded API sections
32
+ - **Grouped props subtabs** when multiple categories exist in the JSON
33
+ - **Optional Docs link** when `meta.docsUrl` is present and `page-link="true"` is used