@brillout/docpress 0.16.33 → 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/Layout.tsx CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  } from './MenuModal/toggleMenuModal.js'
28
28
  import { MenuModal } from './MenuModal.js'
29
29
  import { autoScrollNav_SSR } from './autoScrollNav.js'
30
- import { initializeChoiceGroup_SSR } from './code-blocks/hooks/useSelectedChoice.js'
30
+ import { initializeChoiceGroup_SSR } from './code-blocks/hooks/useCurrentSelection.js'
31
31
  import { SearchLink } from './docsearch/SearchLink.js'
32
32
  import { navigate } from 'vike/client/router'
33
33
  import { css } from './utils/css.js'
@@ -1,13 +1,6 @@
1
1
  .choice-group {
2
2
  position: relative;
3
3
 
4
- &:hover {
5
- .choice-group__selects,
6
- .copy-button {
7
- opacity: 1;
8
- }
9
- }
10
-
11
4
  /* layout */
12
5
  --top-position: 10px;
13
6
  --border-color: hsl(0, 0%, 75%) hsl(0, 0%, 72%) hsl(0, 0%, 72%) hsl(0, 0%, 75%);
@@ -20,8 +13,6 @@
20
13
  z-index: 3;
21
14
  top: var(--top-position);
22
15
  right: 42px;
23
- opacity: 0;
24
- transition: opacity 0.5s ease-in-out;
25
16
  }
26
17
 
27
18
  .choice-select {
@@ -53,65 +44,10 @@
53
44
  align-items: center;
54
45
  flex-wrap: nowrap;
55
46
  background: #fff;
56
- padding: 0 3px 0 5px;
47
+ padding: 0 6px;
57
48
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
58
49
  cursor: pointer;
59
50
  transition: background 120ms ease;
60
-
61
- span {
62
- flex: 1;
63
- }
64
-
65
- &::after {
66
- width: 15px;
67
- text-align: end;
68
- --animation-width: 2px;
69
- position: relative;
70
- padding-right: var(--animation-width);
71
- padding-left: var(--animation-width);
72
- font-size: 1.13em;
73
- color: #666;
74
- left: 0;
75
- transition: left 500ms ease, opacity 150ms ease-in-out;
76
- opacity: 0;
77
- }
78
-
79
- .choice-select__list:hover &:hover::after {
80
- opacity: 1;
81
- }
82
-
83
- &[aria-selected='true'] {
84
- &::after {
85
- content: '»' !important;
86
- opacity: 1;
87
- }
88
-
89
- .choice-select__list:hover &:not(:hover)::after {
90
- opacity: 0;
91
- }
92
-
93
- &:hover::after {
94
- left: var(--animation-width);
95
- }
96
- }
97
-
98
- &[aria-disabled='true'] {
99
- &::after {
100
- content: '⊘';
101
- font-size: 1em;
102
- left: 1px;
103
- }
104
- }
105
-
106
- &:not([aria-selected='true']):not([aria-disabled='true']) {
107
- &::after {
108
- content: '«';
109
- }
110
-
111
- &:hover::after {
112
- left: calc(-1 * var(--animation-width));
113
- }
114
- }
115
51
  }
116
52
 
