@brillout/docpress 0.16.34 → 0.16.35
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/code-blocks/components/ChoiceGroup.css +2 -66
- package/code-blocks/components/ChoiceGroup.tsx +3 -3
- package/code-blocks/components/Pre.css +1 -8
- package/code-blocks/components/Tabs.css +53 -0
- package/code-blocks/components/Tabs.tsx +91 -0
- package/code-blocks/hooks/useRestoreScroll.ts +6 -2
- package/code-blocks/utils/generateChoiceGroupCode.ts +43 -61
- package/components/index.ts +1 -0
- package/dist/code-blocks/components/Tabs.d.ts +6 -0
- package/dist/code-blocks/components/Tabs.js +57 -0
- package/dist/code-blocks/hooks/useCurrentSelection.d.ts +11 -0
- package/dist/code-blocks/hooks/useCurrentSelection.js +34 -0
- package/dist/code-blocks/hooks/useLocalStorage.d.ts +10 -0
- package/dist/code-blocks/hooks/useLocalStorage.js +31 -0
- package/dist/code-blocks/hooks/useRestoreScroll.d.ts +12 -0
- package/dist/code-blocks/hooks/useRestoreScroll.js +27 -0
- package/dist/code-blocks/utils/generateChoiceGroupCode.js +38 -56
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/index.ts +10 -1
- package/package.json +2 -2
|
@@ -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
|
|
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:
|
|
82
|
+
cursor: not-allowed;
|
|
147
83
|
opacity: 0.8;
|
|
148
84
|
}
|
|
149
85
|
|
|
@@ -133,7 +133,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
133
133
|
parentChoiceGroup,
|
|
134
134
|
} = choiceGroup
|
|
135
135
|
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
136
|
-
const
|
|
136
|
+
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
137
137
|
const [expanded, setExpanded] = useState(false)
|
|
138
138
|
const selectedIndex = choices.indexOf(selectedChoice)
|
|
139
139
|
const height = 25
|
|
@@ -188,7 +188,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
188
188
|
style={{ height }}
|
|
189
189
|
onClick={handleOnClick}
|
|
190
190
|
>
|
|
191
|
-
|
|
191
|
+
{choice}
|
|
192
192
|
</div>
|
|
193
193
|
))}
|
|
194
194
|
</div>
|
|
@@ -197,7 +197,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
197
197
|
function handleOnClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
|
198
198
|
e.stopPropagation()
|
|
199
199
|
const el = e.currentTarget
|
|
200
|
-
|
|
200
|
+
setPrevPosition(el)
|
|
201
201
|
if (el.getAttribute('aria-selected') === 'true') {
|
|
202
202
|
next()
|
|
203
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
-
|
|
30
|
+
const setPrevPosition = (el: Element) => {
|
|
31
|
+
prevPositionRef.current = { top: el.getBoundingClientRect().top, el }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return setPrevPosition
|
|
31
35
|
}
|
|
@@ -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,7 @@ 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
|
|
29
|
-
|
|
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
|
-
|
|
28
|
+
const { choiceGroup, mergedChoiceNodes } = resolveChoiceGroupNodes(choiceNodes)
|
|
43
29
|
const attributes: MdxJsxAttribute[] = []
|
|
44
30
|
const children: MdxJsxFlowElement[] = []
|
|
45
31
|
|
|
@@ -73,55 +59,15 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
|
|
|
73
59
|
if (parent.data?.customDataParentChoiceGroup) {
|
|
74
60
|
const { lvl: parentLvl, ...parentChoiceGroup } = parent.data.customDataParentChoiceGroup
|
|
75
61
|
|
|
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
|
-
})
|
|
62
|
+
attributes.push(expressionToAttribute('parentChoiceGroup', parentChoiceGroup))
|
|
98
63
|
|
|
99
64
|
lvl = parentLvl + 1
|
|
100
65
|
parent.data.customDataParentChoiceGroup = undefined
|
|
101
66
|
}
|
|
102
67
|
|
|
103
|
-
attributes.push(
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
})
|
|
68
|
+
attributes.push(
|
|
69
|
+
expressionToAttribute('choiceGroup', { ...choiceGroup, hidden: choiceNodes.length === 1 || hide, lvl }),
|
|
70
|
+
)
|
|
125
71
|
|
|
126
72
|
const choiceGroupNode: MdxJsxFlowElement = {
|
|
127
73
|
type: 'mdxJsxFlowElement',
|
|
@@ -142,7 +88,9 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
|
|
|
142
88
|
return choiceGroupNode
|
|
143
89
|
}
|
|
144
90
|
|
|
145
|
-
function
|
|
91
|
+
function resolveChoiceGroupNodes(choiceNodes: ChoiceNode[]) {
|
|
92
|
+
const vikeConfig = getVikeConfig()
|
|
93
|
+
const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue)
|
|
146
94
|
const { choices: choicesConfig } = vikeConfig.config.docpress
|
|
147
95
|
const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig }
|
|
148
96
|
|
|
@@ -163,5 +111,39 @@ function findChoiceGroup(vikeConfig: VikeConfig, choices: string[]) {
|
|
|
163
111
|
disabled,
|
|
164
112
|
}
|
|
165
113
|
|
|
166
|
-
|
|
114
|
+
const mergedChoiceNodes: ChoiceNode[] = choiceGroup.choices.map((choice) => {
|
|
115
|
+
const node = choiceNodes.find((node) => node.choiceValue === choice)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
choiceValue: choice,
|
|
119
|
+
children: node?.children ?? [],
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return { choiceGroup, mergedChoiceNodes }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function expressionToAttribute(name: string, value: unknown): MdxJsxAttribute {
|
|
127
|
+
return {
|
|
128
|
+
type: 'mdxJsxAttribute',
|
|
129
|
+
name,
|
|
130
|
+
value: {
|
|
131
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
132
|
+
value: '',
|
|
133
|
+
data: {
|
|
134
|
+
estree: {
|
|
135
|
+
type: 'Program',
|
|
136
|
+
sourceType: 'module',
|
|
137
|
+
comments: [],
|
|
138
|
+
body: [
|
|
139
|
+
// @ts-ignore: Missing properties in type definition
|
|
140
|
+
{
|
|
141
|
+
type: 'ExpressionStatement',
|
|
142
|
+
expression: valueToEstree(value),
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
}
|
|
167
149
|
}
|
package/components/index.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -14,16 +14,7 @@ const CHOICES_BUILT_IN = {
|
|
|
14
14
|
};
|
|
15
15
|
function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
16
16
|
let lvl = 0;
|
|
17
|
-
const
|
|
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 { choiceGroup, mergedChoiceNodes } = resolveChoiceGroupNodes(choiceNodes);
|
|
27
18
|
const attributes = [];
|
|
28
19
|
const children = [];
|
|
29
20
|
for (const choiceNode of mergedChoiceNodes) {
|
|
@@ -54,53 +45,11 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
|
54
45
|
}
|
|
55
46
|
if (parent.data?.customDataParentChoiceGroup) {
|
|
56
47
|
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
|
-
});
|
|
48
|
+
attributes.push(expressionToAttribute('parentChoiceGroup', parentChoiceGroup));
|
|
79
49
|
lvl = parentLvl + 1;
|
|
80
50
|
parent.data.customDataParentChoiceGroup = undefined;
|
|
81
51
|
}
|
|
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
|
-
});
|
|
52
|
+
attributes.push(expressionToAttribute('choiceGroup', { ...choiceGroup, hidden: choiceNodes.length === 1 || hide, lvl }));
|
|
104
53
|
const choiceGroupNode = {
|
|
105
54
|
type: 'mdxJsxFlowElement',
|
|
106
55
|
name: 'ChoiceGroup',
|
|
@@ -117,7 +66,9 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
|
117
66
|
}
|
|
118
67
|
return choiceGroupNode;
|
|
119
68
|
}
|
|
120
|
-
function
|
|
69
|
+
function resolveChoiceGroupNodes(choiceNodes) {
|
|
70
|
+
const vikeConfig = getVikeConfig();
|
|
71
|
+
const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue);
|
|
121
72
|
const { choices: choicesConfig } = vikeConfig.config.docpress;
|
|
122
73
|
const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig };
|
|
123
74
|
const groupName = Object.keys(choicesAll).find((key) => {
|
|
@@ -135,5 +86,36 @@ function findChoiceGroup(vikeConfig, choices) {
|
|
|
135
86
|
...choicesAll[groupName],
|
|
136
87
|
disabled,
|
|
137
88
|
};
|
|
138
|
-
|
|
89
|
+
const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
|
|
90
|
+
const node = choiceNodes.find((node) => node.choiceValue === choice);
|
|
91
|
+
return {
|
|
92
|
+
choiceValue: choice,
|
|
93
|
+
children: node?.children ?? [],
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
return { choiceGroup, mergedChoiceNodes };
|
|
97
|
+
}
|
|
98
|
+
function expressionToAttribute(name, value) {
|
|
99
|
+
return {
|
|
100
|
+
type: 'mdxJsxAttribute',
|
|
101
|
+
name,
|
|
102
|
+
value: {
|
|
103
|
+
type: 'mdxJsxAttributeValueExpression',
|
|
104
|
+
value: '',
|
|
105
|
+
data: {
|
|
106
|
+
estree: {
|
|
107
|
+
type: 'Program',
|
|
108
|
+
sourceType: 'module',
|
|
109
|
+
comments: [],
|
|
110
|
+
body: [
|
|
111
|
+
// @ts-ignore: Missing properties in type definition
|
|
112
|
+
{
|
|
113
|
+
type: 'ExpressionStatement',
|
|
114
|
+
expression: valueToEstree(value),
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
139
121
|
}
|
package/dist/components/index.js
CHANGED
package/index.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
/**********/
|
|
2
2
|
/* PUBLIC */
|
|
3
3
|
/**********/
|
|
4
|
-
export {
|
|
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.
|
|
3
|
+
"version": "0.16.35",
|
|
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",
|