@brillout/docpress 0.16.44 → 0.16.45

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.
@@ -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 {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brillout/docpress",
3
- "version": "0.16.44",
3
+ "version": "0.16.45",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@brillout/picocolors": "^1.0.10",