@brillout/docpress 0.16.33 → 0.16.35-commit-797f17e

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.
Files changed (31) hide show
  1. package/Layout.tsx +1 -1
  2. package/code-blocks/components/ChoiceGroup.css +2 -66
  3. package/code-blocks/components/ChoiceGroup.tsx +8 -9
  4. package/code-blocks/components/Pre.css +1 -8
  5. package/code-blocks/components/Tabs.css +53 -0
  6. package/code-blocks/components/Tabs.tsx +91 -0
  7. package/code-blocks/hooks/{useSelectedChoice.ts → useCurrentSelection.ts} +2 -2
  8. package/code-blocks/hooks/useRestoreScroll.ts +6 -2
  9. package/code-blocks/remarkChoiceGroup.ts +5 -5
  10. package/code-blocks/remarkDetype.ts +5 -12
  11. package/code-blocks/remarkPkgManager.ts +0 -9
  12. package/code-blocks/utils/generateChoiceGroupCode.ts +47 -60
  13. package/components/index.ts +1 -0
  14. package/dist/code-blocks/components/Tabs.d.ts +6 -0
  15. package/dist/code-blocks/components/Tabs.js +57 -0
  16. package/dist/code-blocks/hooks/useCurrentSelection.d.ts +11 -0
  17. package/dist/code-blocks/hooks/useCurrentSelection.js +34 -0
  18. package/dist/code-blocks/hooks/useLocalStorage.d.ts +10 -0
  19. package/dist/code-blocks/hooks/useLocalStorage.js +31 -0
  20. package/dist/code-blocks/hooks/useRestoreScroll.d.ts +12 -0
  21. package/dist/code-blocks/hooks/useRestoreScroll.js +27 -0
  22. package/dist/code-blocks/remarkChoiceGroup.d.ts +1 -0
  23. package/dist/code-blocks/remarkChoiceGroup.js +4 -5
  24. package/dist/code-blocks/remarkDetype.js +5 -11
  25. package/dist/code-blocks/remarkPkgManager.js +0 -8
  26. package/dist/code-blocks/utils/generateChoiceGroupCode.js +40 -56
  27. package/dist/components/index.d.ts +1 -0
  28. package/dist/components/index.js +1 -0
  29. package/icons/index.ts +15 -8
  30. package/index.ts +10 -1
  31. package/package.json +2 -2
package/Layout.tsx CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  } from './MenuModal/toggleMenuModal.js'
28
28
  import { MenuModal } from './MenuModal.js'
29
29
  import { autoScrollNav_SSR } from './autoScrollNav.js'
30
- import { initializeChoiceGroup_SSR } from './code-blocks/hooks/useSelectedChoice.js'
30
+ import { initializeChoiceGroup_SSR } from './code-blocks/hooks/useCurrentSelection.js'
31
31
  import { SearchLink } from './docsearch/SearchLink.js'
32
32
  import { navigate } from 'vike/client/router'
33
33
  import { css } from './utils/css.js'
@@ -1,13 +1,6 @@
1
1
  .choice-group {
2
2
  position: relative;
3
3
 
4
- &:hover {
5
- .choice-group__selects,
6
- .copy-button {
7
- opacity: 1;
8
- }
9
- }
10
-
11
4
  /* layout */
12
5
  --top-position: 10px;
13
6
  --border-color: hsl(0, 0%, 75%) hsl(0, 0%, 72%) hsl(0, 0%, 72%) hsl(0, 0%, 75%);
@@ -20,8 +13,6 @@
20
13
  z-index: 3;
21
14
  top: var(--top-position);
22
15
  right: 42px;
23
- opacity: 0;
24
- transition: opacity 0.5s ease-in-out;
25
16
  }
26
17
 
27
18
  .choice-select {
@@ -53,65 +44,10 @@
53
44
  align-items: center;
54
45
  flex-wrap: nowrap;
55
46
  background: #fff;
56
- padding: 0 3px 0 5px;
47
+ padding: 0 6px;
57
48
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
58
49
  cursor: pointer;
59
50
  transition: background 120ms ease;
60
-
61
- span {
62
- flex: 1;
63
- }
64
-
65
- &::after {
66
- width: 15px;
67
- text-align: end;
68
- --animation-width: 2px;
69
- position: relative;
70
- padding-right: var(--animation-width);
71
- padding-left: var(--animation-width);
72
- font-size: 1.13em;
73
- color: #666;
74
- left: 0;
75
- transition: left 500ms ease, opacity 150ms ease-in-out;
76
- opacity: 0;
77
- }
78
-
79
- .choice-select__list:hover &:hover::after {
80
- opacity: 1;
81
- }
82
-
83
- &[aria-selected='true'] {
84
- &::after {
85
- content: '»' !important;
86
- opacity: 1;
87
- }
88
-
89
- .choice-select__list:hover &:not(:hover)::after {
90
- opacity: 0;
91
- }
92
-
93
- &:hover::after {
94
- left: var(--animation-width);
95
- }
96
- }
97
-
98
- &[aria-disabled='true'] {
99
- &::after {
100
- content: '⊘';
101
- font-size: 1em;
102
- left: 1px;
103
- }
104
- }
105
-
106
- &:not([aria-selected='true']):not([aria-disabled='true']) {
107
- &::after {
108
- content: '«';
109
- }
110
-
111
- &:hover::after {
112
- left: calc(-1 * var(--animation-width));
113
- }
114
- }
115
51
  }
116
52
 
