@brillout/docpress 0.16.42 → 0.16.44
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 +126 -40
- package/code-blocks/components/ChoiceGroup.tsx +64 -56
- package/code-blocks/components/Tabs.css +2 -9
- package/code-blocks/components/Tabs.tsx +26 -71
- package/code-blocks/hooks/useCurrentSelection.ts +19 -10
- package/code-blocks/hooks/useMDXComponents.tsx +2 -2
- package/code-blocks/remarkChoiceGroup.ts +1 -1
- package/code-blocks/types.ts +3 -2
- package/code-blocks/utils/generateChoiceGroupCode.ts +6 -6
- package/code-blocks/utils/resolveChoices.ts +15 -0
- package/css/index.css +1 -0
- package/css/sr-only.css +11 -0
- package/dist/code-blocks/components/Tabs.js +10 -44
- package/dist/code-blocks/hooks/useCurrentSelection.js +23 -10
- package/dist/code-blocks/remarkChoiceGroup.js +1 -1
- package/dist/code-blocks/types.d.ts +3 -2
- package/dist/code-blocks/utils/generateChoiceGroupCode.js +6 -4
- package/dist/code-blocks/utils/resolveChoices.d.ts +7 -0
- package/dist/code-blocks/utils/resolveChoices.js +7 -0
- package/dist/renderer/usePageContext.d.ts +1 -0
- package/dist/resolvePageContext.d.ts +1 -0
- package/dist/resolvePageContext.js +3 -0
- package/dist/types/Config.d.ts +8 -6
- package/package.json +1 -1
- package/resolvePageContext.ts +4 -0
- package/types/Config.ts +4 -2
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
.choice-group {
|
|
1
|
+
.choice-group-container {
|
|
2
2
|
position: relative;
|
|
3
3
|
|
|
4
4
|
/* layout */
|
|
5
5
|
--top-position: 10px;
|
|
6
|
-
--border-
|
|
6
|
+
--border-radius: 5px;
|
|
7
|
+
--transition-select-list-duration: 180ms;
|
|
8
|
+
--transition-select-option-duration: 120ms;
|
|
9
|
+
--transition-select-list: top var(--transition-select-list-duration) cubic-bezier(0.2, 0.9, 0.2, 1), clip-path
|
|
10
|
+
var(--transition-select-list-duration) ease-in-out;
|
|
11
|
+
--transition-select-option: background var(--transition-select-option-duration) ease;
|
|
12
|
+
--transition-select-icon: filter 500ms ease, opacity 500ms ease;
|
|
7
13
|
|
|
8
14
|
.choice-group__selects {
|
|
9
15
|
position: absolute;
|
|
@@ -14,39 +20,50 @@
|
|
|
14
20
|
right: 42px;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
.choice-
|
|
18
|
-
|
|
23
|
+
.choice-select__list {
|
|
24
|
+
position: relative;
|
|
19
25
|
font-size: 13.3333px;
|
|
26
|
+
background: transparent;
|
|
27
|
+
height: calc(var(--choice-count) * var(--option-height));
|
|
28
|
+
top: 0;
|
|
20
29
|
|
|
21
30
|
-webkit-user-select: none; /* Safari */
|
|
22
31
|
-moz-user-select: none; /* Firefox */
|
|
23
32
|
-ms-user-select: none; /* IE/Edge legacy */
|
|
24
33
|
user-select: none; /* Standard */
|
|
25
34
|
|
|
26
|
-
border-radius:
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
border-radius: var(--border-radius);
|
|
36
|
+
width: 100%;
|
|
37
|
+
transition: var(--transition-select-list);
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
.choice-
|
|
32
|
-
position:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
.choice-select__border {
|
|
41
|
+
position: absolute;
|
|
42
|
+
left: 0;
|
|
43
|
+
right: 0;
|
|
44
|
+
height: var(--option-height);
|
|
45
|
+
|
|
46
|
+
border-style: solid;
|
|
47
|
+
border-width: 1px 2px 2px 1px;
|
|
48
|
+
border-color: hsl(0, 0%, 75%) hsl(0, 0%, 72%) hsl(0, 0%, 72%) hsl(0, 0%, 75%);
|
|
49
|
+
|
|
50
|
+
border-radius: var(--border-radius);
|
|
51
|
+
pointer-events: none;
|
|
52
|
+
|
|
53
|
+
transition: top var(--transition-select-list-duration) ease-in-out, height var(--transition-select-list-duration)
|
|
54
|
+
ease-in-out;
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
.choice-select__option {
|
|
41
58
|
display: flex;
|
|
59
|
+
height: var(--option-height);
|
|
42
60
|
white-space: nowrap;
|
|
43
61
|
align-items: center;
|
|
44
62
|
flex-wrap: nowrap;
|
|
45
63
|
background: #fff;
|
|
46
64
|
padding: 0 6px;
|
|
47
|
-
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
|
48
65
|
cursor: pointer;
|
|
49
|
-
transition:
|
|
66
|
+
transition: var(--transition-select-option);
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
.choice-select__option-content {
|
|
@@ -61,54 +78,123 @@
|
|
|
61
78
|
|
|
62
79
|
filter: grayscale(100%);
|
|
63
80
|
opacity: 0.8;
|
|
64
|
-
|
|
65
|
-
transition: filter var(--transition), opacity var(--transition);
|
|
81
|
+
transition: var(--transition-select-icon);
|
|
66
82
|
}
|
|
67
83
|
|
|
68
|
-
.choice-
|
|
69
|
-
.choice-
|
|
84
|
+
.choice-select__list:hover .choice-select__option img,
|
|
85
|
+
.choice-select__list[aria-expanded='true'] .choice-select__option img {
|
|
70
86
|
filter: grayscale(0%);
|
|
71
87
|
opacity: 1;
|
|
72
88
|
}
|
|
73
89
|
|
|
74
|
-
.choice-
|
|
75
|
-
overflow: hidden;
|
|
76
|
-
border-width: 1px 2px 2px 1px;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.choice-select[aria-expanded='true'] {
|
|
80
|
-
overflow: visible;
|
|
81
|
-
border-width: 0;
|
|
90
|
+
.choice-select__list[aria-expanded='true'] {
|
|
82
91
|
z-index: 1;
|
|
92
|
+
clip-path: inset(0px round var(--border-radius)) !important;
|
|
93
|
+
.choice-select__border {
|
|
94
|
+
top: 0 !important;
|
|
95
|
+
height: calc(var(--choice-count) * var(--option-height));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
.choice-select__list:not(.hovered) {
|
|
100
|
+
transition: none;
|
|
101
|
+
.choice-select__border {
|
|
102
|
+
transition: none;
|
|
103
|
+
}
|
|
104
|
+
.choice-select__option {
|
|
105
|
+
transition: none;
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
.choice-select__option:last-child {
|
|
91
|
-
|
|
109
|
+
.choice-select__option:not(:last-child) {
|
|
110
|
+
box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.1);
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
.choice-select__option:hover {
|
|
95
114
|
background: #f5f5f5;
|
|
96
115
|
}
|
|
97
116
|
|
|
98
|
-
.choice-select__option
|
|
117
|
+
.choice-select__option:has(.choice-select__radio:checked) {
|
|
99
118
|
background: #eee;
|
|
100
119
|
}
|
|
101
120
|
|
|
102
|
-
.choice-select__option[aria-disabled='true'] {
|
|
103
|
-
color: #999;
|
|
104
|
-
cursor: not-allowed;
|
|
105
|
-
opacity: 0.8;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
121
|
.hidden {
|
|
109
122
|
display: none !important;
|
|
110
123
|
}
|
|
111
124
|
|
|
125
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(1) .choice-select__radio:checked) {
|
|
126
|
+
top: 0;
|
|
127
|
+
clip-path: inset(
|
|
128
|
+
calc(0 * var(--option-height)) 0 calc((var(--choice-count) - 0 - 1) * var(--option-height)) 0 round
|
|
129
|
+
var(--border-radius)
|
|
130
|
+
);
|
|
131
|
+
.choice-select__border {
|
|
132
|
+
top: 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(2) .choice-select__radio:checked) {
|
|
136
|
+
top: calc(-1 * var(--option-height));
|
|
137
|
+
clip-path: inset(
|
|
138
|
+
calc(1 * var(--option-height)) 0 calc((var(--choice-count) - 1 - 1) * var(--option-height)) 0 round
|
|
139
|
+
var(--border-radius)
|
|
140
|
+
);
|
|
141
|
+
.choice-select__border {
|
|
142
|
+
top: var(--option-height);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(3) .choice-select__radio:checked) {
|
|
146
|
+
top: calc(-2 * var(--option-height));
|
|
147
|
+
clip-path: inset(
|
|
148
|
+
calc(2 * var(--option-height)) 0 calc((var(--choice-count) - 2 - 1) * var(--option-height)) 0 round
|
|
149
|
+
var(--border-radius)
|
|
150
|
+
);
|
|
151
|
+
.choice-select__border {
|
|
152
|
+
top: calc(2 * var(--option-height));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(4) .choice-select__radio:checked) {
|
|
156
|
+
top: calc(-3 * var(--option-height));
|
|
157
|
+
clip-path: inset(
|
|
158
|
+
calc(3 * var(--option-height)) 0 calc((var(--choice-count) - 3 - 1) * var(--option-height)) 0 round
|
|
159
|
+
var(--border-radius)
|
|
160
|
+
);
|
|
161
|
+
.choice-select__border {
|
|
162
|
+
top: calc(3 * var(--option-height));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(5) .choice-select__radio:checked) {
|
|
166
|
+
top: calc(-4 * var(--option-height));
|
|
167
|
+
clip-path: inset(
|
|
168
|
+
calc(4 * var(--option-height)) 0 calc((var(--choice-count) - 4 - 1) * var(--option-height)) 0 round
|
|
169
|
+
var(--border-radius)
|
|
170
|
+
);
|
|
171
|
+
.choice-select__border {
|
|
172
|
+
top: calc(4 * var(--option-height));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(6) .choice-select__radio:checked) {
|
|
176
|
+
top: calc(-5 * var(--option-height));
|
|
177
|
+
clip-path: inset(
|
|
178
|
+
calc(5 * var(--option-height)) 0 calc((var(--choice-count) - 5 - 1) * var(--option-height)) 0 round
|
|
179
|
+
var(--border-radius)
|
|
180
|
+
);
|
|
181
|
+
.choice-select__border {
|
|
182
|
+
top: calc(5 * var(--option-height));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
.choice-select__list:has(.choice-select__option:nth-of-type(7) .choice-select__radio:checked) {
|
|
186
|
+
top: calc(-6 * var(--option-height));
|
|
187
|
+
clip-path: inset(
|
|
188
|
+
calc(6 * var(--option-height)) 0 calc((var(--choice-count) - 6 - 1) * var(--option-height)) 0 round
|
|
189
|
+
var(--border-radius)
|
|
190
|
+
);
|
|
191
|
+
.choice-select__border {
|
|
192
|
+
top: calc(6 * var(--option-height));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.choice-group {
|
|
112
198
|
/* choice visibility logic */
|
|
113
199
|
select:has(option:nth-of-type(1):not(:checked)) ~ .choice:nth-of-type(1),
|
|
114
200
|
select:has(option:nth-of-type(2):not(:checked)) ~ .choice:nth-of-type(2),
|
|
@@ -1,37 +1,40 @@
|
|
|
1
|
-
export { ChoiceGroup,
|
|
1
|
+
export { ChoiceGroup, ChoiceGroupContainer }
|
|
2
2
|
|
|
3
3
|
import type { ChoiceGroup as TChoiceGroup, ChoiceGroupWithParent } from '../types.js'
|
|
4
|
-
import React, {
|
|
4
|
+
import React, { useId, useState } from 'react'
|
|
5
5
|
import { usePageContext } from '../../renderer/usePageContext.js'
|
|
6
6
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
|
|
7
7
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
|
|
8
8
|
import { cls } from '../../utils/cls.js'
|
|
9
9
|
import './ChoiceGroup.css'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
function useCustomSelectsContext() {
|
|
14
|
-
const ctx = useContext(CustomSelectsContainerContext)
|
|
15
|
-
if (!ctx) throw new Error('useCustomSelectsContext must be used inside provider')
|
|
16
|
-
return ctx
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function CustomSelectsContainer({
|
|
11
|
+
function ChoiceGroupContainer({
|
|
20
12
|
children,
|
|
21
13
|
choiceGroupAll,
|
|
22
14
|
}: { children: React.ReactNode; choiceGroupAll: ChoiceGroupWithParent[] }) {
|
|
23
|
-
|
|
15
|
+
const renderCustomSelect = (choiceGroupAll ?? []).some((choiceGroup) => choiceGroup.lvl === 0 && !choiceGroup.hidden)
|
|
16
|
+
return (
|
|
17
|
+
<div className="choice-group-container">
|
|
18
|
+
{children}
|
|
19
|
+
{renderCustomSelect && (
|
|
20
|
+
<div className={`choice-group__selects`}>
|
|
21
|
+
{(choiceGroupAll ?? []).map((choiceGroup) => (
|
|
22
|
+
<CustomSelect key={choiceGroup.name} choiceGroup={choiceGroup} />
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
)}
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
function ChoiceGroup({ children, choiceGroup }: { children: React.ReactNode; choiceGroup: TChoiceGroup }) {
|
|
27
|
-
const { name: groupName, choices, default: defaultChoice
|
|
31
|
+
const { name: groupName, choices, default: defaultChoice } = choiceGroup
|
|
28
32
|
const [selectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
29
|
-
const { choiceGroupAll } = useCustomSelectsContext()
|
|
30
33
|
|
|
31
34
|
return (
|
|
32
|
-
<div
|
|
35
|
+
<div className="choice-group">
|
|
33
36
|
{/* Hidden select used to control choice visibility via CSS */}
|
|
34
|
-
<select name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
|
|
37
|
+
<select data-choice-group={groupName} name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
|
|
35
38
|
{choices.map(({ name: choice }) => (
|
|
36
39
|
<option key={choice} value={choice}>
|
|
37
40
|
{choice}
|
|
@@ -39,69 +42,73 @@ function ChoiceGroup({ children, choiceGroup }: { children: React.ReactNode; cho
|
|
|
39
42
|
))}
|
|
40
43
|
</select>
|
|
41
44
|
{children}
|
|
42
|
-
{lvl === 0 && !choiceGroup.hidden && (
|
|
43
|
-
<div className={`choice-group__selects`}>
|
|
44
|
-
{(choiceGroupAll ?? []).map((choiceGroup) => (
|
|
45
|
-
<CustomSelect key={choiceGroup.name} choiceGroup={choiceGroup} />
|
|
46
|
-
))}
|
|
47
|
-
</div>
|
|
48
|
-
)}
|
|
49
45
|
</div>
|
|
50
46
|
)
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
const OPTION_HEIGHT = 25
|
|
54
50
|
function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
55
|
-
const
|
|
51
|
+
const radioId = useId()
|
|
52
|
+
const choicesAll = usePageContext().resolved.choices
|
|
56
53
|
const { name: groupName, emptyChoices, default: defaultChoice, hidden, parentChoiceGroup, isBuiltIn } = choiceGroup
|
|
57
54
|
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
58
55
|
const [expanded, setExpanded] = useState(false)
|
|
56
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
59
57
|
const [parentSelectedChoice] = useCurrentSelection(parentChoiceGroup?.name || '', parentChoiceGroup?.default || '')
|
|
60
58
|
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
61
59
|
|
|
62
|
-
const
|
|
60
|
+
const choices = (isBuiltIn ? choiceGroup : choicesAll![groupName]!).choices
|
|
63
61
|
const isHidden = parentChoiceGroup ? !parentChoiceGroup.choices.includes(parentSelectedChoice) : hidden
|
|
64
62
|
const isEmptyChoice = (choice: string) => emptyChoices.includes(choice)
|
|
65
63
|
const filteredChoices = choices.filter((choice) => !isEmptyChoice(choice.name))
|
|
66
64
|
const selectedIndex = filteredChoices.findIndex((choice) => choice.name === selectedChoice)
|
|
67
|
-
const rectTop = -selectedIndex * OPTION_HEIGHT
|
|
68
65
|
|
|
69
66
|
return (
|
|
70
67
|
<div
|
|
71
68
|
id={`choicesFor-${groupName}`}
|
|
72
|
-
aria-haspopup="listbox"
|
|
73
69
|
aria-expanded={expanded}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
role="radiogroup"
|
|
71
|
+
className={cls([
|
|
72
|
+
'choice-select__list',
|
|
73
|
+
(isHidden || isEmptyChoice(selectedChoice)) && 'hidden',
|
|
74
|
+
isHovered && 'hovered',
|
|
75
|
+
])}
|
|
76
|
+
style={{ '--option-height': `${OPTION_HEIGHT}px`, '--choice-count': filteredChoices.length }}
|
|
77
|
+
onMouseEnter={() => {
|
|
78
|
+
setExpanded(true)
|
|
79
|
+
setIsHovered(true)
|
|
80
|
+
}}
|
|
77
81
|
onMouseLeave={() => setExpanded(false)}
|
|
82
|
+
onTransitionEnd={() => {
|
|
83
|
+
if (!expanded) setIsHovered(false)
|
|
84
|
+
}}
|
|
78
85
|
onClick={() => {
|
|
79
86
|
if (!expanded) next()
|
|
80
87
|
}}
|
|
88
|
+
data-choice-group={groupName}
|
|
81
89
|
>
|
|
82
|
-
<div
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
</div>
|
|
90
|
+
<div className="choice-select__border" />
|
|
91
|
+
{filteredChoices.map(({ name: choice, icon, iconStyle }) => (
|
|
92
|
+
<label
|
|
93
|
+
id={`choice-${choice}`}
|
|
94
|
+
key={choice}
|
|
95
|
+
className={`choice-select__option`}
|
|
96
|
+
onClick={(e) => handleOnClick(e, choice)}
|
|
97
|
+
>
|
|
98
|
+
<input
|
|
99
|
+
type="radio"
|
|
100
|
+
className="choice-select__radio sr-only"
|
|
101
|
+
name={`radio-${radioId}`}
|
|
102
|
+
value={choice}
|
|
103
|
+
checked={selectedChoice === choice}
|
|
104
|
+
readOnly
|
|
105
|
+
/>
|
|
106
|
+
<span className="choice-select__option-content">
|
|
107
|
+
{icon && <img src={icon} alt="" aria-hidden="true" style={iconStyle} />}
|
|
108
|
+
{choice}
|
|
109
|
+
</span>
|
|
110
|
+
</label>
|
|
111
|
+
))}
|
|
105
112
|
</div>
|
|
106
113
|
)
|
|
107
114
|
|
|
@@ -109,11 +116,12 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
109
116
|
const nextIndex = (selectedIndex + 1) % filteredChoices.length
|
|
110
117
|
setSelectedChoice(filteredChoices[nextIndex]!.name)
|
|
111
118
|
}
|
|
112
|
-
function handleOnClick(e: React.MouseEvent<
|
|
113
|
-
e.
|
|
119
|
+
function handleOnClick(e: React.MouseEvent<HTMLLabelElement, MouseEvent>, choice: string) {
|
|
120
|
+
e.preventDefault()
|
|
114
121
|
const el = e.currentTarget
|
|
115
122
|
setPrevPosition(el)
|
|
116
|
-
|
|
123
|
+
const isSame = selectedChoice === choice
|
|
124
|
+
if (isSame) {
|
|
117
125
|
next()
|
|
118
126
|
} else {
|
|
119
127
|
setSelectedChoice(choice)
|
|
@@ -2,16 +2,9 @@
|
|
|
2
2
|
-webkit-tap-highlight-color: transparent;
|
|
3
3
|
|
|
4
4
|
/* tablist style logic */
|
|
5
|
-
|
|
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) {
|
|
5
|
+
.choice-tabs__tab:has(.choice-tabs__radio:checked) {
|
|
12
6
|
border-bottom: 2px solid #aaa;
|
|
13
7
|
color: var(--color-text);
|
|
14
|
-
font-weight: 600;
|
|
15
8
|
}
|
|
16
9
|
}
|
|
17
10
|
|
|
@@ -36,7 +29,7 @@
|
|
|
36
29
|
.choice-tabs__tab-content {
|
|
37
30
|
display: inline-flex;
|
|
38
31
|
align-items: center;
|
|
39
|
-
gap:
|
|
32
|
+
gap: 6px;
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
.choice-tabs__tab img {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Tabs }
|
|
2
2
|
|
|
3
|
-
import React from 'react'
|
|
3
|
+
import React, { useId } from 'react'
|
|
4
4
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
|
|
5
5
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
|
|
6
6
|
import { usePageContext } from '../../renderer/usePageContext.js'
|
|
@@ -8,89 +8,44 @@ import { assertUsage } from '../../utils/assert.js'
|
|
|
8
8
|
import './Tabs.css'
|
|
9
9
|
|
|
10
10
|
function Tabs({ choice, hide = [] }: { choice: string; hide: string[] }) {
|
|
11
|
+
const radioId = useId()
|
|
11
12
|
const groupName = choice
|
|
12
13
|
const pageContext = usePageContext()
|
|
13
|
-
const choicesAll = pageContext.
|
|
14
|
+
const choicesAll = pageContext.resolved.choices
|
|
14
15
|
assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`)
|
|
15
|
-
|
|
16
16
|
const { choices, default: defaultChoice } = choicesAll[groupName]
|
|
17
17
|
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
18
18
|
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
19
19
|
const isHidden = (choice: string) => hide.includes(choice)
|
|
20
|
-
const filteredChoices = choices.filter((choice) => !isHidden(choice.name))
|
|
21
|
-
const selectedIndex = filteredChoices.findIndex((choice) => choice.name === selectedChoice)
|
|
22
20
|
|
|
23
21
|
return (
|
|
24
|
-
<div className="choice-tabs"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
onKeyDown={handleOnKeyDown}
|
|
45
|
-
>
|
|
22
|
+
<div className="choice-tabs">
|
|
23
|
+
<div
|
|
24
|
+
id={`choicesFor-${groupName}`}
|
|
25
|
+
className="choice-tabs__tab-list"
|
|
26
|
+
role="radiogroup"
|
|
27
|
+
data-choice-group={groupName}
|
|
28
|
+
>
|
|
29
|
+
{choices.map(({ name: choice, icon, iconStyle }) => (
|
|
30
|
+
<label key={choice} className="choice-tabs__tab" style={{ display: isHidden(choice) ? 'none' : undefined }}>
|
|
31
|
+
<input
|
|
32
|
+
className="choice-tabs__radio sr-only"
|
|
33
|
+
type="radio"
|
|
34
|
+
name={`radio-${radioId}`}
|
|
35
|
+
value={choice}
|
|
36
|
+
checked={selectedChoice === choice}
|
|
37
|
+
onChange={(e) => {
|
|
38
|
+
setPrevPosition(e.currentTarget)
|
|
39
|
+
setSelectedChoice(choice)
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
46
42
|
<span className="choice-tabs__tab-content">
|
|
47
|
-
<img src={icon} alt="" aria-hidden="true" style={iconStyle} />
|
|
43
|
+
{icon && <img src={icon} alt="" aria-hidden="true" style={iconStyle} />}
|
|
48
44
|
{choice}
|
|
49
45
|
</span>
|
|
50
|
-
</
|
|
46
|
+
</label>
|
|
51
47
|
))}
|
|
52
|
-
</
|
|
48
|
+
</div>
|
|
53
49
|
</div>
|
|
54
50
|
)
|
|
55
|
-
|
|
56
|
-
function handleOnClick(e: React.MouseEvent<HTMLLIElement, MouseEvent>, choice: string) {
|
|
57
|
-
setPrevPosition(e.currentTarget)
|
|
58
|
-
setSelectedChoice(choice)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function handleOnKeyDown(e: React.KeyboardEvent<HTMLLIElement>) {
|
|
62
|
-
const el = e.currentTarget
|
|
63
|
-
let nextIndex = selectedIndex
|
|
64
|
-
|
|
65
|
-
switch (e.key) {
|
|
66
|
-
case 'ArrowRight':
|
|
67
|
-
nextIndex = (selectedIndex + 1) % filteredChoices.length
|
|
68
|
-
break
|
|
69
|
-
case 'ArrowLeft':
|
|
70
|
-
nextIndex = (selectedIndex - 1 + filteredChoices.length) % filteredChoices.length
|
|
71
|
-
break
|
|
72
|
-
case 'Home':
|
|
73
|
-
nextIndex = 0
|
|
74
|
-
break
|
|
75
|
-
case 'End':
|
|
76
|
-
nextIndex = filteredChoices.length - 1
|
|
77
|
-
break
|
|
78
|
-
default:
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
e.preventDefault()
|
|
83
|
-
setPrevPosition(el)
|
|
84
|
-
const nextChoice = filteredChoices[nextIndex]!
|
|
85
|
-
setSelectedChoice(nextChoice.name)
|
|
86
|
-
const tabEl = el.parentElement?.parentElement as HTMLDivElement
|
|
87
|
-
|
|
88
|
-
if (!isInViewport(tabEl)) tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' })
|
|
89
|
-
el.focus()
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function isInViewport(el: Element) {
|
|
94
|
-
const rect = el.getBoundingClientRect()
|
|
95
|
-
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth
|
|
96
51
|
}
|
|
@@ -19,19 +19,28 @@ function useCurrentSelection(choiceGroupName: string, defaultValue: string) {
|
|
|
19
19
|
// WARNING: We cannot use `keyPrefix` here: closures don't work because we serialize the function.
|
|
20
20
|
const initializeChoiceGroup_SSR = `initializeChoiceGroup();${initializeChoiceGroup.toString()};`
|
|
21
21
|
function initializeChoiceGroup() {
|
|
22
|
-
const groupsElements =
|
|
22
|
+
const groupsElements = [
|
|
23
|
+
...document.querySelectorAll('select[data-choice-group]'),
|
|
24
|
+
...document.querySelectorAll('div[data-choice-group]'),
|
|
25
|
+
]
|
|
23
26
|
for (const groupEl of groupsElements) {
|
|
24
|
-
const choiceGroupName = groupEl.getAttribute('data-choice-group')
|
|
27
|
+
const choiceGroupName = groupEl.getAttribute('data-choice-group')
|
|
28
|
+
if (!choiceGroupName) continue
|
|
25
29
|
const storageKey = `docpress:choice:${choiceGroupName}`
|
|
26
30
|
const selectedChoice = localStorage.getItem(storageKey)
|
|
27
|
-
if (selectedChoice)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
selectEl.value = selectedChoice
|
|
34
|
-
|
|
31
|
+
if (!selectedChoice) continue
|
|
32
|
+
switch (groupEl.tagName) {
|
|
33
|
+
case 'SELECT':
|
|
34
|
+
const selectEl = groupEl as HTMLSelectElement
|
|
35
|
+
const optionExists = [...selectEl.options].some((opt) => opt.value === selectedChoice)
|
|
36
|
+
if (!optionExists) localStorage.removeItem(storageKey)
|
|
37
|
+
else selectEl.value = selectedChoice
|
|
38
|
+
break
|
|
39
|
+
case 'DIV':
|
|
40
|
+
const radioEl = groupEl.querySelector<HTMLInputElement>(`input[type="radio"][value="${selectedChoice}"]`)
|
|
41
|
+
if (radioEl) radioEl.checked = true
|
|
42
|
+
default:
|
|
43
|
+
break
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
46
|
}
|
|
@@ -3,12 +3,12 @@ export { useMDXComponents }
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import type { UseMdxComponents } from '@mdx-js/mdx'
|
|
5
5
|
import { Pre } from '../components/Pre.js'
|
|
6
|
-
import { ChoiceGroup,
|
|
6
|
+
import { ChoiceGroup, ChoiceGroupContainer } from '../components/ChoiceGroup.js'
|
|
7
7
|
|
|
8
8
|
const useMDXComponents: UseMdxComponents = () => {
|
|
9
9
|
return {
|
|
10
10
|
ChoiceGroup,
|
|
11
|
-
|
|
11
|
+
ChoiceGroupContainer,
|
|
12
12
|
pre: (props) => <Pre {...props} />,
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -86,7 +86,7 @@ const remarkChoiceGroup: Plugin<[], Root> = (): Transformer<Root> => {
|
|
|
86
86
|
// Descend into non-container nodes so that a `CustomSelectsContainer` nested inside another JSX
|
|
87
87
|
// element (e.g. react-tabs `<Tabs>`/`<TabPanel>`, or a `<div>`) still gets visited and its
|
|
88
88
|
// `choiceGroupAll` attribute injected. (Returning 'skip' here would stop the descent.)
|
|
89
|
-
if (node.name !== '
|
|
89
|
+
if (node.name !== 'ChoiceGroupContainer') return
|
|
90
90
|
|
|
91
91
|
const choiceGroupAll: ChoiceGroupWithParent[] = []
|
|
92
92
|
|
package/code-blocks/types.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export type { ChoiceGroup, ChoiceGroupWithParent, ParentChoiceGroup }
|
|
2
2
|
|
|
3
|
-
import type { Config } from '../types/Config.js'
|
|
3
|
+
import type { Config, ChoiceItem } from '../types/Config.js'
|
|
4
4
|
|
|
5
|
-
type ChoiceGroup = NonNullable<Config['choices']>[string] & {
|
|
5
|
+
type ChoiceGroup = Omit<NonNullable<Config['choices']>[string], 'choices'> & {
|
|
6
6
|
name: string
|
|
7
|
+
choices: ChoiceItem[]
|
|
7
8
|
emptyChoices: string[]
|
|
8
9
|
hidden: boolean
|
|
9
10
|
lvl: number
|
|
@@ -8,6 +8,7 @@ import type { MdxJsxAttribute, MdxJsxFlowElement, MdxJsxFlowElementData } from '
|
|
|
8
8
|
import { getVikeConfig } from 'vike/plugin'
|
|
9
9
|
import { assertUsage } from '../../utils/assert.js'
|
|
10
10
|
import { valueToEstree } from 'estree-util-value-to-estree'
|
|
11
|
+
import { resolveChoices } from './resolveChoices.js'
|
|
11
12
|
|
|
12
13
|
type ChoiceNode = {
|
|
13
14
|
choiceValue: string
|
|
@@ -130,7 +131,7 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
|
|
|
130
131
|
if (lvl === 0) {
|
|
131
132
|
return {
|
|
132
133
|
type: 'mdxJsxFlowElement',
|
|
133
|
-
name: '
|
|
134
|
+
name: 'ChoiceGroupContainer',
|
|
134
135
|
attributes: [],
|
|
135
136
|
children: [choiceGroupNode],
|
|
136
137
|
}
|
|
@@ -143,7 +144,7 @@ function resolveChoiceGroupNodes(choiceNodes: ChoiceNode[]) {
|
|
|
143
144
|
const vikeConfig = getVikeConfig()
|
|
144
145
|
const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue)
|
|
145
146
|
const { choices: choicesConfig } = vikeConfig.config.docpress
|
|
146
|
-
const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig }
|
|
147
|
+
const choicesAll = resolveChoices({ ...CHOICES_BUILT_IN, ...choicesConfig })
|
|
147
148
|
|
|
148
149
|
// Resolve to the group that defines ALL of the block's values. Matching a group that merely
|
|
149
150
|
// shares ANY value would mis-resolve a custom group that collides with a built-in on a single
|
|
@@ -153,13 +154,12 @@ function resolveChoiceGroupNodes(choiceNodes: ChoiceNode[]) {
|
|
|
153
154
|
)
|
|
154
155
|
assertUsage(groupName, `Missing group name for [${choices}]. Define it in +docpress.choices.`)
|
|
155
156
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
)
|
|
157
|
+
const group = choicesAll[groupName]!
|
|
158
|
+
const emptyChoices = group.choices.filter((choice) => !choices.includes(choice.name)).map((choice) => choice.name)
|
|
159
159
|
|
|
160
160
|
const choiceGroup = {
|
|
161
161
|
name: groupName,
|
|
162
|
-
...
|
|
162
|
+
...group,
|
|
163
163
|
emptyChoices,
|
|
164
164
|
}
|
|
165
165
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { resolveChoices }
|
|
2
|
+
export type { ResolvedChoices }
|
|
3
|
+
|
|
4
|
+
import type { Choice, ChoiceItem } from '../../types/Config.js'
|
|
5
|
+
|
|
6
|
+
type ResolvedChoices = Record<string, Omit<Choice, 'choices'> & { choices: ChoiceItem[] }>
|
|
7
|
+
|
|
8
|
+
function resolveChoices(choicesConfig: Record<string, Choice>): ResolvedChoices {
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.entries(choicesConfig).map(([name, group]) => [name, { ...group, choices: group.choices.map(resolveChoice) }]),
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
function resolveChoice(choice: string | ChoiceItem): ChoiceItem {
|
|
14
|
+
return typeof choice === 'string' ? { name: choice } : choice
|
|
15
|
+
}
|
package/css/index.css
CHANGED
package/css/sr-only.css
ADDED
|
@@ -1,61 +1,27 @@
|
|
|
1
1
|
export { Tabs };
|
|
2
|
-
import React from 'react';
|
|
2
|
+
import React, { useId } from 'react';
|
|
3
3
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js';
|
|
4
4
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js';
|
|
5
5
|
import { usePageContext } from '../../renderer/usePageContext.js';
|
|
6
6
|
import { assertUsage } from '../../utils/assert.js';
|
|
7
7
|
import './Tabs.css';
|
|
8
8
|
function Tabs({ choice, hide = [] }) {
|
|
9
|
+
const radioId = useId();
|
|
9
10
|
const groupName = choice;
|
|
10
11
|
const pageContext = usePageContext();
|
|
11
|
-
const choicesAll = pageContext.
|
|
12
|
+
const choicesAll = pageContext.resolved.choices;
|
|
12
13
|
assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`);
|
|
13
14
|
const { choices, default: defaultChoice } = choicesAll[groupName];
|
|
14
15
|
const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice);
|
|
15
16
|
const setPrevPosition = useRestoreScroll([selectedChoice]);
|
|
16
17
|
const isHidden = (choice) => hide.includes(choice);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
return (React.createElement("div", { className: "choice-tabs" },
|
|
19
|
+
React.createElement("div", { id: `choicesFor-${groupName}`, className: "choice-tabs__tab-list", role: "radiogroup", "data-choice-group": groupName }, choices.map(({ name: choice, icon, iconStyle }) => (React.createElement("label", { key: choice, className: "choice-tabs__tab", style: { display: isHidden(choice) ? 'none' : undefined } },
|
|
20
|
+
React.createElement("input", { className: "choice-tabs__radio sr-only", type: "radio", name: `radio-${radioId}`, value: choice, checked: selectedChoice === choice, onChange: (e) => {
|
|
21
|
+
setPrevPosition(e.currentTarget);
|
|
22
|
+
setSelectedChoice(choice);
|
|
23
|
+
} }),
|
|
22
24
|
React.createElement("span", { className: "choice-tabs__tab-content" },
|
|
23
|
-
React.createElement("img", { src: icon, alt: "", "aria-hidden": "true", style: iconStyle }),
|
|
25
|
+
icon && React.createElement("img", { src: icon, alt: "", "aria-hidden": "true", style: iconStyle }),
|
|
24
26
|
choice)))))));
|
|
25
|
-
function handleOnClick(e, choice) {
|
|
26
|
-
setPrevPosition(e.currentTarget);
|
|
27
|
-
setSelectedChoice(choice);
|
|
28
|
-
}
|
|
29
|
-
function handleOnKeyDown(e) {
|
|
30
|
-
const el = e.currentTarget;
|
|
31
|
-
let nextIndex = selectedIndex;
|
|
32
|
-
switch (e.key) {
|
|
33
|
-
case 'ArrowRight':
|
|
34
|
-
nextIndex = (selectedIndex + 1) % filteredChoices.length;
|
|
35
|
-
break;
|
|
36
|
-
case 'ArrowLeft':
|
|
37
|
-
nextIndex = (selectedIndex - 1 + filteredChoices.length) % filteredChoices.length;
|
|
38
|
-
break;
|
|
39
|
-
case 'Home':
|
|
40
|
-
nextIndex = 0;
|
|
41
|
-
break;
|
|
42
|
-
case 'End':
|
|
43
|
-
nextIndex = filteredChoices.length - 1;
|
|
44
|
-
break;
|
|
45
|
-
default:
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
e.preventDefault();
|
|
49
|
-
setPrevPosition(el);
|
|
50
|
-
const nextChoice = filteredChoices[nextIndex];
|
|
51
|
-
setSelectedChoice(nextChoice.name);
|
|
52
|
-
const tabEl = el.parentElement?.parentElement;
|
|
53
|
-
if (!isInViewport(tabEl))
|
|
54
|
-
tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' });
|
|
55
|
-
el.focus();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function isInViewport(el) {
|
|
59
|
-
const rect = el.getBoundingClientRect();
|
|
60
|
-
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
|
|
61
27
|
}
|
|
@@ -15,20 +15,33 @@ function useCurrentSelection(choiceGroupName, defaultValue) {
|
|
|
15
15
|
// WARNING: We cannot use `keyPrefix` here: closures don't work because we serialize the function.
|
|
16
16
|
const initializeChoiceGroup_SSR = `initializeChoiceGroup();${initializeChoiceGroup.toString()};`;
|
|
17
17
|
function initializeChoiceGroup() {
|
|
18
|
-
const groupsElements =
|
|
18
|
+
const groupsElements = [
|
|
19
|
+
...document.querySelectorAll('select[data-choice-group]'),
|
|
20
|
+
...document.querySelectorAll('div[data-choice-group]'),
|
|
21
|
+
];
|
|
19
22
|
for (const groupEl of groupsElements) {
|
|
20
23
|
const choiceGroupName = groupEl.getAttribute('data-choice-group');
|
|
24
|
+
if (!choiceGroupName)
|
|
25
|
+
continue;
|
|
21
26
|
const storageKey = `docpress:choice:${choiceGroupName}`;
|
|
22
27
|
const selectedChoice = localStorage.getItem(storageKey);
|
|
23
|
-
if (selectedChoice)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
if (!selectedChoice)
|
|
29
|
+
continue;
|
|
30
|
+
switch (groupEl.tagName) {
|
|
31
|
+
case 'SELECT':
|
|
32
|
+
const selectEl = groupEl;
|
|
33
|
+
const optionExists = [...selectEl.options].some((opt) => opt.value === selectedChoice);
|
|
34
|
+
if (!optionExists)
|
|
35
|
+
localStorage.removeItem(storageKey);
|
|
36
|
+
else
|
|
37
|
+
selectEl.value = selectedChoice;
|
|
38
|
+
break;
|
|
39
|
+
case 'DIV':
|
|
40
|
+
const radioEl = groupEl.querySelector(`input[type="radio"][value="${selectedChoice}"]`);
|
|
41
|
+
if (radioEl)
|
|
42
|
+
radioEl.checked = true;
|
|
43
|
+
default:
|
|
44
|
+
break;
|
|
32
45
|
}
|
|
33
46
|
}
|
|
34
47
|
}
|
|
@@ -70,7 +70,7 @@ const remarkChoiceGroup = () => {
|
|
|
70
70
|
// Descend into non-container nodes so that a `CustomSelectsContainer` nested inside another JSX
|
|
71
71
|
// element (e.g. react-tabs `<Tabs>`/`<TabPanel>`, or a `<div>`) still gets visited and its
|
|
72
72
|
// `choiceGroupAll` attribute injected. (Returning 'skip' here would stop the descent.)
|
|
73
|
-
if (node.name !== '
|
|
73
|
+
if (node.name !== 'ChoiceGroupContainer')
|
|
74
74
|
return;
|
|
75
75
|
const choiceGroupAll = [];
|
|
76
76
|
visit(node, 'mdxJsxFlowElement', (child) => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export type { ChoiceGroup, ChoiceGroupWithParent, ParentChoiceGroup };
|
|
2
|
-
import type { Config } from '../types/Config.js';
|
|
3
|
-
type ChoiceGroup = NonNullable<Config['choices']>[string] & {
|
|
2
|
+
import type { Config, ChoiceItem } from '../types/Config.js';
|
|
3
|
+
type ChoiceGroup = Omit<NonNullable<Config['choices']>[string], 'choices'> & {
|
|
4
4
|
name: string;
|
|
5
|
+
choices: ChoiceItem[];
|
|
5
6
|
emptyChoices: string[];
|
|
6
7
|
hidden: boolean;
|
|
7
8
|
lvl: number;
|
|
@@ -2,6 +2,7 @@ export { generateChoiceGroupCode, expressionToAttribute };
|
|
|
2
2
|
import { getVikeConfig } from 'vike/plugin';
|
|
3
3
|
import { assertUsage } from '../../utils/assert.js';
|
|
4
4
|
import { valueToEstree } from 'estree-util-value-to-estree';
|
|
5
|
+
import { resolveChoices } from './resolveChoices.js';
|
|
5
6
|
// TODO: determine icon representation for CHOICES_BUILT_IN given lack of SVG/file import support
|
|
6
7
|
// use SVG URLs for now
|
|
7
8
|
const CHOICES_BUILT_IN = {
|
|
@@ -107,7 +108,7 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
|
|
|
107
108
|
if (lvl === 0) {
|
|
108
109
|
return {
|
|
109
110
|
type: 'mdxJsxFlowElement',
|
|
110
|
-
name: '
|
|
111
|
+
name: 'ChoiceGroupContainer',
|
|
111
112
|
attributes: [],
|
|
112
113
|
children: [choiceGroupNode],
|
|
113
114
|
};
|
|
@@ -118,16 +119,17 @@ function resolveChoiceGroupNodes(choiceNodes) {
|
|
|
118
119
|
const vikeConfig = getVikeConfig();
|
|
119
120
|
const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue);
|
|
120
121
|
const { choices: choicesConfig } = vikeConfig.config.docpress;
|
|
121
|
-
const choicesAll = { ...CHOICES_BUILT_IN, ...choicesConfig };
|
|
122
|
+
const choicesAll = resolveChoices({ ...CHOICES_BUILT_IN, ...choicesConfig });
|
|
122
123
|
// Resolve to the group that defines ALL of the block's values. Matching a group that merely
|
|
123
124
|
// shares ANY value would mis-resolve a custom group that collides with a built-in on a single
|
|
124
125
|
// value — e.g. a `runtime` group [Node, Bun, Deno, Cloudflare] sharing `Bun` with `pkgManager`.
|
|
125
126
|
const groupName = Object.keys(choicesAll).find((key) => choices.every((choice) => choicesAll[key].choices.some(({ name }) => name === choice)));
|
|
126
127
|
assertUsage(groupName, `Missing group name for [${choices}]. Define it in +docpress.choices.`);
|
|
127
|
-
const
|
|
128
|
+
const group = choicesAll[groupName];
|
|
129
|
+
const emptyChoices = group.choices.filter((choice) => !choices.includes(choice.name)).map((choice) => choice.name);
|
|
128
130
|
const choiceGroup = {
|
|
129
131
|
name: groupName,
|
|
130
|
-
...
|
|
132
|
+
...group,
|
|
131
133
|
emptyChoices,
|
|
132
134
|
};
|
|
133
135
|
const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { resolveChoices };
|
|
2
|
+
export type { ResolvedChoices };
|
|
3
|
+
import type { Choice, ChoiceItem } from '../../types/Config.js';
|
|
4
|
+
type ResolvedChoices = Record<string, Omit<Choice, 'choices'> & {
|
|
5
|
+
choices: ChoiceItem[];
|
|
6
|
+
}>;
|
|
7
|
+
declare function resolveChoices(choicesConfig: Record<string, Choice>): ResolvedChoices;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { resolveChoices };
|
|
2
|
+
function resolveChoices(choicesConfig) {
|
|
3
|
+
return Object.fromEntries(Object.entries(choicesConfig).map(([name, group]) => [name, { ...group, choices: group.choices.map(resolveChoice) }]));
|
|
4
|
+
}
|
|
5
|
+
function resolveChoice(choice) {
|
|
6
|
+
return typeof choice === 'string' ? { name: choice } : choice;
|
|
7
|
+
}
|
|
@@ -16,6 +16,7 @@ declare function usePageContextLegacy(): {
|
|
|
16
16
|
pageTitle: string | null;
|
|
17
17
|
documentTitle: string;
|
|
18
18
|
activeCategoryName: string;
|
|
19
|
+
choices: import("../code-blocks/utils/resolveChoices.js").ResolvedChoices | undefined;
|
|
19
20
|
};
|
|
20
21
|
declare function usePageContext(): PageContext;
|
|
21
22
|
declare function PageContextProvider({ pageContext, children, }: {
|
|
@@ -4,6 +4,7 @@ import { jsxToTextContent } from './utils/jsxToTextContent.js';
|
|
|
4
4
|
import pc from '@brillout/picocolors';
|
|
5
5
|
import { parseMarkdownMini } from './parseMarkdownMini.js';
|
|
6
6
|
import { determineNavItemsColumnLayout } from './determineNavItemsColumnLayout.js';
|
|
7
|
+
import { resolveChoices } from './code-blocks/utils/resolveChoices.js';
|
|
7
8
|
function resolvePageContext(pageContext) {
|
|
8
9
|
const config = pageContext.globalContext.config.docpress;
|
|
9
10
|
const { urlPathname } = pageContext;
|
|
@@ -45,6 +46,7 @@ function resolvePageContext(pageContext) {
|
|
|
45
46
|
}
|
|
46
47
|
// Don't show landing page in navigation
|
|
47
48
|
navItemsAll = navItemsAll.filter((navItem) => navItem.url !== '/');
|
|
49
|
+
const choices = config.choices && resolveChoices(config.choices);
|
|
48
50
|
const resolved = {
|
|
49
51
|
navItemsAll,
|
|
50
52
|
navItemsDetached,
|
|
@@ -54,6 +56,7 @@ function resolvePageContext(pageContext) {
|
|
|
54
56
|
pageTitle,
|
|
55
57
|
documentTitle,
|
|
56
58
|
activeCategoryName,
|
|
59
|
+
choices,
|
|
57
60
|
};
|
|
58
61
|
return resolved;
|
|
59
62
|
}
|
package/dist/types/Config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Config };
|
|
1
|
+
export type { Config, Choice, ChoiceItem };
|
|
2
2
|
import type { HeadingDefinition, HeadingDetachedDefinition } from './Heading.js';
|
|
3
3
|
import type React from 'react';
|
|
4
4
|
type Config = {
|
|
@@ -47,11 +47,13 @@ type Category = string | {
|
|
|
47
47
|
/** Hide from Algolia search */
|
|
48
48
|
hide?: boolean;
|
|
49
49
|
};
|
|
50
|
+
/** A choice. A plain `string` is shorthand for `{ name: string }` (no icon). */
|
|
51
|
+
type ChoiceItem = {
|
|
52
|
+
name: string;
|
|
53
|
+
icon?: string;
|
|
54
|
+
iconStyle?: React.CSSProperties;
|
|
55
|
+
};
|
|
50
56
|
type Choice = {
|
|
51
|
-
choices:
|
|
52
|
-
name: string;
|
|
53
|
-
icon: string;
|
|
54
|
-
iconStyle?: React.CSSProperties;
|
|
55
|
-
}[];
|
|
57
|
+
choices: (string | ChoiceItem)[];
|
|
56
58
|
default: string;
|
|
57
59
|
};
|
package/package.json
CHANGED
package/resolvePageContext.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { jsxToTextContent } from './utils/jsxToTextContent.js'
|
|
|
18
18
|
import pc from '@brillout/picocolors'
|
|
19
19
|
import { parseMarkdownMini } from './parseMarkdownMini.js'
|
|
20
20
|
import { determineNavItemsColumnLayout } from './determineNavItemsColumnLayout.js'
|
|
21
|
+
import { resolveChoices } from './code-blocks/utils/resolveChoices.js'
|
|
21
22
|
|
|
22
23
|
type PageSectionResolved = {
|
|
23
24
|
url: string | null
|
|
@@ -80,6 +81,8 @@ function resolvePageContext(pageContext: PageContextServer) {
|
|
|
80
81
|
// Don't show landing page in navigation
|
|
81
82
|
navItemsAll = navItemsAll.filter((navItem) => navItem.url !== '/')
|
|
82
83
|
|
|
84
|
+
const choices = config.choices && resolveChoices(config.choices)
|
|
85
|
+
|
|
83
86
|
const resolved = {
|
|
84
87
|
navItemsAll,
|
|
85
88
|
navItemsDetached,
|
|
@@ -89,6 +92,7 @@ function resolvePageContext(pageContext: PageContextServer) {
|
|
|
89
92
|
pageTitle,
|
|
90
93
|
documentTitle,
|
|
91
94
|
activeCategoryName,
|
|
95
|
+
choices,
|
|
92
96
|
}
|
|
93
97
|
return resolved
|
|
94
98
|
}
|
package/types/Config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Config }
|
|
1
|
+
export type { Config, Choice, ChoiceItem }
|
|
2
2
|
|
|
3
3
|
import type { HeadingDefinition, HeadingDetachedDefinition } from './Heading.js'
|
|
4
4
|
import type React from 'react'
|
|
@@ -62,7 +62,9 @@ type Category =
|
|
|
62
62
|
hide?: boolean
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** A choice. A plain `string` is shorthand for `{ name: string }` (no icon). */
|
|
66
|
+
type ChoiceItem = { name: string; icon?: string; iconStyle?: React.CSSProperties }
|
|
65
67
|
type Choice = {
|
|
66
|
-
choices:
|
|
68
|
+
choices: (string | ChoiceItem)[]
|
|
67
69
|
default: string
|
|
68
70
|
}
|