@tutorialkit-rb/astro 1.5.2-rb.0.1.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.
- package/README.md +14 -0
- package/dist/default/components/DownloadButton.tsx +44 -0
- package/dist/default/components/HeadTags.astro +3 -0
- package/dist/default/components/LoginButton.tsx +55 -0
- package/dist/default/components/Logo.astro +30 -0
- package/dist/default/components/MainContainer.astro +86 -0
- package/dist/default/components/MetaTags.astro +44 -0
- package/dist/default/components/MobileContentToggle.astro +44 -0
- package/dist/default/components/NavCard.astro +23 -0
- package/dist/default/components/NavWrapper.tsx +11 -0
- package/dist/default/components/OpenInStackblitzLink.tsx +37 -0
- package/dist/default/components/PageLoadingIndicator.astro +66 -0
- package/dist/default/components/ResizablePanel.astro +247 -0
- package/dist/default/components/ThemeSwitch.tsx +24 -0
- package/dist/default/components/TopBar.astro +20 -0
- package/dist/default/components/TopBarWrapper.astro +30 -0
- package/dist/default/components/TutorialContent.astro +48 -0
- package/dist/default/components/WorkspacePanelWrapper.tsx +25 -0
- package/dist/default/components/setup.ts +20 -0
- package/dist/default/components/webcontainer.ts +46 -0
- package/dist/default/env-default.d.ts +19 -0
- package/dist/default/layouts/Layout.astro +98 -0
- package/dist/default/pages/[...slug].astro +39 -0
- package/dist/default/pages/index.astro +25 -0
- package/dist/default/stores/auth-store.ts +6 -0
- package/dist/default/stores/theme-store.ts +32 -0
- package/dist/default/stores/view-store.ts +5 -0
- package/dist/default/styles/base.css +11 -0
- package/dist/default/styles/markdown.css +400 -0
- package/dist/default/styles/panel.css +7 -0
- package/dist/default/styles/variables.css +396 -0
- package/dist/default/utils/constants.ts +6 -0
- package/dist/default/utils/content/files-ref.ts +25 -0
- package/dist/default/utils/content/squash.ts +37 -0
- package/dist/default/utils/content.ts +446 -0
- package/dist/default/utils/logger.ts +56 -0
- package/dist/default/utils/logo.ts +17 -0
- package/dist/default/utils/nav.ts +65 -0
- package/dist/default/utils/publicAsset.ts +27 -0
- package/dist/default/utils/routes.ts +34 -0
- package/dist/default/utils/url.ts +22 -0
- package/dist/default/utils/workspace.ts +31 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +972 -0
- package/dist/integrations.d.ts +10 -0
- package/dist/remark/callouts.d.ts +3 -0
- package/dist/remark/import-file.d.ts +7 -0
- package/dist/remark/index.d.ts +2 -0
- package/dist/types.d.ts +9 -0
- package/dist/utils.d.ts +2 -0
- package/dist/vite-plugins/core.d.ts +2 -0
- package/dist/vite-plugins/css.d.ts +4 -0
- package/dist/vite-plugins/override-components.d.ts +78 -0
- package/dist/vite-plugins/store.d.ts +2 -0
- package/dist/webcontainer-files/cache.d.ts +21 -0
- package/dist/webcontainer-files/cache.spec.d.ts +1 -0
- package/dist/webcontainer-files/constants.d.ts +4 -0
- package/dist/webcontainer-files/filesmap.d.ts +38 -0
- package/dist/webcontainer-files/filesmap.spec.d.ts +1 -0
- package/dist/webcontainer-files/index.d.ts +8 -0
- package/dist/webcontainer-files/utils.d.ts +6 -0
- package/package.json +85 -0
- package/types.d.ts +12 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { classNames } from '@tutorialkit-rb/react';
|
|
3
|
+
|
|
4
|
+
export type Type = 'horizontal' | 'vertical';
|
|
5
|
+
export type Priority = 'min' | 'max';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
id: string;
|
|
9
|
+
type?: Type;
|
|
10
|
+
pos?: string;
|
|
11
|
+
min?: string;
|
|
12
|
+
max?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
sidePanelClass?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IResizablePanel {
|
|
18
|
+
mainPanel(): HTMLElement;
|
|
19
|
+
sidePanel(): HTMLElement | undefined;
|
|
20
|
+
divider(): HTMLElement | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
id,
|
|
25
|
+
type = 'horizontal',
|
|
26
|
+
min = '0%',
|
|
27
|
+
pos = '50%',
|
|
28
|
+
max = '100%',
|
|
29
|
+
class: className = '',
|
|
30
|
+
sidePanelClass = '',
|
|
31
|
+
} = Astro.props;
|
|
32
|
+
|
|
33
|
+
// check if there is a `slot` defined with name `b`
|
|
34
|
+
const hasSidePanel = Astro.slots.has('b');
|
|
35
|
+
|
|
36
|
+
if (!hasSidePanel) {
|
|
37
|
+
// if we don't have a side panel, we make the first panel full width
|
|
38
|
+
pos = '100%';
|
|
39
|
+
min = '100%';
|
|
40
|
+
max = '100%';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const panelClass = classNames('overflow-hidden', { 'h-full': type === 'horizontal' });
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<resizable-panel class={className} data-id={id} data-type={type} data-pos={pos} data-min={min} data-max={max}>
|
|
47
|
+
<div
|
|
48
|
+
data-id="container"
|
|
49
|
+
class={classNames('sm:grid relative w-full h-full max-w-full', {
|
|
50
|
+
'sm:grid-cols-[var(--pos)_1fr]': type === 'horizontal',
|
|
51
|
+
'sm:grid-rows-[var(--pos)_1fr]': type !== 'horizontal',
|
|
52
|
+
})}
|
|
53
|
+
style=`--pos: ${pos}`
|
|
54
|
+
>
|
|
55
|
+
<!-- It's important to keep the inline script here because it restores the position and blocks rendering to avoid flickering -->
|
|
56
|
+
<script is:inline define:vars={{ id, hasSidePanel }}>
|
|
57
|
+
if (!hasSidePanel) {
|
|
58
|
+
// if we don't have a side panel, we don't have to restore the handle
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sessionStorageKey = `tk_resizable_panel_${id}`;
|
|
63
|
+
|
|
64
|
+
const $container = document.querySelector(`resizable-panel[data-id="${id}"] > div`);
|
|
65
|
+
const pos = sessionStorage.getItem(sessionStorageKey);
|
|
66
|
+
|
|
67
|
+
if (pos) {
|
|
68
|
+
$container.style.setProperty('--pos', pos);
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
<div class={panelClass} data-id="main-panel">
|
|
72
|
+
<slot name="a" />
|
|
73
|
+
</div>
|
|
74
|
+
{
|
|
75
|
+
hasSidePanel && (
|
|
76
|
+
<>
|
|
77
|
+
<div class={`${panelClass} ${sidePanelClass}`} data-id="side-panel">
|
|
78
|
+
<slot name="b" />
|
|
79
|
+
</div>
|
|
80
|
+
<div
|
|
81
|
+
data-id="divider"
|
|
82
|
+
class={classNames('absolute z-90 transition-colors hover:bg-gray-500/13', {
|
|
83
|
+
'w-0 h-full left-[var(--pos)] cursor-ew-resize p-0 px-1.5 -translate-x-1/2': type === 'horizontal',
|
|
84
|
+
'h-0 w-full top-[var(--pos)] cursor-ns-resize p-0 py-2 -translate-y-1/2': type !== 'horizontal',
|
|
85
|
+
})}
|
|
86
|
+
/>
|
|
87
|
+
</>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
</div>
|
|
91
|
+
</resizable-panel>
|
|
92
|
+
|
|
93
|
+
<script>
|
|
94
|
+
import type { Type, IResizablePanel } from './ResizablePanel.astro';
|
|
95
|
+
|
|
96
|
+
class ResizablePanel extends HTMLElement implements IResizablePanel {
|
|
97
|
+
readonly #id = this.dataset.id as string;
|
|
98
|
+
readonly #type = this.dataset.type as Type;
|
|
99
|
+
readonly #min = this.dataset.min as string;
|
|
100
|
+
readonly #max = this.dataset.max as string;
|
|
101
|
+
|
|
102
|
+
#pos = this.dataset.pos as string;
|
|
103
|
+
#width = 0;
|
|
104
|
+
#height = 0;
|
|
105
|
+
#dragging = false;
|
|
106
|
+
|
|
107
|
+
#container: HTMLElement;
|
|
108
|
+
#mainPanel: HTMLElement;
|
|
109
|
+
#sidePanel: HTMLElement | undefined;
|
|
110
|
+
#divider: HTMLElement | undefined;
|
|
111
|
+
|
|
112
|
+
constructor() {
|
|
113
|
+
super();
|
|
114
|
+
|
|
115
|
+
this.#container = this.querySelector(':scope > [data-id="container"]') as HTMLElement;
|
|
116
|
+
this.#mainPanel = this.#container.querySelector(':scope > [data-id="main-panel"]') as HTMLElement;
|
|
117
|
+
this.#sidePanel = this.#container.querySelector(':scope > [data-id="side-panel"]') as HTMLElement | undefined;
|
|
118
|
+
this.#divider = this.#container.querySelector(':scope > [data-id="divider"]') as HTMLElement | undefined;
|
|
119
|
+
|
|
120
|
+
this.#width = this.#container.clientWidth;
|
|
121
|
+
this.#height = this.#container.clientHeight;
|
|
122
|
+
|
|
123
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
124
|
+
const { clientWidth, clientHeight } = entries[0].target;
|
|
125
|
+
|
|
126
|
+
this.#width = clientWidth;
|
|
127
|
+
this.#height = clientHeight;
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
resizeObserver.observe(this.#container);
|
|
131
|
+
|
|
132
|
+
// only if we have a divider, which means we have a side panel, we restore the position
|
|
133
|
+
if (this.#divider) {
|
|
134
|
+
this.#pos = sessionStorage.getItem(`tk_resizable_panel_${this.#id}`) ?? this.#pos;
|
|
135
|
+
|
|
136
|
+
this.#setPosition(this.#pos);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
mainPanel(): HTMLElement {
|
|
141
|
+
return this.#mainPanel;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
sidePanel(): HTMLElement | undefined {
|
|
145
|
+
return this.#sidePanel;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
divider(): HTMLElement | undefined {
|
|
149
|
+
return this.#divider;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
connectedCallback() {
|
|
153
|
+
this.#divider?.addEventListener('mousedown', this.#onMouseDown.bind(this));
|
|
154
|
+
this.#divider?.addEventListener('touchstart', this.#onMouseDown.bind(this), { passive: true });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
disconnectedCallback() {
|
|
158
|
+
this.#divider?.removeEventListener('mousedown', this.#onMouseDown.bind(this));
|
|
159
|
+
|
|
160
|
+
window.removeEventListener('mouseup', this.#onMouseUp);
|
|
161
|
+
window.removeEventListener('mousemove', this.#onMouseMove);
|
|
162
|
+
|
|
163
|
+
window.removeEventListener('touchend', this.#onMouseUp);
|
|
164
|
+
window.removeEventListener('touchmove', this.#onTouchMove);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#onMouseMove(mouseMoveEvent: MouseEvent) {
|
|
168
|
+
if (!this.#dragging) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.#updatePosition(mouseMoveEvent.clientX, mouseMoveEvent.clientY);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
#onTouchMove(event: TouchEvent) {
|
|
176
|
+
if (!this.#dragging) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.#updatePosition(event.touches[0].clientX, event.touches[0].clientY);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#onMouseUp() {
|
|
184
|
+
this.#dragging = false;
|
|
185
|
+
|
|
186
|
+
sessionStorage.setItem(`tk_resizable_panel_${this.#id}`, this.#pos);
|
|
187
|
+
|
|
188
|
+
window.removeEventListener('mouseup', this.#onMouseUp);
|
|
189
|
+
window.removeEventListener('mousemove', this.#onMouseMove);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#onMouseDown(event: MouseEvent | TouchEvent) {
|
|
193
|
+
if ('button' in event && event.button !== 0) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
event.preventDefault();
|
|
198
|
+
|
|
199
|
+
this.#dragging = true;
|
|
200
|
+
|
|
201
|
+
window.addEventListener('mousemove', this.#onMouseMove.bind(this));
|
|
202
|
+
window.addEventListener('mouseup', this.#onMouseUp.bind(this));
|
|
203
|
+
|
|
204
|
+
window.addEventListener('touchmove', this.#onTouchMove.bind(this));
|
|
205
|
+
window.addEventListener('touchend', this.#onMouseUp.bind(this));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
#updatePosition(x: number, y: number) {
|
|
209
|
+
const { top, left } = this.#container.getBoundingClientRect();
|
|
210
|
+
|
|
211
|
+
const posPx = this.#type === 'horizontal' ? x - left : y - top;
|
|
212
|
+
const size = this.#type === 'horizontal' ? this.#width : this.#height;
|
|
213
|
+
|
|
214
|
+
const pos = `${(100 * posPx) / size}%`;
|
|
215
|
+
|
|
216
|
+
this.#setPosition(pos);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#setPosition(pos: string) {
|
|
220
|
+
const size = this.#type === 'horizontal' ? this.#width : this.#height;
|
|
221
|
+
|
|
222
|
+
let minPx = parseFloat(this.#min);
|
|
223
|
+
let maxPx = parseFloat(this.#max);
|
|
224
|
+
let posPx = parseFloat(pos);
|
|
225
|
+
|
|
226
|
+
minPx = (size * minPx) / 100;
|
|
227
|
+
maxPx = (size * maxPx) / 100;
|
|
228
|
+
posPx = (size * posPx) / 100;
|
|
229
|
+
|
|
230
|
+
if (minPx < 0) {
|
|
231
|
+
minPx += size;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (maxPx < 0) {
|
|
235
|
+
maxPx += size;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
posPx = Math.max(minPx, Math.min(maxPx, posPx));
|
|
239
|
+
|
|
240
|
+
this.#pos = `${(100 * posPx) / size}%`;
|
|
241
|
+
|
|
242
|
+
this.#container.style.setProperty('--pos', this.#pos);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
customElements.define('resizable-panel', ResizablePanel);
|
|
247
|
+
</script>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { memo, useEffect, useState } from 'react';
|
|
3
|
+
import { themeStore, toggleTheme } from '../stores/theme-store';
|
|
4
|
+
|
|
5
|
+
export const ThemeSwitch = memo(() => {
|
|
6
|
+
const theme = useStore(themeStore);
|
|
7
|
+
const [domLoaded, setDomLoaded] = useState(false);
|
|
8
|
+
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
setDomLoaded(true);
|
|
11
|
+
}, []);
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
domLoaded && (
|
|
15
|
+
<button
|
|
16
|
+
className="flex items-center text-2xl text-tk-elements-topBar-iconButton-iconColor hover:text-tk-elements-topBar-iconButton-iconColorHover transition-theme bg-tk-elements-topBar-iconButton-backgroundColor hover:bg-tk-elements-topBar-iconButton-backgroundColorHover p-1 rounded-md"
|
|
17
|
+
title="Toggle Theme"
|
|
18
|
+
onClick={() => toggleTheme()}
|
|
19
|
+
>
|
|
20
|
+
<div className={theme === 'dark' ? 'i-ph-sun-dim-duotone' : 'i-ph-moon-stars-duotone'} />
|
|
21
|
+
</button>
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<nav
|
|
2
|
+
class="bg-tk-elements-topBar-backgroundColor transition-theme border-b border-tk-elements-app-borderColor flex max-w-full items-center p-3 px-4 min-h-[56px]"
|
|
3
|
+
>
|
|
4
|
+
<div class="flex flex-1">
|
|
5
|
+
<slot name="logo" />
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="mr-2">
|
|
9
|
+
<slot name="download-button" />
|
|
10
|
+
</div>
|
|
11
|
+
<div class="mr-2">
|
|
12
|
+
<slot name="open-in-stackblitz-link" />
|
|
13
|
+
</div>
|
|
14
|
+
<div>
|
|
15
|
+
<slot name="theme-switch" />
|
|
16
|
+
</div>
|
|
17
|
+
<div>
|
|
18
|
+
<slot name="login-button" />
|
|
19
|
+
</div>
|
|
20
|
+
</nav>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { TopBar } from 'tutorialkit:override-components';
|
|
3
|
+
import type { Lesson } from '@tutorialkit-rb/types';
|
|
4
|
+
import { ThemeSwitch } from './ThemeSwitch';
|
|
5
|
+
import { LoginButton } from './LoginButton';
|
|
6
|
+
import { DownloadButton } from './DownloadButton';
|
|
7
|
+
import { OpenInStackblitzLink } from './OpenInStackblitzLink';
|
|
8
|
+
import Logo from './Logo.astro';
|
|
9
|
+
import { useAuth } from './setup';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
logoLink: string;
|
|
13
|
+
openInStackBlitz: Lesson['data']['openInStackBlitz'];
|
|
14
|
+
downloadAsZip: Lesson['data']['downloadAsZip'];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { logoLink, openInStackBlitz, downloadAsZip } = Astro.props;
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
<TopBar>
|
|
21
|
+
<Logo slot="logo" logoLink={logoLink ?? '/'} />
|
|
22
|
+
|
|
23
|
+
{downloadAsZip && <DownloadButton client:load transition:persist slot="download-button" />}
|
|
24
|
+
|
|
25
|
+
{openInStackBlitz && <OpenInStackblitzLink client:load transition:persist slot="open-in-stackblitz-link" />}
|
|
26
|
+
|
|
27
|
+
<ThemeSwitch client:load transition:persist slot="theme-switch" />
|
|
28
|
+
|
|
29
|
+
{useAuth && <LoginButton client:load transition:persist slot="login-button" />}
|
|
30
|
+
</TopBar>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { AstroComponentFactory } from 'astro/runtime/server/index.js';
|
|
3
|
+
import type { Lesson } from '@tutorialkit-rb/types';
|
|
4
|
+
import NavCard from './NavCard.astro';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
lesson: Lesson<AstroComponentFactory>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { lesson } = Astro.props;
|
|
11
|
+
const { Markdown, editPageLink, prev, next } = lesson;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<div class="flex flex-col h-full overflow-auto scrollbar-transparent p-6 sm:p-8">
|
|
15
|
+
<div class="markdown-content text-tk-elements-content-textColor">
|
|
16
|
+
<Markdown />
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
{
|
|
20
|
+
editPageLink && (
|
|
21
|
+
<div class="pb-4 mt-8 border-b border-tk-border-secondary">
|
|
22
|
+
<a
|
|
23
|
+
href={editPageLink}
|
|
24
|
+
class="inline-flex flex-items-center text-tk-elements-link-secondaryColor hover:text-tk-elements-link-secondaryColorHover hover:underline"
|
|
25
|
+
>
|
|
26
|
+
<span class="icon i-ph-note-pencil pointer-events-none h-5 w-5 mr-2" />
|
|
27
|
+
<span>{lesson.data.i18n!.editPageText}</span>
|
|
28
|
+
</a>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
<div class="grid grid-cols-[1fr_1fr] gap-4 mt-8 mb-6">
|
|
34
|
+
<div class="flex">
|
|
35
|
+
{prev && <NavCard lesson={prev} type="prev" />}
|
|
36
|
+
</div>
|
|
37
|
+
<div class="flex">
|
|
38
|
+
{next && <NavCard lesson={next} type="next" />}
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<a
|
|
43
|
+
class="inline-block mt-auto font-size-3.5 underline text-tk-elements-link-secondaryColor hover:text-tk-elements-link-secondaryColorHover"
|
|
44
|
+
href="https://webcontainers.io/"
|
|
45
|
+
>
|
|
46
|
+
{lesson.data.i18n!.webcontainerLinkText}
|
|
47
|
+
</a>
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useStore } from '@nanostores/react';
|
|
2
|
+
import { WorkspacePanel } from '@tutorialkit-rb/react';
|
|
3
|
+
import type { Lesson } from '@tutorialkit-rb/types';
|
|
4
|
+
import { useEffect } from 'react';
|
|
5
|
+
import { Dialog } from 'tutorialkit:override-components';
|
|
6
|
+
import { themeStore } from '../stores/theme-store.js';
|
|
7
|
+
import { tutorialStore } from './webcontainer.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
lesson: Lesson;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function WorkspacePanelWrapper({ lesson }: Props) {
|
|
14
|
+
const theme = useStore(themeStore);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
tutorialStore.setLesson(lesson);
|
|
18
|
+
}, [lesson]);
|
|
19
|
+
|
|
20
|
+
if (import.meta.env.SSR || !tutorialStore.lesson) {
|
|
21
|
+
tutorialStore.setLesson(lesson, { ssr: import.meta.env.SSR });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return <WorkspacePanel dialog={Dialog} tutorialStore={tutorialStore} theme={theme} />;
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This code must be executed before WebContainer boots and be executed as soon as possible.
|
|
3
|
+
* This ensures that when the authentication flow is complete in a popup, the popup is closed quickly.
|
|
4
|
+
*/
|
|
5
|
+
import { auth } from '@webcontainer/api';
|
|
6
|
+
import { authStore } from '../stores/auth-store.js';
|
|
7
|
+
|
|
8
|
+
const authConfig = __WC_CONFIG__;
|
|
9
|
+
|
|
10
|
+
export const useAuth = __ENTERPRISE__ && !!authConfig;
|
|
11
|
+
|
|
12
|
+
// this condition is here to make sure the branch is removed by esbuild if it evaluates to false
|
|
13
|
+
if (__ENTERPRISE__) {
|
|
14
|
+
if (authConfig && !import.meta.env.SSR) {
|
|
15
|
+
authStore.set(auth.init(authConfig));
|
|
16
|
+
|
|
17
|
+
auth.on('auth-failed', (reason) => authStore.set({ status: 'auth-failed', ...reason }));
|
|
18
|
+
auth.on('logged-out', () => authStore.set({ status: 'need-auth' }));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// eslint-disable-next-line -- must be imported first
|
|
2
|
+
import { useAuth } from './setup.js';
|
|
3
|
+
|
|
4
|
+
import { safeBoot, TutorialStore } from '@tutorialkit-rb/runtime';
|
|
5
|
+
import { auth, WebContainer } from '@webcontainer/api';
|
|
6
|
+
import { joinPaths } from '../utils/url.js';
|
|
7
|
+
|
|
8
|
+
interface WebContainerContext {
|
|
9
|
+
readonly useAuth: boolean;
|
|
10
|
+
loggedIn: () => Promise<void>;
|
|
11
|
+
loaded: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export let webcontainer: Promise<WebContainer> = new Promise(() => {
|
|
15
|
+
// noop for ssr
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!import.meta.env.SSR) {
|
|
19
|
+
webcontainer = Promise.resolve(useAuth ? auth.loggedIn() : null).then(() => safeBoot({ workdirName: 'tutorial' }));
|
|
20
|
+
|
|
21
|
+
webcontainer.then(() => {
|
|
22
|
+
webcontainerContext.loaded = true;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const tutorialStore = new TutorialStore({
|
|
27
|
+
webcontainer,
|
|
28
|
+
useAuth,
|
|
29
|
+
basePathname: joinPaths(import.meta.env.BASE_URL, '/'),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export async function login() {
|
|
33
|
+
auth.startAuthFlow({ popup: true });
|
|
34
|
+
|
|
35
|
+
await auth.loggedIn();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function logout() {
|
|
39
|
+
auth.logout({ ignoreRevokeError: true });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const webcontainerContext: WebContainerContext = {
|
|
43
|
+
useAuth,
|
|
44
|
+
loggedIn: () => auth.loggedIn(),
|
|
45
|
+
loaded: false,
|
|
46
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/// <reference path="../.astro/types.d.ts" />
|
|
2
|
+
/// <reference types="astro/client" />
|
|
3
|
+
|
|
4
|
+
interface WebContainerConfig {
|
|
5
|
+
editorOrigin: string;
|
|
6
|
+
clientId: string;
|
|
7
|
+
scope: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
declare module 'tutorialkit:override-components' {
|
|
11
|
+
const topBar: typeof import('./src/default/components/TopBar.astro').default;
|
|
12
|
+
const headTags: typeof import('./src/default/components/HeadTags.astro').default;
|
|
13
|
+
const dialog: typeof import('@tutorialkit-rb/react/dialog').default;
|
|
14
|
+
|
|
15
|
+
export { topBar as TopBar, dialog as Dialog, headTags as HeadTags };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare const __ENTERPRISE__: boolean;
|
|
19
|
+
declare const __WC_CONFIG__: WebContainerConfig | undefined;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { HeadTags } from 'tutorialkit:override-components';
|
|
3
|
+
import { ViewTransitions } from 'astro:transitions';
|
|
4
|
+
import type { MetaTagsConfig } from '@tutorialkit-rb/types';
|
|
5
|
+
import MetaTags from '../components/MetaTags.astro';
|
|
6
|
+
import { readPublicAsset } from '../utils/publicAsset';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: string;
|
|
10
|
+
meta?: MetaTagsConfig;
|
|
11
|
+
}
|
|
12
|
+
const { title, meta } = Astro.props;
|
|
13
|
+
const faviconUrl = readPublicAsset('favicon.svg');
|
|
14
|
+
const canonicalUrl = Astro.site ? new URL(Astro.url.pathname, Astro.site).toString() : null;
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
<!doctype html>
|
|
18
|
+
<html lang="en" transition:animate="none" class="h-full overflow-hidden">
|
|
19
|
+
<head>
|
|
20
|
+
<HeadTags>
|
|
21
|
+
<title slot="title">{meta?.title || title}</title>
|
|
22
|
+
|
|
23
|
+
<Fragment slot="links">
|
|
24
|
+
{canonicalUrl && <link rel="canonical" href={canonicalUrl} />}
|
|
25
|
+
{faviconUrl ? <link rel="icon" type="image/svg+xml" href={faviconUrl} /> : null}
|
|
26
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
27
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
28
|
+
<link
|
|
29
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
30
|
+
rel="stylesheet"
|
|
31
|
+
/>
|
|
32
|
+
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;700&display=swap" rel="stylesheet" />
|
|
33
|
+
</Fragment>
|
|
34
|
+
|
|
35
|
+
<MetaTags slot="meta" meta={meta} />
|
|
36
|
+
</HeadTags>
|
|
37
|
+
<ViewTransitions />
|
|
38
|
+
<script is:inline>
|
|
39
|
+
setTutorialKitTheme();
|
|
40
|
+
|
|
41
|
+
function setTutorialKitTheme() {
|
|
42
|
+
let theme = localStorage.getItem('tk_theme');
|
|
43
|
+
|
|
44
|
+
if (!theme) {
|
|
45
|
+
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
document.querySelector('html')?.setAttribute('data-theme', theme);
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
<script>
|
|
52
|
+
import { swapFunctions as builtInSwap } from 'astro:transitions/client';
|
|
53
|
+
|
|
54
|
+
declare global {
|
|
55
|
+
function setTutorialKitTheme(): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
document.addEventListener('astro:before-swap', (event) => {
|
|
59
|
+
event.swap = () => {
|
|
60
|
+
const { newDocument } = event;
|
|
61
|
+
|
|
62
|
+
builtInSwap.deselectScripts(newDocument);
|
|
63
|
+
builtInSwap.swapRootAttributes(newDocument);
|
|
64
|
+
|
|
65
|
+
setTutorialKitTheme();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Keep the dynamically injected style sheet from Codemirror on all transitions.
|
|
69
|
+
*/
|
|
70
|
+
const codemirrorStyles = document.head.querySelector('style[data-astro-transition-persist="codemirror"]');
|
|
71
|
+
|
|
72
|
+
builtInSwap.swapHeadElements(newDocument);
|
|
73
|
+
|
|
74
|
+
if (codemirrorStyles) {
|
|
75
|
+
document.head.insertAdjacentElement('afterbegin', codemirrorStyles);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// only swap the main area of the page so we keep the webcontainer iframe intact
|
|
79
|
+
const newMain = newDocument.querySelector('main[data-swap-root]');
|
|
80
|
+
const oldMain = document.querySelector('main[data-swap-root]');
|
|
81
|
+
|
|
82
|
+
if (newMain && oldMain) {
|
|
83
|
+
builtInSwap.swapBodyElement(newMain, oldMain);
|
|
84
|
+
|
|
85
|
+
// delete extraneous route announcer
|
|
86
|
+
document.querySelector('.astro-route-announcer')?.remove();
|
|
87
|
+
} else {
|
|
88
|
+
// fallback to built-in body swap semantics
|
|
89
|
+
builtInSwap.swapBodyElement(newDocument.body, document.body);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
</script>
|
|
94
|
+
</head>
|
|
95
|
+
<body class="h-full text-black relative overflow-hidden">
|
|
96
|
+
<slot />
|
|
97
|
+
</body>
|
|
98
|
+
</html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { InferGetStaticPropsType } from 'astro';
|
|
3
|
+
import TopBarWrapper from '../components/TopBarWrapper.astro';
|
|
4
|
+
import MainContainer from '../components/MainContainer.astro';
|
|
5
|
+
import PageLoadingIndicator from '../components/PageLoadingIndicator.astro';
|
|
6
|
+
import Layout from '../layouts/Layout.astro';
|
|
7
|
+
import '../styles/base.css';
|
|
8
|
+
import '@tutorialkit/custom.css';
|
|
9
|
+
import { generateStaticRoutes } from '../utils/routes';
|
|
10
|
+
|
|
11
|
+
export async function getStaticPaths() {
|
|
12
|
+
return generateStaticRoutes();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
|
16
|
+
|
|
17
|
+
const { lesson, logoLink, navList, title } = Astro.props as Props;
|
|
18
|
+
const meta = lesson.data?.meta ?? {};
|
|
19
|
+
|
|
20
|
+
// use lesson's default title and a default description for SEO metadata
|
|
21
|
+
meta.title ??= title;
|
|
22
|
+
meta.description ??= 'A TutorialKit interactive lesson';
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<Layout title={title} meta={meta}>
|
|
26
|
+
<PageLoadingIndicator />
|
|
27
|
+
|
|
28
|
+
<div id="previews-container" style="display: none;"></div>
|
|
29
|
+
|
|
30
|
+
<main class="max-w-full flex flex-col h-full overflow-hidden" data-swap-root>
|
|
31
|
+
<TopBarWrapper
|
|
32
|
+
logoLink={logoLink ?? '/'}
|
|
33
|
+
openInStackBlitz={lesson.data.openInStackBlitz}
|
|
34
|
+
downloadAsZip={lesson.data.downloadAsZip}
|
|
35
|
+
/>
|
|
36
|
+
|
|
37
|
+
<MainContainer lesson={lesson} navList={navList} />
|
|
38
|
+
</main>
|
|
39
|
+
</Layout>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
import MetaTags from '../components/MetaTags.astro';
|
|
3
|
+
import { getTutorial } from '../utils/content';
|
|
4
|
+
import { joinPaths } from '../utils/url';
|
|
5
|
+
|
|
6
|
+
const tutorial = await getTutorial();
|
|
7
|
+
|
|
8
|
+
const lesson = tutorial.lessons[0];
|
|
9
|
+
const part = lesson.part && tutorial.parts[lesson.part.id];
|
|
10
|
+
const chapter = lesson.chapter && part?.chapters[lesson.chapter.id];
|
|
11
|
+
|
|
12
|
+
const slug = [part?.slug, chapter?.slug, lesson.slug].filter(Boolean).join('/');
|
|
13
|
+
const meta = lesson.data?.meta ?? {};
|
|
14
|
+
|
|
15
|
+
meta.title ??= [lesson.part?.title, lesson.chapter?.title, lesson.data.title].filter(Boolean).join(' / ');
|
|
16
|
+
meta.description ??= 'A TutorialKit interactive lesson';
|
|
17
|
+
|
|
18
|
+
const redirect = joinPaths(import.meta.env.BASE_URL, `/${slug}`);
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
<!doctype html>
|
|
22
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
23
|
+
<title>Redirecting to {redirect}</title>
|
|
24
|
+
<MetaTags slot="meta" meta={meta} />
|
|
25
|
+
<meta http-equiv="refresh" content=`0;url=${redirect}` />
|