117
53
  .choice-select[aria-expanded='false'] {
@@ -143,7 +79,7 @@
143
79
 
144
80
  .choice-select__option[aria-disabled='true'] {
145
81
  color: #999;
146
- cursor: default;
82
+ cursor: not-allowed;
147
83
  opacity: 0.8;
148
84
  }
149
85
 
@@ -1,7 +1,7 @@
1
1
  export { ChoiceGroup, CustomSelectsContainer }
2
2
 
3
3
  import React, { createContext, useContext, useEffect, useState } from 'react'
4
- import { useSelectedChoice } from '../hooks/useSelectedChoice.js'
4
+ import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
5
5
  import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
6
6
  import { cls } from '../../utils/cls.js'
7
7
  import './ChoiceGroup.css'
@@ -93,8 +93,7 @@ type ChoiceGroupProps = {
93
93
 
94
94
  function ChoiceGroup({ children, choiceGroup, parentChoiceGroup }: ChoiceGroupProps) {
95
95
  const { name: groupName, choices, default: defaultChoice, lvl } = choiceGroup
96
- // TODO/after-PR-merge rename useSelectedChoice useCurrentSelection
97
- const [selectedChoice] = useSelectedChoice(groupName, defaultChoice)
96
+ const [selectedChoice] = useCurrentSelection(groupName, defaultChoice)
98
97
  const { choiceGroupAll, registerChoiceGroup } = useCustomSelectsContext()
99
98
 
100
99
  useEffect(() => registerChoiceGroup(choiceGroup, parentChoiceGroup), [])
@@ -110,7 +109,7 @@ function ChoiceGroup({ children, choiceGroup, parentChoiceGroup }: ChoiceGroupPr
110
109
  ))}
111
110
  </select>
112
111
  {children}
113
- {lvl === 0 && (
112
+ {lvl === 0 && !choiceGroup.hidden && (
114
113
  <div className={`choice-group__selects`}>
115
114
  {choiceGroupAll
116
115
  .slice()
@@ -133,8 +132,8 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
133
132
  hidden,
134
133
  parentChoiceGroup,
135
134
  } = choiceGroup
136
- const [selectedChoice, setSelectedChoice] = useSelectedChoice(groupName, defaultChoice)
137
- const prevPositionRef = useRestoreScroll([selectedChoice])
135
+ const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
136
+ const setPrevPosition = useRestoreScroll([selectedChoice])
138
137
  const [expanded, setExpanded] = useState(false)
139
138
  const selectedIndex = choices.indexOf(selectedChoice)
140
139
  const height = 25
@@ -153,7 +152,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
153
152
  }
154
153
  function isHidden() {
155
154
  if (parentChoiceGroup) {
156
- const [parentSelectedChoice] = useSelectedChoice(parentChoiceGroup.name, parentChoiceGroup.default)
155
+ const [parentSelectedChoice] = useCurrentSelection(parentChoiceGroup.name, parentChoiceGroup.default)
157
156
  return !parentChoiceGroup.choices.includes(parentSelectedChoice)
158
157
  }
159
158
  return hidden
@@ -189,7 +188,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
189
188
  style={{ height }}
190
189
  onClick={handleOnClick}
191
190
  >
192
- <span>{choice}</span>
191
+ {choice}
193
192
  </div>
194
193
  ))}
195
194
  </div>
@@ -198,7 +197,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
198
197
  function handleOnClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
199
198
  e.stopPropagation()
200
199
  const el = e.currentTarget
201
- prevPositionRef.current = { top: el.getBoundingClientRect().top, el }
200
+ setPrevPosition(el)
202
201
  if (el.getAttribute('aria-selected') === 'true') {
203
202
  next()
204
203
  } else if (el.getAttribute('aria-disabled') === 'false') {
@@ -13,12 +13,6 @@ pre {
13
13
  position: relative;
14
14
  }
15
15
 
16
- &:hover {
17
- .copy-button {
18
- opacity: 1;
19
- }
20
- }
21
-
22
16
  .copy-button {
23
17
  position: absolute !important;
24
18
  top: 10px;
@@ -28,8 +22,7 @@ pre {
28
22
  height: 25px;
29
23
  width: 30px;
30
24
  background-color: #f7f7f7;
31
- opacity: 0;
32
- transition: opacity 0.5s ease-in-out, background-color 0.4s ease-in-out;
25
+ transition: background-color 0.4s ease-in-out;
33
26
 
34
27
  &:not(:hover) {
35
28
  background-color: #eee;
@@ -0,0 +1,53 @@
1
+ .choice-tabs {
2
+ -webkit-tap-highlight-color: transparent;
3
+
4
+ /* tablist style logic */
5
+ select:has(option:nth-of-type(1):checked) ~ ul[role='tablist'] li:nth-of-type(1),
6
+ select:has(option:nth-of-type(2):checked) ~ ul[role='tablist'] li:nth-of-type(2),
7
+ select:has(option:nth-of-type(3):checked) ~ ul[role='tablist'] li:nth-of-type(3),
8
+ select:has(option:nth-of-type(4):checked) ~ ul[role='tablist'] li:nth-of-type(4),
9
+ select:has(option:nth-of-type(5):checked) ~ ul[role='tablist'] li:nth-of-type(5),
10
+ select:has(option:nth-of-type(6):checked) ~ ul[role='tablist'] li:nth-of-type(6),
11
+ select:has(option:nth-of-type(7):checked) ~ ul[role='tablist'] li:nth-of-type(7) {
12
+ border-bottom: 2px solid #aaa;
13
+ color: var(--color-text);
14
+ }
15
+ }
16
+
17
+ .choice-tabs__tab-list {
18
+ border-bottom: 1px solid #eaeaea;
19
+ margin: 0 0 10px;
20
+ padding: 0;
21
+ }
22
+
23
+ .choice-tabs__tab {
24
+ display: inline-block;
25
+ bottom: -1px;
26
+ position: relative;
27
+ list-style: none;
28
+ padding: 6px 12px;
29
+ cursor: pointer;
30
+ font-weight: 500;
31
+ /* lighten --color-text by 20% */
32
+ color: color-mix(in srgb, var(--color-text) 80%, white 20%);
33
+ }
34
+
35
+ @media screen and (max-width: 400px) {
36
+ .choice-tabs__tab {
37
+ padding: 6px 4px;
38
+ font-size: 0.87em;
39
+ }
40
+ }
41
+
42
+ .choice-tabs__tab:hover {
43
+ color: var(--color-text);
44
+ }
45
+
46
+ .choice-tabs__tab--disabled {
47
+ color: GrayText;
48
+ cursor: default;
49
+ }
50
+
51
+ .choice-tabs__tab:focus {
52
+ outline: none;
53
+ }
@@ -0,0 +1,91 @@
1
+ export { Tabs }
2
+
3
+ import React from 'react'
4
+ import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
5
+ import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
6
+ import { usePageContext } from '../../renderer/usePageContext.js'
7
+ import { assertUsage } from '../../utils/assert.js'
8
+ import './Tabs.css'
9
+
10
+ function Tabs({ choice }: { choice: string }) {
11
+ const groupName = choice
12
+ const pageContext = usePageContext()
13
+ const choicesAll = pageContext.config.docpress.choices
14
+ assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`)
15
+
16
+ const { choices, default: defaultChoice } = choicesAll[groupName]
17
+ const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
18
+ const setPrevPosition = useRestoreScroll([selectedChoice])
19
+ const selectedIndex = choices.indexOf(selectedChoice)
20
+
21
+ return (
22
+ <div className="choice-tabs" data-choice-group={groupName}>
23
+ {/* Hidden select used to control tablist styling via CSS. */}
24
+ <select name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
25
+ {choices.map((choice, i) => (
26
+ <option key={i} value={choice}>
27
+ {choice}
28
+ </option>
29
+ ))}
30
+ </select>
31
+ <ul id={`choicesFor-${groupName}`} className="choice-tabs__tab-list" role="tablist">
32
+ {choices.map((choice, i) => (
33
+ <li
34
+ key={i}
35
+ id={choice}
36
+ className="choice-tabs__tab"
37
+ role="tab"
38
+ aria-selected={i === selectedIndex}
39
+ tabIndex={i === selectedIndex ? 0 : -1}
40
+ onClick={(e) => handleOnClick(e, i)}
41
+ onKeyDown={handleOnKeyDown}
42
+ >
43
+ {choice}
44
+ </li>
45
+ ))}
46
+ </ul>
47
+ </div>
48
+ )
49
+
50
+ function handleOnClick(e: React.MouseEvent<HTMLLIElement, MouseEvent>, index: number) {
51
+ const el = e.currentTarget
52
+ setPrevPosition(el)
53
+ setSelectedChoice(choices[index]!)
54
+ }
55
+
56
+ function handleOnKeyDown(e: React.KeyboardEvent<HTMLLIElement>) {
57
+ const el = e.currentTarget
58
+ let nextIndex = selectedIndex
59
+
60
+ switch (e.key) {
61
+ case 'ArrowRight':
62
+ nextIndex = (selectedIndex + 1) % choices.length
63
+ break
64
+ case 'ArrowLeft':
65
+ nextIndex = (selectedIndex - 1 + choices.length) % choices.length
66
+ break
67
+ case 'Home':
68
+ nextIndex = 0
69
+ break
70
+ case 'End':
71
+ nextIndex = choices.length - 1
72
+ break
73
+ default:
74
+ return
75
+ }
76
+
77
+ e.preventDefault()
78
+ setPrevPosition(el)
79
+ const nextChoice = choices[nextIndex]!
80
+ setSelectedChoice(nextChoice)
81
+ const tabEl = el.parentElement?.parentElement as HTMLDivElement
82
+
83
+ if (!isInViewport(tabEl)) tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' })
84
+ el.focus()
85
+ }
86
+ }
87
+
88
+ function isInViewport(el: Element) {
89
+ const rect = el.getBoundingClientRect()
90
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth
91
+ }
@@ -1,4 +1,4 @@
1
- export { useSelectedChoice }
1
+ export { useCurrentSelection }
2
2
  export { initializeChoiceGroup_SSR }
3
3
 
4
4
  import { useLocalStorage } from './useLocalStorage.js'
@@ -12,7 +12,7 @@ const keyPrefix = 'docpress'
12
12
  * @param defaultValue Default choice value.
13
13
  * @returns `[selectedChoice, setSelectedChoice]`.
14
14
  */
15
- function useSelectedChoice(choiceGroupName: string, defaultValue: string) {
15
+ function useCurrentSelection(choiceGroupName: string, defaultValue: string) {
16
16
  return useLocalStorage(`${keyPrefix}:choice:${choiceGroupName}`, defaultValue)
17
17
  }
18
18
 
@@ -11,7 +11,7 @@ type ScrollPosition = { top: number; el: Element }
11
11
  * preserving the user’s scroll position.
12
12
  *
13
13
  * @param deps Dependencies that trigger scroll restoration
14
- * @returns Ref holding the tracked element and its previous top position
14
+ * @returns Function to set the previous top position
15
15
  */
16
16
  function useRestoreScroll(deps: React.DependencyList) {
17
17
  const prevPositionRef = useRef<ScrollPosition | null>(null)
@@ -27,5 +27,9 @@ function useRestoreScroll(deps: React.DependencyList) {
27
27
  prevPositionRef.current = null
28
28
  }, deps)
29
29
 
30
- return prevPositionRef
30
+ const setPrevPosition = (el: Element) => {
31
+ prevPositionRef.current = { top: el.getBoundingClientRect().top, el }
32
+ }
33
+
34
+ return setPrevPosition
31
35
  }
@@ -28,9 +28,8 @@ function remarkChoiceGroup() {
28
28
  }
29
29
  })
30
30
 
31
- const replaced = new WeakSet()
32
31
  visit(tree, (node) => {
33
- if (!('children' in node) || replaced.has(node)) return 'skip'
32
+ if (!('children' in node) || node.data?.customDataIsVisited) return 'skip'
34
33
 
35
34
  let start = -1
36
35
  let end = 0
@@ -43,9 +42,10 @@ function remarkChoiceGroup() {
43
42
 
44
43
  for (const choiceNodes of choiceNodesFiltered) {
45
44
  const replacement = generateChoiceGroupCode(choiceNodes, node)
45
+ replacement.data ??= {}
46
+ replacement.data.customDataIsVisited = true
46
47
  replacements.push(replacement)
47
48
  }
48
- replaced.add(replacements)
49
49
 
50
50
  node.children.splice(start, end - start, ...replacements)
51
51
 
@@ -56,7 +56,7 @@ function remarkChoiceGroup() {
56
56
  for (; end < node.children.length; end++) {
57
57
  const child = node.children[end]!
58
58
 
59
- if (!['code', 'mdxJsxFlowElement', 'containerDirective'].includes(child.type)) {
59
+ if (!['code', 'containerDirective'].includes(child.type)) {
60
60
  process()
61
61
  continue
62
62
  }
@@ -85,7 +85,6 @@ function filterChoices(nodes: ChoiceNode['children']) {
85
85
  .map((node) => {
86
86
  const choice = node.data!.customDataChoice!
87
87
  const nodes = nodesByChoice.get(choice) ?? []
88
- node.data!.customDataChoice = undefined
89
88
  nodes.push(node)
90
89
  nodesByChoice.set(choice, nodes)
91
90
  })
@@ -99,6 +98,7 @@ function filterChoices(nodes: ChoiceNode['children']) {
99
98
 
100
99
  declare module 'mdast' {
101
100
  export interface Data {
101
+ customDataIsVisited?: boolean
102
102
  customDataChoice?: string
103
103
  customDataFilter?: string
104
104
  customDataParentChoiceGroup?: {
@@ -33,6 +33,9 @@ function remarkDetype() {
33
33
  // Skip if 'ts-only' meta is present
34
34
  if (node.meta?.includes('ts-only')) return
35
35
 
36
+ // Skip if 'customDataChoice' is 'TypeScript'
37
+ if (node.data?.customDataChoice === 'TypeScript') return
38
+
36
39
  // Collect this node for post-processing
37
40
  code_nodes.push({ codeBlock: node, index, parent })
38
41
  })
@@ -54,9 +57,6 @@ function transformYaml(node: CodeNode) {
54
57
  // Skip wrapping if the YAML code block hasn't changed
55
58
  if (codeBlockContentJs === codeBlock.value) return
56
59
 
57
- const meta = parseMetaString(codeBlock.meta, ['choice'])
58
- const { choice } = meta.props
59
- codeBlock.meta = meta.rest
60
60
  const { position, ...rest } = codeBlock
61
61
 
62
62
  // Create a new code node for the JS version based on the modified YAML
@@ -71,21 +71,16 @@ function transformYaml(node: CodeNode) {
71
71
  { choiceValue: 'TypeScript', children: [codeBlock] },
72
72
  ]
73
73
  const replacement = generateChoiceGroupCode(choiceNodes, parent, true)
74
- replacement.data ??= { customDataChoice: choice, customDataFilter: 'codeLang' }
75
74
 
76
75
  parent.children.splice(index, 1, replacement)
77
76
  }
78
77
 
79
78
  async function transformTsToJs(node: CodeNode, file: VFile) {
80
79
  const { codeBlock, index, parent } = node
81
- const meta = parseMetaString(codeBlock.meta, ['max-width', 'choice'])
80
+ const meta = parseMetaString(codeBlock.meta, ['max-width'])
82
81
  const maxWidth = Number(meta.props['max-width'])
83
- const { choice } = meta.props
84
82
  codeBlock.meta = meta.rest
85
83
 
86
- codeBlock.data ??= { customDataChoice: choice, customDataFilter: 'codeLang' }
87
- if (choice === 'TypeScript') return
88
-
89
84
  let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value)
90
85
  let codeBlockContentJs = ''
91
86
 
@@ -126,7 +121,7 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
126
121
  // No wrapping needed if JS and TS code are still identical
127
122
  if (codeBlockContentJs === codeBlock.value) return
128
123
 
129
- const { position, lang, data, ...rest } = codeBlock
124
+ const { position, lang, ...rest } = codeBlock
130
125
 
131
126
  const tsCode: Code = { ...rest, lang }
132
127
  const jsCode: Code = {
@@ -145,8 +140,6 @@ async function transformTsToJs(node: CodeNode, file: VFile) {
145
140
  const hide = codeBlockReplacedJs === codeBlockContentJs
146
141
  const replacement = generateChoiceGroupCode(choiceNodes, parent, hide)
147
142
 
148
- replacement.data ??= { ...data }
149
-
150
143
  parent.children.splice(index, 1, replacement)
151
144
  }
152
145
 
@@ -4,7 +4,6 @@ import type { Code, Root } from 'mdast'
4
4
  import type { VFile } from '@mdx-js/mdx/internal-create-format-aware-processors'
5
5
  import { visit } from 'unist-util-visit'
6
6
  import convert_ from 'npm-to-yarn'
7
- import { parseMetaString } from './rehypeMetaToProps.js'
8
7
  import { generateChoiceGroupCode } from './utils/generateChoiceGroupCode.js'
9
8
  import { assertUsage } from '../utils/assert.js'
10
9
  import pc from '@brillout/picocolors'
@@ -25,15 +24,8 @@ function remarkPkgManager() {
25
24
  }. Replace it with the equivalent 'npm' command for the package manager toggle to work.`,
26
25
  )
27
26
  if (!node.value.includes('npm ') && !node.value.includes('npx ')) return
28
- let choice: string | undefined = undefined
29
27
  const nodes = new Map<string, Code>()
30
28
 
31
- if (node.meta) {
32
- const meta = parseMetaString(node.meta, ['choice'])
33
- choice = meta.props['choice']
34
- node.meta = meta.rest
35
- }
36
-
37
29
  node.value = node.value.replaceAll('npm i ', 'npm install ')
38
30
  nodes.set('npm', node)
39
31
 
@@ -49,7 +41,6 @@ function remarkPkgManager() {
49
41
  const choiceNodes = [...nodes].map(([name, node]) => ({ choiceValue: name, children: [node] }))
50
42
  const replacement = generateChoiceGroupCode(choiceNodes, parent)
51
43
 
52
- replacement.data ??= { customDataChoice: choice, customDataFilter: replacement.type }
53
44
  parent.children.splice(index, 1, replacement)
54
45
  })
55
46
  }
@@ -1,7 +1,6 @@
1
1
  export { generateChoiceGroupCode }
2
2
  export type { ChoiceNode }
3
3
 
4
- import type { VikeConfig } from 'vike/types'
5
4
  import type { BlockContent, DefinitionContent, Parent } from 'mdast'
6
5
  import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx'
7
6
  import { getVikeConfig } from 'vike/plugin'
@@ -26,20 +25,12 @@ const CHOICES_BUILT_IN: Record<string, { choices: string[]; default: string }> =
26
25
 
27
26
  function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide: boolean = false): MdxJsxFlowElement {
28
27
  let lvl: number = 0
28
+ const customHidden = choiceNodes.some((node) =>
29
+ node.children.some((node) => node.type === 'containerDirective' && node.children[0]!.type !== 'code'),
30
+ )
31
+ const hidden = hide || customHidden
29
32
 
30
- const vikeConfig = getVikeConfig()
31
- const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue)
32
- const choiceGroup = findChoiceGroup(vikeConfig, choices)
33
-
34
- const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
35
- const node = choiceNodes.find((n) => n.choiceValue === choice)
36
-
37
- return {
38
- choiceValue: choice,
39
- children: node?.children ?? [],
40
- }
41
- })
42
-
33
+ const { choiceGroup, mergedChoiceNodes } = resolveChoiceGroupNodes(choiceNodes)
43
34
  const attributes: MdxJsxAttribute[] = []
44
35
  const children: MdxJsxFlowElement[] = []
45
36
 
@@ -73,55 +64,15 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
73
64
  if (parent.data?.customDataParentChoiceGroup) {
74
65
  const { lvl: parentLvl, ...parentChoiceGroup } = parent.data.customDataParentChoiceGroup
75
66
 
76
- attributes.push({
77
- type: 'mdxJsxAttribute',
78
- name: 'parentChoiceGroup',
79
- value: {
80
- type: 'mdxJsxAttributeValueExpression',
81
- value: '',
82
- data: {
83
- estree: {
84
- type: 'Program',
85
- sourceType: 'module',
86
- comments: [],
87
- body: [
88
- // @ts-ignore: Missing properties in type definition
89
- {
90
- type: 'ExpressionStatement',
91
- expression: valueToEstree(parentChoiceGroup),
92
- },
93
- ],
94
- },
95
- },
96
- },
97
- })
67
+ attributes.push(expressionToAttribute('parentChoiceGroup', parentChoiceGroup))
98
68
 
99
69
  lvl = parentLvl + 1
100
70
  parent.data.customDataParentChoiceGroup = undefined
101
71
  }
102
72
 
103
- attributes.push({
104
- type: 'mdxJsxAttribute',
105
- name: 'choiceGroup',
106
- value: {
107
- type: 'mdxJsxAttributeValueExpression',
108
- value: '',
109
- data: {
110
- estree: {
111
- type: 'Program',
112
- sourceType: 'module',
113
- comments: [],
114
- body: [
115
- // @ts-ignore: Missing properties in type definition
116
- {
117
- type: 'ExpressionStatement',
118
- expression: valueToEstree({ ...choiceGroup, hidden: choiceNodes.length === 1 || hide, lvl }),
119
- },
120
- ],
121
- },
122
- },
123
- },
124
- })
73
+ attributes.push(
74
+ expressionToAttribute('choiceGroup', { ...choiceGroup, hidden: choiceNodes.length === 1 || hidden, lvl }),
75
+ )
125
76
 
126
77
  const choiceGroupNode: MdxJsxFlowElement = {
127
78
  type: 'mdxJsxFlowElement',
@@ -142,7 +93,9 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
142
93
  return choiceGroupNode
143
94
  }
144
95
 
145
- function findChoiceGroup(vikeConfig: VikeConfig, choices: string[]) {
96
+ function resolveChoiceGroupNodes(choiceNodes: ChoiceNode[]) {
97
+ const vikeConfig = getVikeConfig()
98
+ const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue)
146
99
  const { choices: choicesConfig } = vikeConfig.config.docpress
147
100
  const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig }
148
101
 
@@ -163,5 +116,39 @@ function findChoiceGroup(vikeConfig: VikeConfig, choices: string[]) {
163
116
  disabled,
164
117
  }
165
118
 
166
- return choiceGroup
119
+ const mergedChoiceNodes: ChoiceNode[] = choiceGroup.choices.map((choice) => {
120
+ const node = choiceNodes.find((node) => node.choiceValue === choice)
121
+
122
+ return {
123
+ choiceValue: choice,
124
+ children: node?.children ?? [],
125
+ }
126
+ })
127
+
128
+ return { choiceGroup, mergedChoiceNodes }
129
+ }
130
+
131
+ function expressionToAttribute(name: string, value: unknown): MdxJsxAttribute {
132
+ return {
133
+ type: 'mdxJsxAttribute',
134
+ name,
135
+ value: {
136
+ type: 'mdxJsxAttributeValueExpression',
137
+ value: '',
138
+ data: {
139
+ estree: {
140
+ type: 'Program',
141
+ sourceType: 'module',
142
+ comments: [],
143
+ body: [
144
+ // @ts-ignore: Missing properties in type definition
145
+ {
146
+ type: 'ExpressionStatement',
147
+ expression: valueToEstree(value),
148
+ },
149
+ ],
150
+ },
151
+ },
152
+ },
153
+ }
167
154
  }
