@tutorialkit-rb/react 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/dist/BootScreen.d.ts +7 -0
- package/dist/BootScreen.js +37 -0
- package/dist/Button.d.ts +6 -0
- package/dist/Button.js +12 -0
- package/dist/Nav.d.ts +7 -0
- package/dist/Nav.js +38 -0
- package/dist/Panels/EditorPanel.d.ts +25 -0
- package/dist/Panels/EditorPanel.js +83 -0
- package/dist/Panels/PreviewPanel.d.ts +20 -0
- package/dist/Panels/PreviewPanel.js +166 -0
- package/dist/Panels/TerminalPanel.d.ts +7 -0
- package/dist/Panels/TerminalPanel.js +45 -0
- package/dist/Panels/WorkspacePanel.d.ts +14 -0
- package/dist/Panels/WorkspacePanel.js +166 -0
- package/dist/core/CodeMirrorEditor/BinaryContent.d.ts +1 -0
- package/dist/core/CodeMirrorEditor/BinaryContent.js +4 -0
- package/dist/core/CodeMirrorEditor/cm-theme.d.ts +8 -0
- package/dist/core/CodeMirrorEditor/cm-theme.js +167 -0
- package/dist/core/CodeMirrorEditor/indent.d.ts +2 -0
- package/dist/core/CodeMirrorEditor/indent.js +49 -0
- package/dist/core/CodeMirrorEditor/index.d.ts +39 -0
- package/dist/core/CodeMirrorEditor/index.js +201 -0
- package/dist/core/CodeMirrorEditor/languages.d.ts +3 -0
- package/dist/core/CodeMirrorEditor/languages.js +118 -0
- package/dist/core/CodeMirrorEditor/themes/vscode-dark.d.ts +2 -0
- package/dist/core/CodeMirrorEditor/themes/vscode-dark.js +75 -0
- package/dist/core/ContextMenu.d.ts +29 -0
- package/dist/core/ContextMenu.js +61 -0
- package/dist/core/Dialog.d.ts +15 -0
- package/dist/core/Dialog.js +12 -0
- package/dist/core/FileTree.d.ts +18 -0
- package/dist/core/FileTree.js +194 -0
- package/dist/core/Terminal/index.d.ts +15 -0
- package/dist/core/Terminal/index.js +57 -0
- package/dist/core/Terminal/theme.d.ts +2 -0
- package/dist/core/Terminal/theme.js +31 -0
- package/dist/core/types.d.ts +1 -0
- package/dist/core/types.js +1 -0
- package/dist/core.d.ts +3 -0
- package/dist/core.js +3 -0
- package/dist/hooks/useOutsideClick.d.ts +1 -0
- package/dist/hooks/useOutsideClick.js +14 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/styles/cm.css +100 -0
- package/dist/styles/nav.module.css +108 -0
- package/dist/styles/resize-panel.module.css +28 -0
- package/dist/styles/terminal.css +8 -0
- package/dist/utils/classnames.d.ts +16 -0
- package/dist/utils/classnames.js +47 -0
- package/dist/utils/debounce.d.ts +1 -0
- package/dist/utils/debounce.js +13 -0
- package/dist/utils/mobile.d.ts +1 -0
- package/dist/utils/mobile.js +4 -0
- package/package.json +116 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TutorialStore } from '@tutorialkit-rb/runtime';
|
|
2
|
+
interface Props {
|
|
3
|
+
className?: string;
|
|
4
|
+
tutorialStore: TutorialStore;
|
|
5
|
+
}
|
|
6
|
+
export declare function BootScreen({ className, tutorialStore }: Props): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { classNames } from './utils/classnames.js';
|
|
5
|
+
export function BootScreen({ className, tutorialStore }) {
|
|
6
|
+
const steps = useStore(tutorialStore.steps);
|
|
7
|
+
const { startWebContainerText, noPreviewNorStepsText } = tutorialStore.lesson?.data.i18n ?? {};
|
|
8
|
+
const bootStatus = useStore(tutorialStore.bootStatus);
|
|
9
|
+
// workaround to prevent the hydration error caused by bootStatus always being 'unknown' server-side
|
|
10
|
+
const [isClient, setIsClient] = useState(false);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
setIsClient(true);
|
|
13
|
+
}, []);
|
|
14
|
+
return (_jsx("div", { className: classNames('flex-grow w-full flex justify-center items-center text-sm', className), children: isClient && bootStatus === 'blocked' ? (_jsx(Button, { onClick: () => tutorialStore.unblockBoot(), children: startWebContainerText })) : steps ? (_jsx("ul", { className: "space-y-1", children: steps.map((step, index) => (_jsxs("li", { className: "flex items-center", children: [step.status === 'idle' ? (_jsx("div", { className: "inline-block mr-2 i-ph-circle-duotone scale-120 text-tk-elements-status-disabled-iconColor" })) : step.status === 'running' ? (_jsx("div", { className: "inline-block mr-2 i-svg-spinners-90-ring-with-bg scale-105 text-tk-elements-status-active-iconColor" })) : step.status === 'completed' ? (_jsx("div", { className: "inline-block mr-2 i-ph-check-circle-duotone scale-120 text-tk-elements-status-positive-iconColor" })) : step.status === 'failed' ? (_jsx("div", { className: "inline-block mr-2 i-ph-x-circle-duotone scale-120 text-tk-elements-status-negative-iconColor" })) : (_jsx("div", { className: "inline-block mr-2 i-ph-minus-circle-duotone scale-120 text-tk-elements-status-skipped-iconColor" })), _jsx("span", { className: toTextColor(step.status), children: step.title })] }, index))) })) : (noPreviewNorStepsText) }));
|
|
15
|
+
}
|
|
16
|
+
function toTextColor(status) {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'completed': {
|
|
19
|
+
return 'text-tk-elements-status-positive-textColor';
|
|
20
|
+
}
|
|
21
|
+
case 'failed': {
|
|
22
|
+
return 'text-tk-elements-status-negative-textColor';
|
|
23
|
+
}
|
|
24
|
+
case 'idle': {
|
|
25
|
+
return 'text-tk-elements-status-disabled-textColor';
|
|
26
|
+
}
|
|
27
|
+
case 'running': {
|
|
28
|
+
return 'text-tk-elements-status-active-textColor';
|
|
29
|
+
}
|
|
30
|
+
case 'skipped': {
|
|
31
|
+
return 'text-tk-elements-status-skipped-textColor';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function Button({ children, onClick }) {
|
|
36
|
+
return (_jsx("button", { className: "flex font-500 disabled:opacity-32 items-center text-sm ml-2 px-4 py-1 rounded-md bg-tk-elements-bootScreen-primaryButton-backgroundColor text-tk-elements-bootScreen-primaryButton-textColor hover:bg-tk-elements-bootScreen-primaryButton-backgroundColorHover hover:text-tk-elements-bootScreen-primaryButton-textColorHover", onClick: onClick, children: children }));
|
|
37
|
+
}
|
package/dist/Button.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ComponentProps } from 'react';
|
|
2
|
+
interface Props extends ComponentProps<'button'> {
|
|
3
|
+
variant?: 'primary' | 'secondary';
|
|
4
|
+
}
|
|
5
|
+
export declare const Button: import("react").ForwardRefExoticComponent<Omit<Props, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
|
|
6
|
+
export {};
|
package/dist/Button.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { classNames } from './utils/classnames.js';
|
|
4
|
+
export const Button = forwardRef(({ className, variant = 'primary', ...props }, ref) => {
|
|
5
|
+
return (_jsx("button", { ref: ref, ...props, className: classNames(className, 'flex items-center font-500 text-sm px-4 py-1 rounded-md disabled:opacity-32', variant === 'primary' &&
|
|
6
|
+
'bg-tk-elements-primaryButton-backgroundColor text-tk-elements-primaryButton-textColor', !props.disabled &&
|
|
7
|
+
variant === 'primary' &&
|
|
8
|
+
'hover:bg-tk-elements-primaryButton-backgroundColorHover hover:text-tk-elements-primaryButton-textColorHover', variant === 'secondary' &&
|
|
9
|
+
'bg-tk-elements-secondaryButton-backgroundColor text-tk-elements-secondaryButton-textColor', !props.disabled &&
|
|
10
|
+
variant === 'secondary' &&
|
|
11
|
+
'hover:bg-tk-elements-secondaryButton-backgroundColorHover hover:text-tk-elements-secondaryButton-textColorHover') }));
|
|
12
|
+
});
|
package/dist/Nav.d.ts
ADDED
package/dist/Nav.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as Accordion from '@radix-ui/react-accordion';
|
|
3
|
+
import { interpolateString } from '@tutorialkit-rb/types';
|
|
4
|
+
import { AnimatePresence, cubicBezier, motion } from 'framer-motion';
|
|
5
|
+
import { useRef, useState } from 'react';
|
|
6
|
+
import { useOutsideClick } from './hooks/useOutsideClick.js';
|
|
7
|
+
import navStyles from './styles/nav.module.css';
|
|
8
|
+
import { classNames } from './utils/classnames.js';
|
|
9
|
+
const dropdownEasing = cubicBezier(0.4, 0, 0.2, 1);
|
|
10
|
+
export function Nav({ lesson: currentLesson, navList }) {
|
|
11
|
+
const menuRef = useRef(null);
|
|
12
|
+
const [showDropdown, setShowDropdown] = useState(false);
|
|
13
|
+
const { prev, next } = currentLesson;
|
|
14
|
+
const activeItems = [
|
|
15
|
+
currentLesson.part?.id || currentLesson.id,
|
|
16
|
+
currentLesson.chapter?.id || currentLesson.id,
|
|
17
|
+
currentLesson.id,
|
|
18
|
+
];
|
|
19
|
+
useOutsideClick(menuRef, () => setShowDropdown(false));
|
|
20
|
+
return (_jsxs("header", { className: "grid grid-cols-1 sm:grid-cols-[auto_minmax(0,1fr)_auto] h-[82px] gap-0.5 py-4 px-1 text-sm", children: [_jsx("a", { className: classNames('hidden sm:flex cursor-pointer h-full items-center justify-center w-[40px] text-tk-elements-breadcrumbs-navButton-iconColor', !prev ? 'opacity-32 pointer-events-none' : 'hover:text-tk-elements-breadcrumbs-navButton-iconColorHover'), "aria-disabled": !prev, href: prev?.href, children: _jsx("span", { className: "i-ph-arrow-left scale-120" }) }), _jsx("div", { className: "relative", children: _jsxs("div", { "data-state": `${showDropdown ? 'open' : 'closed'}`, className: classNames(navStyles.NavContainer, 'absolute mx-4 sm:mx-0 z-1 left-0 right-0 rounded-[8px] border overflow-hidden z-50'), ref: menuRef, children: [_jsxs("button", { className: classNames(navStyles.ToggleButton, 'flex-1 flex items-center text-left py-3 px-3 w-full overflow-hidden'), onClick: () => setShowDropdown(!showDropdown), children: [_jsxs("div", { className: "flex items-center gap-1 font-light truncate", children: [currentLesson.part && (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:inline", children: currentLesson.part.title }), _jsx("span", { className: classNames('hidden sm:inline', navStyles.Divider), children: "/" })] })), currentLesson.chapter && (_jsxs(_Fragment, { children: [_jsx("span", { className: "hidden sm:inline", children: currentLesson.chapter.title }), _jsx("span", { className: classNames('hidden sm:inline', navStyles.Divider), children: "/" })] })), _jsx("strong", { className: "font-semibold", children: currentLesson.data.title })] }), _jsx("div", { className: classNames(navStyles.ToggleButtonIcon, 'ml-auto w-[30px]', {
|
|
21
|
+
'i-ph-caret-up-bold': showDropdown,
|
|
22
|
+
'i-ph-caret-down-bold': !showDropdown,
|
|
23
|
+
}) })] }), _jsx(AnimatePresence, { children: showDropdown && (_jsx(motion.nav, { initial: { height: 0, y: 0 }, animate: { height: 'auto', y: 0 }, exit: { height: 0, y: 0 }, transition: { duration: 0.2, ease: dropdownEasing }, className: " overflow-hidden transition-theme bg-tk-elements-breadcrumbs-dropdown-backgroundColor", children: _jsx(NavListComponent, { className: "py-5 pl-5 border-t border-tk-elements-breadcrumbs-dropdown-borderColor overflow-auto max-h-[60dvh]", items: navList, activeItems: activeItems, i18n: currentLesson.data.i18n, level: 0 }) })) })] }) }), _jsx("a", { className: classNames('hidden sm:flex cursor-pointer h-full items-center justify-center w-[40px] text-tk-elements-breadcrumbs-navButton-iconColor', !next ? 'opacity-32 pointer-events-none' : 'hover:text-tk-elements-breadcrumbs-navButton-iconColorHover'), "aria-disabled": !next, href: next?.href, children: _jsx("span", { className: "i-ph-arrow-right scale-120" }) })] }));
|
|
24
|
+
}
|
|
25
|
+
function NavListComponent({ items, level, activeItems, className, i18n, }) {
|
|
26
|
+
return (_jsx(Accordion.Root, { asChild: true, collapsible: true, type: "single", defaultValue: `${level}-${activeItems[level]}`, children: _jsx("ul", { className: classNames(className), children: items.map((item, index) => (_jsx(NavListItem, { ...item, index: index, level: level, activeItems: activeItems, i18n: i18n }, item.id))) }) }));
|
|
27
|
+
}
|
|
28
|
+
function NavListItem({ level, type, index, i18n, activeItems, id, title, href, sections }) {
|
|
29
|
+
const isActive = activeItems[level] === id;
|
|
30
|
+
if (!sections) {
|
|
31
|
+
return (_jsx("li", { className: "mr-3 pl-4.5", children: _jsx("a", { className: classNames('w-full inline-block border border-transparent pr-3 transition-theme text-tk-elements-breadcrumbs-dropdown-lessonTextColor hover:text-tk-elements-breadcrumbs-dropdown-lessonTextColorHover px-3 py-1 rounded-1', isActive
|
|
32
|
+
? 'font-semibold text-tk-elements-breadcrumbs-dropdown-lessonTextColorSelected bg-tk-elements-breadcrumbs-dropdown-lessonBackgroundColorSelected'
|
|
33
|
+
: 'bg-tk-elements-breadcrumbs-dropdown-lessonBackgroundColor'), href: href, children: title }) }));
|
|
34
|
+
}
|
|
35
|
+
return (_jsx(Accordion.Item, { asChild: true, value: `${level}-${id}`, children: _jsxs("li", { className: "mt-1.5", children: [_jsxs(Accordion.Trigger, { className: classNames(navStyles.AccordionTrigger, 'flex items-center gap-1 w-full hover:text-primary-700', {
|
|
36
|
+
[`font-semibold ${navStyles.AccordionTriggerActive}`]: isActive,
|
|
37
|
+
}), children: [_jsx("span", { className: `${navStyles.AccordionTriggerIcon} i-ph-caret-right-bold scale-80 text-gray-300` }), _jsx("span", { children: type === 'part' ? interpolateString(i18n.partTemplate, { index: index + 1, title }) : title })] }), _jsx(Accordion.Content, { className: navStyles.AccordionContent, children: _jsx(NavListComponent, { className: "mt-1.5 pl-4.5", items: sections, activeItems: activeItems, i18n: i18n, level: level + 1 }) })] }) }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { I18n } from '@tutorialkit-rb/types';
|
|
2
|
+
import { type ComponentProps } from 'react';
|
|
3
|
+
import { type EditorDocument, type OnChangeCallback as OnEditorChange, type OnScrollCallback as OnEditorScroll } from '../core/CodeMirrorEditor/index.js';
|
|
4
|
+
import { FileTree } from '../core/FileTree.js';
|
|
5
|
+
import type { Theme } from '../core/types.js';
|
|
6
|
+
interface Props {
|
|
7
|
+
theme: Theme;
|
|
8
|
+
id: unknown;
|
|
9
|
+
files: ComponentProps<typeof FileTree>['files'];
|
|
10
|
+
i18n: I18n;
|
|
11
|
+
hideRoot?: boolean;
|
|
12
|
+
fileTreeScope?: string;
|
|
13
|
+
showFileTree?: boolean;
|
|
14
|
+
helpAction?: 'solve' | 'reset';
|
|
15
|
+
editorDocument?: EditorDocument;
|
|
16
|
+
selectedFile?: string | undefined;
|
|
17
|
+
allowEditPatterns?: ComponentProps<typeof FileTree>['allowEditPatterns'];
|
|
18
|
+
onEditorChange?: OnEditorChange;
|
|
19
|
+
onEditorScroll?: OnEditorScroll;
|
|
20
|
+
onHelpClick?: () => void;
|
|
21
|
+
onFileSelect?: (value?: string) => void;
|
|
22
|
+
onFileTreeChange?: ComponentProps<typeof FileTree>['onFileChange'];
|
|
23
|
+
}
|
|
24
|
+
export declare function EditorPanel({ theme, id, files, i18n, hideRoot, fileTreeScope, showFileTree, helpAction, editorDocument, selectedFile, allowEditPatterns, onEditorChange, onEditorScroll, onHelpClick, onFileSelect, onFileTreeChange, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
4
|
+
import { CodeMirrorEditor, } from '../core/CodeMirrorEditor/index.js';
|
|
5
|
+
import { FileTree } from '../core/FileTree.js';
|
|
6
|
+
import resizePanelStyles from '../styles/resize-panel.module.css';
|
|
7
|
+
import { isMobile } from '../utils/mobile.js';
|
|
8
|
+
const DEFAULT_FILE_TREE_SIZE = 25;
|
|
9
|
+
export function EditorPanel({ theme, id, files, i18n, hideRoot, fileTreeScope, showFileTree = true, helpAction, editorDocument, selectedFile, allowEditPatterns, onEditorChange, onEditorScroll, onHelpClick, onFileSelect, onFileTreeChange, }) {
|
|
10
|
+
const fileTreePanelRef = useRef(null);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const { current: fileTreePanel } = fileTreePanelRef;
|
|
13
|
+
if (!fileTreePanel) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (showFileTree) {
|
|
17
|
+
if (fileTreePanel.isCollapsed()) {
|
|
18
|
+
fileTreePanel.resize(DEFAULT_FILE_TREE_SIZE);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (!showFileTree) {
|
|
22
|
+
fileTreePanel.collapse();
|
|
23
|
+
}
|
|
24
|
+
}, [id]);
|
|
25
|
+
return (_jsxs(PanelGroup, { className: "bg-tk-elements-panel-backgroundColor", direction: "horizontal", children: [_jsxs(Panel, { className: "flex flex-col", collapsible: true, defaultSize: 0, minSize: 10, ref: fileTreePanelRef, children: [_jsx("div", { className: "panel-header border-r border-b border-tk-elements-app-borderColor", children: _jsxs("div", { className: "panel-title", children: [_jsx("div", { className: "panel-icon i-ph-tree-structure-duotone shrink-0" }), _jsx("span", { className: "text-sm", children: i18n.filesTitleText })] }) }), _jsx(FileTree, { className: "flex flex-col flex-grow py-2 border-r border-tk-elements-app-borderColor text-sm overflow-y-auto overflow-x-hidden", i18n: i18n, selectedFile: selectedFile, hideRoot: hideRoot ?? true, files: files, scope: fileTreeScope, allowEditPatterns: allowEditPatterns, onFileSelect: onFileSelect, onFileChange: onFileTreeChange })] }), _jsx(PanelResizeHandle, { disabled: !showFileTree, className: resizePanelStyles.PanelResizeHandle, hitAreaMargins: { fine: 8, coarse: 8 } }), _jsxs(Panel, { className: "flex flex-col", defaultSize: 100, minSize: 10, children: [_jsx(FileTab, { i18n: i18n, editorDocument: editorDocument, onHelpClick: onHelpClick, helpAction: helpAction }), _jsx("div", { className: "h-full flex-1 overflow-hidden", children: _jsx(CodeMirrorEditor, { className: "h-full", theme: theme, id: id, doc: editorDocument, autoFocusOnDocumentChange: !isMobile(), onScroll: onEditorScroll, onChange: onEditorChange }) })] })] }));
|
|
26
|
+
}
|
|
27
|
+
function FileTab({ i18n, editorDocument, helpAction, onHelpClick }) {
|
|
28
|
+
const filePath = editorDocument?.filePath;
|
|
29
|
+
const fileName = filePath?.split('/').at(-1) ?? '';
|
|
30
|
+
const icon = fileName ? getFileIcon(fileName) : '';
|
|
31
|
+
return (_jsxs("div", { className: "panel-header border-b border-tk-elements-app-borderColor flex justify-between", children: [_jsxs("div", { className: "panel-title", children: [_jsx("div", { className: `panel-icon scale-125 ${icon}` }), _jsx("span", { className: "text-sm", children: fileName })] }), !!helpAction && (_jsxs("button", { onClick: onHelpClick, disabled: !onHelpClick, className: "panel-button px-2 py-0.5 -mr-1 -my-1", children: [helpAction === 'solve' && _jsx("div", { className: "i-ph-lightbulb-duotone text-lg" }), helpAction === 'solve' && i18n.solveButtonText, helpAction === 'reset' && _jsx("div", { className: "i-ph-clock-counter-clockwise-duotone" }), helpAction === 'reset' && i18n.resetButtonText] }))] }));
|
|
32
|
+
}
|
|
33
|
+
function getFileIcon(fileName) {
|
|
34
|
+
const extension = fileName.split('.').at(-1);
|
|
35
|
+
if (!extension) {
|
|
36
|
+
console.error('Cannot infer file type');
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
switch (extension) {
|
|
40
|
+
case 'ts': {
|
|
41
|
+
return 'i-languages-ts?mask';
|
|
42
|
+
}
|
|
43
|
+
case 'cjs':
|
|
44
|
+
case 'mjs':
|
|
45
|
+
case 'js': {
|
|
46
|
+
return 'i-languages-js?mask';
|
|
47
|
+
}
|
|
48
|
+
case 'html': {
|
|
49
|
+
return 'i-languages-html?mask';
|
|
50
|
+
}
|
|
51
|
+
case 'css': {
|
|
52
|
+
return 'i-languages-css?mask';
|
|
53
|
+
}
|
|
54
|
+
case 'scss':
|
|
55
|
+
case 'sass': {
|
|
56
|
+
return 'i-languages-sass?mask';
|
|
57
|
+
}
|
|
58
|
+
case 'md': {
|
|
59
|
+
return 'i-languages-markdown?mask';
|
|
60
|
+
}
|
|
61
|
+
case 'json': {
|
|
62
|
+
return 'i-languages-json?mask';
|
|
63
|
+
}
|
|
64
|
+
case 'rb': {
|
|
65
|
+
return 'i-languages-ruby?mask';
|
|
66
|
+
}
|
|
67
|
+
case 'ru': {
|
|
68
|
+
return 'i-languages-ruby?mask';
|
|
69
|
+
}
|
|
70
|
+
case 'erb': {
|
|
71
|
+
return 'i-languages-erb?mask';
|
|
72
|
+
}
|
|
73
|
+
case 'gif':
|
|
74
|
+
case 'jpg':
|
|
75
|
+
case 'jpeg':
|
|
76
|
+
case 'png': {
|
|
77
|
+
return 'i-ph-image';
|
|
78
|
+
}
|
|
79
|
+
default: {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { TutorialStore } from '@tutorialkit-rb/runtime';
|
|
2
|
+
import type { I18n } from '@tutorialkit-rb/types';
|
|
3
|
+
interface Props {
|
|
4
|
+
showToggleTerminal?: boolean;
|
|
5
|
+
toggleTerminal?: () => void;
|
|
6
|
+
tutorialStore: TutorialStore;
|
|
7
|
+
i18n: I18n;
|
|
8
|
+
}
|
|
9
|
+
export type ImperativePreviewHandle = {
|
|
10
|
+
reload: () => void;
|
|
11
|
+
};
|
|
12
|
+
export declare const PreviewPanel: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<ImperativePreviewHandle>>>;
|
|
13
|
+
declare global {
|
|
14
|
+
interface Document {
|
|
15
|
+
featurePolicy: {
|
|
16
|
+
allowedFeatures(): string[];
|
|
17
|
+
} | undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
|
+
import { reloadPreview } from '@webcontainer/api/utils';
|
|
4
|
+
import { createElement, forwardRef, memo, useCallback, useState, useEffect, useImperativeHandle, useRef } from 'react';
|
|
5
|
+
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
|
6
|
+
import { BootScreen } from '../BootScreen.js';
|
|
7
|
+
import resizePanelStyles from '../styles/resize-panel.module.css';
|
|
8
|
+
import { classNames } from '../utils/classnames.js';
|
|
9
|
+
const previewsContainer = globalThis.document ? document.getElementById('previews-container') : {};
|
|
10
|
+
export const PreviewPanel = memo(forwardRef(({ showToggleTerminal, toggleTerminal, i18n, tutorialStore }, ref) => {
|
|
11
|
+
const expectedPreviews = useStore(tutorialStore.previews);
|
|
12
|
+
const iframeRefs = useRef([]);
|
|
13
|
+
const onResize = useCallback(() => {
|
|
14
|
+
for (const { ref, container } of iframeRefs.current) {
|
|
15
|
+
if (!ref || !container) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const { left, top, width, height } = container.getBoundingClientRect();
|
|
19
|
+
ref.style.left = `${left}px`;
|
|
20
|
+
ref.style.top = `${top}px`;
|
|
21
|
+
ref.style.height = `${height}px`;
|
|
22
|
+
ref.style.width = `${width}px`;
|
|
23
|
+
}
|
|
24
|
+
}, []);
|
|
25
|
+
const activePreviewsCount = expectedPreviews.reduce((count, preview) => (preview.ready ? count + 1 : count), 0);
|
|
26
|
+
const hasPreviews = activePreviewsCount > 0;
|
|
27
|
+
useImperativeHandle(ref, () => ({
|
|
28
|
+
reload: () => {
|
|
29
|
+
for (const iframe of iframeRefs.current) {
|
|
30
|
+
if (iframe.ref) {
|
|
31
|
+
iframe.ref.src = iframe.ref.src;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}), []);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// we update the iframes position at max fps if we have any
|
|
38
|
+
if (hasPreviews) {
|
|
39
|
+
const cancel = requestAnimationFrameLoop(onResize);
|
|
40
|
+
previewsContainer.style.display = 'block';
|
|
41
|
+
return () => {
|
|
42
|
+
previewsContainer.style.display = 'none';
|
|
43
|
+
cancel();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}, [hasPreviews]);
|
|
48
|
+
adjustLength(iframeRefs.current, activePreviewsCount, newIframeRef);
|
|
49
|
+
preparePreviewsContainer(activePreviewsCount);
|
|
50
|
+
// update preview refs
|
|
51
|
+
for (const [index, iframeRef] of iframeRefs.current.entries()) {
|
|
52
|
+
if (!iframeRef.ref) {
|
|
53
|
+
iframeRef.ref = previewsContainer.children.item(index);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!hasPreviews) {
|
|
57
|
+
return (_jsxs("div", { className: "panel-container transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor", children: [_jsxs("div", { className: "panel-header border-b border-tk-elements-app-borderColor justify-between", children: [_jsxs("div", { className: "panel-title", children: [_jsx("div", { className: "panel-icon i-ph-lightning-duotone" }), _jsx("span", { className: "text-sm", children: i18n.prepareEnvironmentTitleText })] }), showToggleTerminal && (_jsxs("button", { className: "panel-button px-2 py-0.5 -mr-1 -my-1", title: "Toggle Terminal", onClick: () => toggleTerminal?.(), children: [_jsx("span", { className: "panel-button-icon i-ph-terminal-window-duotone" }), _jsx("span", { className: "text-sm", children: i18n.toggleTerminalButtonText })] }))] }), _jsx(BootScreen, { tutorialStore: tutorialStore })] }));
|
|
58
|
+
}
|
|
59
|
+
const previews = expectedPreviews.filter((preview) => preview.ready);
|
|
60
|
+
const defaultSize = 100 / previews.length;
|
|
61
|
+
const minSize = 20;
|
|
62
|
+
const children = [];
|
|
63
|
+
for (const [index, preview] of previews.entries()) {
|
|
64
|
+
children.push(_jsx(Panel, { defaultSize: defaultSize, minSize: minSize, children: _jsx(Preview, { iframe: iframeRefs.current[index], preview: preview, previewCount: previews.length, first: index === 0, last: index === previews.length - 1, toggleTerminal: toggleTerminal, i18n: i18n }) }));
|
|
65
|
+
if (index !== previews.length - 1) {
|
|
66
|
+
children.push(_jsx(PanelResizeHandle, { className: resizePanelStyles.PanelResizeHandle }));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return createElement(PanelGroup, { direction: 'horizontal' }, ...children);
|
|
70
|
+
}));
|
|
71
|
+
PreviewPanel.displayName = 'PreviewPanel';
|
|
72
|
+
function Preview({ preview, iframe, previewCount, first, last, toggleTerminal, i18n }) {
|
|
73
|
+
const previewContainerRef = useRef(null);
|
|
74
|
+
const [currentUrlPath, setCurrentUrlPath] = useState('');
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!iframe.ref) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
iframe.container = previewContainerRef.current;
|
|
80
|
+
if (preview.url) {
|
|
81
|
+
iframe.ref.src = preview.url;
|
|
82
|
+
}
|
|
83
|
+
if (preview.title) {
|
|
84
|
+
iframe.ref.title = preview.title;
|
|
85
|
+
}
|
|
86
|
+
}, [preview.url, iframe.ref]);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!iframe.ref) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const handleLoadMessage = (event) => {
|
|
92
|
+
if (event.data && event.data.type === '$locationChange') {
|
|
93
|
+
const iframeSrc = event.data.location.href;
|
|
94
|
+
if (iframeSrc) {
|
|
95
|
+
const url = new URL(iframeSrc);
|
|
96
|
+
const path = url.pathname.replace('/', '') + url.search;
|
|
97
|
+
setCurrentUrlPath(path);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
window.addEventListener('message', handleLoadMessage);
|
|
102
|
+
return () => {
|
|
103
|
+
window.removeEventListener('message', handleLoadMessage);
|
|
104
|
+
};
|
|
105
|
+
}, [iframe.ref]);
|
|
106
|
+
function reload() {
|
|
107
|
+
if (iframe.ref) {
|
|
108
|
+
reloadPreview(iframe.ref);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return (_jsxs("div", { className: "panel-container", children: [_jsxs("div", { className: classNames('panel-header border-b border-tk-elements-app-borderColor justify-between', {
|
|
112
|
+
'border-l border-tk-elements-app-borderColor': !first,
|
|
113
|
+
}), children: [_jsxs("div", { className: "panel-title", children: [_jsx("button", { onClick: reload, title: i18n.reloadPreviewTitle, className: "panel-button rounded-full p-1.5 -my-1.5 -ml-2", children: _jsx("div", { className: "panel-icon i-ph-arrow-clockwise" }) }), _jsx("span", { className: "text-sm truncate", children: previewTitle(preview, previewCount, i18n) })] }), last && (_jsxs("button", { className: "panel-button px-2 py-0.5 -mr-1 -my-1", title: "Toggle Terminal", onClick: () => toggleTerminal?.(), children: [_jsx("div", { className: "panel-button-icon i-ph-terminal-window-duotone" }), _jsx("span", { className: "text-sm", children: i18n.toggleTerminalButtonText })] }))] }), _jsx("div", { ref: previewContainerRef, className: classNames('h-full w-full flex justify-center items-center', {
|
|
114
|
+
'border-l border-tk-elements-previews-borderColor': !first,
|
|
115
|
+
}) })] }));
|
|
116
|
+
}
|
|
117
|
+
function requestAnimationFrameLoop(loop) {
|
|
118
|
+
let handle;
|
|
119
|
+
const callback = () => {
|
|
120
|
+
loop();
|
|
121
|
+
handle = requestAnimationFrame(callback);
|
|
122
|
+
};
|
|
123
|
+
handle = requestAnimationFrame(callback);
|
|
124
|
+
return () => {
|
|
125
|
+
cancelAnimationFrame(handle);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function previewTitle(preview, previewCount, i18n) {
|
|
129
|
+
if (preview.title) {
|
|
130
|
+
return preview.title;
|
|
131
|
+
}
|
|
132
|
+
if (previewCount === 1) {
|
|
133
|
+
return i18n.defaultPreviewTitleText;
|
|
134
|
+
}
|
|
135
|
+
return `Preview on port ${preview.port}`;
|
|
136
|
+
}
|
|
137
|
+
function preparePreviewsContainer(previewCount) {
|
|
138
|
+
while (previewsContainer.childElementCount < previewCount) {
|
|
139
|
+
const iframe = document.createElement('iframe');
|
|
140
|
+
iframe.className = 'absolute z-10';
|
|
141
|
+
iframe.allow =
|
|
142
|
+
document.featurePolicy?.allowedFeatures().join('; ') ??
|
|
143
|
+
'magnetometer; accelerometer; gyroscope; geolocation; microphone; camera; payment; autoplay; serial; xr-spatial-tracking; cross-origin-isolated';
|
|
144
|
+
previewsContainer.appendChild(iframe);
|
|
145
|
+
}
|
|
146
|
+
for (let i = 0; i < previewsContainer.childElementCount; i++) {
|
|
147
|
+
const preview = previewsContainer.children.item(i);
|
|
148
|
+
if (i < previewCount) {
|
|
149
|
+
preview.classList.remove('hidden');
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
preview.classList.add('hidden');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function newIframeRef() {
|
|
157
|
+
return { ref: undefined, container: undefined };
|
|
158
|
+
}
|
|
159
|
+
function adjustLength(array, expectedSize, newElement) {
|
|
160
|
+
while (array.length < expectedSize) {
|
|
161
|
+
array.push(newElement());
|
|
162
|
+
}
|
|
163
|
+
while (array.length > expectedSize) {
|
|
164
|
+
array.pop();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TutorialStore } from '@tutorialkit-rb/runtime';
|
|
2
|
+
interface TerminalPanelProps {
|
|
3
|
+
theme: 'dark' | 'light';
|
|
4
|
+
tutorialStore: TutorialStore;
|
|
5
|
+
}
|
|
6
|
+
export declare function TerminalPanel({ theme, tutorialStore }: TerminalPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useStore } from '@nanostores/react';
|
|
3
|
+
import { lazy, Suspense, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { classNames } from '../utils/classnames.js';
|
|
5
|
+
const Terminal = lazy(() => import('../core/Terminal/index.js'));
|
|
6
|
+
const ICON_MAP = new Map([
|
|
7
|
+
['output', 'i-ph-newspaper-duotone'],
|
|
8
|
+
['terminal', 'i-ph-terminal-window-duotone'],
|
|
9
|
+
]);
|
|
10
|
+
export function TerminalPanel({ theme, tutorialStore }) {
|
|
11
|
+
const terminalConfig = useStore(tutorialStore.terminalConfig);
|
|
12
|
+
const terminalRefs = useRef({});
|
|
13
|
+
const [domLoaded, setDomLoaded] = useState(false);
|
|
14
|
+
// select the terminal tab by default
|
|
15
|
+
const [tabIndex, setTabIndex] = useState(terminalConfig.activePanel);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setDomLoaded(true);
|
|
18
|
+
}, []);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setTabIndex(terminalConfig.activePanel);
|
|
21
|
+
}, [terminalConfig]);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
return tutorialStore.themeRef.subscribe(() => {
|
|
24
|
+
for (const ref of Object.values(terminalRefs.current)) {
|
|
25
|
+
ref.reloadStyles();
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}, []);
|
|
29
|
+
return (_jsxs("div", { className: "panel-container transition-theme bg-tk-elements-panel-backgroundColor text-tk-elements-panel-textColor", children: [_jsx("div", { className: "panel-tabs-header overflow-x-hidden", children: _jsx("div", { className: "panel-title w-full", children: _jsx("ul", { className: "flex h-full transition-theme border-b border-tk-elements-app-borderColor w-full", role: "tablist", "aria-orientation": "horizontal", children: terminalConfig.panels.map(({ type, title }, index) => {
|
|
30
|
+
const selected = tabIndex === index;
|
|
31
|
+
return (_jsx("li", { children: _jsxs("button", { className: classNames('group h-full px-4 flex items-center gap-1.5 whitespace-nowrap text-sm position-relative transition-theme border-r border-tk-elements-panel-headerTab-borderColor', {
|
|
32
|
+
'bg-tk-elements-panel-headerTab-backgroundColor text-tk-elements-panel-headerTab-textColor hover:bg-tk-elements-panel-headerTab-backgroundColorHover hover:text-tk-elements-panel-headerTab-textColorHover hover:border-tk-elements-panel-headerTab-borderColorHover': !selected,
|
|
33
|
+
'bg-tk-elements-panel-headerTab-backgroundColorActive text-tk-elements-panel-headerTab-textColorActive border-tk-elements-panel-headerTab-borderColorActive': selected,
|
|
34
|
+
'shadow-[0px_1px_0px_0px] shadow-tk-elements-panel-headerTab-backgroundColorActive': selected,
|
|
35
|
+
'border-l': index > 0,
|
|
36
|
+
}), title: title, id: `tk-terminal-tab-${index}`, role: "tab", "aria-selected": selected, "aria-controls": `tk-terminal-tapbanel-${index}`, onClick: () => setTabIndex(index), children: [_jsx("span", { className: classNames(`text-tk-elements-panel-headerTab-iconColor ${ICON_MAP.get(type) ?? ''}`, {
|
|
37
|
+
'group-hover:text-tk-elements-panel-headerTab-iconColorHover': !selected,
|
|
38
|
+
'text-tk-elements-panel-headerTab-iconColorActive': selected,
|
|
39
|
+
}) }), title] }) }, index));
|
|
40
|
+
}) }) }) }), _jsx("div", { className: "h-full overflow-hidden", children: domLoaded && (_jsx(Suspense, { children: terminalConfig.panels.map(({ id, type }, index) => (_jsx(Terminal, { role: "tabpanel", id: `tk-terminal-tapbanel-${index}`, "aria-labelledby": `tk-terminal-tab-${index}`, className: tabIndex !== index ? 'hidden h-full' : 'h-full', theme: theme, readonly: type === 'output', ref: (ref) => (terminalRefs.current[index] = ref), onTerminalReady: (terminal) => {
|
|
41
|
+
tutorialStore.attachTerminal(id, terminal);
|
|
42
|
+
}, onTerminalResize: (cols, rows) => {
|
|
43
|
+
tutorialStore.onTerminalResize(cols, rows);
|
|
44
|
+
} }, id))) })) })] }));
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TutorialStore } from '@tutorialkit-rb/runtime';
|
|
2
|
+
import { type ComponentProps } from 'react';
|
|
3
|
+
import { DialogProvider } from '../core/Dialog.js';
|
|
4
|
+
import type { Theme } from '../core/types.js';
|
|
5
|
+
interface Props {
|
|
6
|
+
tutorialStore: TutorialStore;
|
|
7
|
+
theme: Theme;
|
|
8
|
+
dialog: NonNullable<ComponentProps<typeof DialogProvider>['value']>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* This component is the orchestrator between various interactive components.
|
|
12
|
+
*/
|
|
13
|
+
export declare function WorkspacePanel({ tutorialStore, theme, dialog }: Props): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|