@beatzball/create-litro 0.1.4 → 0.2.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 (88) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/recipes/starlight/recipe.config.d.ts +4 -0
  3. package/dist/recipes/starlight/recipe.config.d.ts.map +1 -0
  4. package/dist/recipes/starlight/recipe.config.js +9 -0
  5. package/dist/recipes/starlight/recipe.config.js.map +1 -0
  6. package/dist/recipes/starlight/recipe.config.ts +11 -0
  7. package/dist/recipes/starlight/template/_data/metadata.js +10 -0
  8. package/dist/recipes/starlight/template/app.ts +18 -0
  9. package/dist/recipes/starlight/template/content/blog/.11tydata.json +1 -0
  10. package/dist/recipes/starlight/template/content/blog/release-notes.md +44 -0
  11. package/dist/recipes/starlight/template/content/blog/welcome.md +44 -0
  12. package/dist/recipes/starlight/template/content/docs/.11tydata.json +1 -0
  13. package/dist/recipes/starlight/template/content/docs/configuration.md +77 -0
  14. package/dist/recipes/starlight/template/content/docs/getting-started.md +53 -0
  15. package/dist/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
  16. package/dist/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
  17. package/dist/recipes/starlight/template/content/docs/installation.md +54 -0
  18. package/dist/recipes/starlight/template/litro.recipe.json +7 -0
  19. package/dist/recipes/starlight/template/nitro.config.ts +57 -0
  20. package/dist/recipes/starlight/template/package.json +26 -0
  21. package/dist/recipes/starlight/template/pages/blog/[slug].ts +125 -0
  22. package/dist/recipes/starlight/template/pages/blog/index.ts +114 -0
  23. package/dist/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
  24. package/dist/recipes/starlight/template/pages/docs/[slug].ts +147 -0
  25. package/dist/recipes/starlight/template/pages/index.ts +135 -0
  26. package/dist/recipes/starlight/template/public/styles/starlight.css +215 -0
  27. package/dist/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
  28. package/dist/recipes/starlight/template/server/routes/[...].ts +57 -0
  29. package/dist/recipes/starlight/template/server/starlight.config.js +29 -0
  30. package/dist/recipes/starlight/template/src/components/sl-aside.ts +91 -0
  31. package/dist/recipes/starlight/template/src/components/sl-badge.ts +76 -0
  32. package/dist/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
  33. package/dist/recipes/starlight/template/src/components/sl-card.ts +91 -0
  34. package/dist/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
  35. package/dist/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
  36. package/dist/recipes/starlight/template/src/components/starlight-header.ts +152 -0
  37. package/dist/recipes/starlight/template/src/components/starlight-page.ts +168 -0
  38. package/dist/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
  39. package/dist/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
  40. package/dist/recipes/starlight/template/src/date-utils.ts +20 -0
  41. package/dist/recipes/starlight/template/src/extract-headings.ts +68 -0
  42. package/dist/recipes/starlight/template/src/route-meta.ts +16 -0
  43. package/dist/recipes/starlight/template/tsconfig.json +14 -0
  44. package/dist/recipes/starlight/template/vite.config.ts +19 -0
  45. package/dist/src/scaffold.test.js +134 -0
  46. package/dist/src/scaffold.test.js.map +1 -1
  47. package/dist/tsconfig.tsbuildinfo +1 -1
  48. package/package.json +1 -1
  49. package/recipes/starlight/recipe.config.ts +11 -0
  50. package/recipes/starlight/template/_data/metadata.js +10 -0
  51. package/recipes/starlight/template/app.ts +18 -0
  52. package/recipes/starlight/template/content/blog/.11tydata.json +1 -0
  53. package/recipes/starlight/template/content/blog/release-notes.md +44 -0
  54. package/recipes/starlight/template/content/blog/welcome.md +44 -0
  55. package/recipes/starlight/template/content/docs/.11tydata.json +1 -0
  56. package/recipes/starlight/template/content/docs/configuration.md +77 -0
  57. package/recipes/starlight/template/content/docs/getting-started.md +53 -0
  58. package/recipes/starlight/template/content/docs/guides-deploying.md +79 -0
  59. package/recipes/starlight/template/content/docs/guides-first-page.md +64 -0
  60. package/recipes/starlight/template/content/docs/installation.md +54 -0
  61. package/recipes/starlight/template/litro.recipe.json +7 -0
  62. package/recipes/starlight/template/nitro.config.ts +57 -0
  63. package/recipes/starlight/template/package.json +26 -0
  64. package/recipes/starlight/template/pages/blog/[slug].ts +125 -0
  65. package/recipes/starlight/template/pages/blog/index.ts +114 -0
  66. package/recipes/starlight/template/pages/blog/tags/[tag].ts +110 -0
  67. package/recipes/starlight/template/pages/docs/[slug].ts +147 -0
  68. package/recipes/starlight/template/pages/index.ts +135 -0
  69. package/recipes/starlight/template/public/styles/starlight.css +215 -0
  70. package/recipes/starlight/template/server/middleware/vite-dev.ts +29 -0
  71. package/recipes/starlight/template/server/routes/[...].ts +57 -0
  72. package/recipes/starlight/template/server/starlight.config.js +29 -0
  73. package/recipes/starlight/template/src/components/sl-aside.ts +91 -0
  74. package/recipes/starlight/template/src/components/sl-badge.ts +76 -0
  75. package/recipes/starlight/template/src/components/sl-card-grid.ts +34 -0
  76. package/recipes/starlight/template/src/components/sl-card.ts +91 -0
  77. package/recipes/starlight/template/src/components/sl-tab-item.ts +35 -0
  78. package/recipes/starlight/template/src/components/sl-tabs.ts +108 -0
  79. package/recipes/starlight/template/src/components/starlight-header.ts +152 -0
  80. package/recipes/starlight/template/src/components/starlight-page.ts +168 -0
  81. package/recipes/starlight/template/src/components/starlight-sidebar.ts +125 -0
  82. package/recipes/starlight/template/src/components/starlight-toc.ts +133 -0
  83. package/recipes/starlight/template/src/date-utils.ts +20 -0
  84. package/recipes/starlight/template/src/extract-headings.ts +68 -0
  85. package/recipes/starlight/template/src/route-meta.ts +16 -0
  86. package/recipes/starlight/template/tsconfig.json +14 -0
  87. package/recipes/starlight/template/vite.config.ts +19 -0
  88. package/src/scaffold.test.ts +148 -0
