@beatzball/create-litro 0.2.1 → 0.4.0

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 (42) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/recipes/starlight/template/app.ts +14 -0
  3. package/dist/recipes/starlight/template/nitro.config.ts +2 -0
  4. package/dist/recipes/starlight/template/package.json +2 -0
  5. package/dist/recipes/starlight/template/pages/docs/[slug].ts +75 -2
  6. package/dist/recipes/starlight/template/pages/index.ts +6 -6
  7. package/dist/recipes/starlight/template/public/styles/highlight.css +108 -0
  8. package/dist/recipes/starlight/template/public/styles/starlight.css +58 -19
  9. package/dist/recipes/starlight/template/src/components/{sl-aside.ts → litro-aside.ts} +5 -5
  10. package/{recipes/starlight/template/src/components/sl-badge.ts → dist/recipes/starlight/template/src/components/litro-badge.ts} +5 -5
  11. package/{recipes/starlight/template/src/components/sl-card-grid.ts → dist/recipes/starlight/template/src/components/litro-card-grid.ts} +7 -7
  12. package/{recipes/starlight/template/src/components/sl-card.ts → dist/recipes/starlight/template/src/components/litro-card.ts} +36 -12
  13. package/dist/recipes/starlight/template/src/components/{sl-tab-item.ts → litro-tab-item.ts} +6 -6
  14. package/{recipes/starlight/template/src/components/sl-tabs.ts → dist/recipes/starlight/template/src/components/litro-tabs.ts} +12 -12
  15. package/dist/recipes/starlight/template/src/components/starlight-header.ts +128 -32
  16. package/dist/recipes/starlight/template/src/components/starlight-page.ts +53 -5
  17. package/dist/recipes/starlight/template/src/highlight.ts +40 -0
  18. package/dist/recipes/starlight/template/src/route-meta.ts +4 -1
  19. package/dist/recipes/starlight/template/vite.config.ts +1 -1
  20. package/dist/src/scaffold.test.js +6 -6
  21. package/dist/src/scaffold.test.js.map +1 -1
  22. package/dist/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +1 -1
  24. package/recipes/starlight/template/app.ts +14 -0
  25. package/recipes/starlight/template/nitro.config.ts +2 -0
  26. package/recipes/starlight/template/package.json +2 -0
  27. package/recipes/starlight/template/pages/docs/[slug].ts +75 -2
  28. package/recipes/starlight/template/pages/index.ts +6 -6
  29. package/recipes/starlight/template/public/styles/highlight.css +108 -0
  30. package/recipes/starlight/template/public/styles/starlight.css +58 -19
  31. package/recipes/starlight/template/src/components/{sl-aside.ts → litro-aside.ts} +5 -5
  32. package/{dist/recipes/starlight/template/src/components/sl-badge.ts → recipes/starlight/template/src/components/litro-badge.ts} +5 -5
  33. package/{dist/recipes/starlight/template/src/components/sl-card-grid.ts → recipes/starlight/template/src/components/litro-card-grid.ts} +7 -7
  34. package/{dist/recipes/starlight/template/src/components/sl-card.ts → recipes/starlight/template/src/components/litro-card.ts} +36 -12
  35. package/recipes/starlight/template/src/components/{sl-tab-item.ts → litro-tab-item.ts} +6 -6
  36. package/{dist/recipes/starlight/template/src/components/sl-tabs.ts → recipes/starlight/template/src/components/litro-tabs.ts} +12 -12
  37. package/recipes/starlight/template/src/components/starlight-header.ts +128 -32
  38. package/recipes/starlight/template/src/components/starlight-page.ts +53 -5
  39. package/recipes/starlight/template/src/highlight.ts +40 -0
  40. package/recipes/starlight/template/src/route-meta.ts +4 -1
  41. package/recipes/starlight/template/vite.config.ts +1 -1
  42. package/src/scaffold.test.ts +6 -6
@@ -1,5 +1,5 @@
1
- import { LitElement, html, css } from 'lit';
2
- import { customElement } from 'lit/decorators.js';
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,31 +10,65 @@ 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('starlight-header')
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
 
