@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.
Files changed (37) hide show
  1. package/EditLink.tsx +3 -3
  2. package/Layout.css +0 -1
  3. package/Layout.tsx +3 -3
  4. package/MenuModal/NavigationWithColumnLayout.tsx +1 -1
  5. package/MenuModal.tsx +5 -5
  6. package/NavItemComponent.css +2 -2
  7. package/code-blocks/components/ChoiceGroup.tsx +16 -7
  8. package/code-blocks/components/Tabs.tsx +4 -1
  9. package/code-blocks/hooks/useCurrentSelection.ts +7 -3
  10. package/code-blocks/remarkChoiceGroup.ts +1 -0
  11. package/code-blocks/types.ts +1 -1
  12. package/code-blocks/utils/generateChoiceGroupCode.ts +1 -0
  13. package/code-blocks/utils/getAvailableChoice.ts +20 -0
  14. package/code-blocks/utils/resolveChoices.ts +5 -2
  15. package/components/HorizontalLine.tsx +1 -1
  16. package/css/colorize-on-hover.css +7 -3
  17. package/css/font.css +1 -2
  18. package/css/heading.css +3 -3
  19. package/css/index.css +1 -0
  20. package/css/link.css +1 -1
  21. package/css/table.css +2 -2
  22. package/css/tokens.css +34 -0
  23. package/css/tooltip.css +4 -4
  24. package/dist/code-blocks/components/Tabs.js +4 -1
  25. package/dist/code-blocks/hooks/useCurrentSelection.js +7 -3
  26. package/dist/code-blocks/remarkChoiceGroup.js +1 -0
  27. package/dist/code-blocks/types.d.ts +1 -0
  28. package/dist/code-blocks/utils/generateChoiceGroupCode.js +1 -0
  29. package/dist/code-blocks/utils/getAvailableChoice.d.ts +3 -0
  30. package/dist/code-blocks/utils/getAvailableChoice.js +14 -0
  31. package/dist/code-blocks/utils/resolveChoices.d.ts +2 -1
  32. package/dist/code-blocks/utils/resolveChoices.js +5 -2
  33. package/dist/components/HorizontalLine.js +1 -1
  34. package/dist/types/Config.d.ts +11 -0
  35. package/package.json +5 -2
  36. package/renderer/onRenderHtml.tsx +5 -0
  37. 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
@@ -1,4 +1,3 @@
1
1
  body {
2
2
  background: var(--color-bg-white);
3
- --color-bg-white: #fdfdfd;
4
3
  }
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>
@@ -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 [selectedChoice] = useCurrentSelection(groupName, defaultChoice)
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
- <option key={choice} value={choice}>
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 [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
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
- const [parentSelectedChoice] = useCurrentSelection(parentChoiceGroup?.name || '', parentChoiceGroup?.default || '')
62
+ let [parentSelectedChoice] = useCurrentSelection(parentChoiceGroup?.name || '', parentChoiceGroup?.default || '')
63
+ let isHidden = hidden
58
64
  const setPrevPosition = useRestoreScroll([selectedChoice])
59
65
 
60
- const choices = (isBuiltIn ? choiceGroup : choicesAll![groupName]!).choices
61
- const isHidden = parentChoiceGroup ? !parentChoiceGroup.choices.includes(parentSelectedChoice) : hidden
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 [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice)
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 optionExists = [...selectEl.options].some((opt) => opt.value === selectedChoice)
36
- if (!optionExists) localStorage.removeItem(storageKey)
37
- else selectEl.value = selectedChoice
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
  })
@@ -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[] } }
@@ -101,6 +101,7 @@ function generateChoiceGroupCode(choiceNodes: ChoiceNode[], parent: Parent, hide
101
101
  name: choiceGroup.name,
102
102
  choice: choiceNode.choiceValue,
103
103
  default: choiceGroup.default,
104
+ emptyChoices: choiceGroup.emptyChoices,
104
105
  lvl,
105
106
  },
106
107
  }),
@@ -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]) => [name, { ...group, choices: group.choices.map(resolveChoice) }]),
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
- --color-active-value: 0.03;
55
- --color-active: rgba(0, 0, 0, var(--color-active-value));
56
- --color-active-double: rgba(0, 0, 0, calc(2 * var(--color-active-value)));
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
@@ -7,7 +7,6 @@ button {
7
7
  font-family: inherit;
8
8
  }
9
9
  body {
10
- --color-text: #323d48;
11
- color: var(--color-text);
10
+ color: var(--color-text, #323d48);
12
11
  line-height: 1.5;
13
12
  }
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
@@ -1,3 +1,4 @@
1
+ @import './tokens.css';
1
2
  @import './reset.css';
2
3
  @import './colorize-on-hover.css';
3
4
  @import './heading.css';
package/css/link.css CHANGED
@@ -2,5 +2,5 @@ a {
2
2
  text-decoration: none;
3
3
  }
4
4
  .doc-page a[href]:not(.nav-item) {
5
- color: #4747ff;
5
+ color: var(--color-primary, #4747ff);
6
6
  }
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 [selectedChoice, setSelectedChoice] = useCurrentSelection(groupName, defaultChoice);
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 optionExists = [...selectEl.options].some((opt) => opt.value === selectedChoice);
34
- if (!optionExists)
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
  }
@@ -90,6 +90,7 @@ const remarkChoiceGroup = () => {
90
90
  name: parentChoiceGroup.name,
91
91
  default: parentChoiceGroup.default,
92
92
  choices: !choiceGroup.hidden ? [parentChoiceGroup.choice] : [],
93
+ emptyChoices: parentChoiceGroup.emptyChoices,
93
94
  },
94
95
  }),
95
96
  });
@@ -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 & {
@@ -82,6 +82,7 @@ function generateChoiceGroupCode(choiceNodes, parent, hide = false) {
82
82
  name: choiceGroup.name,
83
83
  choice: choiceNode.choiceValue,
84
84
  default: choiceGroup.default,
85
+ emptyChoices: choiceGroup.emptyChoices,
85
86
  lvl,
86
87
  },
87
88
  }),
@@ -0,0 +1,3 @@
1
+ export { getAvailableChoice };
2
+ import type { ChoiceItem } from '../../types/Config.js';
3
+ declare function getAvailableChoice(selectedChoice: string, choices: (string | ChoiceItem)[], emptyChoices: string[], defaultChoice: string): string;
@@ -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]) => [name, { ...group, choices: group.choices.map(resolveChoice) }]));
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;
@@ -8,7 +8,7 @@ function HorizontalLine({ primary }) {
8
8
  margin: 0,
9
9
  border: 0,
10
10
  // Same as `.doc-page h2::after`
11
- borderTop: '1px solid rgba(0, 0, 0, 0.1)',
11
+ borderTop: '1px solid var(--color-border, rgba(0, 0, 0, 0.1))',
12
12
  maxWidth: 500,
13
13
  width: '80%',
14
14
  } })));
@@ -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.44",
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": "https://github.com/brillout/docpress",
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