@@ -8,3 +8,4 @@ export * from './HorizontalLine.js'
8
8
  export * from './CodeBlockTransformer.js'
9
9
  export * from './Comment.js'
10
10
  export * from './FileRemoved.js'
11
+ export * from '../code-blocks/components/Tabs.js'
@@ -0,0 +1,6 @@
1
+ export { Tabs };
2
+ import React from 'react';
3
+ import './Tabs.css';
4
+ declare function Tabs({ choice }: {
5
+ choice: string;
6
+ }): React.JSX.Element;
@@ -0,0 +1,57 @@
1
+ export { Tabs };
2
+ import React from 'react';
3
+ import { useCurrentSelection } from '../hooks/useCurrentSelection.js';
4
+ import { useRestoreScroll } from '../hooks/useRestoreScroll.js';
5
+ import { usePageContext } from '../../renderer/usePageContext.js';
6
+ import { assertUsage } from '../../utils/assert.js';
7
+ import './Tabs.css';
8
+ function Tabs({ choice }) {
9
+ const groupName = choice;
10
+ const pageContext = usePageContext();
11
+ const choicesAll = pageContext.config.docpress.choices;
12
+ assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`);
13
+ const { choices, default: defaultChoice } = choicesAll[groupName];
14
+ const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice);
15
+ const setPrevPosition = useRestoreScroll([selectedChoice]);
16
+ const selectedIndex = choices.indexOf(selectedChoice);
17
+ return (React.createElement("div", { className: "choice-tabs", "data-choice-group": groupName },
18
+ React.createElement("select", { name: `choicesFor-${groupName}`, value: selectedChoice, hidden: true, disabled: true }, choices.map((choice, i) => (React.createElement("option", { key: i, value: choice }, choice)))),
19
+ React.createElement("ul", { id: `choicesFor-${groupName}`, className: "choice-tabs__tab-list", role: "tablist" }, choices.map((choice, i) => (React.createElement("li", { key: i, id: choice, className: "choice-tabs__tab", role: "tab", "aria-selected": i === selectedIndex, tabIndex: i === selectedIndex ? 0 : -1, onClick: (e) => handleOnClick(e, i), onKeyDown: handleOnKeyDown }, choice))))));
20
+ function handleOnClick(e, index) {
21
+ const el = e.currentTarget;
22
+ setPrevPosition(el);
23
+ setSelectedChoice(choices[index]);
24
+ }
25
+ function handleOnKeyDown(e) {
26
+ const el = e.currentTarget;
27
+ let nextIndex = selectedIndex;
28
+ switch (e.key) {
29
+ case 'ArrowRight':
30
+ nextIndex = (selectedIndex + 1) % choices.length;
31
+ break;
32
+ case 'ArrowLeft':
33
+ nextIndex = (selectedIndex - 1 + choices.length) % choices.length;
34
+ break;
35
+ case 'Home':
36
+ nextIndex = 0;
37
+ break;
38
+ case 'End':
39
+ nextIndex = choices.length - 1;
40
+ break;
41
+ default:
42
+ return;
43
+ }
44
+ e.preventDefault();
45
+ setPrevPosition(el);
46
+ const nextChoice = choices[nextIndex];
47
+ setSelectedChoice(nextChoice);
48
+ const tabEl = el.parentElement?.parentElement;
49
+ if (!isInViewport(tabEl))
50
+ tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' });
51
+ el.focus();
52
+ }
53
+ }
54
+ function isInViewport(el) {
55
+ const rect = el.getBoundingClientRect();
56
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
57
+ }
@@ -0,0 +1,11 @@
1
+ export { useCurrentSelection };
2
+ export { initializeChoiceGroup_SSR };
3
+ /**
4
+ * Stores and retrieves a selected choice from local storage.
5
+ *
6
+ * @param choiceGroupName Group name for the stored choice.
7
+ * @param defaultValue Default choice value.
8
+ * @returns `[selectedChoice, setSelectedChoice]`.
9
+ */
10
+ declare function useCurrentSelection(choiceGroupName: string, defaultValue: string): readonly [string, (value: string) => void];
11
+ declare const initializeChoiceGroup_SSR: string;
@@ -0,0 +1,34 @@
1
+ export { useCurrentSelection };
2
+ export { initializeChoiceGroup_SSR };
3
+ import { useLocalStorage } from './useLocalStorage.js';
4
+ const keyPrefix = 'docpress';
5
+ /**
6
+ * Stores and retrieves a selected choice from local storage.
7
+ *
8
+ * @param choiceGroupName Group name for the stored choice.
9
+ * @param defaultValue Default choice value.
10
+ * @returns `[selectedChoice, setSelectedChoice]`.
11
+ */
12
+ function useCurrentSelection(choiceGroupName, defaultValue) {
13
+ return useLocalStorage(`${keyPrefix}:choice:${choiceGroupName}`, defaultValue);
14
+ }
15
+ // WARNING: We cannot use `keyPrefix` here: closures don't work because we serialize the function.
16
+ const initializeChoiceGroup_SSR = `initializeChoiceGroup();${initializeChoiceGroup.toString()};`;
17
+ function initializeChoiceGroup() {
18
+ const groupsElements = document.querySelectorAll('[data-choice-group]');
19
+ for (const groupEl of groupsElements) {
20
+ const choiceGroupName = groupEl.getAttribute('data-choice-group');
21
+ const storageKey = `docpress:choice:${choiceGroupName}`;
22
+ const selectedChoice = localStorage.getItem(storageKey);
23
+ if (selectedChoice) {
24
+ const selectEl = groupEl.querySelector(`select[name="choicesFor-${choiceGroupName}"]`);
25
+ const selectedIndex = [...selectEl.options].findIndex((option) => option.value === selectedChoice);
26
+ if (selectedIndex === -1) {
27
+ localStorage.removeItem(storageKey);
28
+ }
29
+ else {
30
+ selectEl.value = selectedChoice;
31
+ }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,10 @@
1
+ export { useLocalStorage };
2
+ /**
3
+ * A simple, generic `useLocalStorage` hook with SSR and cross-tab support.
4
+ *
5
+ * @param storageKey The key used in localStorage.
6
+ * @param clientValue Default value for the client.
7
+ * @param ssrValue Optional fallback for server-side rendering.
8
+ * @returns A tuple `[value, setValue]`.
9
+ */
10
+ declare function useLocalStorage(storageKey: string, clientValue: string, ssrValue?: string): readonly [string, (value: string) => void];
@@ -0,0 +1,31 @@
1
+ export { useLocalStorage };
2
+ import { useCallback, useSyncExternalStore } from 'react';
3
+ /**
4
+ * A simple, generic `useLocalStorage` hook with SSR and cross-tab support.
5
+ *
6
+ * @param storageKey The key used in localStorage.
7
+ * @param clientValue Default value for the client.
8
+ * @param ssrValue Optional fallback for server-side rendering.
9
+ * @returns A tuple `[value, setValue]`.
10
+ */
11
+ function useLocalStorage(storageKey, clientValue, ssrValue) {
12
+ const subscribe = useCallback((callback) => {
13
+ const listener = (e) => {
14
+ if (e.key === storageKey)
15
+ callback();
16
+ };
17
+ window.addEventListener('storage', listener);
18
+ return () => window.removeEventListener('storage', listener);
19
+ }, [storageKey]);
20
+ const getSnapshot = useCallback(() => {
21
+ const storedValue = localStorage.getItem(storageKey);
22
+ return storedValue || clientValue;
23
+ }, [storageKey, clientValue]);
24
+ const setValue = (value) => {
25
+ localStorage.setItem(storageKey, value);
26
+ // Manually dispatch a storage event to force update in the current tab
27
+ window.dispatchEvent(new StorageEvent('storage', { key: storageKey }));
28
+ };
29
+ const value = useSyncExternalStore(subscribe, getSnapshot, () => ssrValue || clientValue);
30
+ return [value, setValue];
31
+ }
@@ -0,0 +1,12 @@
1
+ export { useRestoreScroll };
2
+ import React from 'react';
3
+ /**
4
+ * useRestoreScroll
5
+ *
6
+ * Keeps the page from jumping when content changes,
7
+ * preserving the user’s scroll position.
8
+ *
9
+ * @param deps Dependencies that trigger scroll restoration
10
+ * @returns Function to set the previous top position
11
+ */
12
+ declare function useRestoreScroll(deps: React.DependencyList): (el: Element) => void;
@@ -0,0 +1,27 @@
1
+ export { useRestoreScroll };
2
+ import { useEffect, useRef } from 'react';
3
+ /**
4
+ * useRestoreScroll
5
+ *
6
+ * Keeps the page from jumping when content changes,
7
+ * preserving the user’s scroll position.
8
+ *
9
+ * @param deps Dependencies that trigger scroll restoration
10
+ * @returns Function to set the previous top position
11
+ */
12
+ function useRestoreScroll(deps) {
13
+ const prevPositionRef = useRef(null);
14
+ useEffect(() => {
15
+ if (!prevPositionRef.current)
16
+ return;
17
+ const { top, el } = prevPositionRef.current;
18
+ const delta = el.getBoundingClientRect().top - top;
19
+ if (delta !== 0)
20
+ window.scrollBy(0, delta);
21
+ prevPositionRef.current = null;
22
+ }, deps);
23
+ const setPrevPosition = (el) => {
24
+ prevPositionRef.current = { top: el.getBoundingClientRect().top, el };
25
+ };
26
+ return setPrevPosition;
27
+ }
@@ -3,6 +3,7 @@ import type { Root } from 'mdast';
3
3
  declare function remarkChoiceGroup(): (tree: Root) => void;
4
4
  declare module 'mdast' {
5
5
  interface Data {
6
+ customDataIsVisited?: boolean;
6
7
  customDataChoice?: string;
7
8
  customDataFilter?: string;
8
9
  customDataParentChoiceGroup?: {
@@ -24,9 +24,8 @@ function remarkChoiceGroup() {
24
24
  }
25
25
  }
26
26
  });
27
- const replaced = new WeakSet();
28
27
  visit(tree, (node) => {
29
- if (!('children' in node) || replaced.has(node))
28
+ if (!('children' in node) || node.data?.customDataIsVisited)
30
29
  return 'skip';
31
30
  let start = -1;
32
31
  let end = 0;
@@ -38,16 +37,17 @@ function remarkChoiceGroup() {
38
37
  const replacements = [];
39
38
  for (const choiceNodes of choiceNodesFiltered) {
40
39
  const replacement = generateChoiceGroupCode(choiceNodes, node);
40
+ replacement.data ?? (replacement.data = {});
41
+ replacement.data.customDataIsVisited = true;
41
42
  replacements.push(replacement);
42
43
  }
43
- replaced.add(replacements);
44
44
  node.children.splice(start, end - start, ...replacements);
45
45
  end = start;
46
46
  start = -1;
47
47
  };
48
48
  for (; end < node.children.length; end++) {
49
49
  const child = node.children[end];
50
- if (!['code', 'mdxJsxFlowElement', 'containerDirective'].includes(child.type)) {
50
+ if (!['code', 'containerDirective'].includes(child.type)) {
51
51
  process();
52
52
  continue;
53
53
  }
@@ -72,7 +72,6 @@ function filterChoices(nodes) {
72
72
  .map((node) => {
73
73
  const choice = node.data.customDataChoice;
74
74
  const nodes = nodesByChoice.get(choice) ?? [];
75
- node.data.customDataChoice = undefined;
76
75
  nodes.push(node);
77
76
  nodesByChoice.set(choice, nodes);
78
77
  });
@@ -22,6 +22,9 @@ function remarkDetype() {
22
22
  // Skip if 'ts-only' meta is present
23
23
  if (node.meta?.includes('ts-only'))
24
24
  return;
25
+ // Skip if 'customDataChoice' is 'TypeScript'
26
+ if (node.data?.customDataChoice === 'TypeScript')
27
+ return;
25
28
  // Collect this node for post-processing
26
29
  code_nodes.push({ codeBlock: node, index, parent });
27
30
  });
@@ -41,9 +44,6 @@ function transformYaml(node) {
41
44
  // Skip wrapping if the YAML code block hasn't changed
42
45
  if (codeBlockContentJs === codeBlock.value)
43
46
  return;
44
- const meta = parseMetaString(codeBlock.meta, ['choice']);
45
- const { choice } = meta.props;
46
- codeBlock.meta = meta.rest;
47
47
  const { position, ...rest } = codeBlock;
48
48
  // Create a new code node for the JS version based on the modified YAML
49
49
  const yamlJsCode = {
@@ -56,18 +56,13 @@ function transformYaml(node) {
56
56
  { choiceValue: 'TypeScript', children: [codeBlock] },
57
57
  ];
58
58
  const replacement = generateChoiceGroupCode(choiceNodes, parent, true);
59
- replacement.data ?? (replacement.data = { customDataChoice: choice, customDataFilter: 'codeLang' });
60
59
  parent.children.splice(index, 1, replacement);
61
60
  }
62
61
  async function transformTsToJs(node, file) {
63
62
  const { codeBlock, index, parent } = node;
64
- const meta = parseMetaString(codeBlock.meta, ['max-width', 'choice']);
63
+ const meta = parseMetaString(codeBlock.meta, ['max-width']);
65
64
  const maxWidth = Number(meta.props['max-width']);
66
- const { choice } = meta.props;
67
65
  codeBlock.meta = meta.rest;
68
- codeBlock.data ?? (codeBlock.data = { customDataChoice: choice, customDataFilter: 'codeLang' });
69
- if (choice === 'TypeScript')
70
- return;
71
66
  let codeBlockReplacedJs = replaceFileNameSuffixes(codeBlock.value);
72
67
  let codeBlockContentJs = '';
73
68
  // Remove TypeScript from the TS/TSX/Vue code node
@@ -104,7 +99,7 @@ async function transformTsToJs(node, file) {
104
99
  // No wrapping needed if JS and TS code are still identical
105
100
  if (codeBlockContentJs === codeBlock.value)
106
101
  return;
107
- const { position, lang, data, ...rest } = codeBlock;
102
+ const { position, lang, ...rest } = codeBlock;
108
103
  const tsCode = { ...rest, lang };
109
104
  const jsCode = {
110
105
  ...rest,
@@ -120,7 +115,6 @@ async function transformTsToJs(node, file) {
120
115
  // Add `hide` prop to `<ChoiceGroup>` if the only change was replacing `.ts` with `.js`
121
116
  const hide = codeBlockReplacedJs === codeBlockContentJs;
122
117
  const replacement = generateChoiceGroupCode(choiceNodes, parent, hide);
123
- replacement.data ?? (replacement.data = { ...data });
124
118
  parent.children.splice(index, 1, replacement);
125
119
  }
126
120
  // Replace all '.ts' extensions with '.js'
@@ -1,7 +1,6 @@
1
1
  export { remarkPkgManager };
2
2
  import { visit } from 'unist-util-visit';
3
3
  import convert_ from 'npm-to-yarn';
4
- import { parseMetaString } from './rehypeMetaToProps.js';
5
4
  import { generateChoiceGroupCode } from './utils/generateChoiceGroupCode.js';
6
5
  import { assertUsage } from '../utils/assert.js';
7
6
  import pc from '@brillout/picocolors';
@@ -18,13 +17,7 @@ function remarkPkgManager() {
18
17
  assertUsage(!node.value.includes('pnpm'), `Found a 'pnpm' command in the code block at: ${pc.bold(pc.blue(file.path))}, line ${node.position?.start.line}. Replace it with the equivalent 'npm' command for the package manager toggle to work.`);
19
18
  if (!node.value.includes('npm ') && !node.value.includes('npx '))
20
19
  return;
21
- let choice = undefined;
22
20
  const nodes = new Map();
23
- if (node.meta) {
24
- const meta = parseMetaString(node.meta, ['choice']);
25
- choice = meta.props['choice'];
26
- node.meta = meta.rest;
27
- }
28
21
  node.value = node.value.replaceAll('npm i ', 'npm install ');
29
22
  nodes.set('npm', node);
30
23
  for (const pm of PKG_MANAGERS) {
@@ -37,7 +30,6 @@ function remarkPkgManager() {
37
30
  }
38
31
  const choiceNodes = [...nodes].map(([name, node]) => ({ choiceValue: name, children: [node] }));
39
32
  const replacement = generateChoiceGroupCode(choiceNodes, parent);
40
- replacement.data ?? (replacement.data = { customDataChoice: choice, customDataFilter: replacement.type });
41
33
  parent.children.splice(index, 1, replacement);
42
34
  });
43
35
  };
@@ -14,16 +14,9 @@ const CHOICES_BUILT_IN = {
14
14
  };
15
15
  function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
16
16
  let lvl = 0;
17
- const vikeConfig = getVikeConfig();
18
- const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue);
19
- const choiceGroup = findChoiceGroup(vikeConfig, choices);
20
- const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
21
- const node = choiceNodes.find((n) => n.choiceValue === choice);
22
- return {
23
- choiceValue: choice,
24
- children: node?.children ?? [],
25
- };
26
- });
17
+ const customHidden = choiceNodes.some((node) => node.children.some((node) => node.type === 'containerDirective' && node.children[0].type !== 'code'));
18
+ const hidden = hide || customHidden;
19
+ const { choiceGroup, mergedChoiceNodes } = resolveChoiceGroupNodes(choiceNodes);
27
20
  const attributes = [];
28
21
  const children = [];
29
22
  for (const choiceNode of mergedChoiceNodes) {
@@ -54,53 +47,11 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
54
47
  }
55
48
  if (parent.data?.customDataParentChoiceGroup) {
56
49
  const { lvl: parentLvl, ...parentChoiceGroup } = parent.data.customDataParentChoiceGroup;
57
- attributes.push({
58
- type: 'mdxJsxAttribute',
59
- name: 'parentChoiceGroup',
60
- value: {
61
- type: 'mdxJsxAttributeValueExpression',
62
- value: '',
63
- data: {
64
- estree: {
65
- type: 'Program',
66
- sourceType: 'module',
67
- comments: [],
68
- body: [
69
- // @ts-ignore: Missing properties in type definition
70
- {
71
- type: 'ExpressionStatement',
72
- expression: valueToEstree(parentChoiceGroup),
73
- },
74
- ],
75
- },
76
- },
77
- },
78
- });
50
+ attributes.push(expressionToAttribute('parentChoiceGroup', parentChoiceGroup));
79
51
  lvl = parentLvl + 1;
80
52
  parent.data.customDataParentChoiceGroup = undefined;
81
53
  }
82
- attributes.push({
83
- type: 'mdxJsxAttribute',
84
- name: 'choiceGroup',
85
- value: {
86
- type: 'mdxJsxAttributeValueExpression',
87
- value: '',
88
- data: {
89
- estree: {
90
- type: 'Program',
91
- sourceType: 'module',
92
- comments: [],
93
- body: [
94
- // @ts-ignore: Missing properties in type definition
95
- {
96
- type: 'ExpressionStatement',
97
- expression: valueToEstree({ ...choiceGroup, hidden: choiceNodes.length === 1 || hide, lvl }),
98
- },
99
- ],
100
- },
101
- },
102
- },
103
- });
54
+ attributes.push(expressionToAttribute('choiceGroup', { ...choiceGroup, hidden: choiceNodes.length === 1 || hidden, lvl }));
104
55
  const choiceGroupNode = {
105
56
  type: 'mdxJsxFlowElement',
106
57
  name: 'ChoiceGroup',
@@ -117,7 +68,9 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
117
68
  }
118
69
  return choiceGroupNode;
119
70
  }
120
- function findChoiceGroup(vikeConfig, choices) {
71
+ function resolveChoiceGroupNodes(choiceNodes) {
72
+ const vikeConfig = getVikeConfig();
73
+ const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue);
121
74
  const { choices: choicesConfig } = vikeConfig.config.docpress;
122
75
  const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig };
123
76
  const groupName = Object.keys(choicesAll).find((key) => {
@@ -135,5 +88,36 @@ function findChoiceGroup(vikeConfig, choices) {
135
88
  ...choicesAll[groupName],
136
89
  disabled,
137
90
  };
138
- return choiceGroup;
91
+ const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
92
+ const node = choiceNodes.find((node) => node.choiceValue === choice);
93
+ return {
94
+ choiceValue: choice,
95
+ children: node?.children ?? [],
96
+ };
97
+ });
98
+ return { choiceGroup, mergedChoiceNodes };
99
+ }
100
+ function expressionToAttribute(name, value) {
101
+ return {
102
+ type: 'mdxJsxAttribute',
103
+ name,
104
+ value: {
105
+ type: 'mdxJsxAttributeValueExpression',
106
+ value: '',
107
+ data: {
108
+ estree: {
109
+ type: 'Program',
110
+ sourceType: 'module',
111
+ comments: [],
112
+ body: [
113
+ // @ts-ignore: Missing properties in type definition
114
+ {
115
+ type: 'ExpressionStatement',
116
+ expression: valueToEstree(value),
117
+ },
118
+ ],
119
+ },
120
+ },
121
+ },
122
+ };
139
123
  }
@@ -8,3 +8,4 @@ export * from './HorizontalLine.js';
8
8
  export * from './CodeBlockTransformer.js';
9
9
  export * from './Comment.js';
10
10
  export * from './FileRemoved.js';
11
+ export * from '../code-blocks/components/Tabs.js';
@@ -8,3 +8,4 @@ export * from './HorizontalLine.js';
8
8
  export * from './CodeBlockTransformer.js';
9
9
  export * from './Comment.js';
10
10
  export * from './FileRemoved.js';
11
+ export * from '../code-blocks/components/Tabs.js';
package/icons/index.ts CHANGED
@@ -1,14 +1,21 @@
1
1
  // Playground: https://www.figma.com/design/EFFQgT1aDH5exs2bABaIFJ/Icons?t=y0K2dHfJgpxMIl1e-0
2
- export { default as iconGear } from './gear.svg'
2
+ export { default as iconBluesky } from './bluesky.svg'
3
3
  export { default as iconBooks } from './books.svg'
4
- export { default as iconSeedling } from './seedling.svg'
5
- export { default as iconMagnifyingGlass } from './magnifying-glass.svg'
6
- export { default as iconPencil } from './pencil.svg'
4
+ export { default as iconChangelog } from './changelog.svg'
5
+ export { default as iconCoin } from './coin.svg'
7
6
  export { default as iconCompass } from './compass.svg'
8
- export { default as iconScroll } from './scroll.svg'
7
+ export { default as iconDiscord } from './discord.svg'
8
+ export { default as iconEyes } from './eyes.svg'
9
+ export { default as iconGear } from './gear.svg'
10
+ export { default as iconGithub } from './github.svg'
9
11
  export { default as iconGlobe } from './globe.svg'
10
- export { default as iconPlug } from './plug.svg'
12
+ export { default as iconLanguages } from './languages.svg'
13
+ export { default as iconLinkedin } from './linkedin.svg'
11
14
  export { default as iconLoudspeaker } from './loudspeaker.svg'
15
+ export { default as iconMagnifyingGlass } from './magnifying-glass.svg'
12
16
  export { default as iconMegaphone } from './megaphone.svg'
13
- export { default as iconCoin } from './coin.svg'
14
- export { default as iconEyes } from './eyes.svg'
17
+ export { default as iconPencil } from './pencil.svg'
18
+ export { default as iconPlug } from './plug.svg'
19
+ export { default as iconScroll } from './scroll.svg'
20
+ export { default as iconSeedling } from './seedling.svg'
21
+ export { default as iconTwitter } from './twitter.svg'
package/index.ts CHANGED
@@ -1,7 +1,16 @@
1
1
  /**********/
2
2
  /* PUBLIC */
3
3
  /**********/
4
- export { CodeBlockTransformer, Link, RepoLink, FileAdded, FileRemoved, ImportMeta, Emoji } from './components/index.js'
4
+ export {
5
+ CodeBlockTransformer,
6
+ Link,
7
+ RepoLink,
8
+ FileAdded,
9
+ FileRemoved,
10
+ ImportMeta,
11
+ Emoji,
12
+ Tabs,
13
+ } from './components/index.js'
5
14
  export { MenuToggle } from './Layout.js'
6
15
  export * from './components/Note.js'
7
16
  export * from './icons/index.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brillout/docpress",
3
- "version": "0.16.33",
3
+ "version": "0.16.35-commit-797f17e",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@brillout/picocolors": "^1.0.10",
@@ -54,12 +54,12 @@
54
54
  },
55
55
  "devDependencies": {
56
56
  "@brillout/release-me": "^0.4.8",
57
- "@vitejs/plugin-react": "^6.0.1",
58
57
  "@types/hast": "^3.0.4",
59
58
  "@types/mdast": "^4.0.4",
60
59
  "@types/node": "^24.10.0",
61
60
  "@types/react": "^19.2.2",
62
61
  "@types/react-dom": "^19.2.2",
62
+ "@vitejs/plugin-react": "^6.0.1",
63
63
  "mdast-util-directive": "^3.1.0",
64
64
  "mdast-util-mdx-jsx": "^3.2.0",
65
65
  "vike": "^0.4.255",