@brillout/docpress 0.16.44 → 0.16.46
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/EditLink.tsx +3 -3
- package/Layout.css +0 -1
- package/Layout.tsx +3 -3
- package/MenuModal/NavigationWithColumnLayout.tsx +1 -1
- package/MenuModal.tsx +5 -5
- package/NavItemComponent.css +2 -2
- package/code-blocks/components/ChoiceGroup.tsx +16 -7
- package/code-blocks/components/Tabs.tsx +4 -1
- package/code-blocks/hooks/useCurrentSelection.ts +7 -3
- package/code-blocks/remarkChoiceGroup.ts +1 -0
- package/code-blocks/types.ts +1 -1
- package/code-blocks/utils/generateChoiceGroupCode.ts +1 -0
- package/code-blocks/utils/getAvailableChoice.ts +20 -0
- package/code-blocks/utils/resolveChoices.ts +5 -2
- package/components/HorizontalLine.tsx +1 -1
- package/css/colorize-on-hover.css +7 -3
- package/css/font.css +1 -2
- package/css/heading.css +3 -3
- package/css/index.css +1 -0
- package/css/link.css +1 -1
- package/css/table.css +2 -2
- package/css/tokens.css +34 -0
- package/css/tooltip.css +4 -4
- package/dist/code-blocks/components/Tabs.js +4 -1
- package/dist/code-blocks/hooks/useCurrentSelection.js +7 -3
- package/dist/code-blocks/remarkChoiceGroup.js +1 -0
- package/dist/code-blocks/types.d.ts +1 -0
- package/dist/code-blocks/utils/generateChoiceGroupCode.js +1 -0
- package/dist/code-blocks/utils/getAvailableChoice.d.ts +3 -0
- package/dist/code-blocks/utils/getAvailableChoice.js +14 -0
- package/dist/code-blocks/utils/resolveChoices.d.ts +2 -1
- package/dist/code-blocks/utils/resolveChoices.js +5 -2
- package/dist/components/HorizontalLine.js +1 -1
- package/dist/types/Config.d.ts +11 -0
- package/package.json +5 -2
- package/renderer/onRenderHtml.tsx +5 -0
- package/types/Config.ts +12 -0
package/EditLink.tsx
CHANGED
|
@@ -28,11 +28,11 @@ function EditLink({
|
|
|
28
28
|
paddingBottom: 7,
|
|
29
29
|
paddingLeft: 8,
|
|
30
30
|
paddingRight: 7,
|
|
31
|
-
border: '1px solid #e0e0e0',
|
|
31
|
+
border: '1px solid var(--color-border, #e0e0e0)',
|
|
32
32
|
borderRadius: 7,
|
|
33
33
|
fontSize: '0.91em',
|
|
34
|
-
color: '#6c6c6c',
|
|
35
|
-
background: '#f8f8f8',
|
|
34
|
+
color: 'var(--color-muted, #6c6c6c)',
|
|
35
|
+
background: 'var(--color-surface, #f8f8f8)',
|
|
36
36
|
letterSpacing: 0.4,
|
|
37
37
|
lineHeight: 0,
|
|
38
38
|
...style,
|
package/Layout.css
CHANGED
package/Layout.tsx
CHANGED
|
@@ -82,7 +82,7 @@ function Layout({ children }: { children: React.ReactNode }) {
|
|
|
82
82
|
return (
|
|
83
83
|
<div
|
|
84
84
|
style={{
|
|
85
|
-
['--color-bg-gray']: '#f5f5f5',
|
|
85
|
+
['--color-bg-gray']: 'var(--color-surface, #f5f5f5)',
|
|
86
86
|
['--block-margin']: `${blockMargin}px`,
|
|
87
87
|
// ['--nav-head-height']: `${isLandingPage ? 70 : 63}px`,
|
|
88
88
|
['--nav-head-height']: `63px`,
|
|
@@ -359,7 +359,7 @@ function NavHead({ isNavLeft }: { isNavLeft?: true }) {
|
|
|
359
359
|
margin: 'auto',
|
|
360
360
|
height: 'var(--nav-head-height)',
|
|
361
361
|
fontSize: `min(14.2px, ${isProjectNameShort(name) ? '4.8cqw' : '4.5cqw'})`,
|
|
362
|
-
color: '#666',
|
|
362
|
+
color: 'var(--color-muted, #666)',
|
|
363
363
|
display: 'flex',
|
|
364
364
|
justifyContent: 'center',
|
|
365
365
|
}}
|
|
@@ -670,7 +670,7 @@ function MenuToggle({ menuId, ...props }: PropsDiv & { menuId: number }) {
|
|
|
670
670
|
width: 11,
|
|
671
671
|
marginLeft: 'calc(var(--icon-text-padding) - 1px)',
|
|
672
672
|
flexShrink: 0,
|
|
673
|
-
color: '#888',
|
|
673
|
+
color: 'var(--color-muted, #888)',
|
|
674
674
|
position: 'relative',
|
|
675
675
|
top: 1,
|
|
676
676
|
}}
|
|
@@ -138,7 +138,7 @@ function NavigationWithColumnLayout(props: { navItems: NavItem[] }) {
|
|
|
138
138
|
${/* Button style */ ''}
|
|
139
139
|
.menu-toggle-${i} {
|
|
140
140
|
html.menu-modal-show.menu-modal-show-${i} & {
|
|
141
|
-
color: black !important;
|
|
141
|
+
color: var(--color-text, black) !important;
|
|
142
142
|
[class^='decolorize-'],
|
|
143
143
|
[class*=' decolorize-'] {
|
|
144
144
|
filter: grayscale(0) opacity(1) !important;
|
package/MenuModal.tsx
CHANGED
|
@@ -26,7 +26,7 @@ function MenuModal({ isTopNav, isNavLeftAlwaysHidden_ }: { isTopNav: boolean; is
|
|
|
26
26
|
width: '100%',
|
|
27
27
|
top: 'var(--nav-head-height)',
|
|
28
28
|
zIndex: 199, // maximum value, because docsearch's modal has `z-index: 200`
|
|
29
|
-
background: '#ededef',
|
|
29
|
+
background: 'var(--color-surface, #ededef)',
|
|
30
30
|
transitionProperty: 'opacity',
|
|
31
31
|
transitionTimingFunction: 'ease',
|
|
32
32
|
maxWidth: isNavLeftAlwaysHidden_ ? undefined : bodyMaxWidth,
|
|
@@ -169,8 +169,8 @@ function CloseButton({ className }: { className: string }) {
|
|
|
169
169
|
cx="11.45"
|
|
170
170
|
cy="11.45"
|
|
171
171
|
r="10.607"
|
|
172
|
-
fill="#ececec"
|
|
173
|
-
stroke="#666"
|
|
172
|
+
fill="var(--color-surface, #ececec)"
|
|
173
|
+
stroke="var(--color-muted, #666)"
|
|
174
174
|
strokeDashoffset="251.44"
|
|
175
175
|
strokeLinecap="round"
|
|
176
176
|
strokeLinejoin="round"
|
|
@@ -179,8 +179,8 @@ function CloseButton({ className }: { className: string }) {
|
|
|
179
179
|
/>
|
|
180
180
|
<path
|
|
181
181
|
d="m7.5904 6.2204 3.86 3.86 3.84-3.84a0.92 0.92 0 0 1 0.66-0.29 1 1 0 0 1 1 1 0.9 0.9 0 0 1-0.27 0.66l-3.89 3.84 3.89 3.89a0.9 0.9 0 0 1 0.27 0.61 1 1 0 0 1-1 1 0.92 0.92 0 0 1-0.69-0.27l-3.81-3.86-3.85 3.85a0.92 0.92 0 0 1-0.65 0.28 1 1 0 0 1-1-1 0.9 0.9 0 0 1 0.27-0.66l3.89-3.84-3.89-3.89a0.9 0.9 0 0 1-0.27-0.61 1 1 0 0 1 1-1c0.24 3e-3 0.47 0.1 0.64 0.27z"
|
|
182
|
-
fill="#666"
|
|
183
|
-
stroke="#666"
|
|
182
|
+
fill="var(--color-muted, #666)"
|
|
183
|
+
stroke="var(--color-muted, #666)"
|
|
184
184
|
strokeWidth=".11719"
|
|
185
185
|
/>
|
|
186
186
|
</svg>
|
package/NavItemComponent.css
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
.nav-item-level-4 {
|
|
14
14
|
padding-top: 14px;
|
|
15
15
|
margin-bottom: -1px;
|
|
16
|
-
color: #999;
|
|
16
|
+
color: var(--color-muted, #999);
|
|
17
17
|
font-size: 12px;
|
|
18
18
|
font-weight: 400;
|
|
19
19
|
letter-spacing: 0.15ch;
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
}
|
|
83
83
|
.nav-item-level-3 {
|
|
84
84
|
--shadow-size: 9px;
|
|
85
|
-
--shadow-color: rgba(0, 0, 0, 0.11);
|
|
85
|
+
--shadow-color: var(--color-shadow, rgba(0, 0, 0, 0.11));
|
|
86
86
|
--shadow-size-minus: calc(-1 * var(--shadow-size));
|
|
87
87
|
--shadow-top: inset 0px var(--shadow-size) var(--shadow-size) var(--shadow-size-minus) var(--shadow-color);
|
|
88
88
|
--shadow-bottom: inset 0px var(--shadow-size-minus) var(--shadow-size) var(--shadow-size-minus) var(--shadow-color);
|
|
@@ -5,6 +5,7 @@ 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
|
+
import { getAvailableChoice } from '../utils/getAvailableChoice.js'
|
|
8
9
|
import { cls } from '../../utils/cls.js'
|
|
9
10
|
import './ChoiceGroup.css'
|
|
10
11
|
|
|
@@ -28,15 +29,17 @@ function ChoiceGroupContainer({
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
function ChoiceGroup({ children, choiceGroup }: { children: React.ReactNode; choiceGroup: TChoiceGroup }) {
|
|
31
|
-
const { name: groupName, choices, default: defaultChoice } = choiceGroup
|
|
32
|
-
const [
|
|
32
|
+
const { name: groupName, choices, default: defaultChoice, emptyChoices } = choiceGroup
|
|
33
|
+
const [selectedChoiceStored] = useCurrentSelection(groupName, defaultChoice)
|
|
34
|
+
const selectedChoice = getAvailableChoice(selectedChoiceStored, choices, emptyChoices, defaultChoice)
|
|
33
35
|
|
|
34
36
|
return (
|
|
35
37
|
<div className="choice-group">
|
|
36
38
|
{/* Hidden select used to control choice visibility via CSS */}
|
|
37
39
|
<select data-choice-group={groupName} name={`choicesFor-${groupName}`} value={selectedChoice} hidden disabled>
|
|
38
40
|
{choices.map(({ name: choice }) => (
|
|
39
|
-
|
|
41
|
+
// data-empty is read by the initializeChoiceGroup SSR script (useCurrentSelection.ts)
|
|
42
|
+
<option key={choice} value={choice} data-empty={emptyChoices.includes(choice) ? '' : undefined}>
|
|
40
43
|
{choice}
|
|
41
44
|
</option>
|
|
42
45
|
))}
|
|
@@ -51,14 +54,20 @@ function CustomSelect({ choiceGroup }: { choiceGroup: ChoiceGroupWithParent }) {
|
|
|
51
54
|
const radioId = useId()
|
|
52
55
|
const choicesAll = usePageContext().resolved.choices
|
|
53
56
|
const { name: groupName, emptyChoices, default: defaultChoice, hidden, parentChoiceGroup, isBuiltIn } = choiceGroup
|
|
54
|
-
const
|
|
57
|
+
const choices = (isBuiltIn ? choiceGroup : choicesAll![groupName]!).choices
|
|
58
|
+
const [selectedChoiceStored, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
59
|
+
const selectedChoice = getAvailableChoice(selectedChoiceStored, choices, emptyChoices, defaultChoice)
|
|
55
60
|
const [expanded, setExpanded] = useState(false)
|
|
56
61
|
const [isHovered, setIsHovered] = useState(false)
|
|
57
|
-
|
|
62
|
+
let [parentSelectedChoice] = useCurrentSelection(parentChoiceGroup?.name || '', parentChoiceGroup?.default || '')
|
|
63
|
+
let isHidden = hidden
|
|
58
64
|
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
if (parentChoiceGroup) {
|
|
67
|
+
const { choices, emptyChoices, default: defaultChoice } = parentChoiceGroup
|
|
68
|
+
parentSelectedChoice = getAvailableChoice(parentSelectedChoice, choices, emptyChoices, defaultChoice)
|
|
69
|
+
isHidden = !parentChoiceGroup.choices.includes(parentSelectedChoice)
|
|
70
|
+
}
|
|
62
71
|
const isEmptyChoice = (choice: string) => emptyChoices.includes(choice)
|
|
63
72
|
const filteredChoices = choices.filter((choice) => !isEmptyChoice(choice.name))
|
|
64
73
|
const selectedIndex = filteredChoices.findIndex((choice) => choice.name === selectedChoice)
|
|
@@ -3,6 +3,7 @@ export { Tabs }
|
|
|
3
3
|
import React, { useId } from 'react'
|
|
4
4
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js'
|
|
5
5
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js'
|
|
6
|
+
import { getAvailableChoice } from '../utils/getAvailableChoice.js'
|
|
6
7
|
import { usePageContext } from '../../renderer/usePageContext.js'
|
|
7
8
|
import { assertUsage } from '../../utils/assert.js'
|
|
8
9
|
import './Tabs.css'
|
|
@@ -14,7 +15,9 @@ function Tabs({ choice, hide = [] }: { choice: string; hide: string[] }) {
|
|
|
14
15
|
const choicesAll = pageContext.resolved.choices
|
|
15
16
|
assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`)
|
|
16
17
|
const { choices, default: defaultChoice } = choicesAll[groupName]
|
|
17
|
-
const [
|
|
18
|
+
const [selectedChoiceStored, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
|
|
19
|
+
// A hidden tab can't be shown, so treat `hide` as the unavailable choices (#169)
|
|
20
|
+
const selectedChoice = getAvailableChoice(selectedChoiceStored, choices, hide, defaultChoice)
|
|
18
21
|
const setPrevPosition = useRestoreScroll([selectedChoice])
|
|
19
22
|
const isHidden = (choice: string) => hide.includes(choice)
|
|
20
23
|
|
|
@@ -32,13 +32,17 @@ function initializeChoiceGroup() {
|
|
|
32
32
|
switch (groupEl.tagName) {
|
|
33
33
|
case 'SELECT':
|
|
34
34
|
const selectEl = groupEl as HTMLSelectElement
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
const option = [...selectEl.options].find((opt) => opt.value === selectedChoice)
|
|
36
|
+
// No option: stale choice removed from the config → forget it.
|
|
37
|
+
// data-empty: choice exists in the group but has no content on this page → keep the
|
|
38
|
+
// server-rendered fallback rather than selecting a blank choice (#169).
|
|
39
|
+
if (!option) localStorage.removeItem(storageKey)
|
|
40
|
+
else if (!option.hasAttribute('data-empty')) selectEl.value = selectedChoice
|
|
38
41
|
break
|
|
39
42
|
case 'DIV':
|
|
40
43
|
const radioEl = groupEl.querySelector<HTMLInputElement>(`input[type="radio"][value="${selectedChoice}"]`)
|
|
41
44
|
if (radioEl) radioEl.checked = true
|
|
45
|
+
break
|
|
42
46
|
default:
|
|
43
47
|
break
|
|
44
48
|
}
|
|
@@ -109,6 +109,7 @@ const remarkChoiceGroup: Plugin<[], Root> = (): Transformer<Root> => {
|
|
|
109
109
|
name: parentChoiceGroup.name,
|
|
110
110
|
default: parentChoiceGroup.default,
|
|
111
111
|
choices: !choiceGroup.hidden ? [parentChoiceGroup.choice] : [],
|
|
112
|
+
emptyChoices: parentChoiceGroup.emptyChoices,
|
|
112
113
|
},
|
|
113
114
|
}),
|
|
114
115
|
})
|
package/code-blocks/types.ts
CHANGED
|
@@ -10,5 +10,5 @@ type ChoiceGroup = Omit<NonNullable<Config['choices']>[string], 'choices'> & {
|
|
|
10
10
|
lvl: number
|
|
11
11
|
isBuiltIn?: boolean
|
|
12
12
|
}
|
|
13
|
-
type ParentChoiceGroup = { name: string; default: string }
|
|
13
|
+
type ParentChoiceGroup = { name: string; default: string; emptyChoices: string[] }
|
|
14
14
|
type ChoiceGroupWithParent = ChoiceGroup & { parentChoiceGroup?: ParentChoiceGroup & { choices: string[] } }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { getAvailableChoice }
|
|
2
|
+
|
|
3
|
+
import type { ChoiceItem } from '../../types/Config.js'
|
|
4
|
+
import { resolveChoice } from './resolveChoices.js'
|
|
5
|
+
|
|
6
|
+
// A page may include only some of a group's choices, so a choice selected elsewhere (and persisted in
|
|
7
|
+
// localStorage) can be absent here — which would otherwise render nothing (#169). Resolve to a choice
|
|
8
|
+
// that exists on the current page.
|
|
9
|
+
function getAvailableChoice(
|
|
10
|
+
selectedChoice: string,
|
|
11
|
+
choices: (string | ChoiceItem)[],
|
|
12
|
+
emptyChoices: string[],
|
|
13
|
+
defaultChoice: string,
|
|
14
|
+
): string {
|
|
15
|
+
const isAvailable = (choiceName: string) => !emptyChoices.includes(choiceName)
|
|
16
|
+
if (isAvailable(selectedChoice)) return selectedChoice
|
|
17
|
+
if (isAvailable(defaultChoice)) return defaultChoice
|
|
18
|
+
const choicesResolved = choices.map(resolveChoice)
|
|
19
|
+
return choicesResolved.find((choice) => isAvailable(choice.name))?.name ?? selectedChoice
|
|
20
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { resolveChoices }
|
|
1
|
+
export { resolveChoices, resolveChoice }
|
|
2
2
|
export type { ResolvedChoices }
|
|
3
3
|
|
|
4
4
|
import type { Choice, ChoiceItem } from '../../types/Config.js'
|
|
@@ -7,7 +7,10 @@ type ResolvedChoices = Record<string, Omit<Choice, 'choices'> & { choices: Choic
|
|
|
7
7
|
|
|
8
8
|
function resolveChoices(choicesConfig: Record<string, Choice>): ResolvedChoices {
|
|
9
9
|
return Object.fromEntries(
|
|
10
|
-
Object.entries(choicesConfig).map(([name, group]) => [
|
|
10
|
+
Object.entries(choicesConfig).map(([name, group]) => [
|
|
11
|
+
name,
|
|
12
|
+
{ ...group, choices: group.choices.map(resolveChoice) },
|
|
13
|
+
]),
|
|
11
14
|
)
|
|
12
15
|
}
|
|
13
16
|
function resolveChoice(choice: string | ChoiceItem): ChoiceItem {
|
|
@@ -12,7 +12,7 @@ function HorizontalLine({ primary }: { primary?: true }) {
|
|
|
12
12
|
margin: 0,
|
|
13
13
|
border: 0,
|
|
14
14
|
// Same as `.doc-page h2::after`
|
|
15
|
-
borderTop: '1px solid rgba(0, 0, 0, 0.1)',
|
|
15
|
+
borderTop: '1px solid var(--color-border, rgba(0, 0, 0, 0.1))',
|
|
16
16
|
maxWidth: 500,
|
|
17
17
|
width: '80%',
|
|
18
18
|
}}
|
|
@@ -51,7 +51,11 @@
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
body {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
/* Hover/active tint mixed from the text color so it tracks the active
|
|
55
|
+
appearance (light text -> light tint on dark). The `#000` fallback makes an
|
|
56
|
+
unthemed site identical to the original `rgba(0, 0, 0, value)`:
|
|
57
|
+
color-mix(in srgb, #000 3%, transparent) === rgba(0, 0, 0, 0.03). */
|
|
58
|
+
--color-active-value: 3%;
|
|
59
|
+
--color-active: color-mix(in srgb, var(--color-text, #000) var(--color-active-value), transparent);
|
|
60
|
+
--color-active-double: color-mix(in srgb, var(--color-text, #000) calc(2 * var(--color-active-value)), transparent);
|
|
57
61
|
}
|
package/css/font.css
CHANGED
package/css/heading.css
CHANGED
|
@@ -27,7 +27,7 @@ h3 {
|
|
|
27
27
|
height: 1px;
|
|
28
28
|
top: -45px;
|
|
29
29
|
/* Same as <HorizontalLine> */
|
|
30
|
-
background-color: rgba(0, 0, 0, 0.1);
|
|
30
|
+
background-color: var(--color-border, rgba(0, 0, 0, 0.1));
|
|
31
31
|
--width: min(80%, 500px);
|
|
32
32
|
width: var(--width);
|
|
33
33
|
left: calc((100% - var(--width)) / 2);
|
|
@@ -54,7 +54,7 @@ h3 {
|
|
|
54
54
|
|
|
55
55
|
.doc-page h5 {
|
|
56
56
|
font-size: 0.94em;
|
|
57
|
-
color: #777;
|
|
57
|
+
color: var(--color-muted, #777);
|
|
58
58
|
margin-bottom: 10px;
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -82,7 +82,7 @@ h3 {
|
|
|
82
82
|
font-size: 23px;
|
|
83
83
|
top: 1px;
|
|
84
84
|
left: calc(-1 * (0.75em - var(--hash-offset)));
|
|
85
|
-
color: #aaa;
|
|
85
|
+
color: var(--color-muted, #aaa);
|
|
86
86
|
display: flex;
|
|
87
87
|
align-items: center;
|
|
88
88
|
}
|
package/css/index.css
CHANGED
package/css/link.css
CHANGED
package/css/table.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
table th {
|
|
2
|
-
background-color: #f7f7f7;
|
|
2
|
+
background-color: var(--color-surface, #f7f7f7);
|
|
3
3
|
}
|
|
4
4
|
table {
|
|
5
5
|
border-collapse: collapse;
|
|
@@ -7,7 +7,7 @@ table {
|
|
|
7
7
|
table td,
|
|
8
8
|
table th {
|
|
9
9
|
padding: 8px;
|
|
10
|
-
border: 1px solid #ccc;
|
|
10
|
+
border: 1px solid var(--color-border, #ccc);
|
|
11
11
|
}
|
|
12
12
|
table th {
|
|
13
13
|
text-align: left;
|
package/css/tokens.css
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Color tokens (opt-in, zero default change).
|
|
3
|
+
*
|
|
4
|
+
* No light palette is defined here on purpose: every consumer reads
|
|
5
|
+
* `var(--color-x, <literal>)` and falls back to its original literal, so an
|
|
6
|
+
* unthemed DocPress site renders exactly as before. A site re-themes by setting
|
|
7
|
+
* the `--color-*` variables (on :root or body), and turns on a dark appearance
|
|
8
|
+
* by adding the `dark` class or `[data-theme="dark"]`. There is no
|
|
9
|
+
* `prefers-color-scheme` switch, so the OS setting never changes a site that
|
|
10
|
+
* has not opted in.
|
|
11
|
+
*/
|
|
12
|
+
:root {
|
|
13
|
+
/* --color-text has a single original value and pre-existing consumers that
|
|
14
|
+
read it bare (nav items, tabs), so it is defined here (was `body {}`). It
|
|
15
|
+
stays overridable (a `body`/`:root` override or the dark block below wins).
|
|
16
|
+
The ambiguous greys are intentionally NOT defined here: their consumers fall
|
|
17
|
+
back to their own original literal, so light rendering is unchanged. */
|
|
18
|
+
--color-text: #323d48;
|
|
19
|
+
|
|
20
|
+
/* Long-standing internal aliases, kept resolving to their original defaults. */
|
|
21
|
+
--color-bg-white: var(--color-bg, #fdfdfd);
|
|
22
|
+
--color-bg-gray: var(--color-surface, #f5f5f5);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
:root.dark,
|
|
26
|
+
:root[data-theme='dark'] {
|
|
27
|
+
--color-bg: #0d0f14;
|
|
28
|
+
--color-surface: #161922;
|
|
29
|
+
--color-text: #d7dce5;
|
|
30
|
+
--color-muted: #8b95a5;
|
|
31
|
+
--color-border: #2b313c;
|
|
32
|
+
--color-primary: #8aa0ff;
|
|
33
|
+
--color-shadow: rgba(0, 0, 0, 0.45);
|
|
34
|
+
}
|
package/css/tooltip.css
CHANGED
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
position: absolute;
|
|
20
20
|
left: 50%;
|
|
21
21
|
transform: translate(-50%, 0);
|
|
22
|
-
background: #fdfdfd;
|
|
22
|
+
background: var(--color-bg, #fdfdfd);
|
|
23
23
|
padding: 3px 10px;
|
|
24
|
-
box-shadow: rgb(0 0 0 / 8%) 2px 4px 7px 0px;
|
|
24
|
+
box-shadow: var(--color-shadow, rgb(0 0 0 / 8%)) 2px 4px 7px 0px;
|
|
25
25
|
border-radius: 5px;
|
|
26
|
-
color: #333;
|
|
26
|
+
color: var(--color-text, #333);
|
|
27
27
|
letter-spacing: -0.02em;
|
|
28
|
-
border: 1px solid #e3e3e3;
|
|
28
|
+
border: 1px solid var(--color-border, #e3e3e3);
|
|
29
29
|
z-index: 1;
|
|
30
30
|
/* Atlernative: https://stackoverflow.com/questions/30086913/css-positioned-absolute-element-automatic-width-outside-of-parents-bounds/57379329#57379329
|
|
31
31
|
width: max-content;
|
|
@@ -2,6 +2,7 @@ export { Tabs };
|
|
|
2
2
|
import React, { useId } from 'react';
|
|
3
3
|
import { useCurrentSelection } from '../hooks/useCurrentSelection.js';
|
|
4
4
|
import { useRestoreScroll } from '../hooks/useRestoreScroll.js';
|
|
5
|
+
import { getAvailableChoice } from '../utils/getAvailableChoice.js';
|
|
5
6
|
import { usePageContext } from '../../renderer/usePageContext.js';
|
|
6
7
|
import { assertUsage } from '../../utils/assert.js';
|
|
7
8
|
import './Tabs.css';
|
|
@@ -12,7 +13,9 @@ function Tabs({ choice, hide = [] }) {
|
|
|
12
13
|
const choicesAll = pageContext.resolved.choices;
|
|
13
14
|
assertUsage(choicesAll && choicesAll[groupName], `${groupName} is unknown`);
|
|
14
15
|
const { choices, default: defaultChoice } = choicesAll[groupName];
|
|
15
|
-
const [
|
|
16
|
+
const [selectedChoiceStored, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice);
|
|
17
|
+
// A hidden tab can't be shown, so treat `hide` as the unavailable choices (#169)
|
|
18
|
+
const selectedChoice = getAvailableChoice(selectedChoiceStored, choices, hide, defaultChoice);
|
|
16
19
|
const setPrevPosition = useRestoreScroll([selectedChoice]);
|
|
17
20
|
const isHidden = (choice) => hide.includes(choice);
|
|
18
21
|
return (React.createElement("div", { className: "choice-tabs" },
|
|
@@ -30,16 +30,20 @@ function initializeChoiceGroup() {
|
|
|
30
30
|
switch (groupEl.tagName) {
|
|
31
31
|
case 'SELECT':
|
|
32
32
|
const selectEl = groupEl;
|
|
33
|
-
const
|
|
34
|
-
|
|
33
|
+
const option = [...selectEl.options].find((opt) => opt.value === selectedChoice);
|
|
34
|
+
// No option: stale choice removed from the config → forget it.
|
|
35
|
+
// data-empty: choice exists in the group but has no content on this page → keep the
|
|
36
|
+
// server-rendered fallback rather than selecting a blank choice (#169).
|
|
37
|
+
if (!option)
|
|
35
38
|
localStorage.removeItem(storageKey);
|
|
36
|
-
else
|
|
39
|
+
else if (!option.hasAttribute('data-empty'))
|
|
37
40
|
selectEl.value = selectedChoice;
|
|
38
41
|
break;
|
|
39
42
|
case 'DIV':
|
|
40
43
|
const radioEl = groupEl.querySelector(`input[type="radio"][value="${selectedChoice}"]`);
|
|
41
44
|
if (radioEl)
|
|
42
45
|
radioEl.checked = true;
|
|
46
|
+
break;
|
|
43
47
|
default:
|
|
44
48
|
break;
|
|
45
49
|
}
|
|
@@ -11,6 +11,7 @@ type ChoiceGroup = Omit<NonNullable<Config['choices']>[string], 'choices'> & {
|
|
|
11
11
|
type ParentChoiceGroup = {
|
|
12
12
|
name: string;
|
|
13
13
|
default: string;
|
|
14
|
+
emptyChoices: string[];
|
|
14
15
|
};
|
|
15
16
|
type ChoiceGroupWithParent = ChoiceGroup & {
|
|
16
17
|
parentChoiceGroup?: ParentChoiceGroup & {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { getAvailableChoice };
|
|
2
|
+
import { resolveChoice } from './resolveChoices.js';
|
|
3
|
+
// A page may include only some of a group's choices, so a choice selected elsewhere (and persisted in
|
|
4
|
+
// localStorage) can be absent here — which would otherwise render nothing (#169). Resolve to a choice
|
|
5
|
+
// that exists on the current page.
|
|
6
|
+
function getAvailableChoice(selectedChoice, choices, emptyChoices, defaultChoice) {
|
|
7
|
+
const isAvailable = (choiceName) => !emptyChoices.includes(choiceName);
|
|
8
|
+
if (isAvailable(selectedChoice))
|
|
9
|
+
return selectedChoice;
|
|
10
|
+
if (isAvailable(defaultChoice))
|
|
11
|
+
return defaultChoice;
|
|
12
|
+
const choicesResolved = choices.map(resolveChoice);
|
|
13
|
+
return choicesResolved.find((choice) => isAvailable(choice.name))?.name ?? selectedChoice;
|
|
14
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { resolveChoices };
|
|
1
|
+
export { resolveChoices, resolveChoice };
|
|
2
2
|
export type { ResolvedChoices };
|
|
3
3
|
import type { Choice, ChoiceItem } from '../../types/Config.js';
|
|
4
4
|
type ResolvedChoices = Record<string, Omit<Choice, 'choices'> & {
|
|
5
5
|
choices: ChoiceItem[];
|
|
6
6
|
}>;
|
|
7
7
|
declare function resolveChoices(choicesConfig: Record<string, Choice>): ResolvedChoices;
|
|
8
|
+
declare function resolveChoice(choice: string | ChoiceItem): ChoiceItem;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
export { resolveChoices };
|
|
1
|
+
export { resolveChoices, resolveChoice };
|
|
2
2
|
function resolveChoices(choicesConfig) {
|
|
3
|
-
return Object.fromEntries(Object.entries(choicesConfig).map(([name, group]) => [
|
|
3
|
+
return Object.fromEntries(Object.entries(choicesConfig).map(([name, group]) => [
|
|
4
|
+
name,
|
|
5
|
+
{ ...group, choices: group.choices.map(resolveChoice) },
|
|
6
|
+
]));
|
|
4
7
|
}
|
|
5
8
|
function resolveChoice(choice) {
|
|
6
9
|
return typeof choice === 'string' ? { name: choice } : choice;
|
package/dist/types/Config.d.ts
CHANGED
|
@@ -13,6 +13,17 @@ type Config = {
|
|
|
13
13
|
google: string;
|
|
14
14
|
};
|
|
15
15
|
banner?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Raw HTML injected at the end of `<head>` on every page.
|
|
18
|
+
*
|
|
19
|
+
* Use for a small inline `<script>`/`<style>` that must run before first
|
|
20
|
+
* paint — e.g. a no-flash theme script that reads a cookie and applies the
|
|
21
|
+
* palette before the page renders (relevant for prerendered/static pages,
|
|
22
|
+
* where there is no request to read the cookie from at render time).
|
|
23
|
+
*
|
|
24
|
+
* Emitted verbatim (not escaped), so only pass trusted, self-authored markup.
|
|
25
|
+
*/
|
|
26
|
+
headHtml?: string;
|
|
16
27
|
github: string;
|
|
17
28
|
discord?: string;
|
|
18
29
|
twitter?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brillout/docpress",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.46",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@brillout/picocolors": "^1.0.10",
|
|
@@ -66,7 +66,10 @@
|
|
|
66
66
|
"vike": "^0.4.255",
|
|
67
67
|
"vite": "^8.0.8"
|
|
68
68
|
},
|
|
69
|
-
"repository":
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "https://github.com/brillout/docpress"
|
|
72
|
+
},
|
|
70
73
|
"license": "MIT",
|
|
71
74
|
"publishConfig": {
|
|
72
75
|
"access": "public"
|
|
@@ -30,6 +30,7 @@ async function onRenderHtml(pageContext: PageContextServer): Promise<any> {
|
|
|
30
30
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
31
31
|
${getOpenGraphTags(pageContext.urlPathname, documentTitle, pageContext.globalContext.config.docpress)}
|
|
32
32
|
${getAlgoliaTags(pageContext)}
|
|
33
|
+
${getHeadHtml(pageContext.globalContext.config.docpress)}
|
|
33
34
|
</head>
|
|
34
35
|
<body>
|
|
35
36
|
<div id="page-view">${dangerouslySkipEscape(pageHtml)}</div>
|
|
@@ -37,6 +38,10 @@ async function onRenderHtml(pageContext: PageContextServer): Promise<any> {
|
|
|
37
38
|
</html>`
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
function getHeadHtml(config: Config) {
|
|
42
|
+
return config.headHtml ? dangerouslySkipEscape(config.headHtml) : ''
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
function getAlgoliaTags(pageContext: PageContextServer) {
|
|
41
46
|
const activeCategory = getActiveCategory(pageContext)
|
|
42
47
|
const categoryNameTag = escapeInject`<meta name="algolia:category" content="${activeCategory.name}">`
|
package/types/Config.ts
CHANGED
|
@@ -18,6 +18,18 @@ type Config = {
|
|
|
18
18
|
}
|
|
19
19
|
banner?: string
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Raw HTML injected at the end of `<head>` on every page.
|
|
23
|
+
*
|
|
24
|
+
* Use for a small inline `<script>`/`<style>` that must run before first
|
|
25
|
+
* paint — e.g. a no-flash theme script that reads a cookie and applies the
|
|
26
|
+
* palette before the page renders (relevant for prerendered/static pages,
|
|
27
|
+
* where there is no request to read the cookie from at render time).
|
|
28
|
+
*
|
|
29
|
+
* Emitted verbatim (not escaped), so only pass trusted, self-authored markup.
|
|
30
|
+
*/
|
|
31
|
+
headHtml?: string
|
|
32
|
+
|
|
21
33
|
github: string
|
|
22
34
|
discord?: string
|
|
23
35
|
twitter?: string
|