@@ -0,0 +1,108 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import type { SlTabItem } from './sl-tab-item.js';
4
+
5
+ /**
6
+ * <sl-tabs>
7
+ * <sl-tab-item label="First">Content A</sl-tab-item>
8
+ * <sl-tab-item label="Second">Content B</sl-tab-item>
9
+ * </sl-tabs>
10
+ *
11
+ * Reads slotted <sl-tab-item> elements via the slotchange event.
12
+ * Renders a tab bar in Shadow DOM; clicking selects the tab.
13
+ */
14
+ @customElement('sl-tabs')
15
+ export class SlTabs extends LitElement {
16
+ static override properties = {
17
+ _labels: { type: Array, state: true },
18
+ _selectedIndex: { type: Number, state: true },
19
+ };
20
+
21
+ static override styles = css`
22
+ :host {
23
+ display: block;
24
+ margin: 1.5rem 0;
25
+ }
26
+
27
+ .tab-bar {
28
+ display: flex;
29
+ gap: 0;
30
+ border-bottom: 2px solid var(--sl-color-border, #e8e8e8);
31
+ overflow-x: auto;
32
+ }
33
+
34
+ .tab-btn {
35
+ appearance: none;
36
+ background: none;
37
+ border: none;
38
+ padding: 0.5rem 1rem;
39
+ font: inherit;
40
+ font-size: var(--sl-text-sm, 0.875rem);
41
+ font-weight: 500;
42
+ cursor: pointer;
43
+ color: var(--sl-color-gray-4, #757575);
44
+ border-bottom: 2px solid transparent;
45
+ margin-bottom: -2px;
46
+ white-space: nowrap;
47
+ transition: color 0.15s, border-color 0.15s;
48
+ }
49
+
50
+ .tab-btn:hover {
51
+ color: var(--sl-color-text, #23262f);
52
+ }
53
+
54
+ .tab-btn[aria-selected='true'] {
55
+ color: var(--sl-color-accent, #7c3aed);
56
+ border-bottom-color: var(--sl-color-accent, #7c3aed);
57
+ }
58
+
59
+ .tab-content {
60
+ padding-top: 1rem;
61
+ }
62
+ `;
63
+
64
+ _labels: string[] = [];
65
+ _selectedIndex = 0;
66
+
67
+ private _items(): SlTabItem[] {
68
+ const slot = this.shadowRoot?.querySelector('slot');
69
+ if (!slot) return [];
70
+ return slot.assignedElements().filter(
71
+ (el): el is SlTabItem => el.tagName.toLowerCase() === 'sl-tab-item',
72
+ );
73
+ }
74
+
75
+ private _onSlotChange() {
76
+ const items = this._items();
77
+ this._labels = items.map((item) => item.label || `Tab ${items.indexOf(item) + 1}`);
78
+ this._selectIndex(this._selectedIndex < items.length ? this._selectedIndex : 0, items);
79
+ }
80
+
81
+ private _selectIndex(index: number, items?: SlTabItem[]) {
82
+ const all = items ?? this._items();
83
+ this._selectedIndex = index;
84
+ all.forEach((item, i) => {
85
+ item.selected = i === index;
86
+ });
87
+ }
88
+
89
+ override render() {
90
+ return html`
91
+ <div class="tab-bar" role="tablist">
92
+ ${this._labels.map((label, i) => html`
93
+ <button
94
+ class="tab-btn"
95
+ role="tab"
96
+ aria-selected="${this._selectedIndex === i ? 'true' : 'false'}"
97
+ @click=${() => this._selectIndex(i)}
98
+ >${label}</button>
99
+ `)}
100
+ </div>
101
+ <div class="tab-content">
102
+ <slot @slotchange=${this._onSlotChange}></slot>
103
+ </div>
104
+ `;
105
+ }
106
+ }
107
+
108
+ export default SlTabs;
@@ -0,0 +1,152 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+
4
+ export interface NavItem {
5
+ label: string;
6
+ href: string;
7
+ }
8
+
9
+ /**
10
+ * <starlight-header siteTitle="My Docs" .nav=${nav} currentPath="/docs/getting-started">
11
+ * Top navigation bar with site title, nav links, and dark/light theme toggle.
12
+ */
13
+ @customElement('starlight-header')
14
+ export class StarlightHeader extends LitElement {
15
+ static override properties = {
16
+ siteTitle: { type: String },
17
+ nav: { type: Array },
18
+ currentPath: { type: String },
19
+ _theme: { type: String, state: true },
20
+ };
21
+
22
+ static override styles = css`
23
+ :host {
24
+ display: block;
25
+ }
26
+
27
+ header {
28
+ position: sticky;
29
+ top: 0;
30
+ z-index: 100;
31
+ height: var(--sl-nav-height, 3.5rem);
32
+ background-color: var(--sl-color-bg-nav, #fff);
33
+ border-bottom: 1px solid var(--sl-color-border, #e8e8e8);
34
+ display: flex;
35
+ align-items: center;
36
+ padding: 0 var(--sl-content-pad-x, 1.5rem);
37
+ gap: 1.5rem;
38
+ }
39
+
40
+ .site-title {
41
+ font-size: var(--sl-text-lg, 1.125rem);
42
+ font-weight: 700;
43
+ color: var(--sl-color-text, #23262f);
44
+ text-decoration: none;
45
+ white-space: nowrap;
46
+ }
47
+
48
+ .site-title:hover { opacity: 0.85; }
49
+
50
+ nav {
51
+ display: flex;
52
+ align-items: center;
53
+ gap: 0.25rem;
54
+ flex: 1;
55
+ }
56
+
57
+ nav a {
58
+ padding: 0.35rem 0.75rem;
59
+ font-size: var(--sl-text-sm, 0.875rem);
60
+ font-weight: 500;
61
+ color: var(--sl-color-gray-5, #4b4b4b);
62
+ text-decoration: none;
63
+ border-radius: var(--sl-border-radius, 0.375rem);
64
+ transition: color 0.15s, background-color 0.15s;
65
+ }
66
+
67
+ nav a:hover {
68
+ color: var(--sl-color-text, #23262f);
69
+ background-color: var(--sl-color-gray-2, #e8e8e8);
70
+ }
71
+
72
+ nav a[aria-current='page'] {
73
+ color: var(--sl-color-accent, #7c3aed);
74
+ background-color: var(--sl-color-accent-low, #ede9fe);
75
+ }
76
+
77
+ .theme-toggle {
78
+ margin-left: auto;
79
+ appearance: none;
80
+ background: none;
81
+ border: 1px solid var(--sl-color-border, #e8e8e8);
82
+ border-radius: var(--sl-border-radius, 0.375rem);
83
+ width: 2.25rem;
84
+ height: 2.25rem;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ cursor: pointer;
89
+ font-size: 1rem;
90
+ color: var(--sl-color-text, #23262f);
91
+ transition: background-color 0.15s;
92
+ flex-shrink: 0;
93
+ }
94
+
95
+ .theme-toggle:hover {
96
+ background-color: var(--sl-color-gray-2, #e8e8e8);
97
+ }
98
+ `;
99
+
100
+ siteTitle = '';
101
+ nav: NavItem[] = [];
102
+ currentPath = '';
103
+
104
+ _theme = 'light';
105
+
106
+ override firstUpdated() {
107
+ const stored = (typeof localStorage !== 'undefined'
108
+ ? localStorage.getItem('sl-theme')
109
+ : null) ?? 'light';
110
+ this._theme = stored;
111
+ if (typeof document !== 'undefined') {
112
+ document.documentElement.setAttribute('data-theme', stored);
113
+ }
114
+ }
115
+
116
+ private _toggleTheme() {
117
+ const next = this._theme === 'light' ? 'dark' : 'light';
118
+ this._theme = next;
119
+ if (typeof localStorage !== 'undefined') {
120
+ localStorage.setItem('sl-theme', next);
121
+ }
122
+ if (typeof document !== 'undefined') {
123
+ document.documentElement.setAttribute('data-theme', next);
124
+ }
125
+ }
126
+
127
+ override render() {
128
+ const icon = this._theme === 'dark' ? '☀️' : '🌙';
129
+ const label = this._theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
130
+
131
+ return html`
132
+ <header>
133
+ <a class="site-title" href="/">${this.siteTitle}</a>
134
+ <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
+ `)}
141
+ </nav>
142
+ <button
143
+ class="theme-toggle"
144
+ aria-label="${label}"
145
+ @click="${this._toggleTheme}"
146
+ >${icon}</button>
147
+ </header>
148
+ `;
149
+ }
150
+ }
151
+
152
+ export default StarlightHeader;
@@ -0,0 +1,168 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import type { NavItem } from './starlight-header.js';
4
+ import type { SidebarGroup } from './starlight-sidebar.js';
5
+ import type { TocEntry } from '../extract-headings.js';
6
+
7
+ // Side-effect imports — registers child custom elements
8
+ import './starlight-header.js';
9
+ import './starlight-sidebar.js';
10
+ import './starlight-toc.js';
11
+
12
+ /**
13
+ * <starlight-page
14
+ * siteTitle="My Docs"
15
+ * pageTitle="Getting Started"
16
+ * .nav=${nav}
17
+ * .sidebar=${sidebar}
18
+ * .toc=${toc}
19
+ * currentSlug="getting-started"
20
+ * currentPath="/docs/getting-started"
21
+ * >
22
+ * <div slot="content">…rendered HTML…</div>
23
+ * </starlight-page>
24
+ *
25
+ * Three-column grid layout: sidebar | content | TOC.
26
+ * Responsive: single column below 768px, sidebar/TOC collapsed.
27
+ */
28
+ @customElement('starlight-page')
29
+ export class StarlightPage extends LitElement {
30
+ static override properties = {
31
+ siteTitle: { type: String },
32
+ pageTitle: { type: String },
33
+ nav: { type: Array },
34
+ sidebar: { type: Array },
35
+ toc: { type: Array },
36
+ currentSlug: { type: String },
37
+ currentPath: { type: String },
38
+ noSidebar: { type: Boolean },
39
+ };
40
+
41
+ static override styles = css`
42
+ :host {
43
+ display: block;
44
+ }
45
+
46
+ .page-wrap {
47
+ min-height: 100vh;
48
+ display: flex;
49
+ flex-direction: column;
50
+ }
51
+
52
+ .body {
53
+ display: grid;
54
+ grid-template-columns: var(--sl-sidebar-width, 16rem) 1fr var(--sl-toc-width, 14rem);
55
+ grid-template-areas: 'sidebar content toc';
56
+ flex: 1;
57
+ max-width: 90rem;
58
+ margin: 0 auto;
59
+ width: 100%;
60
+ }
61
+
62
+ .body.no-sidebar {
63
+ grid-template-columns: 1fr;
64
+ grid-template-areas: 'content';
65
+ }
66
+
67
+ .sidebar-wrap {
68
+ grid-area: sidebar;
69
+ border-right: 1px solid var(--sl-color-border, #e8e8e8);
70
+ background-color: var(--sl-color-bg-sidebar, #f6f6f6);
71
+ position: sticky;
72
+ top: var(--sl-nav-height, 3.5rem);
73
+ height: calc(100vh - var(--sl-nav-height, 3.5rem));
74
+ overflow-y: auto;
75
+ }
76
+
77
+ .content-wrap {
78
+ grid-area: content;
79
+ padding: var(--sl-content-pad-y, 2rem) var(--sl-content-pad-x, 1.5rem);
80
+ min-width: 0;
81
+ }
82
+
83
+ .content-inner {
84
+ max-width: var(--sl-content-width, 48rem);
85
+ }
86
+
87
+ .toc-wrap {
88
+ 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
+ border-left: 1px solid var(--sl-color-border, #e8e8e8);
91
+ }
92
+
93
+ .page-title {
94
+ font-size: var(--sl-text-4xl, 2.25rem);
95
+ font-weight: 700;
96
+ color: var(--sl-color-text, #23262f);
97
+ margin: 0 0 1.5rem;
98
+ line-height: 1.15;
99
+ }
100
+
101
+ /* Responsive: hide sidebar and TOC on narrow screens */
102
+ @media (max-width: 72rem) {
103
+ .body {
104
+ grid-template-columns: 1fr var(--sl-toc-width, 14rem);
105
+ grid-template-areas: 'content toc';
106
+ }
107
+
108
+ .sidebar-wrap {
109
+ display: none;
110
+ }
111
+ }
112
+
113
+ @media (max-width: 48rem) {
114
+ .body {
115
+ grid-template-columns: 1fr;
116
+ grid-template-areas: 'content';
117
+ }
118
+
119
+ .toc-wrap {
120
+ display: none;
121
+ }
122
+ }
123
+ `;
124
+
125
+ siteTitle = '';
126
+ pageTitle = '';
127
+ nav: NavItem[] = [];
128
+ sidebar: SidebarGroup[] = [];
129
+ toc: TocEntry[] = [];
130
+ currentSlug = '';
131
+ currentPath = '';
132
+ noSidebar = false;
133
+
134
+ override render() {
135
+ return html`
136
+ <div class="page-wrap">
137
+ <starlight-header
138
+ siteTitle="${this.siteTitle}"
139
+ .nav="${this.nav}"
140
+ currentPath="${this.currentPath}"
141
+ ></starlight-header>
142
+ <div class="body${this.noSidebar ? ' no-sidebar' : ''}">
143
+ ${!this.noSidebar ? html`
144
+ <aside class="sidebar-wrap">
145
+ <starlight-sidebar
146
+ .groups="${this.sidebar}"
147
+ currentSlug="${this.currentSlug}"
148
+ ></starlight-sidebar>
149
+ </aside>
150
+ ` : ''}
151
+ <main class="content-wrap">
152
+ <div class="content-inner">
153
+ ${this.pageTitle ? html`<h1 class="page-title">${this.pageTitle}</h1>` : ''}
154
+ <slot name="content"></slot>
155
+ </div>
156
+ </main>
157
+ ${!this.noSidebar ? html`
158
+ <aside class="toc-wrap">
159
+ <starlight-toc .entries="${this.toc}"></starlight-toc>
160
+ </aside>
161
+ ` : ''}
162
+ </div>
163
+ </div>
164
+ `;
165
+ }
166
+ }
167
+
168
+ export default StarlightPage;
@@ -0,0 +1,125 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+
4
+ export interface SidebarItem {
5
+ label: string;
6
+ slug: string;
7
+ badge?: { text: string; variant?: string };
8
+ }
9
+
10
+ export interface SidebarGroup {
11
+ label: string;
12
+ items: SidebarItem[];
13
+ }
14
+
15
+ /**
16
+ * <starlight-sidebar .groups=${sidebar} currentSlug="getting-started">
17
+ * Renders grouped navigation links for the docs sidebar.
18
+ * The active item (matching currentSlug) is highlighted with aria-current.
19
+ */
20
+ @customElement('starlight-sidebar')
21
+ export class StarlightSidebar extends LitElement {
22
+ static override properties = {
23
+ groups: { type: Array },
24
+ currentSlug: { type: String },
25
+ };
26
+
27
+ static override styles = css`
28
+ :host {
29
+ display: block;
30
+ }
31
+
32
+ nav {
33
+ padding: 1rem 0;
34
+ }
35
+
36
+ .group {
37
+ margin-bottom: 1.5rem;
38
+ }
39
+
40
+ .group-label {
41
+ font-size: var(--sl-text-xs, 0.75rem);
42
+ font-weight: 700;
43
+ text-transform: uppercase;
44
+ letter-spacing: 0.1em;
45
+ color: var(--sl-color-gray-4, #757575);
46
+ padding: 0 1rem;
47
+ margin: 0 0 0.5rem;
48
+ }
49
+
50
+ ul {
51
+ list-style: none;
52
+ padding: 0;
53
+ margin: 0;
54
+ }
55
+
56
+ li {
57
+ margin: 0;
58
+ }
59
+
60
+ a {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ padding: 0.35rem 1rem;
65
+ font-size: var(--sl-text-sm, 0.875rem);
66
+ color: var(--sl-color-gray-5, #4b4b4b);
67
+ text-decoration: none;
68
+ border-left: 2px solid transparent;
69
+ transition: color 0.15s, background-color 0.15s;
70
+ }
71
+
72
+ a:hover {
73
+ color: var(--sl-color-text, #23262f);
74
+ background-color: var(--sl-color-gray-2, #e8e8e8);
75
+ }
76
+
77
+ a[aria-current='page'] {
78
+ color: var(--sl-color-accent, #7c3aed);
79
+ border-left-color: var(--sl-color-accent, #7c3aed);
80
+ background-color: var(--sl-color-accent-low, #ede9fe);
81
+ font-weight: 600;
82
+ }
83
+
84
+ .badge {
85
+ display: inline-block;
86
+ padding: 0.1em 0.45em;
87
+ font-size: var(--sl-text-xs, 0.75rem);
88
+ font-weight: 600;
89
+ border-radius: 9999px;
90
+ background-color: var(--sl-color-accent-low, #ede9fe);
91
+ color: var(--sl-color-accent-high, #5b21b6);
92
+ margin-left: 0.5rem;
93
+ }
94
+ `;
95
+
96
+ groups: SidebarGroup[] = [];
97
+ currentSlug = '';
98
+
99
+ override render() {
100
+ return html`
101
+ <nav aria-label="Site navigation">
102
+ ${this.groups.map(group => html`
103
+ <div class="group">
104
+ <p class="group-label">${group.label}</p>
105
+ <ul>
106
+ ${group.items.map(item => html`
107
+ <li>
108
+ <a
109
+ href="/docs/${item.slug}"
110
+ aria-current="${this.currentSlug === item.slug ? 'page' : 'false'}"
111
+ >
112
+ <span>${item.label}</span>
113
+ ${item.badge ? html`<span class="badge">${item.badge.text}</span>` : ''}
114
+ </a>
115
+ </li>
116
+ `)}
117
+ </ul>
118
+ </div>
119
+ `)}
120
+ </nav>
121
+ `;
122
+ }
123
+ }
124
+
125
+ export default StarlightSidebar;
@@ -0,0 +1,133 @@
1
+ import { LitElement, html, css } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import type { TocEntry } from '../extract-headings.js';
4
+
5
+ /**
6
+ * <starlight-toc .entries=${toc}>
7
+ * Renders the table of contents as anchor links.
8
+ * Active section highlighting is handled by CSS :target pseudo-class.
9
+ */
10
+ @customElement('starlight-toc')
11
+ export class StarlightToc extends LitElement {
12
+ static override properties = {
13
+ entries: { type: Array },
14
+ };
15
+
16
+ static override styles = css`
17
+ :host {
18
+ display: block;
19
+ }
20
+
21
+ nav {
22
+ position: sticky;
23
+ top: calc(var(--sl-nav-height, 3.5rem) + 1rem);
24
+ max-height: calc(100vh - var(--sl-nav-height, 3.5rem) - 2rem);
25
+ overflow-y: auto;
26
+ padding: 0 0.5rem;
27
+ }
28
+
29
+ h2 {
30
+ font-size: var(--sl-text-xs, 0.75rem);
31
+ font-weight: 700;
32
+ text-transform: uppercase;
33
+ letter-spacing: 0.1em;
34
+ color: var(--sl-color-gray-4, #757575);
35
+ margin: 0 0 0.75rem;
36
+ padding: 0;
37
+ border: none;
38
+ }
39
+
40
+ ul {
41
+ list-style: none;
42
+ padding: 0;
43
+ margin: 0;
44
+ }
45
+
46
+ li {
47
+ margin: 0;
48
+ }
49
+
50
+ a {
51
+ display: block;
52
+ padding: 0.2rem 0;
53
+ font-size: var(--sl-text-sm, 0.875rem);
54
+ color: var(--sl-color-gray-4, #757575);
55
+ text-decoration: none;
56
+ transition: color 0.15s;
57
+ border-left: 2px solid transparent;
58
+ }
59
+
60
+ a:hover {
61
+ color: var(--sl-color-text, #23262f);
62
+ }
63
+
64
+ /* Depth indentation */
65
+ .depth-2 a { padding-left: 0.75rem; }
66
+ .depth-3 a { padding-left: 1.5rem; }
67
+ .depth-4 a { padding-left: 2.25rem; }
68
+
69
+ /* Active via [aria-current] set by the click handler */
70
+ li a[aria-current='true'] {
71
+ color: var(--sl-color-accent, #7c3aed);
72
+ border-left-color: var(--sl-color-accent, #7c3aed);
73
+ }
74
+ `;
75
+
76
+ entries: TocEntry[] = [];
77
+
78
+ /**
79
+ * Click handler for TOC anchor links.
80
+ *
81
+ * Heading elements rendered via unsafeHTML live inside shadow roots that
82
+ * native fragment navigation and document.getElementById() cannot reach.
83
+ * Instead of relying on the browser's default hash navigation (which also
84
+ * fires popstate and re-renders the page component with no data), we:
85
+ * 1. Prevent the default navigation.
86
+ * 2. Walk the shadow tree to find the target element.
87
+ * 3. Scroll to it smoothly.
88
+ * 4. Update the URL hash via pushState (does not fire popstate).
89
+ */
90
+ private _handleClick(e: MouseEvent, slug: string) {
91
+ e.preventDefault();
92
+ const target = this._findDeep(document, slug);
93
+ if (target) target.scrollIntoView({ behavior: 'smooth' });
94
+ history.pushState(null, '', `#${slug}`);
95
+ }
96
+
97
+ /** Recursively searches shadow roots for an element matching the CSS id selector. */
98
+ private _findDeep(root: Document | ShadowRoot | Element, id: string): Element | null {
99
+ const sel = `#${CSS.escape(id)}`;
100
+ const direct = root.querySelector(sel);
101
+ if (direct) return direct;
102
+ for (const el of root.querySelectorAll('*')) {
103
+ if (el.shadowRoot) {
104
+ const found = this._findDeep(el.shadowRoot, id);
105
+ if (found) return found;
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ override render() {
112
+ if (!this.entries.length) return html``;
113
+ const currentHash = typeof location !== 'undefined' ? location.hash : '';
114
+ return html`
115
+ <nav aria-label="On this page">
116
+ <h2>On this page</h2>
117
+ <ul>
118
+ ${this.entries.map(entry => html`
119
+ <li class="depth-${entry.depth}">
120
+ <a
121
+ href="#${entry.slug}"
122
+ aria-current="${currentHash === '#' + entry.slug ? 'true' : 'false'}"
123
+ @click=${(e: MouseEvent) => this._handleClick(e, entry.slug)}
124
+ >${entry.text}</a>
125
+ </li>
126
+ `)}
127
+ </ul>
128
+ </nav>
129
+ `;
130
+ }
131
+ }
132
+
133
+ export default StarlightToc;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Formats a Date or date string into a human-readable form.
3
+ * Uses the `en-US` locale with month-name style (e.g. "January 15, 2026").
4
+ */
5
+ export function formatDate(date: Date | string): string {
6
+ const d = date instanceof Date ? date : new Date(date as string);
7
+ return d.toLocaleDateString('en-US', {
8
+ year: 'numeric',
9
+ month: 'long',
10
+ day: 'numeric',
11
+ });
12
+ }
13
+
14
+ /**
15
+ * Returns an ISO 8601 string (e.g. "2026-01-15") suitable for a `datetime` attribute.
16
+ */
17
+ export function isoDate(date: Date | string): string {
18
+ const d = date instanceof Date ? date : new Date(date as string);
19
+ return d.toISOString().slice(0, 10);
20
+ }