Skip to content

Commit 2eaa476

Browse files
authored
Merge pull request #2078 from futurGH/configurable-links
Allow user-configurable links in header & sidebar
2 parents 2e8af77 + c0b4420 commit 2eaa476

File tree

8 files changed

+143
-5
lines changed

8 files changed

+143
-5
lines changed

example/typedoc.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,12 @@
1010
},
1111
"searchGroupBoosts": {
1212
"Classes": 1.5
13+
},
14+
"navigationLinks": {
15+
"Docs": "https://typedoc.org/guides/overview",
16+
"GitHub": "https://github.com/TypeStrong/typedoc"
17+
},
18+
"sidebarLinks": {
19+
"API": "https://typedoc.org/api"
1320
}
1421
}

src/lib/output/themes/default/DefaultThemeRenderContext.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
primaryNavigation,
3232
secondaryNavigation,
3333
settings,
34+
sidebarLinks,
3435
} from "./partials/navigation";
3536
import { parameter } from "./partials/parameter";
3637
import { toolbar } from "./partials/toolbar";
@@ -105,6 +106,7 @@ export class DefaultThemeRenderContext {
105106
members = bind(members, this);
106107
membersGroup = bind(membersGroup, this);
107108
navigation = bind(navigation, this);
109+
sidebarLinks = bind(sidebarLinks, this);
108110
settings = bind(settings, this);
109111
primaryNavigation = bind(primaryNavigation, this);
110112
secondaryNavigation = bind(secondaryNavigation, this);

src/lib/output/themes/default/partials/navigation.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { DefaultThemeRenderContext } from "../DefaultThemeRenderContext";
77
export function navigation(context: DefaultThemeRenderContext, props: PageEvent<Reflection>) {
88
return (
99
<>
10+
{context.sidebarLinks()}
1011
{context.settings()}
1112
{context.primaryNavigation(props)}
1213
{context.secondaryNavigation(props)}
@@ -26,6 +27,20 @@ function buildFilterItem(context: DefaultThemeRenderContext, name: string, displ
2627
);
2728
}
2829

30+
export function sidebarLinks(context: DefaultThemeRenderContext) {
31+
const links = Object.entries(context.options.getValue("sidebarLinks"));
32+
if (!links.length) return null;
33+
return (
34+
<nav id="tsd-sidebar-links" class="tsd-navigation">
35+
{links.map(([label, url]) => (
36+
<a href={url} target="_blank">
37+
{label}
38+
</a>
39+
))}
40+
</nav>
41+
);
42+
}
43+
2944
export function settings(context: DefaultThemeRenderContext) {
3045
const defaultFilters = context.options.getValue("visibilityFilters") as Record<string, boolean>;
3146

src/lib/output/themes/default/partials/toolbar.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,32 @@ export const toolbar = (context: DefaultThemeRenderContext, props: PageEvent<Ref
88
<div class="tsd-toolbar-contents container">
99
<div class="table-cell" id="tsd-search" data-base={context.relativeURL("./")}>
1010
<div class="field">
11-
<label for="tsd-search-field" class="tsd-widget search no-caption">
11+
<label for="tsd-search-field" class="tsd-widget tsd-toolbar-icon search no-caption">
1212
{context.icons.search()}
1313
</label>
1414
<input type="text" id="tsd-search-field" aria-label="Search" />
1515
</div>
1616

17+
<div class="field">
18+
<div id="tsd-toolbar-links">
19+
{Object.entries(context.options.getValue("navigationLinks")).map(([label, url]) => (
20+
<a href={url}>{label}</a>
21+
))}
22+
</div>
23+
</div>
24+
1725
<ul class="results">
1826
<li class="state loading">Preparing search index...</li>
1927
<li class="state failure">The search index is not available</li>
2028
</ul>
2129

22-
<a href={context.relativeURL("index.html")} class="title">
30+
<a href={context.options.getValue("titleLink") ?? context.relativeURL("index.html")} class="title">
2331
{props.project.name}
2432
</a>
2533
</div>
2634

2735
<div class="table-cell" id="tsd-widgets">
28-
<a href="#" class="tsd-widget menu no-caption" data-toggle="menu" aria-label="Menu">
36+
<a href="#" class="tsd-widget tsd-toolbar-icon menu no-caption" data-toggle="menu" aria-label="Menu">
2937
{context.icons.menu()}
3038
</a>
3139
</div>

src/lib/utils/options/declaration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export interface TypeDocOptionMap {
119119
hideGenerator: boolean;
120120
searchInComments: boolean;
121121
cleanOutputDir: boolean;
122+
titleLink: string;
123+
navigationLinks: ManuallyValidatedOption<Record<string, string>>;
124+
sidebarLinks: ManuallyValidatedOption<Record<string, string>>;
122125

123126
commentStyle: typeof CommentStyle;
124127
blockTags: `@${string}`[];

src/lib/utils/options/sources/typedoc.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,49 @@ export function addTypeDocOptions(options: Pick<Options, "addDeclaration">) {
307307
type: ParameterType.Boolean,
308308
defaultValue: true,
309309
});
310+
options.addDeclaration({
311+
name: "titleLink",
312+
help: "Set the link the title in the header points to. Defaults to the documentation homepage.",
313+
type: ParameterType.String,
314+
});
315+
options.addDeclaration({
316+
name: "navigationLinks",
317+
help: "Defines links to be included in the header.",
318+
type: ParameterType.Mixed,
319+
defaultValue: {},
320+
validate(value) {
321+
if (!isObject(value)) {
322+
throw new Error(
323+
`navigationLinks must be an object with string labels as keys and URL values.`
324+
);
325+
}
326+
327+
if (Object.values(value).some((x) => typeof x !== "string")) {
328+
throw new Error(
329+
`All values of navigationLinks must be string URLs.`
330+
);
331+
}
332+
},
333+
});
334+
options.addDeclaration({
335+
name: "sidebarLinks",
336+
help: "Defines links to be included in the sidebar.",
337+
type: ParameterType.Mixed,
338+
defaultValue: {},
339+
validate(value) {
340+
if (!isObject(value)) {
341+
throw new Error(
342+
`sidebarLinks must be an object with string labels as keys and URL values.`
343+
);
344+
}
345+
346+
if (Object.values(value).some((x) => typeof x !== "string")) {
347+
throw new Error(
348+
`All values of sidebarLinks must be string URLs.`
349+
);
350+
}
351+
},
352+
});
310353

311354
///////////////////////////
312355
///// Comment Options /////

src/test/utils/options/default-options.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,32 @@ describe("Default Options", () => {
108108
doesNotThrow(() => opts.setValue("searchGroupBoosts", { Enum: 5 }));
109109
});
110110
});
111+
112+
describe("headerLinks", () => {
113+
it("Should disallow non-objects", () => {
114+
throws(() => opts.setValue("navigationLinks", null as never));
115+
});
116+
117+
it("Should disallow non-strings", () => {
118+
throws(() =>
119+
opts.setValue("navigationLinks", {
120+
Home: true as any as string,
121+
})
122+
);
123+
});
124+
});
125+
126+
describe("sidebarLinks", () => {
127+
it("Should disallow non-objects", () => {
128+
throws(() => opts.setValue("sidebarLinks", null as never));
129+
});
130+
131+
it("Should disallow non-strings", () => {
132+
throws(() =>
133+
opts.setValue("sidebarLinks", {
134+
Home: true as any as string,
135+
})
136+
);
137+
});
138+
});
111139
});

