├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ └── feature_request.yaml ├── actions │ ├── resolve-release-version │ │ └── action.yml │ └── setup-and-build │ │ └── action.yml ├── commit-convention.md └── workflows │ ├── ci.yaml │ ├── integration-test-cli.yaml │ ├── prepare-release.yaml │ ├── publish-commit.yaml │ ├── publish-create-tutorial.yaml │ ├── publish-docs.yaml │ ├── publish-release.yaml │ └── semantic-pr.yaml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .tool-versions ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── demo │ ├── .gitignore │ ├── README.md │ ├── _headers │ ├── astro.config.ts │ ├── icons │ │ └── languages │ │ │ ├── css.svg │ │ │ ├── html.svg │ │ │ ├── js.svg │ │ │ ├── json.svg │ │ │ ├── markdown.svg │ │ │ ├── sass.svg │ │ │ └── ts.svg │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ ├── images │ │ │ ├── accent-color.png │ │ │ └── fieldset-styles.png │ │ ├── logo-dark.svg │ │ └── logo.svg │ ├── src │ │ ├── components │ │ │ ├── Github.tsx │ │ │ └── TopBar.astro │ │ ├── content │ │ │ ├── config.ts │ │ │ └── tutorial │ │ │ │ ├── 1-forms-css │ │ │ │ ├── 1-introduction │ │ │ │ │ ├── 1-welcome │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ ├── style.css │ │ │ │ │ │ │ └── welcome.css │ │ │ │ │ │ └── content.md │ │ │ │ │ └── meta.md │ │ │ │ ├── 2-colors │ │ │ │ │ ├── 1-accent-color │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ └── content.md │ │ │ │ │ ├── 2-progressbar │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ └── content.md │ │ │ │ │ ├── 3-more-progressbar │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ └── content.md │ │ │ │ │ ├── 4-handle-firefox │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ └── content.md │ │ │ │ │ └── meta.md │ │ │ │ ├── 3-fieldset │ │ │ │ │ ├── 1-fieldset-element │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ └── index.html │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ └── index.html │ │ │ │ │ │ └── content.md │ │ │ │ │ ├── 2-a-legend │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ └── index.html │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ └── index.html │ │ │ │ │ │ └── content.md │ │ │ │ │ ├── 3-fieldset-styling │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ └── content.md │ │ │ │ │ ├── 5-focus-within │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ └── content.md │ │ │ │ │ ├── 6-the-end │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ └── style.css │ │ │ │ │ │ └── content.md │ │ │ │ │ └── meta.md │ │ │ │ └── meta.md │ │ │ │ └── meta.md │ │ ├── env.d.ts │ │ └── templates │ │ │ └── default │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── servor │ │ │ ├── cli.js │ │ │ ├── servor.js │ │ │ └── utils │ │ │ │ ├── common.js │ │ │ │ ├── directoryListing.js │ │ │ │ ├── mimeTypes.js │ │ │ │ └── openBrowser.js │ │ │ └── style.css │ ├── tsconfig.json │ └── uno.config.ts └── tutorialkit.dev │ ├── .gitignore │ ├── README.md │ ├── _headers │ ├── astro.config.ts │ ├── package.json │ ├── public │ ├── favicon.svg │ └── tutorialkit-opengraph.png │ ├── src │ ├── assets │ │ ├── brand │ │ │ ├── tutorialkit-logo-dark.svg │ │ │ └── tutorialkit-logo-light.svg │ │ ├── houston.webp │ │ └── tutorialkit-themes.png │ ├── components │ │ ├── Buttons │ │ │ └── Button │ │ │ │ ├── Button.module.scss │ │ │ │ ├── Button.tsx │ │ │ │ └── ButtonTheme.ts │ │ ├── Layout │ │ │ └── Head.astro │ │ ├── PropertyTable.astro │ │ ├── Tabs │ │ │ └── PackageManagerTabs.astro │ │ ├── UI │ │ │ └── UI.astro │ │ └── react-examples │ │ │ ├── Example.astro │ │ │ ├── ExampleCodeMirrorEditor.tsx │ │ │ ├── ExampleFileTree.tsx │ │ │ ├── ExampleSimpleEditor.tsx │ │ │ ├── ExampleTerminal.tsx │ │ │ └── hooks │ │ │ ├── useTheme.ts │ │ │ └── useWebcontainer.ts │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── guides │ │ │ ├── about.mdx │ │ │ ├── creating-content.mdx │ │ │ ├── deployment.mdx │ │ │ ├── how-to-use-tutorialkit-api.mdx │ │ │ ├── images │ │ │ │ ├── tk-cli.png │ │ │ │ ├── tutorialkit-ui.png │ │ │ │ ├── ui-code-editor.png │ │ │ │ ├── ui-dialog.png │ │ │ │ ├── ui-preview.png │ │ │ │ ├── ui-terminal.png │ │ │ │ └── ui-top-bar.png │ │ │ ├── installation.mdx │ │ │ ├── overriding-components.mdx │ │ │ └── ui.mdx │ │ │ ├── index.astro │ │ │ ├── index.mdx │ │ │ └── reference │ │ │ ├── configuration.mdx │ │ │ ├── images │ │ │ ├── theming-breadcrumb-button.png │ │ │ ├── theming-breadcrumb-dropdown.png │ │ │ ├── theming-breadcrumb.png │ │ │ ├── theming-callout.png │ │ │ ├── theming-content.png │ │ │ ├── theming-editor-gutter-fold.png │ │ │ ├── theming-editor-gutter.png │ │ │ ├── theming-editor-search.png │ │ │ ├── theming-editor-tooltip.png │ │ │ ├── theming-editor.png │ │ │ ├── theming-filetree-file.png │ │ │ ├── theming-filetree-folder.png │ │ │ ├── theming-filetree.png │ │ │ ├── theming-navcard.png │ │ │ ├── theming-panel-header-button.png │ │ │ ├── theming-panel-header-tab.png │ │ │ ├── theming-panel-header.png │ │ │ ├── theming-previews.png │ │ │ ├── theming-statuses.png │ │ │ ├── theming-terminal.png │ │ │ └── theming-top-bar.png │ │ │ ├── react-components.mdx │ │ │ ├── theming.mdx │ │ │ └── tutorialkit-api.mdx │ ├── env.d.ts │ └── styles │ │ ├── breakpoints.scss │ │ ├── custom.scss │ │ ├── fonts.scss │ │ └── variables.scss │ ├── tsconfig.json │ └── uno.config.ts ├── e2e ├── README.md ├── astro.config.ts ├── configs │ ├── lessons-in-part.ts │ ├── lessons-in-root.ts │ └── override-components.ts ├── package.json ├── playwright.config.ts ├── public │ └── logo.svg ├── src-custom │ ├── lessons-in-part │ │ ├── content │ │ │ ├── config.ts │ │ │ └── tutorial │ │ │ │ ├── meta.md │ │ │ │ ├── part-one │ │ │ │ ├── lesson-1 │ │ │ │ │ └── content.md │ │ │ │ ├── lesson-2 │ │ │ │ │ └── content.md │ │ │ │ └── meta.md │ │ │ │ └── part-two │ │ │ │ ├── chapter-one │ │ │ │ ├── lesson-3 │ │ │ │ │ └── content.md │ │ │ │ ├── lesson-4 │ │ │ │ │ └── content.md │ │ │ │ └── meta.md │ │ │ │ └── meta.md │ │ └── env.d.ts │ └── lessons-in-root │ │ ├── content │ │ ├── config.ts │ │ └── tutorial │ │ │ ├── lesson-one │ │ │ └── content.md │ │ │ ├── lesson-two │ │ │ └── content.md │ │ │ └── meta.md │ │ └── env.d.ts ├── src │ ├── components │ │ ├── ButtonDeleteFile.tsx │ │ ├── ButtonWriteToFile.tsx │ │ ├── CustomHeadTags.astro │ │ ├── CustomMetadata.astro │ │ ├── Dialog.tsx │ │ └── TopBar.astro │ ├── content │ │ ├── config.ts │ │ └── tutorial │ │ │ ├── meta.md │ │ │ └── tests │ │ │ ├── file-tree │ │ │ ├── allow-edits-disabled │ │ │ │ ├── _files │ │ │ │ │ └── first-level │ │ │ │ │ │ ├── file.js │ │ │ │ │ │ └── second-level │ │ │ │ │ │ └── file.js │ │ │ │ └── content.md │ │ │ ├── allow-edits-enabled │ │ │ │ ├── _files │ │ │ │ │ └── first-level │ │ │ │ │ │ ├── file.js │ │ │ │ │ │ └── second-level │ │ │ │ │ │ └── file.js │ │ │ │ └── content.md │ │ │ ├── allow-edits-glob │ │ │ │ ├── _files │ │ │ │ │ └── first-level │ │ │ │ │ │ ├── file.js │ │ │ │ │ │ └── second-level │ │ │ │ │ │ └── file.js │ │ │ │ └── content.md │ │ │ ├── hidden │ │ │ │ ├── _files │ │ │ │ │ └── example.js │ │ │ │ └── content.md │ │ │ ├── lesson-and-solution │ │ │ │ ├── _files │ │ │ │ │ ├── example.html │ │ │ │ │ └── example.js │ │ │ │ ├── _solution │ │ │ │ │ ├── example.html │ │ │ │ │ └── example.js │ │ │ │ └── content.md │ │ │ ├── meta.md │ │ │ └── no-solution │ │ │ │ ├── _files │ │ │ │ ├── example.html │ │ │ │ └── example.js │ │ │ │ └── content.md │ │ │ ├── filesystem │ │ │ ├── meta.md │ │ │ ├── no-watch │ │ │ │ ├── _files │ │ │ │ │ └── bar.txt │ │ │ │ └── content.mdx │ │ │ ├── watch-glob │ │ │ │ ├── _files │ │ │ │ │ ├── a │ │ │ │ │ │ └── b │ │ │ │ │ │ │ └── baz.txt │ │ │ │ │ └── bar.txt │ │ │ │ └── content.mdx │ │ │ └── watch │ │ │ │ ├── _files │ │ │ │ ├── a │ │ │ │ │ └── b │ │ │ │ │ │ └── baz.txt │ │ │ │ └── bar.txt │ │ │ │ └── content.mdx │ │ │ ├── lesson-order │ │ │ ├── 1-lesson │ │ │ │ └── content.md │ │ │ ├── 2-lesson │ │ │ │ └── content.md │ │ │ ├── 3-lesson │ │ │ │ └── content.md │ │ │ └── meta.md │ │ │ ├── meta.md │ │ │ ├── metadata │ │ │ ├── custom │ │ │ │ └── content.mdx │ │ │ └── meta.md │ │ │ ├── navigation │ │ │ ├── layout-change-all-off │ │ │ │ └── content.md │ │ │ ├── layout-change-from │ │ │ │ └── content.md │ │ │ ├── layout-change-to │ │ │ │ └── content.md │ │ │ ├── meta.md │ │ │ ├── page-one │ │ │ │ └── content.md │ │ │ ├── page-three │ │ │ │ └── content.md │ │ │ └── page-two │ │ │ │ └── content.md │ │ │ ├── preview │ │ │ ├── auto-reload-1-from │ │ │ │ ├── _files │ │ │ │ │ └── index.html │ │ │ │ └── content.md │ │ │ ├── auto-reload-2-to │ │ │ │ ├── _files │ │ │ │ │ └── index.html │ │ │ │ └── content.md │ │ │ ├── auto-reload-3-off │ │ │ │ ├── _files │ │ │ │ │ └── index.html │ │ │ │ └── content.md │ │ │ ├── meta.md │ │ │ ├── multiple │ │ │ │ └── content.md │ │ │ └── single │ │ │ │ ├── _files │ │ │ │ └── index.html │ │ │ │ └── content.mdx │ │ │ └── terminal │ │ │ ├── default │ │ │ └── content.md │ │ │ ├── meta.md │ │ │ └── open-by-default │ │ │ └── content.md │ ├── env.d.ts │ └── templates │ │ ├── default │ │ ├── file-on-template.js │ │ ├── folder-on-template │ │ │ └── .gitkeep │ │ └── index.mjs │ │ └── file-server │ │ └── index.mjs ├── test │ ├── dialog.override-components.test.ts │ ├── file-tree.test.ts │ ├── filesystem.test.ts │ ├── headtags.override-components.test.ts │ ├── lesson-order.test.ts │ ├── metadata.test.ts │ ├── navigation.lessons-in-part.test.ts │ ├── navigation.lessons-in-root.test.ts │ ├── navigation.test.ts │ ├── preview.test.ts │ ├── terminal.test.ts │ ├── topbar.override-components.test.ts │ ├── topbar.test.ts │ └── utils.ts ├── tsconfig.json └── uno.config.ts ├── eslint.config.mjs ├── extensions └── vscode │ ├── .gitignore │ ├── .npmrc │ ├── .vscodeignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── resources │ ├── icons │ │ ├── dark │ │ │ ├── chapter.svg │ │ │ └── lesson.svg │ │ └── light │ │ │ ├── chapter.svg │ │ │ └── lesson.svg │ ├── tutorialkit-icon.png │ └── tutorialkit-screenshot.png │ ├── scripts │ ├── build.mjs │ └── load-schema-worker.mjs │ ├── src │ ├── commands │ │ ├── _helpers.ts │ │ ├── index.ts │ │ ├── tutorialkit.add.ts │ │ ├── tutorialkit.delete.ts │ │ ├── tutorialkit.goto.ts │ │ ├── tutorialkit.initialize.ts │ │ ├── tutorialkit.load-tutorial.ts │ │ ├── tutorialkit.refresh.ts │ │ └── tutorialkit.select-tutorial.ts │ ├── extension.ts │ ├── global-state.ts │ ├── language-server │ │ ├── index.ts │ │ ├── languagePlugin.ts │ │ └── schema.ts │ ├── models │ │ ├── Node.ts │ │ └── tree │ │ │ ├── constants.ts │ │ │ ├── load.ts │ │ │ └── update.ts │ ├── utils │ │ ├── getIcon.ts │ │ └── isTutorialKit.ts │ └── views │ │ └── lessonsTree.ts │ └── tsconfig.json ├── integration ├── cli │ ├── __snapshots__ │ │ ├── npm-built.json │ │ ├── npm-created.json │ │ ├── pnpm-built.json │ │ ├── pnpm-created.json │ │ ├── yarn-built.json │ │ └── yarn-created.json │ └── create-tutorial.test.ts ├── package.json └── theme-resolving │ └── inline-content.test.ts ├── media ├── logo-white.svg └── logo.svg ├── package.json ├── packages ├── astro │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── scripts │ │ └── build.js │ ├── src │ │ ├── default │ │ │ ├── components │ │ │ │ ├── DownloadButton.tsx │ │ │ │ ├── HeadTags.astro │ │ │ │ ├── LoginButton.tsx │ │ │ │ ├── Logo.astro │ │ │ │ ├── MainContainer.astro │ │ │ │ ├── MetaTags.astro │ │ │ │ ├── MobileContentToggle.astro │ │ │ │ ├── NavCard.astro │ │ │ │ ├── NavWrapper.tsx │ │ │ │ ├── OpenInStackblitzLink.tsx │ │ │ │ ├── PageLoadingIndicator.astro │ │ │ │ ├── ResizablePanel.astro │ │ │ │ ├── ThemeSwitch.tsx │ │ │ │ ├── TopBar.astro │ │ │ │ ├── TopBarWrapper.astro │ │ │ │ ├── TutorialContent.astro │ │ │ │ ├── WorkspacePanelWrapper.tsx │ │ │ │ ├── setup.ts │ │ │ │ └── webcontainer.ts │ │ │ ├── env-default.d.ts │ │ │ ├── layouts │ │ │ │ └── Layout.astro │ │ │ ├── pages │ │ │ │ ├── [...slug].astro │ │ │ │ └── index.astro │ │ │ ├── stores │ │ │ │ ├── auth-store.ts │ │ │ │ ├── theme-store.ts │ │ │ │ └── view-store.ts │ │ │ ├── styles │ │ │ │ ├── base.css │ │ │ │ ├── markdown.css │ │ │ │ ├── panel.css │ │ │ │ └── variables.css │ │ │ └── utils │ │ │ │ ├── __snapshots__ │ │ │ │ ├── multiple-parts.json │ │ │ │ ├── single-lesson-no-part.json │ │ │ │ ├── single-part-and-lesson-no-chapter.json │ │ │ │ ├── single-part-chapter-and-lesson.json │ │ │ │ ├── single-part-chapter-and-multiple-lessons.json │ │ │ │ └── single-part-multiple-chapters.json │ │ │ │ ├── constants.ts │ │ │ │ ├── content.spec.ts │ │ │ │ ├── content.ts │ │ │ │ ├── content │ │ │ │ ├── files-ref.spec.ts │ │ │ │ ├── files-ref.ts │ │ │ │ ├── squash.spec.ts │ │ │ │ └── squash.ts │ │ │ │ ├── logger.ts │ │ │ │ ├── logo.ts │ │ │ │ ├── nav.ts │ │ │ │ ├── publicAsset.ts │ │ │ │ ├── routes.ts │ │ │ │ ├── url.spec.ts │ │ │ │ ├── url.ts │ │ │ │ └── workspace.ts │ │ ├── index.ts │ │ ├── integrations.ts │ │ ├── remark │ │ │ ├── callouts.ts │ │ │ ├── import-file.ts │ │ │ └── index.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ ├── vite-plugins │ │ │ ├── core.ts │ │ │ ├── css.ts │ │ │ ├── override-components.ts │ │ │ └── store.ts │ │ └── webcontainer-files │ │ │ ├── cache.spec.ts │ │ │ ├── cache.ts │ │ │ ├── constants.ts │ │ │ ├── filesmap.spec.ts │ │ │ ├── filesmap.ts │ │ │ ├── index.ts │ │ │ └── utils.ts │ ├── test │ │ └── fixtures │ │ │ └── files │ │ │ ├── first.js │ │ │ └── nested │ │ │ └── directory │ │ │ └── second.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── types.d.ts ├── cli │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── _template │ ├── overwrites │ │ ├── src │ │ │ ├── content │ │ │ │ └── tutorial │ │ │ │ │ ├── 1-basics │ │ │ │ │ ├── 1-introduction │ │ │ │ │ │ ├── 1-welcome │ │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ │ └── counter.js │ │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ │ └── counter.js │ │ │ │ │ │ │ └── content.md │ │ │ │ │ │ └── meta.md │ │ │ │ │ └── meta.md │ │ │ │ │ └── meta.md │ │ │ └── templates │ │ │ │ └── default │ │ │ │ ├── .gitignore │ │ │ │ ├── counter.js │ │ │ │ ├── index.html │ │ │ │ ├── javascript.svg │ │ │ │ ├── main.js │ │ │ │ ├── package-lock.json │ │ │ │ ├── package.json │ │ │ │ ├── public │ │ │ │ └── vite.svg │ │ │ │ └── style.css │ │ └── uno.config.ts │ ├── package.json │ ├── scripts │ │ ├── _constants.js │ │ ├── build-release.js │ │ ├── build.js │ │ └── logger.js │ ├── src │ │ ├── commands │ │ │ ├── create │ │ │ │ ├── enterprise.ts │ │ │ │ ├── generate-hosting-config.ts │ │ │ │ ├── git.ts │ │ │ │ ├── hosting-config │ │ │ │ │ ├── _headers.txt │ │ │ │ │ ├── netlify_toml.txt │ │ │ │ │ └── vercel.json │ │ │ │ ├── index.ts │ │ │ │ ├── install-start.ts │ │ │ │ ├── options.ts │ │ │ │ ├── package-manager.ts │ │ │ │ ├── template.ts │ │ │ │ └── types.ts │ │ │ └── eject │ │ │ │ ├── index.ts │ │ │ │ └── options.ts │ │ ├── index.ts │ │ ├── pkg.ts │ │ ├── types.d.ts │ │ └── utils │ │ │ ├── astro-config.ts │ │ │ ├── babel.ts │ │ │ ├── colors.ts │ │ │ ├── messages.ts │ │ │ ├── project.ts │ │ │ ├── random.ts │ │ │ ├── shell.ts │ │ │ ├── tasks.ts │ │ │ ├── words.ts │ │ │ └── workspace-version.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── create-tutorial.test.ts.snap │ │ └── create-tutorial.test.ts │ └── tsconfig.json ├── create-tutorial │ ├── CHANGELOG.md │ ├── package.json │ ├── scripts │ │ └── build.js │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── react │ ├── CHANGELOG.md │ ├── package.json │ ├── scripts │ │ └── build.js │ ├── src │ │ ├── BootScreen.tsx │ │ ├── Button.tsx │ │ ├── Nav.tsx │ │ ├── Panels │ │ │ ├── EditorPanel.tsx │ │ │ ├── PreviewPanel.tsx │ │ │ ├── TerminalPanel.tsx │ │ │ └── WorkspacePanel.tsx │ │ ├── core.ts │ │ ├── core │ │ │ ├── CodeMirrorEditor │ │ │ │ ├── BinaryContent.tsx │ │ │ │ ├── cm-theme.ts │ │ │ │ ├── indent.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── languages.ts │ │ │ │ └── themes │ │ │ │ │ └── vscode-dark.ts │ │ │ ├── ContextMenu.tsx │ │ │ ├── Dialog.tsx │ │ │ ├── FileTree.spec.ts │ │ │ ├── FileTree.tsx │ │ │ ├── Terminal │ │ │ │ ├── index.tsx │ │ │ │ └── theme.ts │ │ │ └── types.ts │ │ ├── css.module.d.ts │ │ ├── hooks │ │ │ └── useOutsideClick.ts │ │ ├── index.ts │ │ ├── styles │ │ │ ├── cm.css │ │ │ ├── nav.module.css │ │ │ ├── resize-panel.module.css │ │ │ └── terminal.css │ │ ├── types.d.ts │ │ └── utils │ │ │ ├── classnames.ts │ │ │ ├── debounce.ts │ │ │ └── mobile.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── runtime │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── lesson-files.spec.ts │ │ ├── lesson-files.ts │ │ ├── store │ │ │ ├── editor.spec.ts │ │ │ ├── editor.ts │ │ │ ├── index.ts │ │ │ ├── previews.spec.ts │ │ │ ├── previews.ts │ │ │ ├── terminal.ts │ │ │ ├── tutorial-runner.spec.ts │ │ │ └── tutorial-runner.ts │ │ ├── tasks.ts │ │ ├── types.d.ts │ │ ├── utils │ │ │ ├── multi-counter.ts │ │ │ ├── promises.ts │ │ │ ├── support.ts │ │ │ └── terminal.ts │ │ └── webcontainer │ │ │ ├── command.spec.ts │ │ │ ├── command.ts │ │ │ ├── editor-config.spec.ts │ │ │ ├── editor-config.ts │ │ │ ├── index.ts │ │ │ ├── on-demand-boot.ts │ │ │ ├── port-info.ts │ │ │ ├── preview-info.spec.ts │ │ │ ├── preview-info.ts │ │ │ ├── shell.ts │ │ │ ├── steps.spec.ts │ │ │ ├── steps.ts │ │ │ ├── terminal-config.spec.ts │ │ │ ├── terminal-config.ts │ │ │ └── utils │ │ │ ├── files.spec.ts │ │ │ └── files.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vitest.config.ts ├── template │ ├── .gitignore │ ├── .vscode │ │ ├── extensions.json │ │ └── launch.json │ ├── README.md │ ├── astro.config.ts │ ├── icons │ │ └── languages │ │ │ ├── css.svg │ │ │ ├── html.svg │ │ │ ├── js.svg │ │ │ ├── json.svg │ │ │ ├── markdown.svg │ │ │ ├── sass.svg │ │ │ └── ts.svg │ ├── package.json │ ├── public │ │ ├── favicon.svg │ │ ├── logo-dark.svg │ │ └── logo.svg │ ├── src │ │ ├── content │ │ │ ├── config.ts │ │ │ └── tutorial │ │ │ │ ├── 1-basics │ │ │ │ ├── 1-introduction │ │ │ │ │ ├── 1-welcome │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ └── src │ │ │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ │ │ └── test │ │ │ │ │ │ │ │ └── bar.js │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ └── src │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ └── content.md │ │ │ │ │ ├── 2-foo │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ ├── bar │ │ │ │ │ │ │ │ └── styles.css │ │ │ │ │ │ │ └── src │ │ │ │ │ │ │ │ ├── index.html │ │ │ │ │ │ │ │ ├── test-1.js │ │ │ │ │ │ │ │ ├── test-10.ts │ │ │ │ │ │ │ │ ├── test-11.jsx │ │ │ │ │ │ │ │ ├── test-12.tsx │ │ │ │ │ │ │ │ ├── test-13.cts │ │ │ │ │ │ │ │ ├── test-14.mts │ │ │ │ │ │ │ │ ├── test-15.svg │ │ │ │ │ │ │ │ ├── test-16.vue │ │ │ │ │ │ │ │ ├── test-2.cjs │ │ │ │ │ │ │ │ ├── test-3.mjs │ │ │ │ │ │ │ │ ├── test-4.css │ │ │ │ │ │ │ │ ├── test-5.md │ │ │ │ │ │ │ │ ├── test-6.png │ │ │ │ │ │ │ │ ├── test-7.jpg │ │ │ │ │ │ │ │ ├── test-8.gif │ │ │ │ │ │ │ │ ├── test-9.xyz │ │ │ │ │ │ │ │ ├── unicorn.js │ │ │ │ │ │ │ │ └── windows_xp.png │ │ │ │ │ │ ├── _solution │ │ │ │ │ │ │ └── src │ │ │ │ │ │ │ │ └── index.html │ │ │ │ │ │ └── content.mdx │ │ │ │ │ ├── 3-bar │ │ │ │ │ │ ├── _files │ │ │ │ │ │ │ └── src │ │ │ │ │ │ │ │ └── index.html │ │ │ │ │ │ └── content.md │ │ │ │ │ └── meta.md │ │ │ │ ├── 2-foo │ │ │ │ │ ├── 1-welcome │ │ │ │ │ │ └── content.md │ │ │ │ │ └── meta.md │ │ │ │ └── meta.md │ │ │ │ ├── 2-advanced │ │ │ │ ├── 1-unicorn │ │ │ │ │ ├── 1-welcome │ │ │ │ │ │ └── content.md │ │ │ │ │ └── meta.md │ │ │ │ └── meta.md │ │ │ │ └── meta.md │ │ ├── env.d.ts │ │ └── templates │ │ │ ├── default │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── src │ │ │ │ └── index.js │ │ │ ├── vite-app-2 │ │ │ ├── .tk-config.json │ │ │ └── foo.txt │ │ │ └── vite-app │ │ │ ├── .gitignore │ │ │ ├── counter.js │ │ │ ├── index.html │ │ │ ├── javascript.svg │ │ │ ├── main.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── public │ │ │ └── vite.svg │ │ │ └── style.css │ ├── tsconfig.json │ └── uno.config.ts ├── test-utils │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── theme │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── index.spec.ts │ │ ├── index.ts │ │ ├── theme.ts │ │ ├── transition-theme.ts │ │ └── utils.ts │ └── tsconfig.json └── types │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── default-localization.ts │ ├── entities │ │ ├── index.ts │ │ └── nav.ts │ ├── files-ref.spec.ts │ ├── files-ref.ts │ ├── index.ts │ ├── schemas │ │ ├── chapter.spec.ts │ │ ├── chapter.ts │ │ ├── common.spec.ts │ │ ├── common.ts │ │ ├── content.spec.ts │ │ ├── content.ts │ │ ├── i18n.ts │ │ ├── index.ts │ │ ├── lesson.spec.ts │ │ ├── lesson.ts │ │ ├── metatags.ts │ │ ├── part.spec.ts │ │ ├── part.ts │ │ ├── tutorial.spec.ts │ │ └── tutorial.ts │ └── utils │ │ ├── interpolation.spec.ts │ │ └── interpolation.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── scripts ├── changelog.mjs ├── changelog │ ├── Package.mjs │ └── generate.mjs └── clean.sh ├── tsconfig.json └── uno.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 120 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/actions/resolve-release-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Resolve version 2 | description: Read "version" of "@tutorialkit/astro" to "steps.resolve-release-version.outputs.version" 3 | 4 | outputs: 5 | version: 6 | description: 'Version of @tutorialkit/astro' 7 | value: ${{ steps.resolve-release-version.outputs.version }} 8 | 9 | runs: 10 | using: composite 11 | 12 | steps: 13 | - name: Resolve release version 14 | id: resolve-release-version 15 | shell: bash 16 | run: echo "version=$(jq -r .version ./packages/astro/package.json)" >> $GITHUB_OUTPUT 17 | -------------------------------------------------------------------------------- /.github/actions/setup-and-build/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup and build 2 | description: Generic setup action 3 | inputs: 4 | node-version: 5 | required: false 6 | description: Node version for setup-node 7 | default: 20.x 8 | 9 | runs: 10 | using: composite 11 | 12 | steps: 13 | - name: Set node version to ${{ inputs.node-version }} 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ inputs.node-version }} 17 | registry-url: 'https://registry.npmjs.org' 18 | 19 | - name: Install pnpm 20 | uses: pnpm/action-setup@v4 21 | 22 | - name: Install & Build 23 | shell: bash 24 | run: | 25 | pnpm install 26 | pnpm build 27 | -------------------------------------------------------------------------------- /.github/workflows/publish-commit.yaml: -------------------------------------------------------------------------------- 1 | # PRs can be published by adding `pkg-pr-new` tag 2 | 3 | name: PR Preview Releases 4 | 5 | on: 6 | push: 7 | branches: [main] 8 | pull_request: 9 | types: [opened, synchronize, labeled] 10 | 11 | jobs: 12 | release: 13 | if: github.repository == 'stackblitz/tutorialkit' && (github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'pkg-pr-new')) 14 | runs-on: ubuntu-latest 15 | name: 'Release: pkg.pr.new' 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - uses: ./.github/actions/setup-and-build 24 | 25 | - name: Publish to pkg.pr.new 26 | run: > 27 | pnpm dlx pkg-pr-new publish --compact --pnpm 28 | ./packages/astro 29 | ./packages/react 30 | ./packages/runtime 31 | ./packages/theme 32 | ./packages/types 33 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yaml: -------------------------------------------------------------------------------- 1 | # This is for publishing documentation manually. 2 | # During release `publish-release.yaml` publishes docs automatically. 3 | name: Publish Documentation 4 | 5 | on: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | publish_docs: 10 | name: Publish documentation 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: ./.github/actions/setup-and-build 19 | 20 | - name: Build docs 21 | run: pnpm run docs:build 22 | 23 | - name: Deploy documentation 24 | uses: cloudflare/pages-action@v1 25 | with: 26 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 27 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 28 | projectName: tutorialkit-docs-page 29 | workingDirectory: 'docs/tutorialkit.dev' 30 | directory: dist 31 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Semantic Pull Request 2 | on: 3 | pull_request_target: 4 | types: [opened, reopened, edited, synchronize] 5 | permissions: 6 | pull-requests: read 7 | jobs: 8 | main: 9 | name: Validate PR Title 10 | runs-on: ubuntu-latest 11 | steps: 12 | # https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.2 13 | - uses: amannn/action-semantic-pull-request@cfb60706e18bc85e8aec535e3c577abe8f70378e 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | with: 17 | subjectPattern: ^(?![A-Z]).+$ 18 | subjectPatternError: | 19 | The subject "{subject}" found in the pull request title "{title}" 20 | didn't match the configured pattern. Please ensure that the subject 21 | doesn't start with an uppercase character. 22 | types: | 23 | fix 24 | feat 25 | chore 26 | build 27 | ci 28 | perf 29 | docs 30 | refactor 31 | revert 32 | test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | pnpm-debug.log* 7 | lerna-debug.log* 8 | node_modules 9 | dist 10 | dist-ssr 11 | e2e/dist-* 12 | *.local 13 | !.vscode/extensions.json 14 | .idea 15 | .DS_Store 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw? 21 | .pnpm-store 22 | /packages/cli/template 23 | tsconfig.tsbuildinfo 24 | tsconfig.build.tsbuildinfo 25 | .tmp 26 | .tmp-* 27 | /e2e/**/test-results 28 | /e2e/**/.astro 29 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shell-emulator=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | .astro 3 | **/*.md 4 | **/*.mdx 5 | __snapshots__ 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true, 8 | "plugins": ["prettier-plugin-astro"], 9 | "overrides": [ 10 | { 11 | "files": "*.astro", 12 | "options": { 13 | "parser": "astro" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 18.18.0 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development Server", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}/packages/template", 9 | "type": "node-terminal" 10 | }, 11 | { 12 | "name": "Run extension", 13 | "type": "extensionHost", 14 | "request": "launch", 15 | "runtimeExecutable": "${execPath}", 16 | "args": [ 17 | "--extensionDevelopmentPath=${workspaceRoot}/extensions/vscode", 18 | "--folder-uri=${workspaceRoot}/packages/template" 19 | ], 20 | "outFiles": ["${workspaceRoot}/extensions/vscode/dist/**/*.js"] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [], 4 | "search.exclude": { 5 | "**/node_modules/**": true, 6 | "**/*.code-search": true, 7 | "cli/_template": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # generated types 2 | .astro 3 | 4 | # environment variables 5 | .env 6 | .env.production 7 | -------------------------------------------------------------------------------- /docs/demo/README.md: -------------------------------------------------------------------------------- 1 | # [demo.tutorialkit.dev](https://demo.tutorialkit.dev) 2 | 3 | ## Local Development 4 | 5 | ### Prerequisites 6 | 7 | - Install [Node.js](https://nodejs.org/en) v18.18 or above. 8 | - Install [pnpm](https://pnpm.io/). 9 | 10 | ### Set up 11 | 12 | Clone this repository and navigate into the cloned directory. 13 | 14 | ``` 15 | git clone git@github.com:stackblitz/tutorialkit.git 16 | cd tutorialkit 17 | ``` 18 | 19 | TutorialKit uses [pnpm workspaces](https://pnpm.io/workspaces). Just run the install command in the root of the project. 20 | 21 | ``` 22 | pnpm install 23 | ``` 24 | 25 | You can now start the demo website by running: 26 | 27 | ``` 28 | pnpm run demo 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/demo/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | Cross-Origin-Embedder-Policy: require-corp 3 | Cross-Origin-Opener-Policy: same-origin 4 | -------------------------------------------------------------------------------- /docs/demo/astro.config.ts: -------------------------------------------------------------------------------- 1 | import tutorialkit from '@tutorialkit/astro'; 2 | import { defineConfig } from 'astro/config'; 3 | 4 | export default defineConfig({ 5 | devToolbar: { 6 | enabled: false, 7 | }, 8 | integrations: [ 9 | tutorialkit({ 10 | components: { 11 | TopBar: './src/components/TopBar.astro', 12 | }, 13 | }), 14 | ], 15 | }); 16 | -------------------------------------------------------------------------------- /docs/demo/icons/languages/css.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/demo/icons/languages/html.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/demo/icons/languages/js.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/demo/icons/languages/json.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/demo/icons/languages/markdown.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/demo/icons/languages/sass.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/demo/icons/languages/ts.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo.tutorialkit.dev", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build && cp _headers ./dist/", 10 | "build_with_check": "astro check && pnpm run build", 11 | "preview": "astro preview" 12 | }, 13 | "dependencies": { 14 | "@tutorialkit/react": "workspace:*", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1" 17 | }, 18 | "devDependencies": { 19 | "@astrojs/check": "^0.7.0", 20 | "@astrojs/react": "^3.6.0", 21 | "@tutorialkit/astro": "workspace:*", 22 | "@tutorialkit/theme": "workspace:*", 23 | "@tutorialkit/types": "workspace:*", 24 | "@types/react": "^18.3.3", 25 | "astro": "^4.15.0", 26 | "prettier-plugin-astro": "^0.14.1", 27 | "typescript": "^5.4.5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/demo/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/demo/public/images/accent-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/demo/public/images/accent-color.png -------------------------------------------------------------------------------- /docs/demo/public/images/fieldset-styles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/demo/public/images/fieldset-styles.png -------------------------------------------------------------------------------- /docs/demo/src/components/TopBar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Github } from './Github'; 3 | --- 4 | 5 | 24 | -------------------------------------------------------------------------------- /docs/demo/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { contentSchema } from '@tutorialkit/types'; 2 | import { defineCollection } from 'astro:content'; 3 | 4 | const tutorial = defineCollection({ 5 | type: 'content', 6 | schema: contentSchema, 7 | }); 8 | 9 | export const collections = { tutorial }; 10 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/1-introduction/1-welcome/_files/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Hello 4 |
5 | 6 | 7 |
8 |
9 |
10 |
11 | 12 | 13 | 24 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/1-introduction/1-welcome/_files/welcome.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Hello, World! 4 | 5 | */ 6 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/1-introduction/1-welcome/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Style up your forms! 4 | slug: welcome 5 | focus: /welcome.css 6 | --- 7 | 8 | :::info{noBorder=true title="Welcome!"} 9 | This is a demo tutorial built with TutorialKit. Although we use it for demonstration purposes, the lessons include actual CSS techniques, so we hope you'll enjoy them and learn something new! 10 | ::: 11 | 12 | # Style up your forms! 13 | 14 | Forms are an incredibly common set of HTML elements – they are a part of almost every web app – but styling them is often not as straightforward as styling a typical `div` or `section`. 15 | 16 | This tutorial will let you learn and experiment with some practical techniques that will help elevate your form's CSS to the next level! 17 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/1-introduction/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: Introduction 4 | slug: introduction 5 | --- 6 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/1-accent-color/_files/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | } 3 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/1-accent-color/_solution/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | accent-color: #ff3399; 3 | } 4 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/1-accent-color/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Accent Color 4 | focus: /style.css 5 | slug: accent-color 6 | --- 7 | 8 | ## Customize the input colors 9 | 10 | 11 | 12 | In the Preview window, we've displayed several native elements: different `input` types and a `progress` bar. Depending on your operating system settings, these will have a different default colors. 13 | 14 | Such colors might not fit your brand, or the current theme of your application. 15 | 16 | Thankfully, you can change them, and the good news is: you only need one CSS property to do that! 17 | 18 | Try setting `accent-color` for the whole document by adding the following code inside the `body` selector: 19 | 20 | ```css add={2} 21 | body { 22 | accent-color: #ff3399; 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/2-progressbar/_files/style.css: -------------------------------------------------------------------------------- 1 | progress { 2 | } 3 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/2-progressbar/_solution/style.css: -------------------------------------------------------------------------------- 1 | progress { 2 | background: none; 3 | } 4 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/2-progressbar/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Progress bar 4 | slug: progress-bar 5 | focus: /style.css 6 | template: default 7 | --- 8 | 9 | ## Styling the `` 10 | 11 | `` is an often overlooked element that allows you to show... well, the completion progress of a task. 12 | 13 | Although `accent-color` that we've set in the previous step already impacts this element, we can customize it even further! 14 | 15 | Let's start by setting removing the border from the element. As you do it, you will notice that it will also change other aspects of the default appearance, like the height and radius. 16 | 17 | ```css add={2} 18 | progress { 19 | border: none; 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/3-more-progressbar/_files/style.css: -------------------------------------------------------------------------------- 1 | progress { 2 | border: none; 3 | } 4 | 5 | progress::-webkit-progress-bar { 6 | } 7 | 8 | progress::-webkit-progress-value { 9 | } 10 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/3-more-progressbar/_solution/style.css: -------------------------------------------------------------------------------- 1 | progress { 2 | border: none; 3 | } 4 | 5 | progress::-webkit-progress-bar { 6 | background: #f6cd86; 7 | border-radius: 5px; 8 | } 9 | 10 | progress::-webkit-progress-value { 11 | background: tomato; 12 | border-radius: 5px; 13 | } 14 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/3-more-progressbar/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: More Progress bar 4 | slug: more-progress-bar 5 | focus: /style.css 6 | template: default 7 | --- 8 | 9 | ## Styling the `` 10 | 11 | Now that the default appearance is turned off, we can start customizing the element. Progress bar consists of two parts: 12 | 13 | - `progress-bar` 14 | - `progress-value` 15 | 16 | Note: this part is not a standardized CSS yet, so when creating these two selectors we will have to use the `-webkit-` vendor prefix, making them into: 17 | 18 | ```css 19 | progress::-webkit-progress-bar { 20 | /* ... */ 21 | } 22 | 23 | progress::-webkit-progress-value { 24 | /* ... */ 25 | } 26 | ``` 27 | 28 | In `style.css` you already see these selectors. Try styling them up: set `background` and `border-radius` to values you like! 29 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/4-handle-firefox/_files/style.css: -------------------------------------------------------------------------------- 1 | progress { 2 | border: none; 3 | } 4 | 5 | progress::-webkit-progress-bar { 6 | background: #f6cd86; 7 | border-radius: 5px; 8 | } 9 | 10 | progress::-webkit-progress-value { 11 | background: tomato; 12 | border-radius: 5px; 13 | } 14 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/4-handle-firefox/_solution/style.css: -------------------------------------------------------------------------------- 1 | @supports selector(::-webkit-progress-bar) { 2 | progress { 3 | border: none; 4 | } 5 | 6 | progress::-webkit-progress-bar { 7 | background: #f6cd86; 8 | border-radius: 5px; 9 | } 10 | 11 | progress::-webkit-progress-value { 12 | background: tomato; 13 | border-radius: 5px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/4-handle-firefox/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Handle Firefox 4 | slug: handle-firefox 5 | focus: /style.css 6 | template: default 7 | --- 8 | 9 | ## What about Firefox? 10 | 11 | If you look at our `` element in Firefox, you will see that it doesn't work that well. Firefox does not support the `progress-bar` and `progress-value` the way WebKit/Chrome browsers do, but setting `border: none` does turn off the defaut styling! 12 | 13 | We should make sure only browsers that can support this kind of customization apply it. We can do it with the `@supports` CSS rule. More specifically: `@supports selector(...)` one. Wrap our custom css code with the following: 14 | 15 | ```css 16 | @supports selector(::-webkit-progress-bar) { 17 | progress { ... 18 | } 19 | ``` 20 | 21 | In `style.css` you already see these selectors. Try styling them up: set `background` and `border-radius` to values you like! 22 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/2-colors/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: Colors 4 | slug: colors 5 | --- 6 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/1-fieldset-element/_files/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | 41 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/1-fieldset-element/_solution/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 | 45 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/1-fieldset-element/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Fieldset element 4 | slug: fieldset-element 5 | focus: /index.html 6 | --- 7 | 8 | The `
` HTML element groups related form controls, such as buttons, inputs, textareas, and labels, within a web form. 9 | 10 | This allows you to apply common styling and functional rules to the entire set of elements. Let's take a closer look at working with fieldsets! 11 | 12 | The current forms includes 6 inputs and we will want each pair to be visually and logically grouped together. 13 | 14 | Create **3 fieldsets** by wrapping them around the markup responsible for displaing the form fields: 15 | 16 | ```html add={1,10} 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | ``` 28 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/2-a-legend/_files/index.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | 34 | 45 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/2-a-legend/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: A legend 4 | slug: a-legend 5 | focus: /index.html 6 | --- 7 | 8 | Even with the default styling `
` already visually separates one group of form controls from another. 9 | It might be better to explain to a form user what each group represents. That's the purpose of a `` element. 10 | 11 | Let's add a legend to each of our fieldsets: 12 | 13 | ```html add={2} 14 |
15 | Step 1 16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | ``` 26 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/3-fieldset-styling/_files/style.css: -------------------------------------------------------------------------------- 1 | fieldset { 2 | /* Add the styles here */ 3 | 4 | border-style: solid; 5 | margin: 1em auto; 6 | padding: 1em; 7 | max-width: 500px; 8 | } 9 | 10 | legend { 11 | /* and here */ 12 | 13 | color: #718096; 14 | font-size: 90%; 15 | padding: 0.2em 0.5em; 16 | } 17 | 18 | fieldset > div { 19 | padding: 0.3em 0.5em; 20 | display: grid; 21 | grid-template-columns: 1fr 3fr; 22 | } 23 | 24 | input { 25 | border: solid 2px #e2e8f0; 26 | padding: 4px; 27 | font-size: 1em; 28 | } 29 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/3-fieldset-styling/_solution/style.css: -------------------------------------------------------------------------------- 1 | fieldset { 2 | background: #f7fafc; 3 | border-color: #e2e8f0; 4 | border-style: solid; 5 | margin: 1em auto; 6 | padding: 1em; 7 | max-width: 500px; 8 | } 9 | 10 | legend { 11 | border: solid 2px #e2e8f0; 12 | border-radius: 4px; 13 | background: #fff; 14 | color: #718096; 15 | font-size: 90%; 16 | padding: 0.2em 0.5em; 17 | } 18 | 19 | fieldset > div { 20 | padding: 0.3em 0.5em; 21 | display: grid; 22 | grid-template-columns: 1fr 3fr; 23 | } 24 | 25 | input { 26 | border: solid 2px #e2e8f0; 27 | padding: 4px; 28 | font-size: 1em; 29 | } 30 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/3-fieldset-styling/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Styling fieldsets 4 | slug: fieldset-styling 5 | focus: /style.css 6 | --- 7 | 8 | Our HTML looks great, but the default styles leave a lot of room for improvements. Thankfully, the way `
` and `` look like can be customized with a standard CSS. 9 | 10 | Here some of these styles have already been applied (note the `border-style: solid` in line #3 – see what changes if you comment it out) but they still need some finishing touches! 11 | 12 | Try adjusting the properties of these elements to make each fieldset look like this: 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/5-focus-within/_files/style.css: -------------------------------------------------------------------------------- 1 | fieldset { 2 | background: #f7fafc; 3 | border-color: #e2e8f0; 4 | border-style: solid; 5 | margin: 1em auto; 6 | padding: 1em; 7 | max-width: 500px; 8 | } 9 | 10 | legend { 11 | color: #718096; 12 | font-size: 90%; 13 | padding: 0.2em 0.5em; 14 | border: solid 2px #e2e8f0; 15 | border-radius: 4px; 16 | background: #fff; 17 | } 18 | 19 | fieldset > div { 20 | padding: 0.3em 0.5em; 21 | display: grid; 22 | grid-template-columns: 1fr 3fr; 23 | } 24 | 25 | input { 26 | border: solid 2px #e2e8f0; 27 | padding: 4px; 28 | font-size: 1em; 29 | } 30 | 31 | label { 32 | padding: 0.5em; 33 | text-align: right; 34 | } 35 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/5-focus-within/_solution/style.css: -------------------------------------------------------------------------------- 1 | fieldset { 2 | background: #f7fafc; 3 | border-color: #e2e8f0; 4 | border-style: solid; 5 | margin: 1em auto; 6 | padding: 1em; 7 | max-width: 500px; 8 | } 9 | 10 | fieldset:focus-within { 11 | background: #00a1; 12 | border-color: #00a6; 13 | } 14 | 15 | fieldset:focus-within legend { 16 | color: #00a9; 17 | border-color: #00a6; 18 | } 19 | 20 | legend { 21 | color: #718096; 22 | font-size: 90%; 23 | padding: 0.2em 0.5em; 24 | border: solid 2px #e2e8f0; 25 | border-radius: 4px; 26 | background: #fff; 27 | } 28 | 29 | fieldset > div { 30 | padding: 0.3em 0.5em; 31 | display: grid; 32 | grid-template-columns: 1fr 3fr; 33 | } 34 | 35 | input { 36 | border: solid 2px #e2e8f0; 37 | padding: 4px; 38 | font-size: 1em; 39 | } 40 | 41 | label { 42 | padding: 0.5em; 43 | text-align: right; 44 | } 45 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/5-focus-within/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Focus within 4 | slug: focus-within 5 | focus: /style.css 6 | --- 7 | A great way to give our visitors a pointer to where they are in the form is the `:focus` state. The most common way to apply it is by highlighting the currently focused form controls like buttons and inputs. But now that the fieldset provides us with a higher level of organization for the form, we can also highlight it to give even better visual guidance! 8 | 9 | Although `
` doesn't have its own `:focus` state, we can use a similar pseudo-selector, called `:focus-within`. It will match whenever a descendant of the targeted element gets focus. In the case of our form, we can add it on fieldset to make it apply style when an input inside focuses. 10 | 11 | ```css 12 | fieldset:focus-within { 13 | background: #00a1; 14 | border-color: #00a6; 15 | } 16 | 17 | fieldset:focus-within legend { 18 | color: #00a9; 19 | border-color: #00a6; 20 | } 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/6-the-end/_files/style.css: -------------------------------------------------------------------------------- 1 | fieldset { 2 | background: #f7fafc; 3 | border-color: #e2e8f0; 4 | border-style: solid; 5 | margin: 1em auto; 6 | padding: 1em; 7 | max-width: 500px; 8 | } 9 | 10 | legend { 11 | color: #718096; 12 | font-size: 90%; 13 | padding: 0.2em 0.5em; 14 | border: solid 2px #e2e8f0; 15 | border-radius: 4px; 16 | background: #fff; 17 | } 18 | 19 | fieldset > div { 20 | padding: 0.3em 0.5em; 21 | display: grid; 22 | grid-template-columns: 1fr 3fr; 23 | } 24 | 25 | input { 26 | border: solid 2px #e2e8f0; 27 | padding: 4px; 28 | font-size: 1em; 29 | } 30 | 31 | label { 32 | padding: 0.5em; 33 | text-align: right; 34 | } 35 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/6-the-end/_solution/style.css: -------------------------------------------------------------------------------- 1 | fieldset { 2 | background: #f7fafc; 3 | border-color: #e2e8f0; 4 | border-style: solid; 5 | margin: 1em auto; 6 | padding: 1em; 7 | max-width: 500px; 8 | } 9 | 10 | fieldset:focus-within { 11 | background: #00a1; 12 | border-color: #00a6; 13 | } 14 | 15 | fieldset:focus-within legend { 16 | color: #00a9; 17 | border-color: #00a6; 18 | } 19 | 20 | legend { 21 | color: #718096; 22 | font-size: 90%; 23 | padding: 0.2em 0.5em; 24 | border: solid 2px #e2e8f0; 25 | border-radius: 4px; 26 | background: #fff; 27 | } 28 | 29 | fieldset > div { 30 | padding: 0.3em 0.5em; 31 | display: grid; 32 | grid-template-columns: 1fr 3fr; 33 | } 34 | 35 | input { 36 | border: solid 2px #e2e8f0; 37 | padding: 4px; 38 | font-size: 1em; 39 | } 40 | 41 | label { 42 | padding: 0.5em; 43 | text-align: right; 44 | } 45 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/6-the-end/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: The end 4 | slug: the-end 5 | focus: /style.css 6 | --- 7 | You've reached the end of this tutorial! We hope you've enjoyed it and learned something new about working with CSS and forms. 8 | 9 | This app was built using the TutorialKit framework and you can make a similar learning resource for your team or open-source community yourself! TutorialKit provides all the necessary tooling, and UI out of the box, so that you can focus on the content. 10 | 11 | To learn more, visit tutorialkit.dev. 12 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/3-fieldset/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: Fieldset 4 | slug: fieldset 5 | --- 6 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/1-forms-css/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: part 3 | title: Forms CSS 4 | slug: forms-tutorial 5 | --- 6 | -------------------------------------------------------------------------------- /docs/demo/src/content/tutorial/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: tutorial 3 | mainCommand: ['npm run dev', 'Starting http server'] 4 | prepareCommands: 5 | - ['npm install', 'Installing dependencies'] 6 | editPageLink: https://github.com/stackblitz/tutorialkit/blob/main/docs/demo/src/content/tutorial/${path}?plain=1 7 | --- 8 | -------------------------------------------------------------------------------- /docs/demo/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/demo/src/templates/default/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | Hello, Forms CSS Tutorial! 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/demo/src/templates/default/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "node servor/cli.js --reload" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /docs/demo/src/templates/default/servor/utils/common.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const os = require('os'); 3 | const net = require('net'); 4 | 5 | const fileWatch = 6 | process.platform !== 'linux' 7 | ? (x, cb) => fs.watch(x, { recursive: true }, cb) 8 | : (x, cb) => { 9 | if (fs.statSync(x).isDirectory()) { 10 | fs.watch(x, cb); 11 | fs.readdirSync(x).forEach((xx) => fileWatch(`${x}/${xx}`, cb)); 12 | } 13 | }; 14 | 15 | module.exports.fileWatch = fileWatch; 16 | 17 | const usePort = (port = 0) => 18 | new Promise((ok, x) => { 19 | const s = net.createServer(); 20 | s.on('error', x); 21 | s.listen(port, () => (a = s.address()) && s.close(() => ok(a.port))); 22 | }); 23 | 24 | module.exports.usePort = usePort; 25 | 26 | const networkIps = Object.values(os.networkInterfaces()) 27 | .reduce((every, i) => [...every, ...i], []) 28 | .filter((i) => i.family === 'IPv4' && i.internal === false) 29 | .map((i) => i.address); 30 | 31 | module.exports.networkIps = networkIps; 32 | -------------------------------------------------------------------------------- /docs/demo/src/templates/default/servor/utils/openBrowser.js: -------------------------------------------------------------------------------- 1 | const childProcess = require('child_process'); 2 | 3 | module.exports = (url) => { 4 | let cmd; 5 | const args = []; 6 | 7 | if (process.platform === 'darwin') { 8 | try { 9 | childProcess.execSync(`osascript openChrome.applescript "${encodeURI(url)}"`, { 10 | cwd: __dirname, 11 | stdio: 'ignore', 12 | }); 13 | return true; 14 | } catch (err) {} 15 | cmd = 'open'; 16 | } else if (process.platform === 'win32') { 17 | cmd = 'cmd.exe'; 18 | args.push('/c', 'start', '""', '/b'); 19 | url = url.replace(/&/g, '^&'); 20 | } else { 21 | cmd = 'xdg-open'; 22 | } 23 | 24 | args.push(url); 25 | childProcess.spawn(cmd, args); 26 | }; 27 | -------------------------------------------------------------------------------- /docs/demo/src/templates/default/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Lato', 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', sans-serif; 3 | color: darkslategrey; 4 | } 5 | -------------------------------------------------------------------------------- /docs/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "baseUrl": "./", 6 | "jsxImportSource": "react", 7 | "paths": { 8 | "@*": ["src/*"] 9 | } 10 | }, 11 | "exclude": ["dist"] 12 | } 13 | -------------------------------------------------------------------------------- /docs/demo/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@tutorialkit/theme'; 2 | 3 | export default defineConfig({ 4 | // add your UnoCSS config here: https://unocss.dev/guide/config-file 5 | }); 6 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/.gitignore: -------------------------------------------------------------------------------- 1 | # generated types 2 | .astro 3 | 4 | # environment variables 5 | .env 6 | .env.production 7 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/README.md: -------------------------------------------------------------------------------- 1 | # [TutorialKit.dev](https://tutorialkit.dev) 2 | 3 | ## Local Development 4 | 5 | ### Prerequisites 6 | 7 | - Install [Node.js](https://nodejs.org/en) v18.18 or above. 8 | - Install [pnpm](https://pnpm.io/). 9 | 10 | ### Set up 11 | 12 | Clone this repository and navigate into the cloned directory. 13 | 14 | ``` 15 | git clone git@github.com:stackblitz/tutorialkit.git 16 | cd tutorialkit 17 | ``` 18 | 19 | TutorialKit uses [pnpm workspaces](https://pnpm.io/workspaces). Just run the install command in the root of the project. 20 | 21 | ``` 22 | pnpm install 23 | ``` 24 | 25 | You can now start the documentation website by running: 26 | 27 | ``` 28 | pnpm run docs 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/_headers: -------------------------------------------------------------------------------- 1 | /* 2 | Cross-Origin-Embedder-Policy: require-corp 3 | Cross-Origin-Opener-Policy: same-origin 4 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorialkit.dev", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro check && astro build && cp _headers ./dist/", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@tutorialkit/react": "workspace:*", 15 | "@webcontainer/api": "1.5.1", 16 | "classnames": "^2.5.1", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1" 19 | }, 20 | "devDependencies": { 21 | "@astrojs/check": "^0.7.0", 22 | "@astrojs/react": "^3.6.0", 23 | "@astrojs/starlight": "^0.23.4", 24 | "@tutorialkit/astro": "workspace:*", 25 | "@tutorialkit/theme": "workspace:*", 26 | "@types/gtag.js": "^0.0.20", 27 | "@types/react": "^18.3.3", 28 | "@types/react-dom": "^18.3.0", 29 | "astro": "^4.15.0", 30 | "sass": "^1.77.6", 31 | "sharp": "^0.32.6", 32 | "starlight-links-validator": "^0.9.0", 33 | "typescript": "^5.4.5", 34 | "unocss": "^0.59.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/public/tutorialkit-opengraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/public/tutorialkit-opengraph.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/assets/houston.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/assets/houston.webp -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/assets/tutorialkit-themes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/assets/tutorialkit-themes.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/components/Buttons/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonTheme } from '@components/Buttons/Button/ButtonTheme'; 2 | import cn from 'classnames'; 3 | import type { FC, ReactNode } from 'react'; 4 | import styles from './Button.module.scss'; 5 | 6 | interface Props { 7 | children?: ReactNode; 8 | theme?: ButtonTheme; 9 | url: string; 10 | } 11 | 12 | export const Button: FC = ({ children, theme = 'default', url }) => ( 13 | 14 | {children} 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/components/Buttons/Button/ButtonTheme.ts: -------------------------------------------------------------------------------- 1 | export type ButtonTheme = 'default' | 'accent'; 2 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/components/PropertyTable.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | required?: boolean; 4 | inherited?: boolean; 5 | values?: string; 6 | type?: string; 7 | } 8 | 9 | const { required = false, inherited = false, values, type } = Astro.props; 10 | --- 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Required{values ? 'Values' : 'Type'}Inherited
{required ? 'yes' : 'no'}{values ? values : type}{inherited ? 'yes' : 'no'}
28 | 29 | 34 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/components/Tabs/PackageManagerTabs.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Tabs, TabItem } from '@astrojs/starlight/components'; 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/components/react-examples/Example.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { classNames } from '@tutorialkit/react'; 3 | import '@tutorialkit/astro/default-theme.css'; 4 | 5 | interface Props { 6 | className?: string; 7 | previewClassName?: string; 8 | } 9 | 10 | const { className, previewClassName } = Astro.props; 11 | --- 12 | 13 |
14 |
20 | 21 |
22 | 23 | 24 |
25 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/components/react-examples/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | export function useTheme() { 4 | const [theme, setTheme] = useState<'dark' | 'light'>(getThemeFromRoot()); 5 | 6 | useEffect(() => { 7 | const observer = new MutationObserver(() => { 8 | setTheme(getThemeFromRoot()); 9 | }); 10 | 11 | observer.observe(window.document.documentElement, { 12 | attributes: true, 13 | attributeFilter: ['data-theme'], 14 | }); 15 | 16 | return () => observer.disconnect(); 17 | }, []); 18 | 19 | return theme; 20 | } 21 | 22 | function getThemeFromRoot() { 23 | return (globalThis.document?.documentElement.getAttribute('data-theme') as 'dark' | 'light') ?? 'light'; 24 | } 25 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/components/react-examples/hooks/useWebcontainer.ts: -------------------------------------------------------------------------------- 1 | import { WebContainer } from '@webcontainer/api'; 2 | import { useEffect } from 'react'; 3 | 4 | let webcontainerBooting = false; 5 | let resolve!: (webcontainer: WebContainer) => void; 6 | 7 | const webcontainerPromise = new Promise((_resolve) => { 8 | resolve = _resolve; 9 | }); 10 | 11 | export function useWebContainer() { 12 | useEffect(() => { 13 | if (!webcontainerBooting) { 14 | webcontainerBooting = true; 15 | 16 | WebContainer.boot({ workdirName: 'example' }).then((webcontainer) => { 17 | resolve(webcontainer); 18 | }); 19 | } 20 | }, []); 21 | 22 | return webcontainerPromise; 23 | } 24 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { docsSchema } from '@astrojs/starlight/schema'; 2 | import { defineCollection } from 'astro:content'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | }; 7 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/guides/images/tk-cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/guides/images/tk-cli.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/guides/images/tutorialkit-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/guides/images/tutorialkit-ui.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/guides/images/ui-code-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/guides/images/ui-code-editor.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/guides/images/ui-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/guides/images/ui-dialog.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/guides/images/ui-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/guides/images/ui-preview.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/guides/images/ui-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/guides/images/ui-terminal.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/guides/images/ui-top-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/guides/images/ui-top-bar.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: TutorialKit 3 | description: Get started with TutorialKit 4 | template: splash 5 | --- 6 | import HomePage from './index.astro'; 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-breadcrumb-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-breadcrumb-button.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-breadcrumb-dropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-breadcrumb-dropdown.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-breadcrumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-breadcrumb.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-callout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-callout.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-content.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor-gutter-fold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor-gutter-fold.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor-gutter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor-gutter.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor-search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor-search.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor-tooltip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor-tooltip.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-editor.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-filetree-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-filetree-file.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-filetree-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-filetree-folder.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-filetree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-filetree.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-navcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-navcard.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-panel-header-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-panel-header-button.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-panel-header-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-panel-header-tab.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-panel-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-panel-header.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-previews.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-previews.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-statuses.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-statuses.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-terminal.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/content/docs/reference/images/theming-top-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/docs/tutorialkit.dev/src/content/docs/reference/images/theming-top-bar.png -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/styles/breakpoints.scss: -------------------------------------------------------------------------------- 1 | $breakpoint-larger: 1440px; 2 | $breakpoint-tablet: 1024px; 3 | $breakpoint-tablet-small: 860px; 4 | $breakpoint-tablet-smaller: 770px; 5 | $breakpoint-small: 640px; 6 | $breakpoint-mobile: 500px; 7 | $breakpoint-tiny: 360px; 8 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/styles/fonts.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); 2 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --layout-content-width: 1440px; 3 | --layout-content-padding-large: 52px; 4 | --layout-content-padding: 24px; 5 | --layout-content-padding-small: 18px; 6 | 7 | --transition-fast: 0.1s ease; 8 | --transition-med: 0.16s ease; 9 | 10 | --sl-sidebar-width: 19rem; 11 | 12 | --sl-text-h1: 2.4rem; 13 | --sl-text-h2: 2rem; 14 | --sl-text-h3: 1.8rem; 15 | --sl-text-h4: 1.6rem; 16 | } 17 | 18 | :root, 19 | [data-theme='dark'] { 20 | --custom-color-text: rgba(255, 255, 255, 0.84); 21 | --custom-color-text-strong: #fff; 22 | 23 | --sl-color-bg-nav: var(--sl-color-bg) !important; 24 | --sl-color-bg-nav: transparent; 25 | --sl-color-bg-sidebar: var(--sl-color-bg); 26 | 27 | --sl-color-text-accent: rgba(255, 255, 255, 0.78); 28 | } 29 | 30 | [data-theme='light'] { 31 | --custom-color-text: rgba(0, 0, 0, 0.72); 32 | --custom-color-text-strong: #000; 33 | 34 | --sl-color-text-accent: rgba(0, 0, 0, 0.8) !important; 35 | } 36 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@*": ["src/*"] 7 | }, 8 | "jsx": "react-jsx", 9 | "jsxImportSource": "react", 10 | "types": ["@types/gtag.js"] 11 | }, 12 | "include": ["src"] 13 | } 14 | -------------------------------------------------------------------------------- /docs/tutorialkit.dev/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@tutorialkit/theme'; 2 | 3 | export default defineConfig({ 4 | // add your UnoCSS config here: https://unocss.dev/guide/config-file 5 | }); 6 | -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- 1 | # UI Tests 2 | 3 | > Tests for verifying TutorialKit works as expected in the browser. Tests are run against locally linked `@tutorialkit` packages. 4 | 5 | ## Running 6 | 7 | - `pnpm exec playwright install chromium --with-deps` - When running the tests first time 8 | - `pnpm test` 9 | 10 | ## Development 11 | 12 | - `pnpm start` - Starts example/fixture project's development server 13 | - `pnpm test:ui` - Start Playwright in UI mode 14 | 15 | ## Structure 16 | 17 | Test cases are located in `test` directory. 18 | Each test file has its own `chapter`, that contains `lesson`s for test cases: 19 | 20 | For example Navigation tests: 21 | 22 | ``` 23 | ├── src/content/tutorial 24 | │ └── tests 25 | │ └──── navigation 26 | │ ├── page-one 27 | │ ├── page-three 28 | │ └── page-two 29 | └── test 30 | └── navigation.test.ts 31 | ``` 32 | 33 | Or File Tree tests: 34 | 35 | ``` 36 | ├── src/content/tutorial 37 | │ └── tests 38 | │ └── file-tree 39 | │ └── lesson-and-solution 40 | └── test 41 | └── file-tree.test.ts 42 | ``` 43 | -------------------------------------------------------------------------------- /e2e/astro.config.ts: -------------------------------------------------------------------------------- 1 | import tutorialkit from '@tutorialkit/astro'; 2 | import { defineConfig } from 'astro/config'; 3 | 4 | export default defineConfig({ 5 | devToolbar: { enabled: false }, 6 | server: { port: 4329 }, 7 | integrations: [tutorialkit()], 8 | }); 9 | -------------------------------------------------------------------------------- /e2e/configs/lessons-in-part.ts: -------------------------------------------------------------------------------- 1 | import tutorialkit from '@tutorialkit/astro'; 2 | import { defineConfig } from 'astro/config'; 3 | 4 | export default defineConfig({ 5 | devToolbar: { enabled: false }, 6 | server: { port: 4332 }, 7 | outDir: './dist-lessons-in-part', 8 | integrations: [tutorialkit()], 9 | srcDir: './src-custom/lessons-in-part', 10 | }); 11 | -------------------------------------------------------------------------------- /e2e/configs/lessons-in-root.ts: -------------------------------------------------------------------------------- 1 | import tutorialkit from '@tutorialkit/astro'; 2 | import { defineConfig } from 'astro/config'; 3 | 4 | export default defineConfig({ 5 | devToolbar: { enabled: false }, 6 | server: { port: 4331 }, 7 | outDir: './dist-lessons-in-root', 8 | integrations: [tutorialkit()], 9 | srcDir: './src-custom/lessons-in-root', 10 | }); 11 | -------------------------------------------------------------------------------- /e2e/configs/override-components.ts: -------------------------------------------------------------------------------- 1 | import tutorialkit from '@tutorialkit/astro'; 2 | import { defineConfig } from 'astro/config'; 3 | 4 | export default defineConfig({ 5 | devToolbar: { enabled: false }, 6 | server: { port: 4330 }, 7 | outDir: './dist-override-components', 8 | integrations: [ 9 | tutorialkit({ 10 | components: { 11 | Dialog: './src/components/Dialog.tsx', 12 | TopBar: './src/components/TopBar.astro', 13 | HeadTags: './src/components/CustomHeadTags.astro', 14 | }, 15 | }), 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /e2e/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/content/config.ts: -------------------------------------------------------------------------------- 1 | import { contentSchema } from '@tutorialkit/types'; 2 | import { defineCollection } from 'astro:content'; 3 | 4 | const tutorial = defineCollection({ 5 | type: 'content', 6 | schema: contentSchema, 7 | }); 8 | 9 | export const collections = { tutorial }; 10 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/content/tutorial/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: tutorial 3 | mainCommand: '' 4 | prepareCommands: [] 5 | --- 6 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/content/tutorial/part-one/lesson-1/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Lesson one 4 | --- 5 | 6 | # Lessons in part test - Lesson one 7 | 8 | Lesson in part without chapter 9 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/content/tutorial/part-one/lesson-2/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Lesson two 4 | --- 5 | 6 | # Lessons in part test - Lesson two 7 | 8 | Lesson in part without chapter 9 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/content/tutorial/part-one/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: part 3 | title: 'Part one' 4 | --- 5 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/content/tutorial/part-two/chapter-one/lesson-3/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Lesson three 4 | --- 5 | 6 | # Lessons in part test - Lesson three 7 | 8 | Lesson in chapter 9 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/content/tutorial/part-two/chapter-one/lesson-4/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Lesson four 4 | --- 5 | 6 | # Lessons in part test - Lesson four 7 | 8 | Lesson in chapter 9 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/content/tutorial/part-two/chapter-one/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: 'Chapter one' 4 | --- 5 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/content/tutorial/part-two/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: part 3 | title: 'Part two' 4 | --- 5 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-part/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-root/content/config.ts: -------------------------------------------------------------------------------- 1 | import { contentSchema } from '@tutorialkit/types'; 2 | import { defineCollection } from 'astro:content'; 3 | 4 | const tutorial = defineCollection({ 5 | type: 'content', 6 | schema: contentSchema, 7 | }); 8 | 9 | export const collections = { tutorial }; 10 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-root/content/tutorial/lesson-one/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Lesson one 4 | --- 5 | 6 | # Lessons in root test - Lesson one 7 | 8 | Lesson in root without part 9 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-root/content/tutorial/lesson-two/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Lesson two 4 | --- 5 | 6 | # Lessons in root test - Lesson two 7 | 8 | Lesson in root without part 9 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-root/content/tutorial/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: tutorial 3 | mainCommand: '' 4 | prepareCommands: [] 5 | --- 6 | -------------------------------------------------------------------------------- /e2e/src-custom/lessons-in-root/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /e2e/src/components/ButtonDeleteFile.tsx: -------------------------------------------------------------------------------- 1 | import { webcontainer } from 'tutorialkit:core'; 2 | 3 | interface Props { 4 | filePath: string; 5 | newContent: string; 6 | 7 | // default to 'webcontainer' 8 | access?: 'store' | 'webcontainer'; 9 | testId?: string; 10 | } 11 | 12 | export function ButtonDeleteFile({ filePath, access = 'webcontainer', testId = 'delete-file' }: Props) { 13 | async function deleteFile() { 14 | switch (access) { 15 | case 'webcontainer': { 16 | const webcontainerInstance = await webcontainer; 17 | 18 | await webcontainerInstance.fs.rm(filePath); 19 | 20 | return; 21 | } 22 | case 'store': { 23 | throw new Error('Delete from store not implemented'); 24 | return; 25 | } 26 | } 27 | } 28 | 29 | return ( 30 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /e2e/src/components/CustomHeadTags.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /e2e/src/components/CustomMetadata.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from 'astro:content'; 3 | const collection = await getCollection('tutorial'); 4 | 5 | const lesson = collection.find((c) => c.data.type === 'lesson' && c.slug.startsWith(Astro.params.slug!))!; 6 | const { custom } = lesson.data; 7 | --- 8 | 9 |

Custom metadata

10 | 11 |
{JSON.stringify(custom, null,2)}
12 | -------------------------------------------------------------------------------- /e2e/src/components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import type DialogType from '@tutorialkit/react/dialog'; 2 | import type { ComponentProps } from 'react'; 3 | import { createPortal } from 'react-dom'; 4 | 5 | export default function Dialog({ title, confirmText, onClose, children }: ComponentProps) { 6 | return createPortal( 7 |
8 |

Custom Dialog

9 |

{title}

10 | 11 | {children} 12 | 13 | 16 |
, 17 | document.body, 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /e2e/src/components/TopBar.astro: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /e2e/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { contentSchema } from '@tutorialkit/types'; 2 | import { defineCollection } from 'astro:content'; 3 | 4 | const tutorial = defineCollection({ 5 | type: 'content', 6 | schema: contentSchema, 7 | }); 8 | 9 | export const collections = { tutorial }; 10 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: tutorial 3 | mainCommand: '' 4 | prepareCommands: [] 5 | downloadAsZip: true 6 | --- 7 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/allow-edits-disabled/_files/first-level/file.js: -------------------------------------------------------------------------------- 1 | export default 'File in first level'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/allow-edits-disabled/_files/first-level/second-level/file.js: -------------------------------------------------------------------------------- 1 | export default 'File in second level'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/allow-edits-disabled/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Allow Edits Disabled 4 | previews: false 5 | terminal: 6 | panels: terminal 7 | --- 8 | 9 | # File Tree test - Allow Edits Disabled 10 | 11 | Option `editor.fileTree.allowEdits` has default `false` value. 12 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/allow-edits-enabled/_files/first-level/file.js: -------------------------------------------------------------------------------- 1 | export default 'File in first level'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/allow-edits-enabled/_files/first-level/second-level/file.js: -------------------------------------------------------------------------------- 1 | export default 'File in second level'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/allow-edits-enabled/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Allow Edits Enabled 4 | previews: false 5 | editor: 6 | fileTree: 7 | allowEdits: true 8 | terminal: 9 | panels: terminal 10 | --- 11 | 12 | # File Tree test - Allow Edits Enabled 13 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/allow-edits-glob/_files/first-level/file.js: -------------------------------------------------------------------------------- 1 | export default 'File in first level'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/allow-edits-glob/_files/first-level/second-level/file.js: -------------------------------------------------------------------------------- 1 | export default 'File in second level'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/allow-edits-glob/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Allow Edits Glob 4 | previews: false 5 | editor: 6 | fileTree: 7 | allowEdits: 8 | # Items in root 9 | - "/*" 10 | # Only "allowed-filename-only.js" inside "/first-level" folder 11 | - "/first-level/allowed-filename-only.js" 12 | # Anything inside "second-level" folders anywhere 13 | - "**/second-level/**" 14 | terminal: 15 | panels: terminal 16 | --- 17 | 18 | # File Tree test - Allow Edits Glob 19 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/hidden/_files/example.js: -------------------------------------------------------------------------------- 1 | export default 'Lesson file example.js content'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/hidden/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Hidden 4 | editor: 5 | fileTree: false 6 | focus: /example.js 7 | --- 8 | 9 | # File Tree test - Hidden 10 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lesson file example.html title 4 | 5 | 6 | Lesson file example.html content 7 | 8 | 9 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/lesson-and-solution/_files/example.js: -------------------------------------------------------------------------------- 1 | export default 'Lesson file example.js content'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Solution file example.html title 4 | 5 | 6 | Solution file example.html content 7 | 8 | 9 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/lesson-and-solution/_solution/example.js: -------------------------------------------------------------------------------- 1 | export default 'Solution file example.js content'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/lesson-and-solution/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Lesson and solution 4 | --- 5 | 6 | # File Tree test - Lesson and solution 7 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: File Tree 4 | --- 5 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/no-solution/_files/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Lesson file example.html title 4 | 5 | 6 | Lesson file example.html content 7 | 8 | 9 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/no-solution/_files/example.js: -------------------------------------------------------------------------------- 1 | export default 'Lesson file example.js content'; 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/file-tree/no-solution/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: No solution 4 | --- 5 | 6 | # File Tree test - No solution 7 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/filesystem/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: File system 4 | --- 5 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/filesystem/no-watch/_files/bar.txt: -------------------------------------------------------------------------------- 1 | Initial content 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/filesystem/no-watch/content.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: No watch 4 | focus: /bar.txt 5 | --- 6 | 7 | import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; 8 | 9 | # Watch filesystem test 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/a/b/baz.txt: -------------------------------------------------------------------------------- 1 | Baz 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/filesystem/watch-glob/_files/bar.txt: -------------------------------------------------------------------------------- 1 | Initial content 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/filesystem/watch-glob/content.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Watch Glob 4 | focus: /bar.txt 5 | filesystem: 6 | watch: ['/*.txt', '/a/**/*', '/src/**/*'] 7 | --- 8 | 9 | import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; 10 | import { ButtonDeleteFile } from '@components/ButtonDeleteFile'; 11 | 12 | # Watch filesystem test 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/filesystem/watch/_files/a/b/baz.txt: -------------------------------------------------------------------------------- 1 | Baz 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/filesystem/watch/_files/bar.txt: -------------------------------------------------------------------------------- 1 | Initial content 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/filesystem/watch/content.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Watch 4 | focus: /bar.txt 5 | filesystem: 6 | watch: true 7 | --- 8 | 9 | import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; 10 | import { ButtonDeleteFile } from '@components/ButtonDeleteFile'; 11 | 12 | # Watch filesystem test 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/lesson-order/1-lesson/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Page one 4 | --- 5 | 6 | # Lesson order test - Page one 7 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/lesson-order/2-lesson/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Page two 4 | --- 5 | 6 | # Lesson order test - Page two 7 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/lesson-order/3-lesson/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Page three 4 | --- 5 | 6 | # Lesson order test - Page three 7 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/lesson-order/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: Lesson order 4 | lessons: 5 | - 2-lesson 6 | - 3-lesson 7 | - 1-lesson 8 | --- 9 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: part 3 | title: Tests 4 | --- 5 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/metadata/custom/content.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Custom 4 | terminal: 5 | panels: terminal 6 | custom: 7 | custom-message: 'Hello world' 8 | numeric-field: 5173 9 | --- 10 | 11 | import CustomMetaData from "@components/CustomMetadata.astro" 12 | 13 | # Metadata test - Custom 14 | 15 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/metadata/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: Metadata 4 | --- 5 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/navigation/layout-change-all-off/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Layout change all off 4 | previews: false 5 | terminal: false 6 | editor: false 7 | --- 8 | 9 | # Navigation test - Layout change all off 10 | 11 | This page should not show previw, editor or terminal. 12 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/navigation/layout-change-from/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Layout change from 4 | template: default 5 | mainCommand: node index.mjs 6 | previews: 7 | - title: "Custom preview" 8 | port: 8000 9 | --- 10 | 11 | # Navigation test - Layout change from 12 | 13 | This page should show previw 14 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/navigation/layout-change-to/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Layout change to 4 | previews: false 5 | terminal: 6 | panels: 7 | - ["terminal", "Custom Terminal"] 8 | --- 9 | 10 | # Navigation test - Layout change to 11 | 12 | This page should not show previw. It should show terminal instead. -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/navigation/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: Navigation 4 | lessons: 5 | - page-one 6 | - page-two 7 | - page-three 8 | - layout-change-all-off 9 | - layout-change-from 10 | - layout-change-to 11 | mainCommand: '' 12 | prepareCommands: [] 13 | --- 14 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/navigation/page-one/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Page one 4 | --- 5 | 6 | # Navigation test - Page one 7 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/navigation/page-three/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Page three 4 | --- 5 | 6 | # Navigation test - Page three 7 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/navigation/page-two/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Page two 4 | --- 5 | 6 | # Navigation test - Page two 7 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/auto-reload-1-from/_files/index.html: -------------------------------------------------------------------------------- 1 | Before 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/auto-reload-1-from/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Auto Reload From 4 | template: file-server 5 | autoReload: true 6 | previews: 7 | - [8000, "Server"] 8 | --- 9 | 10 | # Preview test - Auto Reload From 11 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/auto-reload-2-to/_files/index.html: -------------------------------------------------------------------------------- 1 | After 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/auto-reload-2-to/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Auto Reload To 4 | template: file-server 5 | autoReload: true 6 | previews: 7 | - [8000, "Server"] 8 | --- 9 | 10 | # Preview test - Auto Reload To 11 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/auto-reload-3-off/_files/index.html: -------------------------------------------------------------------------------- 1 | This should not be visible when navigated to 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/auto-reload-3-off/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Auto Reload Off 4 | template: file-server 5 | autoReload: false 6 | previews: 7 | - [8000, "Server"] 8 | --- 9 | 10 | # Preview test - Auto Reload Off 11 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: Preview 4 | mainCommand: 'node ./index.mjs' 5 | --- 6 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/multiple/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Multiple 4 | previews: 5 | - [8000, "First Server"] 6 | - [8000, "Second Server", "/about.html"] 7 | --- 8 | 9 | # Preview test - Multiple 10 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/single/_files/index.html: -------------------------------------------------------------------------------- 1 | Index page 2 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/preview/single/content.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Single 4 | template: file-server 5 | previews: 6 | - [8000, "Node Server"] 7 | --- 8 | 9 | import { ButtonWriteToFile } from '@components/ButtonWriteToFile'; 10 | 11 | # Preview test - Single 12 | 13 | 14 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/terminal/default/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Default 4 | terminal: 5 | panels: terminal 6 | --- 7 | 8 | # Terminal test - Default 9 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/terminal/meta.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: chapter 3 | title: Terminal 4 | --- 5 | -------------------------------------------------------------------------------- /e2e/src/content/tutorial/tests/terminal/open-by-default/content.md: -------------------------------------------------------------------------------- 1 | --- 2 | type: lesson 3 | title: Open by default 4 | terminal: 5 | open: true 6 | panels: "terminal" 7 | --- 8 | 9 | # Terminal test - Open by default 10 | -------------------------------------------------------------------------------- /e2e/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /e2e/src/templates/default/file-on-template.js: -------------------------------------------------------------------------------- 1 | export default 'This file is present on template'; 2 | -------------------------------------------------------------------------------- /e2e/src/templates/default/folder-on-template/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/e2e/src/templates/default/folder-on-template/.gitkeep -------------------------------------------------------------------------------- /e2e/src/templates/default/index.mjs: -------------------------------------------------------------------------------- 1 | import http from 'node:http'; 2 | 3 | const server = http.createServer((req, res) => { 4 | if (req.url === '/' || req.url === '/index.html') { 5 | res.writeHead(200, { 'Content-Type': 'text/html' }); 6 | res.end('Index page'); 7 | 8 | return; 9 | } 10 | 11 | if (req.url === '/about.html') { 12 | res.writeHead(200, { 'Content-Type': 'text/html' }); 13 | res.end('About page'); 14 | 15 | return; 16 | } 17 | 18 | res.writeHead(200, { 'Content-Type': 'text/html' }); 19 | res.end('Not found'); 20 | }); 21 | 22 | server.listen(8000); 23 | -------------------------------------------------------------------------------- /e2e/src/templates/file-server/index.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import http from 'node:http'; 3 | 4 | const server = http.createServer((req, res) => { 5 | if (req.url === '/' || req.url === '/index.html') { 6 | res.writeHead(200, { 'Content-Type': 'text/html' }); 7 | res.end(fs.readFileSync('./index.html', 'utf8')); 8 | 9 | return; 10 | } 11 | 12 | res.writeHead(200, { 'Content-Type': 'text/html' }); 13 | res.end('Not found'); 14 | }); 15 | 16 | server.listen(8000); 17 | -------------------------------------------------------------------------------- /e2e/test/headtags.override-components.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('developer can override HeadTags', async ({ page }) => { 4 | await page.goto('/'); 5 | 6 | const defaultElems = [ 7 | page.locator('title'), 8 | page.locator('meta[name="og:title"]'), 9 | page.locator('link[rel="stylesheet"]').first(), 10 | ]; 11 | const customElems = [ 12 | page.locator('meta[name="e2e-test-custom-meta-tag"][content="custom-content"]'), 13 | page.locator('link[rel="sitemap"]'), 14 | ]; 15 | 16 | for (const e of defaultElems) { 17 | await expect(e).toBeAttached(); 18 | } 19 | 20 | for (const e of customElems) { 21 | await expect(e).toBeAttached(); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/test/lesson-order.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | const BASE_URL = '/tests/lesson-order'; 4 | 5 | test('developer can configure custom order for lessons', async ({ page }) => { 6 | await page.goto(`${BASE_URL}/1-lesson`); 7 | await expect(page.getByRole('heading', { level: 1, name: 'Lesson order test - Page one' })).toBeVisible(); 8 | 9 | // navigation select can take a while to hydrate on page load, click until responsive 10 | await expect(async () => { 11 | const button = page.getByRole('button', { name: 'Tests / Lesson order / Page one' }); 12 | await button.click(); 13 | await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 }); 14 | }).toPass(); 15 | 16 | const list = page.getByRole('region', { name: 'Lesson order' }); 17 | 18 | // configured ordered is [2, 3, 1] 19 | await expect(list.getByRole('listitem').nth(0)).toHaveText('Page two'); 20 | await expect(list.getByRole('listitem').nth(1)).toHaveText('Page three'); 21 | await expect(list.getByRole('listitem').nth(2)).toHaveText('Page one'); 22 | }); 23 | -------------------------------------------------------------------------------- /e2e/test/metadata.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | const BASE_URL = '/tests/metadata'; 4 | 5 | test('developer can pass custom metadata to lesson', async ({ page }) => { 6 | await page.goto(`${BASE_URL}/custom`); 7 | await expect(page.getByRole('heading', { level: 1, name: 'Metadata test - Custom' })).toBeVisible(); 8 | 9 | await expect(page.getByRole('heading', { level: 2, name: 'Custom metadata' })).toBeVisible(); 10 | 11 | await expect(page.getByText('"custom-message": "Hello world"')).toBeVisible(); 12 | await expect(page.getByText('"numeric-field": 5173')).toBeVisible(); 13 | }); 14 | -------------------------------------------------------------------------------- /e2e/test/navigation.lessons-in-root.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('user can navigate between lessons using breadcrumbs', async ({ page }) => { 4 | await page.goto('/lesson-one'); 5 | 6 | await expect(page.getByRole('heading', { level: 1, name: 'Lessons in root test - Lesson one' })).toBeVisible(); 7 | await expect(page.getByText('Lesson in root without part')).toBeVisible(); 8 | 9 | // navigation select can take a while to hydrate on page load, click until responsive 10 | await expect(async () => { 11 | const button = page.getByRole('button', { name: 'Lesson one' }); 12 | await button.click(); 13 | await expect(page.locator('[data-state="open"]', { has: button })).toBeVisible({ timeout: 50 }); 14 | }).toPass(); 15 | 16 | await page.getByRole('navigation').getByRole('link', { name: 'Lesson two' }).click(); 17 | 18 | await expect(page.getByRole('heading', { level: 1, name: 'Lessons in root test - Lesson two' })).toBeVisible(); 19 | }); 20 | -------------------------------------------------------------------------------- /e2e/test/topbar.override-components.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('developer can override TopBar', async ({ page }) => { 4 | await page.goto('/'); 5 | 6 | const nav = page.getByRole('navigation'); 7 | await expect(nav.getByText('Custom Top Bar Mounted')).toBeVisible(); 8 | 9 | // default elements should also be visible 10 | await expect(nav.getByRole('button', { name: 'Download lesson as zip-file' })).toBeVisible(); 11 | await expect(nav.getByRole('button', { name: 'Open in StackBlitz' })).toBeVisible(); 12 | await expect(nav.getByRole('button', { name: 'Toggle Theme' })).toBeVisible(); 13 | }); 14 | -------------------------------------------------------------------------------- /e2e/test/utils.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, readFileSync, existsSync } from 'node:fs'; 2 | import { fileURLToPath } from 'node:url'; 3 | 4 | const TESTS_DIR = fileURLToPath(new URL('../src/content/tutorial/tests', import.meta.url)); 5 | 6 | export function readLessonFilesAndSolution( 7 | ...lessons: string[] 8 | ): Record; solution: Record }> { 9 | return lessons.reduce( 10 | (all, lesson) => ({ 11 | ...all, 12 | [lesson.split('/')[1]]: { 13 | files: readDirFiles(`${TESTS_DIR}/${lesson}/_files`), 14 | solution: readDirFiles(`${TESTS_DIR}/${lesson}/_solution`), 15 | }, 16 | }), 17 | {}, 18 | ); 19 | } 20 | 21 | function readDirFiles(dir: string): Record { 22 | if (!existsSync(dir)) { 23 | return {}; 24 | } 25 | 26 | return readdirSync(dir).reduce( 27 | (files, file) => ({ 28 | ...files, 29 | [file]: readFileSync(`${dir}/${file}`, 'utf8'), 30 | }), 31 | {}, 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "baseUrl": "./", 6 | "jsxImportSource": "react", 7 | "paths": { 8 | "@*": ["src/*"] 9 | } 10 | }, 11 | "include": ["src", "./*.ts", "configs/astro.config.override-components.ts"], 12 | "exclude": ["node_modules", "dist"] 13 | } 14 | -------------------------------------------------------------------------------- /e2e/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@tutorialkit/theme'; 2 | 3 | export default defineConfig({ 4 | // required for TutorialKit monorepo development mode 5 | content: { 6 | pipeline: { 7 | include: '**', 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /extensions/vscode/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /extensions/vscode/.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts = true 2 | -------------------------------------------------------------------------------- /extensions/vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | **/.vscode-test.* 12 | node_modules 13 | -------------------------------------------------------------------------------- /extensions/vscode/resources/icons/dark/chapter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /extensions/vscode/resources/icons/dark/lesson.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /extensions/vscode/resources/icons/light/chapter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /extensions/vscode/resources/icons/light/lesson.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /extensions/vscode/resources/tutorialkit-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/extensions/vscode/resources/tutorialkit-icon.png -------------------------------------------------------------------------------- /extensions/vscode/resources/tutorialkit-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/extensions/vscode/resources/tutorialkit-screenshot.png -------------------------------------------------------------------------------- /extensions/vscode/scripts/load-schema-worker.mjs: -------------------------------------------------------------------------------- 1 | import { parentPort } from 'node:worker_threads'; 2 | import { contentSchema } from '@tutorialkit/types'; 3 | import { zodToJsonSchema } from 'zod-to-json-schema'; 4 | 5 | parentPort.postMessage(zodToJsonSchema(contentSchema)); 6 | -------------------------------------------------------------------------------- /extensions/vscode/src/commands/_helpers.ts: -------------------------------------------------------------------------------- 1 | export function newCommand Promise>(fn: F) { 2 | return async function (this: any, ...args: Parameters): Promise> | undefined> { 3 | try { 4 | return await fn.apply(this, args); 5 | } catch (error: unknown) { 6 | if (error instanceof CancelError) { 7 | return undefined; 8 | } 9 | 10 | throw error; 11 | } 12 | }; 13 | } 14 | 15 | export class CancelError extends Error { 16 | constructor() { 17 | super('operation cancelled'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /extensions/vscode/src/commands/tutorialkit.goto.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export default async (path: string | vscode.Uri | undefined) => { 4 | if (!path) { 5 | return; 6 | } 7 | 8 | /** 9 | * This cast to 'any' makes no sense because if we narrow the type of path 10 | * there are no type errors. So this code: 11 | * 12 | * ```ts 13 | * typeof path === 'string' 14 | * ? await vscode.workspace.openTextDocument(path) 15 | * : await vscode.workspace.openTextDocument(path) 16 | * ; 17 | * ``` 18 | * 19 | * Type check correctly despite being identical to calling the function 20 | * without the branch. 21 | * 22 | * To avoid this TypeScript bug here we just cast to any. 23 | */ 24 | const document = await vscode.workspace.openTextDocument(path as any); 25 | 26 | await vscode.window.showTextDocument(document, { 27 | preserveFocus: true, 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /extensions/vscode/src/commands/tutorialkit.load-tutorial.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { extContext } from '../extension'; 3 | import { setLessonsTreeDataProvider, setLessonsTreeView } from '../global-state'; 4 | import { LessonsTreeDataProvider } from '../views/lessonsTree'; 5 | 6 | export async function loadTutorial(uri: vscode.Uri) { 7 | const treeDataProvider = new LessonsTreeDataProvider(uri, extContext); 8 | 9 | await treeDataProvider.init(); 10 | 11 | const treeView = vscode.window.createTreeView('tutorialkit-lessons-tree', { 12 | treeDataProvider, 13 | canSelectMany: true, 14 | }); 15 | 16 | setLessonsTreeDataProvider(treeDataProvider); 17 | setLessonsTreeView(treeView); 18 | 19 | extContext.subscriptions.push(treeView, treeDataProvider); 20 | 21 | vscode.commands.executeCommand('setContext', 'tutorialkit:tree', true); 22 | } 23 | -------------------------------------------------------------------------------- /extensions/vscode/src/commands/tutorialkit.refresh.ts: -------------------------------------------------------------------------------- 1 | import { getLessonsTreeDataProvider } from '../global-state'; 2 | 3 | export default () => { 4 | getLessonsTreeDataProvider().refresh(); 5 | }; 6 | -------------------------------------------------------------------------------- /extensions/vscode/src/commands/tutorialkit.select-tutorial.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import isTutorialKitWorkspace from '../utils/isTutorialKit'; 3 | import { cmd } from '.'; 4 | 5 | export async function selectTutorial() { 6 | const tutorialWorkpaces = (vscode.workspace.workspaceFolders || []).filter(isTutorialKitWorkspace); 7 | 8 | const selectedWorkspace = 9 | tutorialWorkpaces.length === 1 10 | ? tutorialWorkpaces[0] 11 | : await vscode.window 12 | .showQuickPick( 13 | tutorialWorkpaces.map((workspace) => workspace.name), 14 | { 15 | placeHolder: 'Select a workspace', 16 | }, 17 | ) 18 | .then((selected) => tutorialWorkpaces.find((workspace) => workspace.name === selected)); 19 | 20 | if (selectedWorkspace) { 21 | cmd.loadTutorial(selectedWorkspace.uri); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /extensions/vscode/src/global-state.ts: -------------------------------------------------------------------------------- 1 | import type { TreeView } from 'vscode'; 2 | import type { Node } from './models/Node'; 3 | import type { LessonsTreeDataProvider } from './views/lessonsTree'; 4 | 5 | let lessonsTreeDataProvider: LessonsTreeDataProvider; 6 | let lessonsTreeView: TreeView; 7 | 8 | export function getLessonsTreeDataProvider() { 9 | return lessonsTreeDataProvider; 10 | } 11 | 12 | export function getLessonsTreeView() { 13 | return lessonsTreeView; 14 | } 15 | 16 | export function setLessonsTreeDataProvider(provider: LessonsTreeDataProvider) { 17 | lessonsTreeDataProvider = provider; 18 | } 19 | 20 | export function setLessonsTreeView(treeView: TreeView) { 21 | lessonsTreeView = treeView; 22 | } 23 | -------------------------------------------------------------------------------- /extensions/vscode/src/language-server/schema.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | export function readSchema() { 5 | try { 6 | const fileContent = fs.readFileSync(path.join(__dirname, './schema.json'), 'utf-8'); 7 | return JSON.parse(fileContent); 8 | } catch { 9 | return {}; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /extensions/vscode/src/models/tree/constants.ts: -------------------------------------------------------------------------------- 1 | export const METADATA_FILES = new Set(['meta.md', 'meta.mdx', 'content.md', 'content.mdx']); 2 | export const FILES_FOLDER = '_files'; 3 | export const SOLUTION_FOLDER = '_solution'; 4 | -------------------------------------------------------------------------------- /extensions/vscode/src/utils/getIcon.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function getIcon( 4 | context: vscode.ExtensionContext, 5 | filename: string, 6 | ): { light: string | vscode.Uri; dark: string | vscode.Uri } { 7 | return { 8 | light: vscode.Uri.file(context.asAbsolutePath(`/resources/icons/light/${filename}`)), 9 | dark: vscode.Uri.file(context.asAbsolutePath(`/resources/icons/dark/${filename}`)), 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /extensions/vscode/src/utils/isTutorialKit.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import * as vscode from 'vscode'; 4 | 5 | /** 6 | * Check if the workspace is a TutorialKit workspace by looking for a 7 | * TutorialKit dependency in the package.json file. 8 | * 9 | * @param folder The workspace folder to check. 10 | * @returns True if the workspace is a TutorialKit workspace, false otherwise. 11 | */ 12 | export default function isTutorialKitWorkspace(folder: vscode.WorkspaceFolder): boolean { 13 | const packageJsonPath = path.join(folder.uri.fsPath, 'package.json'); 14 | const packageJsonContent = fs.readFileSync(packageJsonPath, 'utf8'); 15 | const packageJson = JSON.parse(packageJsonContent); 16 | 17 | const tutorialkitDependency = 18 | packageJson.dependencies?.['@tutorialkit/astro'] || packageJson.devDependencies?.['@tutorialkit/astro']; 19 | 20 | return !!tutorialkitDependency; 21 | } 22 | -------------------------------------------------------------------------------- /extensions/vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "module": "ES2022", 6 | "target": "ES2022", 7 | "outDir": "dist", 8 | "lib": ["ES2022"], 9 | "verbatimModuleSyntax": false, 10 | "moduleResolution": "Bundler", 11 | "sourceMap": true, 12 | "rootDir": "." 13 | }, 14 | "include": ["src", "scripts"], 15 | "references": [{ "path": "../../packages/types" }] 16 | } 17 | -------------------------------------------------------------------------------- /integration/cli/__snapshots__/npm-built.json: -------------------------------------------------------------------------------- 1 | [ 2 | "1-basics", 3 | "1-basics-1-introduction-1-welcome-files.json", 4 | "1-basics-1-introduction-1-welcome-solution.json", 5 | "1-basics/1-introduction", 6 | "1-basics/1-introduction/1-welcome", 7 | "1-basics/1-introduction/1-welcome/index.html", 8 | "favicon.svg", 9 | "index.html", 10 | "logo-dark.svg", 11 | "logo.svg", 12 | "template-default.json" 13 | ] -------------------------------------------------------------------------------- /integration/cli/__snapshots__/pnpm-built.json: -------------------------------------------------------------------------------- 1 | [ 2 | "1-basics", 3 | "1-basics-1-introduction-1-welcome-files.json", 4 | "1-basics-1-introduction-1-welcome-solution.json", 5 | "1-basics/1-introduction", 6 | "1-basics/1-introduction/1-welcome", 7 | "1-basics/1-introduction/1-welcome/index.html", 8 | "favicon.svg", 9 | "index.html", 10 | "logo-dark.svg", 11 | "logo.svg", 12 | "template-default.json" 13 | ] -------------------------------------------------------------------------------- /integration/cli/__snapshots__/yarn-built.json: -------------------------------------------------------------------------------- 1 | [ 2 | "1-basics", 3 | "1-basics-1-introduction-1-welcome-files.json", 4 | "1-basics-1-introduction-1-welcome-solution.json", 5 | "1-basics/1-introduction", 6 | "1-basics/1-introduction/1-welcome", 7 | "1-basics/1-introduction/1-welcome/index.html", 8 | "favicon.svg", 9 | "index.html", 10 | "logo-dark.svg", 11 | "logo.svg", 12 | "template-default.json" 13 | ] -------------------------------------------------------------------------------- /integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorialkit-integration", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "test": "vitest --testTimeout=300000" 7 | }, 8 | "dependencies": { 9 | "@tutorialkit/theme": "workspace:*", 10 | "execa": "^9.2.0", 11 | "tempy": "^3.1.0", 12 | "vitest": "^3.0.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integration/theme-resolving/inline-content.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import { getInlineContentForPackage } from '@tutorialkit/theme'; 4 | import { execa } from 'execa'; 5 | import { temporaryDirectory } from 'tempy'; 6 | import { afterAll, expect, test } from 'vitest'; 7 | 8 | const baseDir = path.resolve(__dirname, '../..'); 9 | const cli = path.join(baseDir, 'packages/cli/dist/index.js'); 10 | const tmp = temporaryDirectory(); 11 | 12 | afterAll(async () => { 13 | await fs.rm(tmp, { force: true, recursive: true }); 14 | }); 15 | 16 | test('getInlineContentForPackage finds files from @tutorialkit/astro', async () => { 17 | await execa( 18 | 'node', 19 | [cli, 'create', 'theme-test', '--install', '--no-git', '--no-start', '--package-manager', 'pnpm', '--defaults'], 20 | { cwd: tmp }, 21 | ); 22 | 23 | const content = getInlineContentForPackage({ 24 | name: '@tutorialkit/astro', 25 | pattern: '/dist/default/**/*.astro', 26 | root: `${tmp}/theme-test`, 27 | }); 28 | 29 | expect(content.length).toBeGreaterThan(0); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/astro/README.md: -------------------------------------------------------------------------------- 1 | # @tutorialkit/astro 2 | 3 | This **[Astro integration][astro-integration]** adds [TutorialKit](https://tutorialkit.dev/) to your project so that you can use TutorialKit's tutorial format for your astro content. 4 | 5 | This integration adds routes to serve your tutorial. It uses `@tutorialkit/react` for the dynamic part of the experience. 6 | 7 | ## License 8 | 9 | MIT 10 | 11 | Copyright (c) 2023–present [StackBlitz][stackblitz] 12 | 13 | [stackblitz]: https://stackblitz.com/ 14 | [astro-integration]: https://docs.astro.build/en/guides/integrations-guide/ 15 | -------------------------------------------------------------------------------- /packages/astro/src/default/components/HeadTags.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/astro/src/default/components/Logo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { LOGO_EXTENSIONS } from '../utils/constants'; 3 | import { readLogoFile } from '../utils/logo'; 4 | 5 | interface Props { 6 | logoLink: string; 7 | } 8 | 9 | const { logoLink } = Astro.props; 10 | 11 | const logo = readLogoFile('logo'); 12 | const logoDark = readLogoFile('logo-dark') ?? logo; 13 | 14 | if (!logo) { 15 | console.warn( 16 | [ 17 | `No logo found in public/. Supported filenames are: logo.(${LOGO_EXTENSIONS.join('|')}). `, 18 | `You can overwrite the logo for dark mode by providing a logo-dark.(${LOGO_EXTENSIONS.join('|')}).`, 19 | ].join(''), 20 | ); 21 | } 22 | --- 23 | 24 | 28 | {logo && } 29 | {logo && } 30 | 31 | -------------------------------------------------------------------------------- /packages/astro/src/default/components/NavCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { LessonLink } from '@tutorialkit/types'; 3 | 4 | interface Props { 5 | lesson: LessonLink; 6 | type: 'next' | 'prev'; 7 | } 8 | 9 | const { lesson, type } = Astro.props; 10 | --- 11 | 12 | 19 | 22 | {lesson.title} 23 | 24 | -------------------------------------------------------------------------------- /packages/astro/src/default/components/NavWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Nav } from '@tutorialkit/react'; 2 | import type { Lesson, NavList } from '@tutorialkit/types'; 3 | 4 | interface Props { 5 | lesson: Lesson; 6 | navList: NavList; 7 | } 8 | 9 | export function NavWrapper(props: Props) { 10 | return