22
24
  static override styles = css`
23
25
  :host {
24
26
  display: block;
25
- }
26
-
27
- header {
28
27
  position: sticky;
29
28
  top: 0;
30
29
  z-index: 100;
30
+ }
31
+
32
+ header {
31
33
  height: var(--sl-nav-height, 3.5rem);
32
34
  background-color: var(--sl-color-bg-nav, #fff);
33
35
  border-bottom: 1px solid var(--sl-color-border, #e8e8e8);
34
36
  display: flex;
35
37
  align-items: center;
36
38
  padding: 0 var(--sl-content-pad-x, 1.5rem);
37
- gap: 1.5rem;
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 { opacity: 0.85; }
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: color 0.15s, background-color 0.15s;
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='page'] {
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 = 'light';
144
+ _theme = "light";
105
145
 
106
146
  override firstUpdated() {
107
- const stored = (typeof localStorage !== 'undefined'
108
- ? localStorage.getItem('sl-theme')
109
- : null) ?? 'light';
147
+ const stored =
148
+ (typeof localStorage !== "undefined"
149
+ ? localStorage.getItem("sl-theme")
150
+ : null) ?? "light";
110
151
  this._theme = stored;
111
- if (typeof document !== 'undefined') {
112
- document.documentElement.setAttribute('data-theme', stored);
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 === 'light' ? 'dark' : 'light';
158
+ const next = this._theme === "light" ? "dark" : "light";
118
159
  this._theme = next;
119
- if (typeof localStorage !== 'undefined') {
120
- localStorage.setItem('sl-theme', next);
160
+ if (typeof localStorage !== "undefined") {
161
+ localStorage.setItem("sl-theme", next);
121
162
  }
122
- if (typeof document !== 'undefined') {
123
- document.documentElement.setAttribute('data-theme', next);
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 === 'dark' ? '☀️' : '🌙';
129
- const label = this._theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
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(item => html`
136
- <a
137
- href="${item.href}"
138
- aria-current="${this.currentPath.startsWith(item.href) ? 'page' : 'false'}"
139
- >${item.label}</a>
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
- >${icon}</button>
240
+ >
241
+ ${icon}
242
+ </button>
147
243
  </header>
