@docsector/docsector-reader 2.0.6 → 2.1.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.
package/README.md CHANGED
@@ -68,6 +68,8 @@ Transform Markdown content into beautiful, navigable documentation sites — wit
68
68
  - 🧬 **Scaffolded Homepage Override Wiring** — New consumer projects automatically wire `virtual:docsector-homepage-override` into i18n message building
69
69
  - 🧭 **Quick Links Custom Element** — Use `<d-quick-links>` and `<d-quick-link>` in Markdown to render rich home navigation cards
70
70
  - 🗂️ **API Catalog Well-Known** — Auto-generates `/.well-known/api-catalog` as Linkset JSON for machine-readable API discovery
71
+ - 🗃️ **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
72
+ - 🏷️ **Version Selector Badges** — Every version in the sidebar selector displays a color-coded badge: green for released, orange for draft, red for deprecated; fully customizable via `badge: { label, color, textColor }`
71
73
  - ⚙️ **Single Config File** — Customize branding, links, and languages via `docsector.config.js`
72
74
 
73
75
  ---
@@ -675,7 +677,10 @@ export default {
675
677
  logo: '/images/logo/my-logo.png',
676
678
  name: 'My Project',
677
679
  version: 'v1.0.0',
678
- versions: ['v1.0.0', 'v0.9.0']
680
+ versions: [
681
+ { id: 'v1.0.0', current: true, released: false },
682
+ { id: 'v0.9.0', released: true, status: 'deprecated' }
683
+ ]
679
684
  },
680
685
 