static/style.css

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -825,6 +825,15 @@ input[type="checkbox"]:checked ~ svg .tsd-checkbox-checkmark {
825825
padding-left: 5.5rem;
826826
}
827827

828+
#tsd-sidebar-links a {
829+
margin-top: 0;
830+
margin-bottom: 0.5rem;
831+
line-height: 1.25rem;
832+
}
833+
#tsd-sidebar-links a:last-of-type {
834+
margin-bottom: 0;
835+
}
836+
828837
a.tsd-index-link {
829838
margin: 0.25rem 0;
830839
font-size: 1rem;
@@ -978,7 +987,8 @@ a.tsd-index-link {
978987
right: -40px;
979988
}
980989
#tsd-search .field input,
981-
#tsd-search .title {
990+
#tsd-search .title,
991+
#tsd-toolbar-links a {
982992
transition: opacity 0.2s;
983993
}
984994
#tsd-search .results {
@@ -1022,7 +1032,8 @@ a.tsd-index-link {
10221032
top: 0;
10231033
opacity: 1;
10241034
}
1025-
#tsd-search.has-focus .title {
1035+
#tsd-search.has-focus .title,
1036+
#tsd-search.has-focus #tsd-toolbar-links a {
10261037
z-index: 0;
10271038
opacity: 0;
10281039
}
@@ -1036,6 +1047,22 @@ a.tsd-index-link {
10361047
display: block;
10371048
}
10381049

1050+
#tsd-toolbar-links {
1051+
position: absolute;
1052+
top: 0;
1053+
right: 2rem;
1054+
height: 100%;
1055+
display: flex;
1056+
align-items: center;
1057+
justify-content: flex-end;
1058+
}
1059+
#tsd-toolbar-links a {
1060+
margin-left: 1.5rem;
1061+
}
1062+
#tsd-toolbar-links a:hover {
1063+
text-decoration: underline;
1064+
}
1065+
10391066
.tsd-signature {
10401067
margin: 0 0 1rem 0;
10411068
padding: 1rem 0.5rem;
@@ -1134,6 +1161,11 @@ ul.tsd-type-parameter-list h5 {
11341161
.tsd-page-toolbar .table-cell:first-child {
11351162
width: 100%;
11361163
}
1164+
.tsd-page-toolbar .tsd-toolbar-icon {
1165+
box-sizing: border-box;
1166+
line-height: 0;
1167+
padding: 12px 0;
1168+
}
11371169

11381170
.tsd-page-toolbar--hide {
11391171
transform: translateY(-100%);

0 commit comments

Comments
 (0)