117
53
  .choice-select[aria-expanded='false'] {
@@ -143,7 +79,7 @@
143
79
 
144
80
  .choice-select__option[aria-disabled='true'] {
145
81
  color: #999;
146
- cursor: default;
82
+ cursor: not-allowed;
147
83
  opacity: 0.8;
148
84
  }
149
85
 
@@ -1,7 +1,7 @@
1
1
  export { ChoiceGroup, CustomSelectsContainer }
2
2
 
3
3
  import React, { createContext, useContext, useEffect, useState } from 'react'
4
- import { useSelectedChoice } from '../hooks/useSelectedChoice.js'
4
+ import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
5
5
  import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
6
6
  import { cls } from '../../utils/cls.js'
7
7
  import './ChoiceGroup.css'
@@ -93,8 +93,7 @@ type ChoiceGroupProps = {
93
93
 
94
94
  function ChoiceGroup({ children, choiceGroup, parentChoiceGroup }: ChoiceGroupProps) {
95
95
  const { name: groupName, choices, default: defaultChoice, lvl } = choiceGroup
96
- // TODO/after-PR-merge rename useSelectedChoice useCurrentSelection
97
- const [selectedChoice] = useSelectedChoice(groupName, defaultChoice)
96
+ const [selectedChoice] = useCurrentSelection(groupName, defaultChoice)
98
97
  const { choiceGroupAll, registerChoiceGroup } = useCustomSelectsContext()
99
98
 
100
99
  useEffect(() => registerChoiceGroup(choiceGroup, parentChoiceGroup), [])
@@ -133,8 +132,8 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
133
132
  hidden,
134
133
  parentChoiceGroup,
135
134
  } = choiceGroup
136
- const [selectedChoice, setSelectedChoice] = useSelectedChoice(groupName, defaultChoice)
137
- const prevPositionRef = useRestoreScroll([selectedChoice])
135
+ const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
136
+ const setPrevPosition = useRestoreScroll([selectedChoice])
138
137
  const [expanded, setExpanded] = useState(false)
139
138
  const selectedIndex = choices.indexOf(selectedChoice)
140
139
  const height = 25
@@ -153,7 +152,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
153
152
  }
154
153
  function isHidden() {
155
154
  if (parentChoiceGroup) {
156
- const [parentSelectedChoice] = useSelectedChoice(parentChoiceGroup.name, parentChoiceGroup.default)
155
+ const [parentSelectedChoice] = useCurrentSelection(parentChoiceGroup.name, parentChoiceGroup.default)
157
156
  return !parentChoiceGroup.choices.includes(parentSelectedChoice)
158
157
  }
159
158
  return hidden
@@ -189,7 +188,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
189
188
  style={{ height }}
190
189
  onClick={handleOnClick}
191
190
  >
192
- <span>{choice}</span>
191
+ {choice}
193
192
  </div>
194
193
  ))}
195
194
  </div>
