@beatzball/create-litro 0.3.0 → 0.4.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/CHANGELOG.md +19 -0
- package/LICENSE +78 -62
- package/dist/recipes/starlight/template/package.json +1 -0
- package/dist/recipes/starlight/template/pages/docs/[slug].ts +75 -2
- package/dist/recipes/starlight/template/public/styles/highlight.css +108 -0
- package/dist/recipes/starlight/template/src/components/starlight-header.ts +125 -29
- package/dist/recipes/starlight/template/src/components/starlight-page.ts +48 -4
- package/dist/recipes/starlight/template/src/highlight.ts +40 -0
- package/dist/recipes/starlight/template/src/route-meta.ts +1 -0
- package/package.json +16 -2
- package/recipes/starlight/template/package.json +1 -0
- package/recipes/starlight/template/pages/docs/[slug].ts +75 -2
- package/recipes/starlight/template/public/styles/highlight.css +108 -0
- package/recipes/starlight/template/src/components/starlight-header.ts +125 -29
- package/recipes/starlight/template/src/components/starlight-page.ts +48 -4
- package/recipes/starlight/template/src/highlight.ts +40 -0
- package/recipes/starlight/template/src/route-meta.ts +1 -0
|
@@ -36,6 +36,7 @@ export class StarlightPage extends LitElement {
|
|
|
36
36
|
currentSlug: { type: String },
|
|
37
37
|
currentPath: { type: String },
|
|
38
38
|
noSidebar: { type: Boolean },
|
|
39
|
+
_navOpen: { state: true },
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
static override styles = css`
|
|
@@ -102,6 +103,14 @@ export class StarlightPage extends LitElement {
|
|
|
102
103
|
line-height: 1.15;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
.nav-backdrop {
|
|
107
|
+
position: fixed;
|
|
108
|
+
inset: 0;
|
|
109
|
+
top: var(--sl-nav-height, 3.5rem);
|
|
110
|
+
background: rgba(0, 0, 0, 0.4);
|
|
111
|
+
z-index: 49;
|
|
112
|
+
}
|
|
113
|
+
|
|
105
114
|
/* Responsive: hide sidebar and TOC on narrow screens */
|
|
106
115
|
@media (max-width: 72rem) {
|
|
107
116
|
.body {
|
|
@@ -110,7 +119,20 @@ export class StarlightPage extends LitElement {
|
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
.sidebar-wrap {
|
|
113
|
-
|
|
122
|
+
grid-area: unset;
|
|
123
|
+
position: fixed;
|
|
124
|
+
top: var(--sl-nav-height, 3.5rem);
|
|
125
|
+
left: 0;
|
|
126
|
+
z-index: 50;
|
|
127
|
+
height: calc(100vh - var(--sl-nav-height, 3.5rem));
|
|
128
|
+
width: var(--sl-sidebar-width, 16rem);
|
|
129
|
+
transform: translateX(-100%);
|
|
130
|
+
transition: transform 0.2s ease;
|
|
131
|
+
box-shadow: 2px 0 16px rgba(0, 0, 0, 0.12);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.sidebar-wrap.nav-open {
|
|
135
|
+
transform: translateX(0);
|
|
114
136
|
}
|
|
115
137
|
}
|
|
116
138
|
|
|
@@ -134,18 +156,40 @@ export class StarlightPage extends LitElement {
|
|
|
134
156
|
currentSlug = '';
|
|
135
157
|
currentPath = '';
|
|
136
158
|
noSidebar = false;
|
|
159
|
+
_navOpen = false;
|
|
160
|
+
|
|
161
|
+
override updated(changed: Map<string, unknown>) {
|
|
162
|
+
if (changed.has('currentPath') && this._navOpen) {
|
|
163
|
+
this._navOpen = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private _handleNavToggle() {
|
|
168
|
+
this._navOpen = !this._navOpen;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private _closeNav() {
|
|
172
|
+
this._navOpen = false;
|
|
173
|
+
}
|
|
137
174
|
|
|
138
175
|
override render() {
|
|
176
|
+
const hasSidebar = !this.noSidebar;
|
|
139
177
|
return html`
|
|
140
178
|
<div class="page-wrap">
|
|
141
179
|
<starlight-header
|
|
142
180
|
siteTitle="${this.siteTitle}"
|
|
143
181
|
.nav="${this.nav}"
|
|
144
182
|
currentPath="${this.currentPath}"
|
|
183
|
+
.navOpen="${this._navOpen}"
|
|
184
|
+
.hasSidebar="${hasSidebar}"
|
|
185
|
+
@sl-nav-toggle="${this._handleNavToggle}"
|
|
145
186
|
></starlight-header>
|
|
187
|
+
${hasSidebar && this._navOpen ? html`
|
|
188
|
+
<div class="nav-backdrop" @click="${this._closeNav}"></div>
|
|
189
|
+
` : ''}
|
|
146
190
|
<div class="body${this.noSidebar ? ' no-sidebar' : ''}">
|
|
147
|
-
${
|
|
148
|
-
<aside class="sidebar-wrap">
|
|
191
|
+
${hasSidebar ? html`
|
|
192
|
+
<aside class="sidebar-wrap${this._navOpen ? ' nav-open' : ''}">
|
|
149
193
|
<starlight-sidebar
|
|
150
194
|
.groups="${this.sidebar}"
|
|
151
195
|
currentSlug="${this.currentSlug}"
|
|
@@ -158,7 +202,7 @@ export class StarlightPage extends LitElement {
|
|
|
158
202
|
<slot name="content"></slot>
|
|
159
203
|
</div>
|
|
160
204
|
</main>
|
|
161
|
-
${
|
|
205
|
+
${hasSidebar ? html`
|
|
162
206
|
<aside class="toc-wrap">
|
|
163
207
|
<starlight-toc .entries="${this.toc}"></starlight-toc>
|
|
164
208
|
</aside>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import hljs from 'highlight.js';
|
|
2
|
+
|
|
3
|
+
const NAMED_ENTITY_MAP: Record<string, string> = {
|
|
4
|
+
'&': '&',
|
|
5
|
+
'<': '<',
|
|
6
|
+
'>': '>',
|
|
7
|
+
'"': '"',
|
|
8
|
+
''': "'",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function decodeEntities(str: string): string {
|
|
12
|
+
return str.replace(/&#x[0-9a-fA-F]+;|&#[0-9]+;|&|<|>|"|'/g, m => {
|
|
13
|
+
if (m.startsWith('&#x')) return String.fromCodePoint(parseInt(m.slice(3, -1), 16));
|
|
14
|
+
if (m.startsWith('&#')) return String.fromCodePoint(parseInt(m.slice(2, -1), 10));
|
|
15
|
+
return NAMED_ENTITY_MAP[m]!;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Post-processes an HTML string, replacing every
|
|
21
|
+
* <pre><code class="language-*">…</code></pre>
|
|
22
|
+
* with a syntax-highlighted version produced by highlight.js.
|
|
23
|
+
*
|
|
24
|
+
* Must be called server-side only (SSG build time).
|
|
25
|
+
*/
|
|
26
|
+
export function applyHighlighting(html: string): string {
|
|
27
|
+
return html.replace(
|
|
28
|
+
/<pre><code class="language-([^"]+)">([\s\S]*?)<\/code><\/pre>/g,
|
|
29
|
+
(_match, lang: string, encoded: string) => {
|
|
30
|
+
const code = decodeEntities(encoded);
|
|
31
|
+
let highlighted: string;
|
|
32
|
+
try {
|
|
33
|
+
highlighted = hljs.highlight(code, { language: lang, ignoreIllegals: true }).value;
|
|
34
|
+
} catch {
|
|
35
|
+
highlighted = hljs.highlightAuto(code).value;
|
|
36
|
+
}
|
|
37
|
+
return `<pre><code class="hljs language-${lang}">${highlighted}</code></pre>`;
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
export const starlightHead = [
|
|
11
11
|
'<link rel="stylesheet" href="/shoelace/themes/light.css" />',
|
|
12
12
|
'<link rel="stylesheet" href="/styles/starlight.css" />',
|
|
13
|
+
'<link rel="stylesheet" href="/styles/highlight.css" />',
|
|
13
14
|
'<script>(function(){',
|
|
14
15
|
'var s=localStorage.getItem("sl-theme");',
|
|
15
16
|
'var t=s||(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");',
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beatzball/create-litro",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Scaffold a new Litro app — fullstack SSR, Markdown blog, or docs site. Built on Lit web components and Nitro server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -15,6 +15,20 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"create-litro": "./dist/src/index.js"
|
|
17
17
|
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"create",
|
|
20
|
+
"scaffold",
|
|
21
|
+
"cli",
|
|
22
|
+
"lit",
|
|
23
|
+
"web-components",
|
|
24
|
+
"fullstack",
|
|
25
|
+
"ssr",
|
|
26
|
+
"ssg",
|
|
27
|
+
"nitro",
|
|
28
|
+
"starter",
|
|
29
|
+
"template",
|
|
30
|
+
"typescript"
|
|
31
|
+
],
|
|
18
32
|
"devDependencies": {
|
|
19
33
|
"@types/node": "^22.0.0",
|
|
20
34
|
"typescript": "^5.7.3",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { html } from 'lit';
|
|
1
|
+
import { html, css } from 'lit';
|
|
2
2
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
3
3
|
import { customElement } from 'lit/decorators.js';
|
|
4
4
|
import { LitroPage } from '@beatzball/litro/runtime';
|
|
@@ -8,6 +8,7 @@ import type { Post } from 'litro:content';
|
|
|
8
8
|
import { getPosts } from 'litro:content';
|
|
9
9
|
import { siteConfig } from '../../server/starlight.config.js';
|
|
10
10
|
import { extractHeadings, addHeadingIds } from '../../src/extract-headings.js';
|
|
11
|
+
import { applyHighlighting } from '../../src/highlight.js';
|
|
11
12
|
import { starlightHead } from '../../src/route-meta.js';
|
|
12
13
|
|
|
13
14
|
// Register components used in render()
|
|
@@ -55,7 +56,7 @@ export const pageData = definePageData(async (event) => {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
const toc = extractHeadings(doc.rawBody);
|
|
58
|
-
const body = addHeadingIds(doc.body);
|
|
59
|
+
const body = applyHighlighting(addHeadingIds(doc.body));
|
|
59
60
|
const { prevDoc, nextDoc } = computePrevNext(siteConfig.sidebar, slug);
|
|
60
61
|
const editUrl = siteConfig.editUrlBase
|
|
61
62
|
? `${siteConfig.editUrlBase}/content/docs/${slug}.md`
|
|
@@ -89,6 +90,78 @@ export const routeMeta = {
|
|
|
89
90
|
|
|
90
91
|
@customElement('page-docs-slug')
|
|
91
92
|
export class DocPage extends LitroPage {
|
|
93
|
+
/**
|
|
94
|
+
* Styles injected into page-docs-slug's shadow root so they reach the
|
|
95
|
+
* <div slot="content"> subtree. Global stylesheets (starlight.css,
|
|
96
|
+
* highlight.css) cannot pierce shadow DOM boundaries.
|
|
97
|
+
*/
|
|
98
|
+
static override styles = css`
|
|
99
|
+
/* ── Typography for slotted doc content ─────────────────────────── */
|
|
100
|
+
h1, h2, h3, h4, h5, h6 {
|
|
101
|
+
margin-top: 1.5em; margin-bottom: 0.5em;
|
|
102
|
+
font-weight: 600; line-height: 1.25;
|
|
103
|
+
color: var(--sl-color-text);
|
|
104
|
+
}
|
|
105
|
+
h1 { font-size: var(--sl-text-4xl, 2.25rem); }
|
|
106
|
+
h2 { font-size: var(--sl-text-2xl, 1.5rem); border-bottom: 1px solid var(--sl-color-border, #e8e8e8); padding-bottom: 0.25em; }
|
|
107
|
+
h3 { font-size: var(--sl-text-xl, 1.25rem); }
|
|
108
|
+
h4 { font-size: var(--sl-text-lg, 1.125rem); }
|
|
109
|
+
p { margin-top: 0; margin-bottom: 1rem; line-height: 1.7; }
|
|
110
|
+
a { color: var(--sl-color-text-accent, var(--sl-color-accent)); text-decoration: none; }
|
|
111
|
+
a:hover { text-decoration: underline; }
|
|
112
|
+
code {
|
|
113
|
+
font-family: var(--sl-font-mono, ui-monospace, monospace);
|
|
114
|
+
font-size: 0.875em;
|
|
115
|
+
background-color: var(--sl-color-bg-inline-code, #e8e8e8);
|
|
116
|
+
border: 1px solid var(--sl-color-border, #e8e8e8);
|
|
117
|
+
border-radius: 0.25rem;
|
|
118
|
+
padding: 0.15em 0.4em;
|
|
119
|
+
}
|
|
120
|
+
pre {
|
|
121
|
+
background-color: #0d0e11;
|
|
122
|
+
color: #e2e4e9;
|
|
123
|
+
border-radius: 0.375rem;
|
|
124
|
+
padding: 1rem 1.25rem;
|
|
125
|
+
overflow-x: auto;
|
|
126
|
+
margin: 1.5rem 0;
|
|
127
|
+
font-size: var(--sl-text-sm, 0.875rem);
|
|
128
|
+
line-height: 1.6;
|
|
129
|
+
}
|
|
130
|
+
pre code { background: none; border: none; padding: 0; font-size: inherit; }
|
|
131
|
+
ul, ol { padding-left: 1.5rem; margin: 0 0 1rem; }
|
|
132
|
+
li { margin-bottom: 0.25rem; line-height: 1.7; }
|
|
133
|
+
blockquote {
|
|
134
|
+
margin: 1.5rem 0; padding: 0.75rem 1rem;
|
|
135
|
+
border-left: 4px solid var(--sl-color-accent, #ea580c);
|
|
136
|
+
background-color: var(--sl-color-accent-low, #fff7ed);
|
|
137
|
+
border-radius: 0 0.375rem 0.375rem 0;
|
|
138
|
+
}
|
|
139
|
+
hr { border: none; border-top: 1px solid var(--sl-color-border, #e8e8e8); margin: 2rem 0; }
|
|
140
|
+
img { max-width: 100%; height: auto; }
|
|
141
|
+
table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; font-size: var(--sl-text-sm, 0.875rem); }
|
|
142
|
+
th, td { border: 1px solid var(--sl-color-border, #e8e8e8); padding: 0.5rem 0.75rem; text-align: left; }
|
|
143
|
+
th { background-color: var(--sl-color-gray-1, #f6f6f6); font-weight: 600; }
|
|
144
|
+
|
|
145
|
+
/* ── highlight.js fire theme ─────────────────────────────────────── */
|
|
146
|
+
pre:has(.hljs) { background-color: #0d0d10; color: #cbd5e1; }
|
|
147
|
+
.hljs { color: #cbd5e1; background: transparent; }
|
|
148
|
+
.hljs-keyword, .hljs-selector-tag, .hljs-tag { color: #f97316; }
|
|
149
|
+
.hljs-string, .hljs-attr, .hljs-attribute { color: #38bdf8; }
|
|
150
|
+
.hljs-number, .hljs-literal { color: #fbbf24; }
|
|
151
|
+
.hljs-title, .hljs-title.class_, .hljs-title.function_, .hljs-built_in { color: #fb923c; }
|
|
152
|
+
.hljs-comment { color: #6b7280; font-style: italic; }
|
|
153
|
+
.hljs-variable, .hljs-params { color: #cbd5e1; }
|
|
154
|
+
.hljs-operator, .hljs-punctuation { color: #94a3b8; }
|
|
155
|
+
.hljs-meta, .hljs-meta .hljs-keyword { color: #38bdf8; }
|
|
156
|
+
.hljs-type { color: #fb923c; }
|
|
157
|
+
.hljs-deletion { color: #f87171; background: rgba(248,113,113,.1); }
|
|
158
|
+
.hljs-addition { color: #4ade80; background: rgba(74,222,128,.1); }
|
|
159
|
+
.hljs-section, .hljs-selector-class, .hljs-selector-id { color: #fb923c; }
|
|
160
|
+
.hljs-symbol, .hljs-bullet, .hljs-link { color: #38bdf8; }
|
|
161
|
+
.hljs-emphasis { font-style: italic; }
|
|
162
|
+
.hljs-strong { font-weight: bold; }
|
|
163
|
+
`;
|
|
164
|
+
|
|
92
165
|
override render() {
|
|
93
166
|
const data = this.serverData as DocPageData | null;
|
|
94
167
|
if (!data?.doc) return html`<p>Loading…</p>`;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/* highlight.js — fire theme (always dark background) */
|
|
2
|
+
|
|
3
|
+
pre:has(.hljs) {
|
|
4
|
+
background-color: #0d0d10;
|
|
5
|
+
color: #cbd5e1;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
[data-theme="dark"] pre:has(.hljs) {
|
|
9
|
+
background-color: #0d0d10;
|
|
10
|
+
color: #cbd5e1;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.hljs {
|
|
14
|
+
color: #cbd5e1;
|
|
15
|
+
background: transparent;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Keywords: const, let, if, class, return … */
|
|
19
|
+
.hljs-keyword,
|
|
20
|
+
.hljs-selector-tag,
|
|
21
|
+
.hljs-tag {
|
|
22
|
+
color: #f97316;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* Strings and attribute values */
|
|
26
|
+
.hljs-string,
|
|
27
|
+
.hljs-attr,
|
|
28
|
+
.hljs-attribute {
|
|
29
|
+
color: #38bdf8;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Numbers and boolean literals */
|
|
33
|
+
.hljs-number,
|
|
34
|
+
.hljs-literal {
|
|
35
|
+
color: #fbbf24;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Function/class names, built-ins */
|
|
39
|
+
.hljs-title,
|
|
40
|
+
.hljs-title.class_,
|
|
41
|
+
.hljs-title.function_,
|
|
42
|
+
.hljs-built_in {
|
|
43
|
+
color: #fb923c;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Comments */
|
|
47
|
+
.hljs-comment {
|
|
48
|
+
color: #6b7280;
|
|
49
|
+
font-style: italic;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Variables and parameters */
|
|
53
|
+
.hljs-variable,
|
|
54
|
+
.hljs-params {
|
|
55
|
+
color: #cbd5e1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Operators and punctuation */
|
|
59
|
+
.hljs-operator,
|
|
60
|
+
.hljs-punctuation {
|
|
61
|
+
color: #94a3b8;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Meta / imports / decorators */
|
|
65
|
+
.hljs-meta,
|
|
66
|
+
.hljs-meta .hljs-keyword {
|
|
67
|
+
color: #38bdf8;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Type annotations */
|
|
71
|
+
.hljs-type {
|
|
72
|
+
color: #fb923c;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Diff deletions */
|
|
76
|
+
.hljs-deletion {
|
|
77
|
+
color: #f87171;
|
|
78
|
+
background: rgba(248, 113, 113, 0.1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Diff additions */
|
|
82
|
+
.hljs-addition {
|
|
83
|
+
color: #4ade80;
|
|
84
|
+
background: rgba(74, 222, 128, 0.1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Section / selector */
|
|
88
|
+
.hljs-section,
|
|
89
|
+
.hljs-selector-class,
|
|
90
|
+
.hljs-selector-id {
|
|
91
|
+
color: #fb923c;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* Symbol / bullet */
|
|
95
|
+
.hljs-symbol,
|
|
96
|
+
.hljs-bullet,
|
|
97
|
+
.hljs-link {
|
|
98
|
+
color: #38bdf8;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Emphasis */
|
|
102
|
+
.hljs-emphasis {
|
|
103
|
+
font-style: italic;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.hljs-strong {
|
|
107
|
+
font-weight: bold;
|
|
108
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { LitElement, html, css } from
|
|
2
|
-
import { customElement } from
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement } from "lit/decorators.js";
|
|
3
3
|
|
|
4
4
|
export interface NavItem {
|
|
5
5
|
label: string;
|
|
@@ -10,12 +10,14 @@ export interface NavItem {
|
|
|
10
10
|
* <starlight-header siteTitle="My Docs" .nav=${nav} currentPath="/docs/getting-started">
|
|
11
11
|
* Top navigation bar with site title, nav links, and dark/light theme toggle.
|
|
12
12
|
*/
|
|
13
|
-
@customElement(
|
|
13
|
+
@customElement("starlight-header")
|
|
14
14
|
export class StarlightHeader extends LitElement {
|
|
15
15
|
static override properties = {
|
|
16
16
|
siteTitle: { type: String },
|
|
17
17
|
nav: { type: Array },
|
|
18
18
|
currentPath: { type: String },
|
|
19
|
+
navOpen: { type: Boolean },
|
|
20
|
+
hasSidebar: { type: Boolean },
|
|
19
21
|
_theme: { type: String, state: true },
|
|
20
22
|
};
|
|
21
23
|
|
|
@@ -34,7 +36,39 @@ export class StarlightHeader extends LitElement {
|
|
|
34
36
|
display: flex;
|
|
35
37
|
align-items: center;
|
|
36
38
|
padding: 0 var(--sl-content-pad-x, 1.5rem);
|
|
37
|
-
gap:
|
|
39
|
+
gap: 1rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.menu-btn {
|
|
43
|
+
display: none;
|
|
44
|
+
appearance: none;
|
|
45
|
+
background: none;
|
|
46
|
+
border: 1px solid var(--sl-color-border, #e8e8e8);
|
|
47
|
+
border-radius: var(--sl-border-radius, 0.375rem);
|
|
48
|
+
width: 2.25rem;
|
|
49
|
+
height: 2.25rem;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
color: var(--sl-color-text, #23262f);
|
|
54
|
+
transition: background-color 0.15s;
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
padding: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.menu-btn:hover {
|
|
60
|
+
background-color: var(--sl-color-gray-2, #e8e8e8);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.menu-btn svg {
|
|
64
|
+
width: 1.1rem;
|
|
65
|
+
height: 1.1rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@media (max-width: 72rem) {
|
|
69
|
+
.menu-btn {
|
|
70
|
+
display: flex;
|
|
71
|
+
}
|
|
38
72
|
}
|
|
39
73
|
|
|
40
74
|
.site-title {
|
|
@@ -45,7 +79,9 @@ export class StarlightHeader extends LitElement {
|
|
|
45
79
|
white-space: nowrap;
|
|
46
80
|
}
|
|
47
81
|
|
|
48
|
-
.site-title:hover {
|
|
82
|
+
.site-title:hover {
|
|
83
|
+
opacity: 0.85;
|
|
84
|
+
}
|
|
49
85
|
|
|
50
86
|
nav {
|
|
51
87
|
display: flex;
|
|
@@ -61,7 +97,9 @@ export class StarlightHeader extends LitElement {
|
|
|
61
97
|
color: var(--sl-color-gray-5, #4b4b4b);
|
|
62
98
|
text-decoration: none;
|
|
63
99
|
border-radius: var(--sl-border-radius, 0.375rem);
|
|
64
|
-
transition:
|
|
100
|
+
transition:
|
|
101
|
+
color 0.15s,
|
|
102
|
+
background-color 0.15s;
|
|
65
103
|
}
|
|
66
104
|
|
|
67
105
|
nav a:hover {
|
|
@@ -69,7 +107,7 @@ export class StarlightHeader extends LitElement {
|
|
|
69
107
|
background-color: var(--sl-color-gray-2, #e8e8e8);
|
|
70
108
|
}
|
|
71
109
|
|
|
72
|
-
nav a[aria-current=
|
|
110
|
+
nav a[aria-current="page"] {
|
|
73
111
|
color: var(--sl-color-accent, #7c3aed);
|
|
74
112
|
background-color: var(--sl-color-accent-low, #ede9fe);
|
|
75
113
|
}
|
|
@@ -97,53 +135,111 @@ export class StarlightHeader extends LitElement {
|
|
|
97
135
|
}
|
|
98
136
|
`;
|
|
99
137
|
|
|
100
|
-
siteTitle =
|
|
138
|
+
siteTitle = "";
|
|
101
139
|
nav: NavItem[] = [];
|
|
102
|
-
currentPath =
|
|
140
|
+
currentPath = "";
|
|
141
|
+
navOpen = false;
|
|
142
|
+
hasSidebar = false;
|
|
103
143
|
|
|
104
|
-
_theme =
|
|
144
|
+
_theme = "light";
|
|
105
145
|
|
|
106
146
|
override firstUpdated() {
|
|
107
|
-
const stored =
|
|
108
|
-
|
|
109
|
-
|
|
147
|
+
const stored =
|
|
148
|
+
(typeof localStorage !== "undefined"
|
|
149
|
+
? localStorage.getItem("sl-theme")
|
|
150
|
+
: null) ?? "light";
|
|
110
151
|
this._theme = stored;
|
|
111
|
-
if (typeof document !==
|
|
112
|
-
document.documentElement.setAttribute(
|
|
152
|
+
if (typeof document !== "undefined") {
|
|
153
|
+
document.documentElement.setAttribute("data-theme", stored);
|
|
113
154
|
}
|
|
114
155
|
}
|
|
115
156
|
|
|
116
157
|
private _toggleTheme() {
|
|
117
|
-
const next = this._theme ===
|
|
158
|
+
const next = this._theme === "light" ? "dark" : "light";
|
|
118
159
|
this._theme = next;
|
|
119
|
-
if (typeof localStorage !==
|
|
120
|
-
localStorage.setItem(
|
|
160
|
+
if (typeof localStorage !== "undefined") {
|
|
161
|
+
localStorage.setItem("sl-theme", next);
|
|
121
162
|
}
|
|
122
|
-
if (typeof document !==
|
|
123
|
-
document.documentElement.setAttribute(
|
|
163
|
+
if (typeof document !== "undefined") {
|
|
164
|
+
document.documentElement.setAttribute("data-theme", next);
|
|
124
165
|
}
|
|
125
166
|
}
|
|
126
167
|
|
|
168
|
+
private _toggleNav() {
|
|
169
|
+
this.dispatchEvent(
|
|
170
|
+
new CustomEvent("sl-nav-toggle", { bubbles: true, composed: true }),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
127
174
|
override render() {
|
|
128
|
-
const icon = this._theme ===
|
|
129
|
-
const label =
|
|
175
|
+
const icon = this._theme === "dark" ? "☀️" : "🌙";
|
|
176
|
+
const label =
|
|
177
|
+
this._theme === "dark" ? "Switch to light mode" : "Switch to dark mode";
|
|
130
178
|
|
|
131
179
|
return html`
|
|
132
180
|
<header>
|
|
181
|
+
${this.hasSidebar
|
|
182
|
+
? html`
|
|
183
|
+
<button
|
|
184
|
+
class="menu-btn"
|
|
185
|
+
aria-label="${this.navOpen
|
|
186
|
+
? "Close navigation"
|
|
187
|
+
: "Open navigation"}"
|
|
188
|
+
aria-expanded="${this.navOpen}"
|
|
189
|
+
@click="${this._toggleNav}"
|
|
190
|
+
>
|
|
191
|
+
${this.navOpen
|
|
192
|
+
? html`
|
|
193
|
+
<svg
|
|
194
|
+
viewBox="0 0 24 24"
|
|
195
|
+
fill="none"
|
|
196
|
+
stroke="currentColor"
|
|
197
|
+
stroke-width="2"
|
|
198
|
+
stroke-linecap="round"
|
|
199
|
+
aria-hidden="true"
|
|
200
|
+
>
|
|
201
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
202
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
203
|
+
</svg>
|
|
204
|
+
`
|
|
205
|
+
: html`
|
|
206
|
+
<svg
|
|
207
|
+
viewBox="0 0 24 24"
|
|
208
|
+
fill="none"
|
|
209
|
+
stroke="currentColor"
|
|
210
|
+
stroke-width="2"
|
|
211
|
+
stroke-linecap="round"
|
|
212
|
+
aria-hidden="true"
|
|
213
|
+
>
|
|
214
|
+
<line x1="3" y1="6" x2="21" y2="6" />
|
|
215
|
+
<line x1="3" y1="12" x2="21" y2="12" />
|
|
216
|
+
<line x1="3" y1="18" x2="21" y2="18" />
|
|
217
|
+
</svg>
|
|
218
|
+
`}
|
|
219
|
+
</button>
|
|
220
|
+
`
|
|
221
|
+
: ""}
|
|
133
222
|
<a class="site-title" href="/">${this.siteTitle}</a>
|
|
134
223
|
<nav aria-label="Main navigation">
|
|
135
|
-
${this.nav.map(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
224
|
+
${this.nav.map(
|
|
225
|
+
(item) => html`
|
|
226
|
+
<a
|
|
227
|
+
href="${item.href}"
|
|
228
|
+
aria-current="${this.currentPath.startsWith(item.href)
|
|
229
|
+
? "page"
|
|
230
|
+
: "false"}"
|
|
231
|
+
>${item.label}</a
|
|
232
|
+
>
|
|
233
|
+
`,
|
|
234
|
+
)}
|
|
141
235
|
</nav>
|
|
142
236
|
<button
|
|
143
237
|
class="theme-toggle"
|
|
144
238
|
aria-label="${label}"
|
|
145
239
|
@click="${this._toggleTheme}"
|
|
146
|
-
|
|
240
|
+
>
|
|
241
|
+
${icon}
|
|
242
|
+
</button>
|
|
147
243
|
</header>
|
|
148
244
|
`;
|
|
149
245
|
}
|