docusaurus-plugin-glossary 1.1.2 → 1.3.1
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/README.md +116 -36
- package/lib/index.js +246 -0
- package/{remark → lib/remark}/glossary-terms.js +98 -49
- package/lib/theme/GlossaryTerm/index.js +130 -0
- package/{theme → lib/theme}/GlossaryTerm/index.test.js +19 -0
- package/{theme → lib/theme}/GlossaryTerm/styles.module.css +51 -13
- package/package.json +32 -9
- package/index.js +0 -158
- package/theme/GlossaryTerm/index.js +0 -69
- /package/{components → lib/components}/GlossaryPage.js +0 -0
- /package/{components → lib/components}/GlossaryPage.module.css +0 -0
- /package/{components → lib/components}/GlossaryPage.test.js +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Creates a remark plugin that automatically detects and replaces glossary terms in markdown
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* @param {object} options - Plugin options
|
|
9
9
|
* @param {Array} options.terms - Array of glossary term objects with {term, definition}
|
|
10
10
|
* @param {string} options.glossaryPath - Path to glossary JSON file (optional, if terms not provided)
|
|
@@ -12,11 +12,11 @@ const fs = require('fs');
|
|
|
12
12
|
* @param {string} options.siteDir - Docusaurus site directory (required if using glossaryPath)
|
|
13
13
|
* @returns {function} Remark plugin function
|
|
14
14
|
*/
|
|
15
|
-
function remarkGlossaryTerms({
|
|
16
|
-
terms = [],
|
|
15
|
+
export default function remarkGlossaryTerms({
|
|
16
|
+
terms = [],
|
|
17
17
|
glossaryPath = null,
|
|
18
18
|
routePath = '/glossary',
|
|
19
|
-
siteDir = null
|
|
19
|
+
siteDir = null,
|
|
20
20
|
} = {}) {
|
|
21
21
|
let glossaryTerms = terms;
|
|
22
22
|
|
|
@@ -44,13 +44,11 @@ function remarkGlossaryTerms({
|
|
|
44
44
|
|
|
45
45
|
// Sort terms by length (longest first) to avoid partial matches
|
|
46
46
|
// e.g., "Application Programming Interface" should match before "API"
|
|
47
|
-
const sortedTerms = Array.from(termMap.entries()).sort(
|
|
48
|
-
(a, b) => b[0].length - a[0].length
|
|
49
|
-
);
|
|
47
|
+
const sortedTerms = Array.from(termMap.entries()).sort((a, b) => b[0].length - a[0].length);
|
|
50
48
|
|
|
51
49
|
// If no terms, return a no-op transformer
|
|
52
50
|
if (sortedTerms.length === 0) {
|
|
53
|
-
return
|
|
51
|
+
return tree => tree;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
/**
|
|
@@ -71,29 +69,50 @@ function remarkGlossaryTerms({
|
|
|
71
69
|
for (const [lowerTerm, termObj] of sortedTerms) {
|
|
72
70
|
const term = termObj.term;
|
|
73
71
|
let searchIndex = 0;
|
|
74
|
-
|
|
72
|
+
|
|
75
73
|
while (searchIndex < textLower.length) {
|
|
76
74
|
const index = textLower.indexOf(lowerTerm, searchIndex);
|
|
77
75
|
if (index === -1) break;
|
|
78
76
|
|
|
79
|
-
// Check if it's a whole word match
|
|
77
|
+
// Check if it's a whole word match, with simple plural tolerance ('s' or 'es')
|
|
80
78
|
const beforeChar = index > 0 ? textLower[index - 1] : ' ';
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
const afterIndex = index + lowerTerm.length;
|
|
80
|
+
const afterChar = afterIndex < textLower.length ? textLower[afterIndex] : ' ';
|
|
81
|
+
|
|
82
|
+
let matchLength = term.length;
|
|
83
|
+
let isWordBoundary = !/\w/.test(beforeChar) && !/\w/.test(afterChar);
|
|
84
|
+
|
|
85
|
+
// Allow trailing 's' plural (e.g., webhook -> webhooks)
|
|
86
|
+
if (!isWordBoundary && afterChar === 's') {
|
|
87
|
+
const nextChar = afterIndex + 1 < textLower.length ? textLower[afterIndex + 1] : ' ';
|
|
88
|
+
if (!/\w/.test(nextChar)) {
|
|
89
|
+
isWordBoundary = true;
|
|
90
|
+
matchLength = term.length + 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Allow trailing 'es' plural (e.g., API -> APIs, box -> boxes)
|
|
95
|
+
if (
|
|
96
|
+
!isWordBoundary &&
|
|
97
|
+
afterChar === 'e' &&
|
|
98
|
+
afterIndex + 1 < textLower.length &&
|
|
99
|
+
textLower[afterIndex + 1] === 's'
|
|
100
|
+
) {
|
|
101
|
+
const nextChar = afterIndex + 2 < textLower.length ? textLower[afterIndex + 2] : ' ';
|
|
102
|
+
if (!/\w/.test(nextChar)) {
|
|
103
|
+
isWordBoundary = true;
|
|
104
|
+
matchLength = term.length + 2;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
88
107
|
|
|
89
108
|
if (isWordBoundary) {
|
|
90
109
|
matches.push({
|
|
91
110
|
index,
|
|
92
|
-
length:
|
|
111
|
+
length: matchLength,
|
|
93
112
|
term: term,
|
|
94
113
|
termObj: termObj,
|
|
95
114
|
// Store original case from the text
|
|
96
|
-
originalText: text.substring(index, index +
|
|
115
|
+
originalText: text.substring(index, index + matchLength),
|
|
97
116
|
});
|
|
98
117
|
}
|
|
99
118
|
|
|
@@ -120,7 +139,7 @@ function remarkGlossaryTerms({
|
|
|
120
139
|
if (match.index > lastIndex) {
|
|
121
140
|
result.push({
|
|
122
141
|
type: 'text',
|
|
123
|
-
value: text.substring(lastIndex, match.index)
|
|
142
|
+
value: text.substring(lastIndex, match.index),
|
|
124
143
|
});
|
|
125
144
|
}
|
|
126
145
|
|
|
@@ -132,25 +151,25 @@ function remarkGlossaryTerms({
|
|
|
132
151
|
{
|
|
133
152
|
type: 'mdxJsxAttribute',
|
|
134
153
|
name: 'term',
|
|
135
|
-
value: match.termObj.term
|
|
154
|
+
value: match.termObj.term,
|
|
136
155
|
},
|
|
137
156
|
{
|
|
138
157
|
type: 'mdxJsxAttribute',
|
|
139
158
|
name: 'definition',
|
|
140
|
-
value: match.termObj.definition || ''
|
|
159
|
+
value: match.termObj.definition || '',
|
|
141
160
|
},
|
|
142
161
|
{
|
|
143
162
|
type: 'mdxJsxAttribute',
|
|
144
163
|
name: 'routePath',
|
|
145
|
-
value: routePath
|
|
146
|
-
}
|
|
164
|
+
value: routePath,
|
|
165
|
+
},
|
|
147
166
|
],
|
|
148
167
|
children: [
|
|
149
168
|
{
|
|
150
169
|
type: 'text',
|
|
151
|
-
value: match.originalText
|
|
152
|
-
}
|
|
153
|
-
]
|
|
170
|
+
value: match.originalText,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
154
173
|
});
|
|
155
174
|
|
|
156
175
|
lastIndex = match.index + match.length;
|
|
@@ -160,14 +179,14 @@ function remarkGlossaryTerms({
|
|
|
160
179
|
if (lastIndex < text.length) {
|
|
161
180
|
result.push({
|
|
162
181
|
type: 'text',
|
|
163
|
-
value: text.substring(lastIndex)
|
|
182
|
+
value: text.substring(lastIndex),
|
|
164
183
|
});
|
|
165
184
|
}
|
|
166
185
|
|
|
167
186
|
return result.length > 0 ? result : [{ type: 'text', value: text }];
|
|
168
187
|
}
|
|
169
188
|
|
|
170
|
-
return
|
|
189
|
+
return tree => {
|
|
171
190
|
let usedGlossaryTerm = false;
|
|
172
191
|
visit(tree, 'text', (node, index, parent) => {
|
|
173
192
|
// Skip text nodes inside code blocks, links, or existing MDX components
|
|
@@ -185,8 +204,10 @@ function remarkGlossaryTerms({
|
|
|
185
204
|
const replacements = replaceTermsInText(node.value);
|
|
186
205
|
|
|
187
206
|
// If we have replacements, replace the single text node with multiple nodes
|
|
188
|
-
if (
|
|
189
|
-
|
|
207
|
+
if (
|
|
208
|
+
replacements.length > 1 ||
|
|
209
|
+
(replacements.length === 1 && replacements[0].type !== 'text')
|
|
210
|
+
) {
|
|
190
211
|
// Convert to text elements for paragraph context if needed
|
|
191
212
|
const newNodes = replacements.map(replacement => {
|
|
192
213
|
if (replacement.type === 'mdxJsxFlowElement') {
|
|
@@ -196,7 +217,7 @@ function remarkGlossaryTerms({
|
|
|
196
217
|
type: 'mdxJsxTextElement',
|
|
197
218
|
name: replacement.name,
|
|
198
219
|
attributes: replacement.attributes,
|
|
199
|
-
children: replacement.children
|
|
220
|
+
children: replacement.children,
|
|
200
221
|
};
|
|
201
222
|
}
|
|
202
223
|
}
|
|
@@ -217,9 +238,11 @@ function remarkGlossaryTerms({
|
|
|
217
238
|
|
|
218
239
|
// Inject MDX import for GlossaryTerm if we used it anywhere in this file
|
|
219
240
|
if (usedGlossaryTerm) {
|
|
241
|
+
// Create import node matching MDX v3 expectations
|
|
242
|
+
// Both 'value' (the import string) and 'data.estree' (the parsed AST) are required
|
|
220
243
|
const importNode = {
|
|
221
244
|
type: 'mdxjsEsm',
|
|
222
|
-
value: "import GlossaryTerm from '@theme/GlossaryTerm'
|
|
245
|
+
value: "import GlossaryTerm from '@theme/GlossaryTerm'",
|
|
223
246
|
data: {
|
|
224
247
|
estree: {
|
|
225
248
|
type: 'Program',
|
|
@@ -230,26 +253,52 @@ function remarkGlossaryTerms({
|
|
|
230
253
|
specifiers: [
|
|
231
254
|
{
|
|
232
255
|
type: 'ImportDefaultSpecifier',
|
|
233
|
-
local: { type: 'Identifier', name: 'GlossaryTerm' }
|
|
234
|
-
}
|
|
256
|
+
local: { type: 'Identifier', name: 'GlossaryTerm' },
|
|
257
|
+
},
|
|
235
258
|
],
|
|
236
|
-
source: {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
259
|
+
source: {
|
|
260
|
+
type: 'Literal',
|
|
261
|
+
value: '@theme/GlossaryTerm',
|
|
262
|
+
raw: "'@theme/GlossaryTerm'",
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
},
|
|
241
268
|
};
|
|
242
269
|
|
|
243
270
|
// Avoid duplicate imports if already present
|
|
244
|
-
const hasExistingImport =
|
|
245
|
-
(
|
|
246
|
-
|
|
271
|
+
const hasExistingImport =
|
|
272
|
+
Array.isArray(tree.children) &&
|
|
273
|
+
tree.children.some(n => {
|
|
274
|
+
if (n.type !== 'mdxjsEsm') return false;
|
|
275
|
+
// Check value string (for older MDX format)
|
|
276
|
+
if (typeof n.value === 'string' && n.value.includes('@theme/GlossaryTerm')) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
// Check estree data (for newer MDX format)
|
|
280
|
+
if (n.data?.estree?.body) {
|
|
281
|
+
return n.data.estree.body.some(
|
|
282
|
+
stmt =>
|
|
283
|
+
stmt.type === 'ImportDeclaration' && stmt.source?.value === '@theme/GlossaryTerm'
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return false;
|
|
287
|
+
});
|
|
288
|
+
|
|
247
289
|
if (!hasExistingImport) {
|
|
290
|
+
// Place import at the very beginning of the file (before all other nodes)
|
|
291
|
+
// This ensures it's available when MDX compiles the JSX elements
|
|
292
|
+
if (!Array.isArray(tree.children)) {
|
|
293
|
+
tree.children = [];
|
|
294
|
+
}
|
|
295
|
+
// Insert at the very beginning (index 0) to ensure it's processed first
|
|
248
296
|
tree.children.unshift(importNode);
|
|
297
|
+
// Debug: verify import was added (remove in production)
|
|
298
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
299
|
+
console.log('[glossary-plugin] Injected GlossaryTerm import');
|
|
300
|
+
}
|
|
249
301
|
}
|
|
250
302
|
}
|
|
251
303
|
};
|
|
252
304
|
}
|
|
253
|
-
|
|
254
|
-
module.exports = remarkGlossaryTerms;
|
|
255
|
-
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React, { useMemo, useState, useRef, useEffect, useCallback } from 'react';
|
|
2
|
+
import { usePluginData } from '@docusaurus/useGlobalData';
|
|
3
|
+
import styles from './styles.module.css';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GlossaryTerm component - displays an inline term with tooltip
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import GlossaryTerm from '@theme/GlossaryTerm';
|
|
10
|
+
*
|
|
11
|
+
* <GlossaryTerm term="API" definition="Application Programming Interface" />
|
|
12
|
+
* or
|
|
13
|
+
* <GlossaryTerm term="API">custom display text</GlossaryTerm>
|
|
14
|
+
*
|
|
15
|
+
* @param {object} props
|
|
16
|
+
* @param {string} props.term - The glossary term
|
|
17
|
+
* @param {string} props.definition - The definition to show in tooltip
|
|
18
|
+
* @param {string} props.routePath - Route path to glossary page (default: '/glossary')
|
|
19
|
+
* @param {React.ReactNode} props.children - Optional custom display text
|
|
20
|
+
*/
|
|
21
|
+
export default function GlossaryTerm({ term, definition, routePath = '/glossary', children }) {
|
|
22
|
+
const [showTooltip, setShowTooltip] = useState(false);
|
|
23
|
+
const [tooltipStyle, setTooltipStyle] = useState(null);
|
|
24
|
+
const [placement, setPlacement] = useState('top'); // 'top' | 'bottom'
|
|
25
|
+
const wrapperRef = useRef(null);
|
|
26
|
+
const tooltipRef = useRef(null);
|
|
27
|
+
|
|
28
|
+
const updatePosition = useCallback(() => {
|
|
29
|
+
if (!wrapperRef.current || !tooltipRef.current) return;
|
|
30
|
+
const wrapperRect = wrapperRef.current.getBoundingClientRect();
|
|
31
|
+
const tooltipRect = tooltipRef.current.getBoundingClientRect();
|
|
32
|
+
|
|
33
|
+
const viewportWidth = window.innerWidth;
|
|
34
|
+
const viewportHeight = window.innerHeight;
|
|
35
|
+
|
|
36
|
+
const preferredGap = 8; // px
|
|
37
|
+
|
|
38
|
+
// Decide top vs bottom based on available space
|
|
39
|
+
const hasSpaceAbove = wrapperRect.top >= tooltipRect.height + preferredGap;
|
|
40
|
+
const hasSpaceBelow = viewportHeight - wrapperRect.bottom >= tooltipRect.height + preferredGap;
|
|
41
|
+
const nextPlacement = hasSpaceAbove || !hasSpaceBelow ? 'top' : 'bottom';
|
|
42
|
+
|
|
43
|
+
let top;
|
|
44
|
+
if (nextPlacement === 'top') {
|
|
45
|
+
top = wrapperRect.top - tooltipRect.height - preferredGap;
|
|
46
|
+
} else {
|
|
47
|
+
top = wrapperRect.bottom + preferredGap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Center horizontally on the wrapper, then clamp within viewport with margin
|
|
51
|
+
const horizontalMargin = 8;
|
|
52
|
+
let left = wrapperRect.left + wrapperRect.width / 2 - tooltipRect.width / 2;
|
|
53
|
+
left = Math.max(
|
|
54
|
+
horizontalMargin,
|
|
55
|
+
Math.min(left, viewportWidth - tooltipRect.width - horizontalMargin)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
setPlacement(nextPlacement);
|
|
59
|
+
setTooltipStyle({ top: Math.max(4, top), left });
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!showTooltip) return;
|
|
64
|
+
updatePosition();
|
|
65
|
+
const onScroll = () => updatePosition();
|
|
66
|
+
const onResize = () => updatePosition();
|
|
67
|
+
window.addEventListener('scroll', onScroll, true);
|
|
68
|
+
window.addEventListener('resize', onResize);
|
|
69
|
+
return () => {
|
|
70
|
+
window.removeEventListener('scroll', onScroll, true);
|
|
71
|
+
window.removeEventListener('resize', onResize);
|
|
72
|
+
};
|
|
73
|
+
}, [showTooltip, updatePosition]);
|
|
74
|
+
|
|
75
|
+
// Pull definition/route from plugin global data if not provided
|
|
76
|
+
const pluginData = usePluginData('docusaurus-plugin-glossary');
|
|
77
|
+
const effectiveDefinition = useMemo(() => {
|
|
78
|
+
if (definition && typeof definition === 'string' && definition.length > 0) {
|
|
79
|
+
return definition;
|
|
80
|
+
}
|
|
81
|
+
const terms = (pluginData && pluginData.terms) || [];
|
|
82
|
+
const found = terms.find(
|
|
83
|
+
t => typeof t.term === 'string' && t.term.toLowerCase() === String(term).toLowerCase()
|
|
84
|
+
);
|
|
85
|
+
return found && found.definition ? found.definition : undefined;
|
|
86
|
+
}, [definition, pluginData, term]);
|
|
87
|
+
|
|
88
|
+
const effectiveRoutePath = useMemo(() => {
|
|
89
|
+
if (routePath && typeof routePath === 'string' && routePath.length > 0) return routePath;
|
|
90
|
+
return (pluginData && pluginData.routePath) || '/glossary';
|
|
91
|
+
}, [pluginData, routePath]);
|
|
92
|
+
|
|
93
|
+
const displayText = children || term;
|
|
94
|
+
const termId = term.toLowerCase().replace(/\s+/g, '-');
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<span ref={wrapperRef} className={styles.glossaryTermWrapper}>
|
|
98
|
+
<a
|
|
99
|
+
href={`${effectiveRoutePath}#${termId}`}
|
|
100
|
+
className={styles.glossaryTerm}
|
|
101
|
+
onMouseEnter={() => setShowTooltip(true)}
|
|
102
|
+
onMouseLeave={() => setShowTooltip(false)}
|
|
103
|
+
onFocus={() => setShowTooltip(true)}
|
|
104
|
+
onBlur={() => setShowTooltip(false)}
|
|
105
|
+
aria-describedby={`tooltip-${termId}`}
|
|
106
|
+
>
|
|
107
|
+
{displayText}
|
|
108
|
+
</a>
|
|
109
|
+
{effectiveDefinition && (
|
|
110
|
+
<span
|
|
111
|
+
ref={tooltipRef}
|
|
112
|
+
id={`tooltip-${termId}`}
|
|
113
|
+
className={
|
|
114
|
+
`${styles.tooltip} ${showTooltip ? styles.tooltipVisible : ''} ` +
|
|
115
|
+
`${placement === 'top' ? styles.tooltipTop : styles.tooltipBottom} ` +
|
|
116
|
+
`${styles.tooltipFloating}`
|
|
117
|
+
}
|
|
118
|
+
role="tooltip"
|
|
119
|
+
style={
|
|
120
|
+
showTooltip && tooltipStyle
|
|
121
|
+
? { top: `${tooltipStyle.top}px`, left: `${tooltipStyle.left}px` }
|
|
122
|
+
: undefined
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
<strong>{term}:</strong> {effectiveDefinition}
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
128
|
+
</span>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -113,4 +113,23 @@ describe('GlossaryTerm', () => {
|
|
|
113
113
|
const tooltip = screen.getByRole('tooltip');
|
|
114
114
|
expect(tooltip).toHaveAttribute('id', 'tooltip-api');
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
it('positions tooltip within viewport and adds placement classes', async () => {
|
|
118
|
+
const user = userEvent.setup();
|
|
119
|
+
render(<GlossaryTerm term="Edge" definition="Near the boundary of the viewport" />);
|
|
120
|
+
|
|
121
|
+
const link = screen.getByRole('link');
|
|
122
|
+
await user.hover(link);
|
|
123
|
+
|
|
124
|
+
const tooltip = screen.getByRole('tooltip');
|
|
125
|
+
expect(tooltip).toHaveClass('tooltipVisible');
|
|
126
|
+
expect(tooltip).toHaveClass('tooltipFloating');
|
|
127
|
+
// One of the placement classes should be present
|
|
128
|
+
const hasPlacement =
|
|
129
|
+
tooltip.classList.contains('tooltipTop') || tooltip.classList.contains('tooltipBottom');
|
|
130
|
+
expect(hasPlacement).toBe(true);
|
|
131
|
+
// Inline style should include computed top/left
|
|
132
|
+
expect(tooltip.style.top).toMatch(/px$/);
|
|
133
|
+
expect(tooltip.style.left).toMatch(/px$/);
|
|
134
|
+
});
|
|
116
135
|
});
|
|
@@ -4,17 +4,19 @@
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
.glossaryTerm {
|
|
7
|
-
color: var(--ifm-color-
|
|
7
|
+
color: var(--ifm-font-color-base);
|
|
8
8
|
text-decoration: none;
|
|
9
|
-
border-bottom: 1px dotted
|
|
9
|
+
border-bottom: 1px dotted;
|
|
10
|
+
border-bottom-color: color-mix(in srgb, var(--ifm-font-color-base) 20%, transparent);
|
|
10
11
|
cursor: help;
|
|
11
|
-
font-weight: 500;
|
|
12
12
|
transition: all 0.2s;
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
14
|
|
|
15
15
|
.glossaryTerm:hover {
|
|
16
16
|
color: var(--ifm-color-primary-dark);
|
|
17
17
|
border-bottom-style: solid;
|
|
18
|
+
border-bottom-color: var(--ifm-color-primary-dark);
|
|
19
|
+
border-bottom: none;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
.tooltip {
|
|
@@ -40,25 +42,55 @@
|
|
|
40
42
|
visibility: hidden;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
|
|
45
|
+
/* When we compute viewport-safe placement via JS */
|
|
46
|
+
.tooltipFloating {
|
|
47
|
+
position: fixed;
|
|
48
|
+
bottom: auto;
|
|
49
|
+
transform: none;
|
|
50
|
+
left: 0; /* overridden inline */
|
|
51
|
+
top: 0; /* overridden inline */
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Base arrow removed; arrows are defined per-placement to avoid inversion */
|
|
55
|
+
|
|
56
|
+
/* Arrow for top placement (tooltip above anchor) */
|
|
57
|
+
.tooltipTop::after {
|
|
44
58
|
content: '';
|
|
45
59
|
position: absolute;
|
|
46
|
-
top: 100%;
|
|
47
60
|
left: 50%;
|
|
48
61
|
transform: translateX(-50%);
|
|
62
|
+
bottom: -6px;
|
|
49
63
|
border: 6px solid transparent;
|
|
50
64
|
border-top-color: var(--ifm-background-surface-color);
|
|
51
65
|
}
|
|
52
|
-
|
|
53
|
-
.tooltip::before {
|
|
66
|
+
.tooltipTop::before {
|
|
54
67
|
content: '';
|
|
55
68
|
position: absolute;
|
|
56
|
-
top: 100%;
|
|
57
69
|
left: 50%;
|
|
58
70
|
transform: translateX(-50%);
|
|
71
|
+
bottom: -7px;
|
|
59
72
|
border: 7px solid transparent;
|
|
60
73
|
border-top-color: var(--ifm-color-emphasis-300);
|
|
61
|
-
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Arrow for bottom placement (tooltip below anchor) */
|
|
77
|
+
.tooltipBottom::after {
|
|
78
|
+
content: '';
|
|
79
|
+
position: absolute;
|
|
80
|
+
left: 50%;
|
|
81
|
+
transform: translateX(-50%);
|
|
82
|
+
top: -6px;
|
|
83
|
+
border: 6px solid transparent;
|
|
84
|
+
border-bottom-color: var(--ifm-background-surface-color);
|
|
85
|
+
}
|
|
86
|
+
.tooltipBottom::before {
|
|
87
|
+
content: '';
|
|
88
|
+
position: absolute;
|
|
89
|
+
left: 50%;
|
|
90
|
+
transform: translateX(-50%);
|
|
91
|
+
top: -7px;
|
|
92
|
+
border: 7px solid transparent;
|
|
93
|
+
border-bottom-color: var(--ifm-color-emphasis-300);
|
|
62
94
|
}
|
|
63
95
|
|
|
64
96
|
.tooltipVisible {
|
|
@@ -79,13 +111,19 @@
|
|
|
79
111
|
border-color: var(--ifm-color-emphasis-400);
|
|
80
112
|
}
|
|
81
113
|
|
|
82
|
-
|
|
114
|
+
/* Dark mode arrow overrides scoped to placement to avoid upside-down arrows */
|
|
115
|
+
[data-theme='dark'] .tooltipTop::after {
|
|
83
116
|
border-top-color: var(--ifm-background-surface-color);
|
|
84
117
|
}
|
|
85
|
-
|
|
86
|
-
[data-theme='dark'] .tooltip::before {
|
|
118
|
+
[data-theme='dark'] .tooltipTop::before {
|
|
87
119
|
border-top-color: var(--ifm-color-emphasis-400);
|
|
88
120
|
}
|
|
121
|
+
[data-theme='dark'] .tooltipBottom::after {
|
|
122
|
+
border-bottom-color: var(--ifm-background-surface-color);
|
|
123
|
+
}
|
|
124
|
+
[data-theme='dark'] .tooltipBottom::before {
|
|
125
|
+
border-bottom-color: var(--ifm-color-emphasis-400);
|
|
126
|
+
}
|
|
89
127
|
|
|
90
128
|
/* Responsive adjustments */
|
|
91
129
|
@media (max-width: 768px) {
|
package/package.json
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docusaurus-plugin-glossary",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "A Docusaurus plugin for creating and managing glossary terms with auto-generated pages and inline tooltips",
|
|
5
|
-
"
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./remark/glossary-terms": "./lib/remark/glossary-terms.js"
|
|
12
|
+
},
|
|
6
13
|
"files": [
|
|
7
|
-
"
|
|
8
|
-
"components/",
|
|
9
|
-
"theme/",
|
|
10
|
-
"remark/",
|
|
14
|
+
"lib/",
|
|
11
15
|
"README.md",
|
|
12
16
|
"LICENSE"
|
|
13
17
|
],
|
|
14
18
|
"scripts": {
|
|
19
|
+
"build": "tsc && node scripts/build.js",
|
|
20
|
+
"watch": "node scripts/watch.js",
|
|
15
21
|
"test": "jest",
|
|
16
22
|
"test:watch": "jest --watch",
|
|
17
23
|
"test:coverage": "jest --coverage",
|
|
18
24
|
"example:start": "npm --prefix examples/docusaurus-v3 run start",
|
|
19
25
|
"example:build": "npm --prefix examples/docusaurus-v3 run build",
|
|
20
26
|
"example:serve": "npm --prefix examples/docusaurus-v3 run serve",
|
|
21
|
-
"
|
|
27
|
+
"example:clear": "npm --prefix examples/docusaurus-v3 run clear",
|
|
28
|
+
"prepublishOnly": "npm run build && npm test",
|
|
22
29
|
"version": "npm version",
|
|
23
30
|
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,md}\"",
|
|
24
31
|
"format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,json,css,md}\""
|
|
@@ -49,7 +56,8 @@
|
|
|
49
56
|
},
|
|
50
57
|
"dependencies": {
|
|
51
58
|
"fs-extra": "^11.0.0",
|
|
52
|
-
"unist-util-visit": "^5.0.0"
|
|
59
|
+
"unist-util-visit": "^5.0.0",
|
|
60
|
+
"validate-peer-dependencies": "^2.2.0"
|
|
53
61
|
},
|
|
54
62
|
"engines": {
|
|
55
63
|
"node": ">=16.14"
|
|
@@ -57,6 +65,8 @@
|
|
|
57
65
|
"devDependencies": {
|
|
58
66
|
"@babel/preset-env": "^7.28.5",
|
|
59
67
|
"@babel/preset-react": "^7.28.5",
|
|
68
|
+
"@docusaurus/tsconfig": "^3.9.2",
|
|
69
|
+
"@docusaurus/types": "^3.9.2",
|
|
60
70
|
"@testing-library/jest-dom": "^6.9.1",
|
|
61
71
|
"@testing-library/react": "^16.3.0",
|
|
62
72
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -64,6 +74,19 @@
|
|
|
64
74
|
"identity-obj-proxy": "^3.0.0",
|
|
65
75
|
"jest": "^30.2.0",
|
|
66
76
|
"jest-environment-jsdom": "^30.2.0",
|
|
67
|
-
"prettier": "^3.6.2"
|
|
77
|
+
"prettier": "^3.6.2",
|
|
78
|
+
"typescript": "^5.7.3"
|
|
79
|
+
},
|
|
80
|
+
"browser": {
|
|
81
|
+
"path": false,
|
|
82
|
+
"url": false,
|
|
83
|
+
"fs": false,
|
|
84
|
+
"fs-extra": false,
|
|
85
|
+
"graceful-fs": false,
|
|
86
|
+
"jsonfile": false,
|
|
87
|
+
"util": false,
|
|
88
|
+
"assert": false,
|
|
89
|
+
"stream": false,
|
|
90
|
+
"constants": false
|
|
68
91
|
}
|
|
69
92
|
}
|