@@ -198,7 +197,7 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
198
197
  function handleOnClick(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
199
198
  e.stopPropagation()
200
199
  const el = e.currentTarget
201
- prevPositionRef.current = { top: el.getBoundingClientRect().top, el }
200
+ setPrevPosition(el)
202
201
  if (el.getAttribute('aria-selected') === 'true') {
203
202
  next()
204
203
  } else if (el.getAttribute('aria-disabled') === 'false') {
@@ -13,12 +13,6 @@ pre {
13
13
  position: relative;
14
14
  }
15
15
 
16
- &:hover {
17
- .copy-button {
18
- opacity: 1;
19
- }
20
- }
21
-
22
16
  .copy-button {
23
17
  position: absolute !important;
24
18
  top: 10px;
@@ -28,8 +22,7 @@ pre {
28
22
  height: 25px;
29
23
  width: 30px;
30
24
  background-color: #f7f7f7;
31
- opacity: 0;
32
- transition: opacity 0.5s ease-in-out, background-color 0.4s ease-in-out;
25
+ transition: background-color 0.4s ease-in-out;
33
26
 
34
27
  &:not(:hover) {
35
28
  background-color: #eee;
@@ -0,0 +1,53 @@
1
+ .choice-tabs {
2
+ -webkit-tap-highlight-color: transparent;
3
+
4
+ /* tablist style logic */
5
+ select:has(option:nth-of-type(1):checked) ~ ul[role='tablist'] li:nth-of-type(1),
6
+ select:has(option:nth-of-type(2):checked) ~ ul[role='tablist'] li:nth-of-type(2),
7
+ select:has(option:nth-of-type(3):checked) ~ ul[role='tablist'] li:nth-of-type(3),
8
+ select:has(option:nth-of-type(4):checked) ~ ul[role='tablist'] li:nth-of-type(4),
9
+ select:has(option:nth-of-type(5):checked) ~ ul[role='tablist'] li:nth-of-type(5),
10
+ select:has(option:nth-of-type(6):checked) ~ ul[role='tablist'] li:nth-of-type(6),
11
+ select:has(option:nth-of-type(7):checked) ~ ul[role='tablist'] li:nth-of-type(7) {
12
+ border-bottom: 2px solid #aaa;
13
+ color: var(--color-text);
14
+ }
15
+ }
16
+
17
+ .choice-tabs__tab-list {
18
+ border-bottom: 1px solid #eaeaea;
19
+ margin: 0 0 10px;
20
+ padding: 0;
21
+ }
22
+
23
+ .choice-tabs__tab {
24
+ display: inline-block;
25
+ bottom: -1px;
26
+ position: relative;
27
+ list-style: none;
28
+ padding: 6px 12px;
29
+ cursor: pointer;
30
+ font-weight: 500;
31
+ /* lighten --color-text by 20% */
32
+ color: color-mix(in srgb, var(--color-text) 80%, white 20%);
33
+ }
34
+
35
+ @media screen and (max-width: 400px) {
36
+ .choice-tabs__tab {
37
+ padding: 6px 4px;
38
+ font-size: 0.87em;
39
+ }
40
+ }
41
+
42
+ .choice-tabs__tab:hover {
43
+ color: var(--color-text);
44
+ }
45
+
46
+ .choice-tabs__tab--disabled {
47
+ color: GrayText;
48
+ cursor: default;
49
+ }
50
+
51
+ .choice-tabs__tab:focus {
52
+ outline: none;
53
+ }
@@ -0,0 +1,91 @@
1
+ export { Tabs }
2
+
3
+ import React from 'react'
4
+ import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
5
+ import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
6
+ import { usePageContext } from '../../renderer/usePageContext.js'
7
+ import { assertUsage } from '../../utils/assert.js'
8
+ import './Tabs.css'
9
+
10
+ function Tabs({ choice }: { choice: string }) {
11
+ const groupName = choice
12
+ const pageContext = usePageContext()
13
+ const choicesAll = pageContext.config.docpress.choices
14
+ assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`)
15
+
16
+ const { choices, default: defaultChoice } = choicesAll[groupName]
17
+ const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
18
+ const setPrevPosition = useRestoreScroll([selectedChoice])
19
+ const selectedIndex = choices.indexOf(selectedChoice)
20
+
21
+ return (
22
+ <div className="choice-tabs" data-choice-group={groupName}>
23
+ {/* Hidden select used to control tablist styling via CSS. */}
24
+ <select name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
25
+ {choices.map((choice, i) => (
26
+ <option key={i} value={choice}>
27
+ {choice}
28
+ </option>
29
+ ))}
30
+ </select>
31
+ <ul id={`choicesFor-${groupName}`} className="choice-tabs__tab-list" role="tablist">
32
+ {choices.map((choice, i) => (
33
+ <li
34
+ key={i}
35
+ id={choice}
36
+ className="choice-tabs__tab"
37
+ role="tab"
38
+ aria-selected={i === selectedIndex}
39
+ tabIndex={i === selectedIndex ? 0 : -1}
40
+ onClick={(e) => handleOnClick(e, i)}
41
+ onKeyDown={handleOnKeyDown}
42
+ >
43
+ {choice}
44
+ </li>
45
+ ))}
46
+ </ul>
47
+ </div>
48
+ )
49
+
50
+ function handleOnClick(e: React.MouseEvent<HTMLLIElement, MouseEvent>, index: number) {
51
+ const el = e.currentTarget
52
+ setPrevPosition(el)
53
+ setSelectedChoice(choices[index]!)
54
+ }
55
+
56
+ function handleOnKeyDown(e: React.KeyboardEvent<HTMLLIElement>) {
57
+ const el = e.currentTarget
58
+ let nextIndex = selectedIndex
59
+
60
+ switch (e.key) {
61
+ case 'ArrowRight':
62
+ nextIndex = (selectedIndex + 1) % choices.length
63
+ break
64
+ case 'ArrowLeft':
65
+ nextIndex = (selectedIndex - 1 + choices.length) % choices.length
66
+ break
67
+ case 'Home':
68
+ nextIndex = 0
69
+ break
70
+ case 'End':
71
+ nextIndex = choices.length - 1
72
+ break
73
+ default:
74
+ return
75
+ }
76
+
77
+ e.preventDefault()
78
+ setPrevPosition(el)
79
+ const nextChoice = choices[nextIndex]!
80
+ setSelectedChoice(nextChoice)
81
+ const tabEl = el.parentElement?.parentElement as HTMLDivElement
82
+
83
+ if (!isInViewport(tabEl)) tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' })
84
+ el.focus()
85
+ }
86
+ }
87
+
88
+ function isInViewport(el: Element) {
89
+ const rect = el.getBoundingClientRect()
90
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth
91
+ }
@@ -1,4 +1,4 @@
1
- export { useSelectedChoice }
1
+ export { useCurrentSelection }
2
2
  export { initializeChoiceGroup_SSR }
3
3
 
4
4
  import { useLocalStorage } from './useLocalStorage.js'
@@ -12,7 +12,7 @@ const keyPrefix = 'docpress'
12
12
  * @param defaultValue Default choice value.
13
13
  * @returns `[selectedChoice, setSelectedChoice]`.
14
14
  */
15
- function useSelectedChoice(choiceGroupName: string, defaultValue: string) {
15
+ function useCurrentSelection(choiceGroupName: string, defaultValue: string) {
16
16
  return useLocalStorage(`${keyPrefix}:choice:${choiceGroupName}`, defaultValue)
17
17
  }
18
18
 
@@ -11,7 +11,7 @@ type ScrollPosition = { top: number; el: Element }
11
11
  * preserving the user’s scroll position.
12
12
  *
13
13
  * @param deps Dependencies that trigger scroll restoration
14
- * @returns Ref holding the tracked element and its previous top position
14
+ * @returns Function to set the previous top position
15
15
  */
16
16
  function useRestoreScroll(deps: React.DependencyList) {
17
17
  const prevPositionRef = useRef<ScrollPosition | null>(null)
@@ -27,5 +27,9 @@ function useRestoreScroll(deps: React.DependencyList) {
27
27
  prevPositionRef.current = null
28
28
  }, deps)
29
29
 
30
- return prevPositionRef
30
+ const setPrevPosition = (el: Element) => {
31
+ prevPositionRef.current = { top: el.getBoundingClientRect().top, el }
32
+ }
33
+
34
+ return setPrevPosition
31
35
  }
@@ -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
- type: 'mdxJsxAttribute',
105
- name: 'choiceGroup',
106
- value: {
107
- type: 'mdxJsxAttributeValueExpression',
108
- value: '',
109
- data: {
110
- estree: {
111
- type: 'Program',
112
- sourceType: 'module',
113
- comments: [],
114
- body: [
115
- // @ts-ignore: Missing properties in type definition
116
- {
117
- type: 'ExpressionStatement',
118
- expression: valueToEstree({ ...choiceGroup, hidden: choiceNodes.length === 1 || hide, lvl }),
119
- },
120
- ],
121
- },
122
- },
123
- },
124
- })
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 findChoiceGroup(vikeConfig: VikeConfig, choices: string[]) {
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
- return choiceGroup
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
  }
@@ -8,3 +8,4 @@ export * from './HorizontalLine.js'
8
8
  export * from './CodeBlockTransformer.js'
9
9
  export * from './Comment.js'
10
10
  export * from './FileRemoved.js'
11
+ export * from '../code-blocks/components/Tabs.js'
@@ -0,0 +1,6 @@
1
+ export { Tabs };
2
+ import React from 'react';
3
+ import './Tabs.css';
4
+ declare function Tabs({ choice }: {
5
+ choice: string;
6
+ }): React.JSX.Element;
@@ -0,0 +1,57 @@
1
+ export { Tabs };
2
+ import React from 'react';
3
+ import { useCurrentSelection } from '../hooks/useCurrentSelection.js';
4
+ import { useRestoreScroll } from '../hooks/useRestoreScroll.js';
5
+ import { usePageContext } from '../../renderer/usePageContext.js';
6
+ import { assertUsage } from '../../utils/assert.js';
7
+ import './Tabs.css';
8
+ function Tabs({ choice }) {
9
+ const groupName = choice;
10
+ const pageContext = usePageContext();
11
+ const choicesAll = pageContext.config.docpress.choices;
12
+ assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`);
13
+ const { choices, default: defaultChoice } = choicesAll[groupName];
14
+ const [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice);
15
+ const setPrevPosition = useRestoreScroll([selectedChoice]);
16
+ const selectedIndex = choices.indexOf(selectedChoice);
17
+ return (React.createElement("div", { className: "choice-tabs", "data-choice-group": groupName },
18
+ React.createElement("select", { name: `choicesFor-${groupName}`, value: selectedChoice, hidden: true, disabled: true }, choices.map((choice, i) => (React.createElement("option", { key: i, value: choice }, choice)))),
19
+ React.createElement("ul", { id: `choicesFor-${groupName}`, className: "choice-tabs__tab-list", role: "tablist" }, choices.map((choice, i) => (React.createElement("li", { key: i, id: choice, className: "choice-tabs__tab", role: "tab", "aria-selected": i === selectedIndex, tabIndex: i === selectedIndex ? 0 : -1, onClick: (e) => handleOnClick(e, i), onKeyDown: handleOnKeyDown }, choice))))));
20
+ function handleOnClick(e, index) {
21
+ const el = e.currentTarget;
22
+ setPrevPosition(el);
23
+ setSelectedChoice(choices[index]);
24
+ }
25
+ function handleOnKeyDown(e) {
26
+ const el = e.currentTarget;
27
+ let nextIndex = selectedIndex;
28
+ switch (e.key) {
29
+ case 'ArrowRight':
30
+ nextIndex = (selectedIndex + 1) % choices.length;
31
+ break;
32
+ case 'ArrowLeft':
33
+ nextIndex = (selectedIndex - 1 + choices.length) % choices.length;
34
+ break;
35
+ case 'Home':
36
+ nextIndex = 0;
37
+ break;
38
+ case 'End':
39
+ nextIndex = choices.length - 1;
40
+ break;
41
+ default:
42
+ return;
43
+ }
44
+ e.preventDefault();
45
+ setPrevPosition(el);
46
+ const nextChoice = choices[nextIndex];
47
+ setSelectedChoice(nextChoice);
48
+ const tabEl = el.parentElement?.parentElement;
49
+ if (!isInViewport(tabEl))
50
+ tabEl.scrollIntoView({ block: 'start', behavior: 'smooth' });
51
+ el.focus();
52
+ }
53
+ }
54
+ function isInViewport(el) {
55
+ const rect = el.getBoundingClientRect();
56
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth;
57
+ }
@@ -0,0 +1,11 @@
1
+ export { useCurrentSelection };
2
+ export { initializeChoiceGroup_SSR };
3
+ /**
4
+ * Stores and retrieves a selected choice from local storage.
5
+ *
6
+ * @param choiceGroupName Group name for the stored choice.
7
+ * @param defaultValue Default choice value.
8
+ * @returns `[selectedChoice, setSelectedChoice]`.
9
+ */
10
+ declare function useCurrentSelection(choiceGroupName: string, defaultValue: string): readonly [string, (value: string) => void];
11
+ declare const initializeChoiceGroup_SSR: string;
@@ -0,0 +1,34 @@
1
+ export { useCurrentSelection };
2
+ export { initializeChoiceGroup_SSR };
3
+ import { useLocalStorage } from './useLocalStorage.js';
4
+ const keyPrefix = 'docpress';
5
+ /**
6
+ * Stores and retrieves a selected choice from local storage.
7
+ *
8
+ * @param choiceGroupName Group name for the stored choice.
9
+ * @param defaultValue Default choice value.
10
+ * @returns `[selectedChoice, setSelectedChoice]`.
11
+ */
12
+ function useCurrentSelection(choiceGroupName, defaultValue) {
13
+ return useLocalStorage(`${keyPrefix}:choice:${choiceGroupName}`, defaultValue);
14
+ }
15
+ // WARNING: We cannot use `keyPrefix` here: closures don't work because we serialize the function.
16
+ const initializeChoiceGroup_SSR = `initializeChoiceGroup();${initializeChoiceGroup.toString()};`;
17
+ function initializeChoiceGroup() {
18
+ const groupsElements = document.querySelectorAll('[data-choice-group]');
19
+ for (const groupEl of groupsElements) {
20
+ const choiceGroupName = groupEl.getAttribute('data-choice-group');
21
+ const storageKey = `docpress:choice:${choiceGroupName}`;
22
+ const selectedChoice = localStorage.getItem(storageKey);
23
+ if (selectedChoice) {
24
+ const selectEl = groupEl.querySelector(`select[name="choicesFor-${choiceGroupName}"]`);
25
+ const selectedIndex = [...selectEl.options].findIndex((option) => option.value === selectedChoice);
26
+ if (selectedIndex === -1) {
27
+ localStorage.removeItem(storageKey);
28
+ }
29
+ else {
30
+ selectEl.value = selectedChoice;
31
+ }
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,10 @@
1
+ export { useLocalStorage };
2
+ /**
3
+ * A simple, generic `useLocalStorage` hook with SSR and cross-tab support.
4
+ *
5
+ * @param storageKey The key used in localStorage.
6
+ * @param clientValue Default value for the client.
7
+ * @param ssrValue Optional fallback for server-side rendering.
8
+ * @returns A tuple `[value, setValue]`.
9
+ */
10
+ declare function useLocalStorage(storageKey: string, clientValue: string, ssrValue?: string): readonly [string, (value: string) => void];
@@ -0,0 +1,31 @@
1
+ export { useLocalStorage };
2
+ import { useCallback, useSyncExternalStore } from 'react';
3
+ /**
4
+ * A simple, generic `useLocalStorage` hook with SSR and cross-tab support.
5
+ *
6
+ * @param storageKey The key used in localStorage.
7
+ * @param clientValue Default value for the client.
8
+ * @param ssrValue Optional fallback for server-side rendering.
9
+ * @returns A tuple `[value, setValue]`.
10
+ */
11
+ function useLocalStorage(storageKey, clientValue, ssrValue) {
12
+ const subscribe = useCallback((callback) => {
13
+ const listener = (e) => {
14
+ if (e.key === storageKey)
15
+ callback();
16
+ };
17
+ window.addEventListener('storage', listener);
18
+ return () => window.removeEventListener('storage', listener);
19
+ }, [storageKey]);
20
+ const getSnapshot = useCallback(() => {
21
+ const storedValue = localStorage.getItem(storageKey);
22
+ return storedValue || clientValue;
23
+ }, [storageKey, clientValue]);
24
+ const setValue = (value) => {
25
+ localStorage.setItem(storageKey, value);
26
+ // Manually dispatch a storage event to force update in the current tab
27
+ window.dispatchEvent(new StorageEvent('storage', { key: storageKey }));
28
+ };
29
+ const value = useSyncExternalStore(subscribe, getSnapshot, () => ssrValue || clientValue);
30
+ return [value, setValue];
31
+ }
@@ -0,0 +1,12 @@
1
+ export { useRestoreScroll };
2
+ import React from 'react';
3
+ /**
4
+ * useRestoreScroll
5
+ *
6
+ * Keeps the page from jumping when content changes,
7
+ * preserving the user’s scroll position.
8
+ *
9
+ * @param deps Dependencies that trigger scroll restoration
10
+ * @returns Function to set the previous top position
11
+ */
12
+ declare function useRestoreScroll(deps: React.DependencyList): (el: Element) => void;
@@ -0,0 +1,27 @@
1
+ export { useRestoreScroll };
2
+ import { useEffect, useRef } from 'react';
3
+ /**
4
+ * useRestoreScroll
5
+ *
6
+ * Keeps the page from jumping when content changes,
7
+ * preserving the user’s scroll position.
8
+ *
9
+ * @param deps Dependencies that trigger scroll restoration
10
+ * @returns Function to set the previous top position
11
+ */
12
+ function useRestoreScroll(deps) {
13
+ const prevPositionRef = useRef(null);
14
+ useEffect(() => {
15
+ if (!prevPositionRef.current)
16
+ return;
17
+ const { top, el } = prevPositionRef.current;
18
+ const delta = el.getBoundingClientRect().top - top;
19
+ if (delta !== 0)
20
+ window.scrollBy(0, delta);
21
+ prevPositionRef.current = null;
22
+ }, deps);
23
+ const setPrevPosition = (el) => {
24
+ prevPositionRef.current = { top: el.getBoundingClientRect().top, el };
25
+ };
26
+ return setPrevPosition;
27
+ }
@@ -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 vikeConfig = getVikeConfig();
18
- const choices = choiceNodes.map((choiceNode) => choiceNode.choiceValue);
19
- const choiceGroup = findChoiceGroup(vikeConfig, choices);
20
- const mergedChoiceNodes = choiceGroup.choices.map((choice) => {
21
- const node = choiceNodes.find((n) => n.choiceValue === choice);
22
- return {
23
- choiceValue: choice,
24
- children: node?.children ?? [],
25
- };
26
- });
17
+ const { 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 findChoiceGroup(vikeConfig, choices) {
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
- return choiceGroup;
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
  }
@@ -8,3 +8,4 @@ export * from './HorizontalLine.js';
8
8
  export * from './CodeBlockTransformer.js';
9
9
  export * from './Comment.js';
10
10
  export * from './FileRemoved.js';
11
+ export * from '../code-blocks/components/Tabs.js';
@@ -8,3 +8,4 @@ export * from './HorizontalLine.js';
8
8
  export * from './CodeBlockTransformer.js';
9
9
  export * from './Comment.js';
10
10
  export * from './FileRemoved.js';
11
+ export * from '../code-blocks/components/Tabs.js';
package/icons/index.ts CHANGED
@@ -1,14 +1,21 @@
1
1
  // Playground: https://www.figma.com/design/EFFQgT1aDH5exs2bABaIFJ/Icons?t=y0K2dHfJgpxMIl1e-0
2
- export { default as iconGear } from './gear.svg'
2
+ export { default as iconBluesky } from './bluesky.svg'
3
3
  export { default as iconBooks } from './books.svg'
4
- export { default as iconSeedling } from './seedling.svg'
5
- export { default as iconMagnifyingGlass } from './magnifying-glass.svg'
6
- export { default as iconPencil } from './pencil.svg'
4
+ export { default as iconChangelog } from './changelog.svg'
5
+ export { default as iconCoin } from './coin.svg'
7
6
  export { default as iconCompass } from './compass.svg'
8
- export { default as iconScroll } from './scroll.svg'
7
+ export { default as iconDiscord } from './discord.svg'
8
+ export { default as iconEyes } from './eyes.svg'
9
+ export { default as iconGear } from './gear.svg'
10
+ export { default as iconGithub } from './github.svg'
9
11
  export { default as iconGlobe } from './globe.svg'
10
- export { default as iconPlug } from './plug.svg'
12
+ export { default as iconLanguages } from './languages.svg'
13
+ export { default as iconLinkedin } from './linkedin.svg'
11
14
  export { default as iconLoudspeaker } from './loudspeaker.svg'
15
+ export { default as iconMagnifyingGlass } from './magnifying-glass.svg'
12
16
  export { default as iconMegaphone } from './megaphone.svg'
13
- export { default as iconCoin } from './coin.svg'
14
- export { default as iconEyes } from './eyes.svg'
17
+ export { default as iconPencil } from './pencil.svg'
18
+ export { default as iconPlug } from './plug.svg'
19
+ export { default as iconScroll } from './scroll.svg'
20
+ export { default as iconSeedling } from './seedling.svg'
21
+ export { default as iconTwitter } from './twitter.svg'
package/index.ts CHANGED
@@ -1,7 +1,16 @@
1
1
  /**********/
2
2
  /* PUBLIC */
3
3
  /**********/
4
- export { CodeBlockTransformer, Link, RepoLink, FileAdded, FileRemoved, ImportMeta, Emoji } from './components/index.js'
4
+ export {
5
+ CodeBlockTransformer,
6
+ Link,
7
+ RepoLink,
8
+ FileAdded,
9
+ FileRemoved,
10
+ ImportMeta,
11
+ Emoji,
12
+ Tabs,
13
+ } from './components/index.js'
5
14
  export { MenuToggle } from './Layout.js'
6
15
  export * from './components/Note.js'
7
16
  export * from './icons/index.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brillout/docpress",
3
- "version": "0.16.33",
3
+ "version": "0.16.35",
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",