@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.
@@ -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-color: hsl(0, 0%, 75%) hsl(0, 0%, 72%) hsl(0, 0%, 72%) hsl(0, 0%, 75%);
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-select {
18
- background: #eee;
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: 5px;
27
- border-style: solid;
28
- border-color: var(--border-color);
35
+ border-radius: var(--border-radius);
36
+ width: 100%;
37
+ transition: var(--transition-select-list);
29
38
  }
30
39
 
31
- .choice-select__list {
32
- position: relative;
33
- border-radius: 5px;
34
- border-color: var(--border-color);
35
- width: 100%;
36
- overflow: hidden;
37
- transition: top 180ms cubic-bezier(0.2, 0.9, 0.2, 1);
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: background 120ms ease;
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
- --transition: 500ms ease;
65
- transition: filter var(--transition), opacity var(--transition);
81
+ transition: var(--transition-select-icon);
66
82
  }
67
83
 
68
- .choice-select:hover .choice-select__option img,
69
- .choice-select[aria-expanded='true'] .choice-select__option img {
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-select[aria-expanded='false'] {
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
- .choice-select__list {
85
- border-style: solid;
86
- border-width: 1px 2px 2px 1px;
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
- border-bottom: none;
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[aria-selected='true'] {
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, CustomSelectsContainer }
1
+ export { ChoiceGroup, ChoiceGroupContainer }
2
2
 
3
3
  import type { ChoiceGroup as TChoiceGroup, ChoiceGroupWithParent } from '../types.js'
4
- import React, { createContext, useContext, useState } from '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
- const CustomSelectsContainerContext = createContext<{ choiceGroupAll: ChoiceGroupWithParent[] } | undefined>(undefined)
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
- return <CustomSelectsContainerContext value={{ choiceGroupAll }}>{children}</CustomSelectsContainerContext>
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, lvl } = choiceGroup
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 data-choice-group={groupName} data-lvl={lvl} className="choice-group">
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 choicesAll = usePageContext().config.docpress.choices
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 { choices } = isBuiltIn ? choiceGroup : choicesAll![groupName]!
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
- className={cls(['choice-select', (isHidden || isEmptyChoice(selectedChoice)) && 'hidden'])}
75
- style={{ height: OPTION_HEIGHT }}
76
- onMouseEnter={() => setExpanded(true)}
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
- aria-activedescendant={`choice-${selectedChoice}`}
84
- role="listbox"
85
- className="choice-select__list"
86
- style={{ top: rectTop, height: filteredChoices.length * OPTION_HEIGHT }}
87
- >
88
- {filteredChoices.map(({ name: choice, icon, iconStyle }, i) => (
89
- <div
90
- id={`choice-${choice}`}
91
- key={choice}
92
- aria-selected={i === selectedIndex}
93
- role="option"
94
- className="choice-select__option"
95
- style={{ height: OPTION_HEIGHT }}
96
- onClick={(e) => handleOnClick(e, choice)}
97
- >
98
- <span className="choice-select__option-content">
99
- <img src={icon} alt="" aria-hidden="true" style={iconStyle} />
100
- {choice}
101
- </span>
102
- </div>
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<HTMLDivElement, MouseEvent>, choice: string) {
113
- e.stopPropagation()
119
+ function handleOnClick(e: React.MouseEvent<HTMLLabelElement, MouseEvent>, choice: string) {
120
+ e.preventDefault()
114
121
  const el = e.currentTarget
115
122
  setPrevPosition(el)
116
- if (el.getAttribute('aria-selected') === 'true') {
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
- 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) {
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: 2px;
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.config.docpress.choices
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" data-choice-group={groupName}>
25
- {/* Hidden select used to control tablist styling via CSS. */}
26
- <select name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
27
- {choices.map(({ name: choice }) => (
28
- <option key={choice} value={choice}>
29
- {choice}
30
- </option>
31
- ))}
32
- </select>
33
- <ul id={`choicesFor-${groupName}`} className="choice-tabs__tab-list" role="tablist">
34
- {choices.map(({ name: choice, icon, iconStyle }, i) => (
35
- <li
36
- key={choice}
37
- id={`tab-${choice}`}
38
- style={{ display: isHidden(choice) ? 'none' : undefined }}
39
- className="choice-tabs__tab"
40
- role="tab"
41
- aria-selected={i === selectedIndex}
42
- tabIndex={i === selectedIndex ? 0 : -1}
43
- onClick={(e) => handleOnClick(e, choice)}
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
- </li>
46
+ </label>
51
47
  ))}
52
- </ul>
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 = document.querySelectorAll<HTMLDivElement>('[data-choice-group]')
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
- const selectEl = groupEl.querySelector<HTMLSelectElement>(`select[name="choicesFor-${choiceGroupName}"]`)!
29
- const selectedIndex = [...selectEl.options].findIndex((option) => option.value === selectedChoice)
30
- if (selectedIndex === -1) {
31
- localStorage.removeItem(storageKey)
32
- } else {
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, CustomSelectsContainer } from '../components/ChoiceGroup.js'
6
+ import { ChoiceGroup, ChoiceGroupContainer } from '../components/ChoiceGroup.js'
7
7
 
8
8
  const useMDXComponents: UseMdxComponents = () => {
9
9
  return {
10
10
  ChoiceGroup,
11
- CustomSelectsContainer,
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 !== 'CustomSelectsContainer') return
89
+ if (node.name !== 'ChoiceGroupContainer') return
90
90
 
91
91
  const choiceGroupAll: ChoiceGroupWithParent[] = []
92
92
 
@@ -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: 'CustomSelectsContainer',
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 emptyChoices = choicesAll[groupName]!.choices.filter((choice) => !choices.includes(choice.name)).map(
157
- (choice) => choice.name,
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
- ...choicesAll[groupName]!,
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
@@ -8,3 +8,4 @@
8
8
  @import './table.css';
9
9
  @import './tooltip.css';
10
10
  @import '@docsearch/css/dist/style.css';
11
+ @import './sr-only.css';
@@ -0,0 +1,11 @@
1
+ .sr-only {
2
+ position: absolute;
3
+ width: 1px;
4
+ height: 1px;
5
+ padding: 0;
6
+ margin: -1px;
7
+ overflow: hidden;
8
+ clip: rect(0, 0, 0, 0);
9
+ white-space: nowrap;
10
+ border: 0;
11
+ }
@@ -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.config.docpress.choices;
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
- const filteredChoices = choices.filter((choice) => !isHidden(choice.name));
18
- const selectedIndex = filteredChoices.findIndex((choice) => choice.name === selectedChoice);
19
- return (React.createElement("div", { className: "choice-tabs", "data-choice-group": groupName },
20
- React.createElement("select", { name: `choicesFor-${groupName}`, value: selectedChoice, hidden: true, disabled: true }, choices.map(({ name: choice }) => (React.createElement("option", { key: choice, value: choice }, choice)))),
21
- React.createElement("ul", { id: `choicesFor-${groupName}`, className: "choice-tabs__tab-list", role: "tablist" }, choices.map(({ name: choice, icon, iconStyle }, i) => (React.createElement("li", { key: choice, id: `tab-${choice}`, style: { display: isHidden(choice) ? 'none' : undefined }, className: "choice-tabs__tab", role: "tab", "aria-selected": i === selectedIndex, tabIndex: i === selectedIndex ? 0 : -1, onClick: (e) => handleOnClick(e, choice), onKeyDown: handleOnKeyDown },
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 = document.querySelectorAll('[data-choice-group]');
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
- 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
- }
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 !== 'CustomSelectsContainer')
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: 'CustomSelectsContainer',
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 emptyChoices = choicesAll[groupName].choices.filter((choice) => !choices.includes(choice.name)).map((choice) => choice.name);
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
- ...choicesAll[groupName],
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, }: {
@@ -16,4 +16,5 @@ declare function resolvePageContext(pageContext: PageContextServer): {
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
  };
@@ -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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brillout/docpress",
3
- "version": "0.16.42",
3
+ "version": "0.16.44",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@brillout/picocolors": "^1.0.10",
@@ -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: { name: string; icon: string; iconStyle?: React.CSSProperties }[]
68
+ choices: (string | ChoiceItem)[]
67
69
  default: string
68
70
  }