681
686
  links: {
@@ -754,6 +759,10 @@ export default {
754
759
  }
755
760
  ```
756
761
 
762
+ The current version keeps the normal unprefixed routes such as `/guide/getting-started/overview/`. Archived major versions can be placed under `src/pages/.old/<version>/` with the same book/index/Markdown layout, and are exposed with a URL prefix such as `/v0.x/guide/getting-started/overview/`.
763
+
764
+ Every version shows a release badge in the selector. Released versions default to `released`; versions with `released: false` or `status: 'draft'` default to `draft`; versions with `status: 'deprecated'` or `deprecated: true` default to `deprecated` in red. Use `badge: { label, color, textColor }` when you need custom badge copy or colors.
765
+
757
766
  ### MCP (optional)
758
767
 
759
768
  ```javascript
@@ -777,15 +786,17 @@ import { buildMessages } from '@docsector/docsector-reader/i18n'
777
786
  import homePageOverride from 'virtual:docsector-homepage-override'
778
787
 
779
788
  const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
780
- const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
789
+ const currentMdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
790
+ const oldMdModules = import.meta.glob('../pages/.old/**/*.md', { eager: true, query: '?raw', import: 'default' })
791
+ const mdModules = { ...currentMdModules, ...oldMdModules }
781
792
 
782
793
  import boot from 'pages/boot'
783
- import { books } from 'virtual:docsector-books'
794
+ import { books, pageEntries } from 'virtual:docsector-books'
784
795
 
785
- export default buildMessages({ langModules, mdModules, books, boot, homePageOverride })
796
+ export default buildMessages({ langModules, mdModules, books, pageEntries, boot, homePageOverride })
786
797
  ```
787
798
 
788
- > `books` is the preferred source because it preserves per-book registries and avoids path collisions when two books reuse the same route key.
799
+ > `pageEntries` is the preferred source because it preserves per-book and per-version registries and avoids path collisions when books or archived versions reuse the same route key.
789
800
 
790
801
  ### Language files
791
802
 
package/bin/docsector.js CHANGED
@@ -23,7 +23,7 @@ const packageRoot = resolve(__dirname, '..')
23
23
  const args = process.argv.slice(2)
24
24
  const command = args[0]
25
25
 
26
- const VERSION = '2.0.6'
26
+ const VERSION = '2.1.0'
27
27
 
28
28
  const HELP = `
29
29
  Docsector Reader v${VERSION}
@@ -105,7 +105,10 @@ export default {
105
105
  branding: {
106
106
  logo: '/images/logo.png',
107
107
  name: 'My Documentation',
108
- version: 'v0.1.0'
108
+ version: 'v0.1.0',
109
+ versions: [
110
+ { id: 'v0.1.0', current: true, released: false }
111
+ ]
109
112
  },
110
113
 
111
114
  // @ Links
@@ -269,13 +272,15 @@ import homePageOverride from 'virtual:docsector-homepage-override'
269
272
  // @ Import language HJSON files (Vite-compatible eager import)
270
273
  const langModules = import.meta.glob('./languages/*.hjson', { eager: true })
271
274
  // @ Import markdown files (Vite-compatible eager import as raw strings)
272
- const mdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
275
+ const currentMdModules = import.meta.glob('../pages/**/*.md', { eager: true, query: '?raw', import: 'default' })
276
+ const oldMdModules = import.meta.glob('../pages/.old/**/*.md', { eager: true, query: '?raw', import: 'default' })
277
+ const mdModules = { ...currentMdModules, ...oldMdModules }
273
278
 
274
279
  // @ Import pages
275
280
  import boot from 'pages/boot'
276
- import { allPages as pages } from 'virtual:docsector-books'
281
+ import { books, pageEntries } from 'virtual:docsector-books'
277
282
 
278
- export default buildMessages({ langModules, mdModules, pages, boot, homePageOverride })
283
+ export default buildMessages({ langModules, mdModules, books, pageEntries, boot, homePageOverride })
279
284
  `
280
285
 
281
286
  const TEMPLATE_I18N_HJSON = `\
@@ -15,7 +15,19 @@ export default {
15
15
  // Project name displayed in the sidebar
16
16
  name: 'Docsector Reader',
17
17
  // Version label displayed next to the name
18
- version: 'v' + pkg.version
18
+ version: 'v' + pkg.version,
19
+ versions: [
20
+ {
21
+ id: 'v' + pkg.version,
22
+ current: true,
23
+ released: false
24
+ },
25
+ {
26
+ id: 'v0.x',
27
+ released: true,
28
+ status: 'deprecated'
29
+ }
30
+ ]
19
31
  },
20
32
 
21
33
  // @ Links
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docsector/docsector-reader",
3
- "version": "2.0.6",
3
+ "version": "2.1.0",
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",
@@ -7,7 +7,7 @@ import { useI18n } from 'vue-i18n'
7
7
  import tags from '@docsector/tags'
8
8
  import DMenuItem from './DMenuItem.vue'
9
9
  import docsectorConfig from 'docsector.config.js'
10
- import { allBooks } from 'virtual:docsector-books'
10
+ import { allBooks, booksByVersion, versions } from 'virtual:docsector-books'
11
11
  import { namespacedLabelI18nPath, routeSubpageSourceI18nPath } from '../i18n/path'
12
12
 
13
13
  const $q = useQuasar()
@@ -20,19 +20,93 @@ const links = docsectorConfig.links || {}
20
20
 
21
21
  const term = ref(null)
22
22
  const founds = ref(false)
23
- const version = ref(branding.version || 'v0.x')
24
- const versions = ref(branding.versions || ['v0.x'])
25
23
  const items = ref([])
26
24
  const scrolling = ref(null)
27
25
 
28
26
  const subpage = computed(() => {
29
27
  const parent = $route.matched[0]?.path
30
28
  const child = $route.matched[1]?.path
29
+ if (!parent || !child) return '/overview'
31
30
  return child.substring(parent.length)
32
31
  })
33
32
 
33
+ const activeVersionId = computed(() => {
34
+ return $route.matched?.[0]?.meta?.version ?? versions?.[0]?.id ?? ''
35
+ })
36
+
37
+ const activeBooks = computed(() => {
38
+ if (activeVersionId.value && booksByVersion?.[activeVersionId.value]?.allBooks) {
39
+ return booksByVersion[activeVersionId.value].allBooks
40
+ }
41
+
42
+ return allBooks || []
43
+ })
44
+
45
+ const draftReleaseStatuses = new Set(['draft', 'unreleased', 'preview', 'next'])
46
+
47
+ const normalizeVersionBadge = (item) => {
48
+ const configuredStatus = item.deprecated === true
49
+ ? 'deprecated'
50
+ : (item.releaseStatus || item.status)
51
+ const explicitlyReleased = item.released !== undefined ? item.released !== false : null
52
+ const released = configuredStatus === 'deprecated'
53
+ ? true
54
+ : (explicitlyReleased ?? !draftReleaseStatuses.has(String(configuredStatus || '').toLowerCase()))
55
+ const releaseStatus = configuredStatus || (released ? 'released' : 'draft')
56
+ const rawBadge = item.badge ?? item.releaseBadge
57
+ const deprecated = releaseStatus === 'deprecated'
58
+ const defaultColor = deprecated ? 'negative' : (released ? 'positive' : 'warning')
59
+ const defaultTextColor = (deprecated || released) ? 'white' : 'dark'
60
+
61
+ if (rawBadge === false || rawBadge === null) {
62
+ return { label: releaseStatus, color: defaultColor, textColor: defaultTextColor }
63
+ }
64
+
65
+ if (typeof rawBadge === 'string') {
66
+ return { label: rawBadge, color: defaultColor, textColor: defaultTextColor }
67
+ }
68
+
69
+ if (typeof rawBadge === 'object' && rawBadge !== null) {
70
+ const label = rawBadge.label || rawBadge.text || releaseStatus
71
+ if (!label) {
72
+ return null
73
+ }
74
+
75
+ return {
76
+ ...rawBadge,
77
+ label,
78
+ color: rawBadge.color || defaultColor,
79
+ textColor: rawBadge.textColor || defaultTextColor
80
+ }
81
+ }
82
+
83
+ return { label: releaseStatus, color: defaultColor, textColor: defaultTextColor }
84
+ }
85
+
86
+ const versionOptions = computed(() => {
87
+ return (versions || [])
88
+ .filter(item => item && item.id)
89
+ .map(item => ({
90
+ label: item.label || item.id,
91
+ value: item.id,
92
+ badge: normalizeVersionBadge(item),
93
+ released: item.released !== false,
94
+ deprecated: item.deprecated === true || item.releaseStatus === 'deprecated' || item.status === 'deprecated',
95
+ releaseStatus: item.releaseStatus || item.status || (item.released === false ? 'draft' : 'released')
96
+ }))
97
+ })
98
+
99
+ const activeVersionOption = computed(() => {
100
+ return versionOptions.value.find(item => item.value === activeVersionId.value) || null
101
+ })
102
+
103
+ const version = computed({
104
+ get: () => activeVersionId.value,
105
+ set: (versionId) => onVersionChange(versionId)
106
+ })
107
+
34
108
  const defaultBookId = computed(() => {
35
- const sortedBooks = [...(allBooks || [])]
109
+ const sortedBooks = [...activeBooks.value]
36
110
  .filter(book => book && typeof book.id === 'string' && book.id.length > 0)
37
111
  .sort((a, b) => {
38
112
  const orderA = Number.isFinite(a.order) ? a.order : Number.MAX_SAFE_INTEGER
@@ -52,13 +126,97 @@ const currentBookId = computed(() => {
52
126
  return defaultBookId.value
53
127
  })
54
128
 
129
+ const normalizeRoutePath = (path) => {
130
+ const normalized = String(path || '').trim()
131
+ if (normalized === '' || normalized === '/') {
132
+ return '/'
133
+ }
134
+
135
+ const sanitized = normalized.replace(/\/+$/, '')
136
+ return sanitized === '' ? '/' : sanitized
137
+ }
138
+
139
+ const getTopRoutes = () => {
140
+ return ($router.options.routes || []).slice(0, -2)
141
+ }
142
+
143
+ const routeHasSubpage = (route, subpageName) => {
144
+ return (route.children || []).some(child => child.path === subpageName)
145
+ }
146
+
147
+ const routeToSubpagePath = (route, subpageName) => {
148
+ return `${route.path.replace(/\/$/, '')}/${subpageName}/`
149
+ }
150
+
151
+ const getCurrentSubpageName = () => {
152
+ return String(subpage.value || '/overview').replace(/^\/+|\/+$/g, '') || 'overview'
153
+ }
154
+
155
+ const getFirstRoutePathByVersion = (versionId, preferredBook = null) => {
156
+ const routes = getTopRoutes()
157
+
158
+ for (const preferBook of [preferredBook, null]) {
159
+ for (const route of routes) {
160
+ if (route?.meta?.version !== versionId) continue
161
+ if (preferBook && (route.meta?.book ?? route.meta?.type) !== preferBook) continue
162
+ if (!routeHasSubpage(route, 'overview')) continue
163
+
164
+ const hasInternalLink = typeof route.meta?.link?.to === 'string' && route.meta.link.to.trim().length > 0
165
+ if (hasInternalLink) continue
166
+
167
+ return routeToSubpagePath(route, 'overview')
168
+ }
169
+ }
170
+
171
+ return '/'
172
+ }
173
+
174
+ const getEquivalentRoutePath = (versionId) => {
175
+ const routeMeta = $route.matched?.[0]?.meta || {}
176
+ const book = routeMeta.book ?? routeMeta.type ?? currentBookId.value
177
+ const pagePath = routeMeta.pagePath
178
+ const subpageName = getCurrentSubpageName()
179
+
180
+ if (book && typeof pagePath === 'string') {
181
+ const equivalentRoute = getTopRoutes().find(route => {
182
+ return route?.meta?.version === versionId &&
183
+ (route.meta?.book ?? route.meta?.type) === book &&
184
+ route.meta?.pagePath === pagePath &&
185
+ routeHasSubpage(route, subpageName)
186
+ })
187
+
188
+ if (equivalentRoute) {
189
+ return routeToSubpagePath(equivalentRoute, subpageName)
190
+ }
191
+ }
192
+
193
+ return getFirstRoutePathByVersion(versionId, book)
194
+ }
195
+
196
+ function onVersionChange (versionId) {
197
+ if (!versionId || versionId === activeVersionId.value) return
198
+
199
+ const targetVersion = (versions || []).find(item => item.id === versionId)
200
+ if (!targetVersion) return
201
+
202
+ if (typeof targetVersion.url === 'string' && targetVersion.url.trim().length > 0) {
203
+ openURL(targetVersion.url)
204
+ return
205
+ }
206
+
207
+ const targetPath = getEquivalentRoutePath(versionId)
208
+ if (normalizeRoutePath($route.path) !== normalizeRoutePath(targetPath)) {
209
+ $router.push(targetPath)
210
+ }
211
+ }
212
+
55
213
  const searchTerm = (term) => {
56
214
  if (term.length > 1) {
57
215
  term = term.toLowerCase()
58
216
  const locale = $q.localStorage.getItem('setting.language')
59
217
  founds.value = []
60
218
 
61
- for (const [index, group] of items.value.entries()) {
219
+ for (const group of items.value) {
62
220
  searchTermIterate(group, term, locale)
63
221
  }
64
222
  } else {
@@ -74,13 +232,14 @@ const searchTermIterate = (items, term, locale) => {
74
232
  } else if (typeof items === 'object') {
75
233
  const item = items
76
234
  const path = item.path
235
+ const tagPath = item.meta?.unversionedPath || path
77
236
  founds.value[path] = false
78
237
 
79
238
  // @ search in i18n/tags.hjson
80
239
  if (tags[locale] && Object.keys(tags[locale]).length > 0) {
81
- founds.value[path] = tags[locale][path]?.indexOf(term) !== -1
240
+ founds.value[path] = tags[locale][tagPath]?.indexOf(term) !== -1
82
241
  if (founds.value[path] === false && locale !== 'en-US') {
83
- founds.value[path] = tags['en-US'][path]?.indexOf(term) !== -1
242
+ founds.value[path] = tags['en-US'][tagPath]?.indexOf(term) !== -1
84
243
  }
85
244
  }
86
245
 
@@ -181,11 +340,13 @@ onBeforeUnmount(() => {
181
340
  })
182
341
 
183
342
  const buildMenuItems = () => {
184
- const routes = ($router.options.routes || []).slice(0, -2) // Delete last 2 routes
343
+ const routes = getTopRoutes()
185
344
  const activeBook = currentBookId.value
345
+ const activeVersion = activeVersionId.value
186
346
 
187
347
  const filteredRoutes = routes.filter(route => {
188
348
  const routeBook = route?.meta?.book ?? route?.meta?.type
349
+ if (activeVersion && route?.meta?.version !== activeVersion) return false
189
350
  if (!activeBook) return true
190
351
  return routeBook === activeBook
191
352
  })
@@ -201,7 +362,7 @@ const buildMenuItems = () => {
201
362
  })
202
363
 
203
364
  // # Route
204
- const basepath = route.path.split('/')[2]
365
+ const basepath = route.meta?.menuGroupPath || route.path.split('/')[2]
205
366
  const header = route.meta?.menu?.header
206
367
 
207
368
  if (header !== undefined && basepath !== nodeBasepath) {
@@ -227,7 +388,7 @@ const rebuildItems = () => {
227
388
  }
228
389
 
229
390
  rebuildItems()
230
- watch(currentBookId, rebuildItems)
391
+ watch([currentBookId, activeVersionId], rebuildItems)
231
392
  </script>
232
393
 
233
394
  <template>
@@ -254,10 +415,44 @@ watch(currentBookId, rebuildItems)
254
415
  <div class="text-weight-medium">{{ branding.name || 'Docsector' }}</div>
255
416
  <div class="text-caption q-pt-xs">{{ t('system.documentation') }}</div>
256
417
  <q-select class="q-mr-md"
257
- v-model="version" :options="versions"
418
+ v-model="version" :options="versionOptions"
419
+ emit-value map-options
258
420
  dense options-dense
259
421
  behavior="menu"
260
- />
422
+ >
423
+ <template v-slot:selected>
424
+ <div v-if="activeVersionOption" class="version-select-option">
425
+ <span class="version-select-label">{{ activeVersionOption.label }}</span>
426
+ <q-badge
427
+ v-if="activeVersionOption.badge"
428
+ class="version-select-badge"
429
+ :color="activeVersionOption.badge.color || 'warning'"
430
+ :text-color="activeVersionOption.badge.textColor || 'dark'"
431
+ :outline="activeVersionOption.badge.outline === true"
432
+ >
433
+ {{ activeVersionOption.badge.label }}
434
+ </q-badge>
435
+ </div>
436
+ </template>
437
+ <template v-slot:option="scope">
438
+ <q-item v-bind="scope.itemProps">
439
+ <q-item-section>
440
+ <div class="version-select-option">
441
+ <span class="version-select-label">{{ scope.opt.label }}</span>
442
+ <q-badge
443
+ v-if="scope.opt.badge"
444
+ class="version-select-badge"
445
+ :color="scope.opt.badge.color || 'warning'"
446
+ :text-color="scope.opt.badge.textColor || 'dark'"
447
+ :outline="scope.opt.badge.outline === true"
448
+ >
449
+ {{ scope.opt.badge.label }}
450
+ </q-badge>
451
+ </div>
452
+ </q-item-section>
453
+ </q-item>
454
+ </template>
455
+ </q-select>
261
456
  </div>
262
457
  </div>
263
458
 
@@ -470,6 +665,23 @@ body.body--light
470
665
  width: 30px
471
666
  height: 3px
472
667
 
668
+ .version-select-option
669
+ display: flex
670
+ align-items: center
671
+ gap: 6px
672
+ min-width: 0
673
+ max-width: 100%
674
+
675
+ .version-select-label
676
+ overflow: hidden
677
+ text-overflow: ellipsis
678
+ white-space: nowrap
679
+
680
+ .version-select-badge
681
+ flex: 0 0 auto
682
+ font-size: 10px
683
+ line-height: 1
684
+
473
685
  // Search
474
686
  label[for="search"]
475
687
  z-index: 2
@@ -37,6 +37,11 @@ const subpage = computed(() => {
37
37
  })
38
38
 
39
39
  const fileKey = computed(() => {
40
+ const sourcePathBase = route.matched?.[0]?.meta?.sourcePathBase
41
+ if (typeof sourcePathBase === 'string' && sourcePathBase.length > 0) {
42
+ return `${sourcePathBase}.${subpage.value}.${locale.value}.md`
43
+ }
44
+
40
45
  const base = store.state.page.base
41
46
  if (!base) return ''
42
47
  return `${base}.${subpage.value}.${locale.value}.md`
@@ -20,7 +20,17 @@ function normalizeEditBaseUrl (url = '') {
20
20
 
21
21
  const base = normalizeEditBaseUrl(docsectorConfig.github?.editBaseUrl || '')
22
22
 
23
+ function getCurrentSubpageName () {
24
+ const relative = store.state.page.relative || '/overview'
25
+ return String(relative).replace(/^\/+|\/+$/g, '') || 'overview'
26
+ }
27
+
23
28
  function routePathToSourcePath (path = '') {
29
+ const sourcePathBase = route.matched?.[0]?.meta?.sourcePathBase
30
+ if (typeof sourcePathBase === 'string' && sourcePathBase.length > 0) {
31
+ return `/${sourcePathBase}.${getCurrentSubpageName()}`
32
+ }
33
+
24
34
  const cleanPath = String(path)
25
35
  .replace(/\/index\.html$/, '')
26
36
  .replace(/\/+$/, '')
@@ -131,34 +141,38 @@ const languages = computed(() => {
131
141
  return `${i18nLocalesAvailable}/${i18nLocales.length}`
132
142
  })
133
143
 
134
- const prev = computed(() => {
135
- const base = store.state.page.base
136
- const routes = router.options.routes.slice(0, -2)
137
-
138
- for (let i = 0; i < routes.length; i++) {
139
- if ('/' + base === routes[i].path) {
140
- if (i > 0) {
141
- return routes[i - 1].path
142
- }
143
- }
144
+ const normalizeRoutePath = (path) => {
145
+ const normalized = String(path || '').trim()
146
+ if (normalized === '' || normalized === '/') {
147
+ return '/'
144
148
  }
145
149
 
146
- return ''
150
+ const sanitized = normalized.replace(/\/+$/, '')
151
+ return sanitized === '' ? '/' : sanitized
152
+ }
153
+
154
+ const getVersionSiblingPath = (offset) => {
155
+ const versionId = route.matched?.[0]?.meta?.version ?? null
156
+ const currentPath = normalizeRoutePath(route.matched?.[0]?.path || `/${store.state.page.base}`)
157
+ const routes = router.options.routes
158
+ .slice(0, -2)
159
+ .filter(item => !versionId || item?.meta?.version === versionId)
160
+
161
+ const index = routes.findIndex(item => normalizeRoutePath(item.path) === currentPath)
162
+ if (index < 0) return ''
163
+
164
+ const sibling = routes[index + offset]
165
+ if (!sibling) return ''
166
+
167
+ return sibling.path
168
+ }
169
+
170
+ const prev = computed(() => {
171
+ return getVersionSiblingPath(-1)
147
172
  })
148
173
 
149
174
  const next = computed(() => {
150
- const base = store.state.page.base
151
- const routes = router.options.routes.slice(0, -2)
152
-
153
- for (let i = 0; i < routes.length; i++) {
154
- if ('/' + base === routes[i].path) {
155
- if (typeof routes[i + 1] !== 'undefined') {
156
- return routes[i + 1].path
157
- }
158
- }
159
- }
160
-
161
- return ''
175
+ return getVersionSiblingPath(1)
162
176
  })
163
177
 
164
178
  const hideRemoteHomeFooterMeta = computed(() => {
package/src/css/app.sass CHANGED
@@ -179,7 +179,7 @@ body.body--dark
179
179
  margin: 0.3em 0 0.1em
180
180
 
181
181
  code:not(pre > code)
182
- padding: 3px 5px
182
+ padding: 1px 3px
183
183
  margin: 0 2px
184
184
  border-radius: 2px
185
185
  white-space: normal
@@ -288,6 +288,7 @@ body.body--dark
288
288
  overflow-x: auto
289
289
  -webkit-overflow-scrolling: touch
290
290
  max-width: 100%
291
+ margin-bottom: 10px
291
292
  table
292
293
  width: max-content
293
294
  border: 1px solid #eee