@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.
@@ -0,0 +1,634 @@
1
+ <script setup>
2
+ import { computed, ref, watch } from 'vue'
3
+ import { mdiClose, mdiMagnify } from '@quasar/extras/mdi-v7'
4
+ import { useRouter } from 'vue-router'
5
+
6
+ import DBlockApiEntry from './DBlockApiEntry.js'
7
+ import {
8
+ createApiBlockModel,
9
+ defaultInnerTabName,
10
+ getApiCount,
11
+ getFilteredApi
12
+ } from './api-block-model'
13
+
14
+ const BASE_URL = import.meta.env.BASE_URL || '/'
15
+ const router = useRouter()
16
+
17
+ defineOptions({
18
+ name: 'DBlockApi'
19
+ })
20
+
21
+ const props = defineProps({
22
+ src: {
23
+ type: String,
24
+ required: true
25
+ },
26
+ title: {
27
+ type: String,
28
+ default: ''
29
+ },
30
+ pageLink: Boolean
31
+ })
32
+
33
+ const inputRef = ref(null)
34
+ const filter = ref('')
35
+ const loading = ref(true)
36
+ const errorMessage = ref('')
37
+ const apiModel = ref(createApiBlockModel(props.src, {}))
38
+ const currentTab = ref(null)
39
+ const currentInnerTab = ref(defaultInnerTabName)
40
+ const scrollAreaHeights = ref({})
41
+
42
+ const SCROLL_AREA_MAX_HEIGHT = 640
43
+
44
+ let requestIndex = 0
45
+
46
+ const inputIcon = computed(() => (filter.value !== '' ? mdiClose : mdiMagnify))
47
+ const nameBanner = computed(() => props.title || apiModel.value.title || 'API reference')
48
+ const nothingToShow = computed(() => apiModel.value.nothingToShow)
49
+ const tabsList = computed(() => apiModel.value.tabs)
50
+ const innerTabsList = computed(() => apiModel.value.innerTabs)
51
+ const filteredApi = computed(() => {
52
+ return getFilteredApi(
53
+ apiModel.value.api,
54
+ filter.value,
55
+ tabsList.value,
56
+ innerTabsList.value
57
+ )
58
+ })
59
+ const filteredApiCount = computed(() => {
60
+ return getApiCount(filteredApi.value, tabsList.value, innerTabsList.value)
61
+ })
62
+ const docsLink = computed(() => {
63
+ if (!props.pageLink || !apiModel.value.docsLink) {
64
+ return ''
65
+ }
66
+
67
+ return apiModel.value.docsLink
68
+ })
69
+
70
+ watch(tabsList, (tabs) => {
71
+ currentTab.value = tabs[0] || null
72
+ }, {
73
+ immediate: true
74
+ })
75
+
76
+ watch(currentTab, (value) => {
77
+ const nextInnerTabs = innerTabsList.value[value] || [defaultInnerTabName]
78
+ currentInnerTab.value = nextInnerTabs[0]
79
+ })
80
+
81
+ watch(() => props.src, () => {
82
+ loadApi()
83
+ }, {
84
+ immediate: true
85
+ })
86
+
87
+ function normalizeResourcePath(value = '') {
88
+ const raw = String(value || '').trim()
89
+
90
+ if (raw === '') {
91
+ return ''
92
+ }
93
+
94
+ if (/^(?:[a-z]+:)?\/\//i.test(raw)) {
95
+ return raw
96
+ }
97
+
98
+ const trimmedBase = String(BASE_URL).replace(/\/$/, '')
99
+
100
+ if (raw.startsWith('/')) {
101
+ return `${trimmedBase}${raw}` || raw
102
+ }
103
+
104
+ const normalized = raw.replace(/^\.\//, '')
105
+
106
+ return `${trimmedBase}/${normalized}`
107
+ }
108
+
109
+ async function loadApi() {
110
+ const currentRequest = ++requestIndex
111
+
112
+ loading.value = true
113
+ errorMessage.value = ''
114
+ filter.value = ''
115
+ apiModel.value = createApiBlockModel(props.src, {})
116
+ scrollAreaHeights.value = {}
117
+
118
+ const resolvedSrc = normalizeResourcePath(props.src)
119
+
120
+ if (!resolvedSrc) {
121
+ errorMessage.value = 'API source is missing.'
122
+ loading.value = false
123
+ return
124
+ }
125
+
126
+ try {
127
+ const response = await fetch(resolvedSrc, {
128
+ headers: {
129
+ Accept: 'application/json'
130
+ }
131
+ })
132
+
133
+ if (!response.ok) {
134
+ throw new Error(`Unable to load API JSON (${response.status}).`)
135
+ }
136
+
137
+ const json = await response.json()
138
+
139
+ if (currentRequest !== requestIndex) {
140
+ return
141
+ }
142
+
143
+ apiModel.value = createApiBlockModel(props.src, json)
144
+ } catch (error) {
145
+ if (currentRequest !== requestIndex) {
146
+ return
147
+ }
148
+
149
+ errorMessage.value = error?.message || `Unable to load API JSON: ${props.src}`
150
+ } finally {
151
+ if (currentRequest === requestIndex) {
152
+ loading.value = false
153
+ }
154
+ }
155
+ }
156
+
157
+ function onSearchFieldClick() {
158
+ inputRef.value?.focus()
159
+ }
160
+
161
+ function onFilterClick() {
162
+ if (filter.value !== '') {
163
+ filter.value = ''
164
+ return
165
+ }
166
+
167
+ onSearchFieldClick()
168
+ }
169
+
170
+ function onDocsButtonClick() {
171
+ if (!docsLink.value) {
172
+ return
173
+ }
174
+
175
+ if (docsLink.value.startsWith('/')) {
176
+ router.push(docsLink.value)
177
+ return
178
+ }
179
+
180
+ window.open(docsLink.value, '_blank', 'noopener,noreferrer')
181
+ }
182
+
183
+ function getScrollAreaKey(tab, innerTab = defaultInnerTabName) {
184
+ return `${tab}::${innerTab}`
185
+ }
186
+
187
+ function onScrollContentResize(tab, innerTab, size) {
188
+ const nextHeight = Math.min(Math.max(Math.ceil(size?.height || 0), 1), SCROLL_AREA_MAX_HEIGHT)
189
+ const key = getScrollAreaKey(tab, innerTab)
190
+
191
+ if (scrollAreaHeights.value[key] !== nextHeight) {
192
+ scrollAreaHeights.value[key] = nextHeight
193
+ }
194
+ }
195
+
196
+ function getScrollAreaStyle(tab, innerTab = defaultInnerTabName) {
197
+ const key = getScrollAreaKey(tab, innerTab)
198
+ const height = scrollAreaHeights.value[key]
199
+
200
+ return {
201
+ height: `${height || SCROLL_AREA_MAX_HEIGHT}px`,
202
+ maxHeight: `${SCROLL_AREA_MAX_HEIGHT}px`
203
+ }
204
+ }
205
+ </script>
206
+
207
+ <template>
208
+ <q-card class="doc-api q-my-xl" flat>
209
+ <div class="doc-api__toolbar row items-center q-pr-sm">
210
+ <div class="doc-api__title">{{ nameBanner }}</div>
211
+
212
+ <div
213
+ class="col doc-api__search-field row items-center no-wrap"
214
+ @click="onSearchFieldClick"
215
+ >
216
+ <input
217
+ ref="inputRef"
218
+ v-model="filter"
219
+ class="col doc-api__search text-right"
220
+ name="filter"
221
+ placeholder="Filter..."
222
+ >
223
+ <q-btn
224
+ :icon="inputIcon"
225
+ class="header-btn q-ml-xs"
226
+ dense
227
+ flat
228
+ round
229
+ @click="onFilterClick"
230
+ />
231
+ </div>
232
+
233
+ <q-btn
234
+ v-if="docsLink"
235
+ class="q-ml-sm header-btn doc-api__page-link"
236
+ size="sm"
237
+ padding="xs sm"
238
+ no-caps
239
+ outline
240
+ type="button"
241
+ @click="onDocsButtonClick"
242
+ >
243
+ <q-icon name="launch" />
244
+ <div class="q-ml-xs">Docs</div>
245
+ </q-btn>
246
+ </div>
247
+
248
+ <q-linear-progress
249
+ v-if="loading"
250
+ color="primary"
251
+ indeterminate
252
+ class="q-mt-xs"
253
+ />
254
+ <template v-else-if="errorMessage !== ''">
255
+ <q-separator class="doc-api__separator" />
256
+ <div class="doc-api__nothing-to-show">
257
+ <div class="doc-api__feedback-title">{{ errorMessage }}</div>
258
+ <div class="doc-api__feedback-copy">Check the JSON path, network response, or payload shape.</div>
259
+ </div>
260
+ </template>
261
+ <template v-else-if="nothingToShow">
262
+ <q-separator class="doc-api__separator" />
263
+ <div class="doc-api__nothing-to-show">Nothing to display</div>
264
+ </template>
265
+ <template v-else>
266
+ <q-tabs
267
+ v-model="currentTab"
268
+ class="header-tabs"
269
+ active-color="primary"
270
+ indicator-color="primary"
271
+ align="left"
272
+ :breakpoint="0"
273
+ >
274
+ <q-tab
275
+ v-for="tab in tabsList"
276
+ :key="`api-tab-${tab}`"
277
+ :name="tab"
278
+ class="header-btn"
279
+ >
280
+ <div class="row no-wrap items-center">
281
+ <span class="q-mr-xs text-capitalize">{{ tab }}</span>
282
+ <q-badge
283
+ v-if="filteredApiCount[tab]?.overall"
284
+ :label="filteredApiCount[tab].overall"
285
+ color="primary"
286
+ />
287
+ </div>
288
+ </q-tab>
289
+ </q-tabs>
290
+
291
+ <q-separator class="doc-api__separator" />
292
+
293
+ <q-tab-panels v-model="currentTab" animated>
294
+ <q-tab-panel
295
+ v-for="tab in tabsList"
296
+ :key="tab"
297
+ :name="tab"
298
+ class="q-pa-none"
299
+ >
300
+ <div
301
+ v-if="(innerTabsList[tab] || []).length !== 1"
302
+ class="doc-api__container row no-wrap items-stretch"
303
+ >
304
+ <div class="col-auto">
305
+ <q-tabs
306
+ v-model="currentInnerTab"
307
+ class="header-tabs doc-api__subtabs"
308
+ active-color="primary"
309
+ indicator-color="primary"
310
+ :breakpoint="0"
311
+ vertical
312
+ dense
313
+ shrink
314
+ >
315
+ <q-tab
316
+ v-for="innerTab in innerTabsList[tab]"
317
+ :key="`api-inner-tab-${innerTab}`"
318
+ :name="innerTab"
319
+ class="doc-api__subtabs-item header-btn"
320
+ >
321
+ <div class="row no-wrap items-center self-stretch q-pl-sm">
322
+ <span class="q-mr-xs text-capitalize">{{ innerTab }}</span>
323
+ <div class="col" />
324
+ <q-badge
325
+ v-if="filteredApiCount[tab]?.category?.[innerTab]"
326
+ :label="filteredApiCount[tab].category[innerTab]"
327
+ color="primary"
328
+ />
329
+ </div>
330
+ </q-tab>
331
+ </q-tabs>
332
+ </div>
333
+
334
+ <q-separator vertical class="doc-api__splitter" />
335
+
336
+ <q-tab-panels
337
+ v-model="currentInnerTab"
338
+ class="col doc-api__content-panels"
339
+ animated
340
+ transition-prev="slide-down"
341
+ transition-next="slide-up"
342
+ >
343
+ <q-tab-panel
344
+ v-for="innerTab in innerTabsList[tab]"
345
+ :key="innerTab"
346
+ :name="innerTab"
347
+ class="q-pa-none"
348
+ >
349
+ <q-scroll-area
350
+ class="doc-api__scroll-area"
351
+ :style="getScrollAreaStyle(tab, innerTab)"
352
+ >
353
+ <div class="doc-api__scroll-content">
354
+ <q-resize-observer @resize="onScrollContentResize(tab, innerTab, $event)" />
355
+ <d-block-api-entry
356
+ :type="tab"
357
+ :definition="filteredApi[tab][innerTab]"
358
+ />
359
+ </div>
360
+ </q-scroll-area>
361
+ </q-tab-panel>
362
+ </q-tab-panels>
363
+ </div>
364
+
365
+ <div v-else class="doc-api__container">
366
+ <q-scroll-area
367
+ class="doc-api__scroll-area"
368
+ :style="getScrollAreaStyle(tab, defaultInnerTabName)"
369
+ >
370
+ <div class="doc-api__scroll-content">
371
+ <q-resize-observer @resize="onScrollContentResize(tab, defaultInnerTabName, $event)" />
372
+ <d-block-api-entry
373
+ :type="tab"
374
+ :definition="filteredApi[tab][defaultInnerTabName]"
375
+ />
376
+ </div>
377
+ </q-scroll-area>
378
+ </div>
379
+ </q-tab-panel>
380
+ </q-tab-panels>
381
+ </template>
382
+ </q-card>
383
+ </template>
384
+
385
+ <style lang="sass">
386
+ body.body--light
387
+ --d-api-bg: linear-gradient(180deg, #f7faf5 0%, #ffffff 100%)
388
+ --d-api-border: rgba(52, 85, 54, 0.14)
389
+ --d-api-shadow: rgba(52, 85, 54, 0.08)
390
+ --d-api-surface: rgba(48, 88, 58, 0.04)
391
+ --d-api-text: #26352b
392
+ --d-api-muted: #597060
393
+ --d-api-divider: rgba(52, 85, 54, 0.14)
394
+ --d-api-token-bg: #eef3ed
395
+ --d-api-token-border: rgba(52, 85, 54, 0.14)
396
+ --d-api-token-text: #405148
397
+ --d-api-added-bg: #ffe6e1
398
+ --d-api-added-border: #d95d47
399
+ --d-api-added-text: #b33e28
400
+ --d-api-input-placeholder: #70867a
401
+ --d-api-tab-color: #5d705f
402
+ --d-api-tab-hover: #30483a
403
+ --d-api-tab-active: #6c5928
404
+ --d-api-tab-bg: transparent
405
+ --d-api-tab-bg-hover: rgba(48, 88, 58, 0.05)
406
+ --d-api-tab-bg-active: rgba(48, 88, 58, 0.08)
407
+ --d-api-tab-border: transparent
408
+
409
+ body.body--dark
410
+ --d-api-bg: linear-gradient(180deg, rgba(226, 255, 234, 0.05) 0%, rgba(255, 255, 255, 0.02) 100%)
411
+ --d-api-border: rgba(214, 245, 224, 0.14)
412
+ --d-api-shadow: rgba(0, 0, 0, 0.3)
413
+ --d-api-surface: rgba(255, 255, 255, 0.04)
414
+ --d-api-text: #e8efe9
415
+ --d-api-muted: #a9b9ae
416
+ --d-api-divider: rgba(214, 245, 224, 0.14)
417
+ --d-api-token-bg: rgba(255, 255, 255, 0.06)
418
+ --d-api-token-border: rgba(255, 255, 255, 0.1)
419
+ --d-api-token-text: #d8e4db
420
+ --d-api-added-bg: rgba(217, 93, 71, 0.12)
421
+ --d-api-added-border: #ff7b72
422
+ --d-api-added-text: #ffb0a7
423
+ --d-api-input-placeholder: #92a69c
424
+ --d-api-tab-color: #c5d2ca
425
+ --d-api-tab-hover: #eef7f0
426
+ --d-api-tab-active: #f0d790
427
+ --d-api-tab-bg: rgba(255, 255, 255, 0.06)
428
+ --d-api-tab-bg-hover: rgba(255, 255, 255, 0.1)
429
+ --d-api-tab-bg-active: rgba(240, 215, 144, 0.12)
430
+ --d-api-tab-border: rgba(255, 255, 255, 0.08)
431
+
432
+ .doc-api
433
+ border: 1px solid var(--d-api-border)
434
+ border-radius: 20px
435
+ background: var(--d-api-bg)
436
+ box-shadow: 0 18px 36px var(--d-api-shadow)
437
+ overflow: hidden
438
+
439
+ .header-btn
440
+ color: var(--d-api-muted)
441
+ transition: color 0.2s ease, background-color 0.2s ease
442
+
443
+ &:hover
444
+ color: var(--d-api-text)
445
+
446
+ .header-tabs
447
+ color: var(--d-api-muted)
448
+ background: transparent
449
+
450
+ .q-tabs__content
451
+ gap: 0.375rem
452
+
453
+ .q-tab
454
+ color: var(--d-api-tab-color)
455
+ background: var(--d-api-tab-bg)
456
+ border: 1px solid var(--d-api-tab-border)
457
+ border-radius: 12px
458
+ transition: color 0.2s ease, background-color 0.2s ease
459
+
460
+ &:hover
461
+ color: var(--d-api-tab-hover)
462
+ background: var(--d-api-tab-bg-hover)
463
+
464
+ &.q-tab--active
465
+ color: var(--d-api-tab-active)
466
+ background: var(--d-api-tab-bg-active)
467
+
468
+ .q-tab__label,
469
+ .q-tab__content
470
+ color: var(--d-api-tab-active)
471
+
472
+ &:not(.q-tab--active)
473
+ .q-tab__label,
474
+ .q-tab__content
475
+ color: inherit
476
+
477
+ .q-tab
478
+ min-height: 44px
479
+
480
+ .doc-api__toolbar
481
+ gap: 0.75rem
482
+ padding: 1rem 1rem 0.75rem
483
+
484
+ .doc-api__title
485
+ font-size: 1.05rem
486
+ font-weight: 700
487
+ color: var(--d-api-text)
488
+
489
+ .doc-api__separator.q-separator--horizontal
490
+ margin: 0 !important
491
+ height: 1px !important
492
+ min-height: 1px
493
+ border: 0
494
+ padding: 0
495
+ background: var(--d-api-divider)
496
+
497
+ .doc-api__splitter.q-separator--vertical
498
+ margin: 0 !important
499
+ width: 1px !important
500
+ min-width: 1px
501
+ height: auto !important
502
+ min-height: 100%
503
+ border: 0
504
+ padding: 0
505
+ flex: 0 0 1px
506
+ align-self: stretch
507
+ background: var(--d-api-divider)
508
+ opacity: 1
509
+
510
+ .doc-api__container
511
+ align-items: stretch
512
+ max-height: 640px
513
+
514
+ .doc-api__scroll-area
515
+ width: 100%
516
+
517
+ .q-scrollarea__content
518
+ min-width: 100%
519
+
520
+ .doc-api__scroll-content
521
+ min-width: 100%
522
+
523
+ .doc-api__content-panels
524
+ background: transparent
525
+
526
+ .doc-api__nothing-to-show
527
+ padding: 1rem
528
+ color: var(--d-api-muted)
529
+
530
+ .doc-api__feedback-title
531
+ color: var(--d-api-text)
532
+ font-weight: 600
533
+
534
+ .doc-api__feedback-copy
535
+ margin-top: 0.25rem
536
+
537
+ .doc-api__subtabs .q-tabs__content
538
+ padding: 8px 0
539
+
540
+ .doc-api__subtabs-item
541
+ justify-content: left
542
+ min-height: 36px !important
543
+
544
+ .q-tab__content
545
+ width: 100%
546
+
547
+ .doc-api__subtabs,
548
+ .doc-api__subtabs-item
549
+ border-radius: 0 !important
550
+
551
+ .doc-api__search-field
552
+ cursor: text
553
+ min-width: 10em !important
554
+ padding: 0 0.25rem 0 0.75rem
555
+ border: 1px solid var(--d-api-border)
556
+ border-radius: 999px
557
+ background: var(--d-api-surface)
558
+
559
+ .doc-api__page-link
560
+ text-decoration: none !important
561
+
562
+ .doc-api__search
563
+ border: 0
564
+ outline: 0
565
+ background: none
566
+ color: var(--d-api-text)
567
+ width: 1px !important
568
+ height: 37px
569
+
570
+ &::placeholder
571
+ color: var(--d-api-input-placeholder)
572
+
573
+ .doc-api-entry
574
+ padding: 12px 16px 10px
575
+ color: var(--d-api-text)
576
+
577
+ .doc-api-entry
578
+ padding: 6px
579
+
580
+ & + &
581
+ border-top: 1px solid var(--d-api-divider)
582
+
583
+ .doc-api-entry__expand-btn
584
+ margin-left: 4px
585
+ color: var(--d-api-muted)
586
+
587
+ .doc-api-entry__item
588
+ min-height: 0
589
+
590
+ & + &
591
+ margin-top: 2px
592
+
593
+ .doc-api-entry__subitem
594
+ padding: 2px 0 0 8px
595
+ border-radius: 12px
596
+
597
+ > div
598
+ border: 1px solid var(--d-api-divider) !important
599
+ border-radius: inherit
600
+
601
+ > div + div
602
+ margin-top: 6px
603
+
604
+ .doc-api-entry__type
605
+ line-height: 22px
606
+
607
+ .doc-api-entry__value
608
+ color: var(--d-api-muted)
609
+ line-height: 1.45
610
+
611
+ .doc-api-entry--indent
612
+ padding-left: 8px
613
+
614
+ .doc-api .doc-token
615
+ margin: 4px
616
+ display: inline-block
617
+ padding: 0.15rem 0.45rem
618
+ border-radius: 999px
619
+ background-color: var(--d-api-token-bg)
620
+ border: 1px solid var(--d-api-token-border)
621
+ color: var(--d-api-token-text)
622
+
623
+ .doc-api-entry__added-in,
624
+ .doc-api-entry__pill
625
+ font-size: 0.78rem
626
+ letter-spacing: 0.04em
627
+ line-height: 1.4em
628
+
629
+ .doc-api-entry__added-in
630
+ font-size: 0.68rem
631
+ color: var(--d-api-added-text)
632
+ border-color: var(--d-api-added-border)
633
+ background-color: var(--d-api-added-bg)
634
+ </style>