148
244
  `;
149
245
  }
@@ -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`
@@ -86,8 +87,12 @@ export class StarlightPage extends LitElement {
86
87
 
87
88
  .toc-wrap {
88
89
  grid-area: toc;
89
- padding: var(--sl-content-pad-y, 2rem) 0 var(--sl-content-pad-y, 2rem) var(--sl-content-pad-x, 1.5rem);
90
90
  border-left: 1px solid var(--sl-color-border, #e8e8e8);
91
+ position: sticky;
92
+ top: var(--sl-nav-height, 3.5rem);
93
+ height: calc(100vh - var(--sl-nav-height, 3.5rem));
94
+ overflow-y: auto;
95
+ padding: var(--sl-content-pad-y, 2rem) 0 var(--sl-content-pad-y, 2rem) var(--sl-content-pad-x, 1.5rem);
91
96
  }
92
97
 
93
98
  .page-title {
@@ -98,6 +103,14 @@ export class StarlightPage extends LitElement {
98
103
  line-height: 1.15;
99
104
  }
100
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
+
101
114
  /* Responsive: hide sidebar and TOC on narrow screens */
102
115
  @media (max-width: 72rem) {
103
116
  .body {
@@ -106,7 +119,20 @@ export class StarlightPage extends LitElement {
106
119
  }
107
120
 
108
121
  .sidebar-wrap {
109
- display: none;
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);
110
136
  }
111
137
  }
112
138
 
@@ -130,18 +156,40 @@ export class StarlightPage extends LitElement {
130
156
  currentSlug = '';
131
157
  currentPath = '';
132
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
+ }
133
174
 
134
175
  override render() {
176
+ const hasSidebar = !this.noSidebar;
135
177
  return html`
136
178
  <div class="page-wrap">
137
179
  <starlight-header
138
180
  siteTitle="${this.siteTitle}"
139
181
  .nav="${this.nav}"
140
182
  currentPath="${this.currentPath}"
183
+ .navOpen="${this._navOpen}"
184
+ .hasSidebar="${hasSidebar}"
185
+ @sl-nav-toggle="${this._handleNavToggle}"
141
186
  ></starlight-header>
187
+ ${hasSidebar && this._navOpen ? html`
188
+ <div class="nav-backdrop" @click="${this._closeNav}"></div>
189
+ ` : ''}
142
190
  <div class="body${this.noSidebar ? ' no-sidebar' : ''}">
143
- ${!this.noSidebar ? html`
144
- <aside class="sidebar-wrap">
191
+ ${hasSidebar ? html`
192
+ <aside class="sidebar-wrap${this._navOpen ? ' nav-open' : ''}">
145
193
  <starlight-sidebar
146
194
  .groups="${this.sidebar}"
147
195
  currentSlug="${this.currentSlug}"
@@ -154,7 +202,7 @@ export class StarlightPage extends LitElement {
154
202
  <slot name="content"></slot>
155
203
  </div>
156
204
  </main>
157
- ${!this.noSidebar ? html`
205
+ ${hasSidebar ? html`
158
206
  <aside class="toc-wrap">
159
207
  <starlight-toc .entries="${this.toc}"></starlight-toc>
160
208
  </aside>
@@ -0,0 +1,40 @@
1
+ import hljs from 'highlight.js';
2
+
3
+ const NAMED_ENTITY_MAP: Record<string, string> = {
4
+ '&amp;': '&',
5
+ '&lt;': '<',
6
+ '&gt;': '>',
7
+ '&quot;': '"',
8
+ '&#39;': "'",
9
+ };
10
+
11
+ function decodeEntities(str: string): string {
12
+ return str.replace(/&#x[0-9a-fA-F]+;|&#[0-9]+;|&amp;|&lt;|&gt;|&quot;|&#39;/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
+ }
@@ -8,9 +8,12 @@
8
8
  * from localStorage, preventing a flash of the wrong theme on reload.
9
9
  */
10
10
  export const starlightHead = [
11
+ '<link rel="stylesheet" href="/shoelace/themes/light.css" />',
11
12
  '<link rel="stylesheet" href="/styles/starlight.css" />',
13
+ '<link rel="stylesheet" href="/styles/highlight.css" />',
12
14
  '<script>(function(){',
13
- 'var t=localStorage.getItem("sl-theme")||"light";',
15
+ 'var s=localStorage.getItem("sl-theme");',
16
+ 'var t=s||(window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");',
14
17
  'document.documentElement.setAttribute("data-theme",t);',
15
18
  '})();</script>',
16
19
  ].join('');
@@ -3,7 +3,7 @@ import litroContentPlugin from '@beatzball/litro/vite';
3
3
 
4
4
  export default defineConfig({
5
5
  plugins: [litroContentPlugin()],
6
- base: '/_litro/',
6
+ base: process.env.LITRO_BASE_PATH ? `${process.env.LITRO_BASE_PATH}/_litro/` : '/_litro/',
7
7
  resolve: {
8
8
  conditions: ['source', 'browser', 'module', 'import', 'default'],
9
9
  },
@@ -279,12 +279,12 @@ describe('scaffold', () => {
279
279
  expect(existsSync(join(targetDir, 'src/components/starlight-header.ts'))).toBe(true);
280
280
  expect(existsSync(join(targetDir, 'src/components/starlight-sidebar.ts'))).toBe(true);
281
281
  expect(existsSync(join(targetDir, 'src/components/starlight-toc.ts'))).toBe(true);
282
- expect(existsSync(join(targetDir, 'src/components/sl-card.ts'))).toBe(true);
283
- expect(existsSync(join(targetDir, 'src/components/sl-card-grid.ts'))).toBe(true);
284
- expect(existsSync(join(targetDir, 'src/components/sl-badge.ts'))).toBe(true);
285
- expect(existsSync(join(targetDir, 'src/components/sl-aside.ts'))).toBe(true);
286
- expect(existsSync(join(targetDir, 'src/components/sl-tabs.ts'))).toBe(true);
287
- expect(existsSync(join(targetDir, 'src/components/sl-tab-item.ts'))).toBe(true);
282
+ expect(existsSync(join(targetDir, 'src/components/litro-card.ts'))).toBe(true);
283
+ expect(existsSync(join(targetDir, 'src/components/litro-card-grid.ts'))).toBe(true);
284
+ expect(existsSync(join(targetDir, 'src/components/litro-badge.ts'))).toBe(true);
285
+ expect(existsSync(join(targetDir, 'src/components/litro-aside.ts'))).toBe(true);
286
+ expect(existsSync(join(targetDir, 'src/components/litro-tabs.ts'))).toBe(true);
287
+ expect(existsSync(join(targetDir, 'src/components/litro-tab-item.ts'))).toBe(true);
288
288
  // Content
289
289
  expect(existsSync(join(targetDir, 'content/docs/.11tydata.json'))).toBe(true);
290
290
  expect(existsSync(join(targetDir, 'content/docs/getting-started.md'))).toBe(true);