├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
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 |
29 |
30 |
41 |
--------------------------------------------------------------------------------
/docs/demo/src/content/tutorial/1-forms-css/3-fieldset/1-fieldset-element/_solution/index.html:
--------------------------------------------------------------------------------
1 |
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 | First name:
20 |
21 |
22 |
23 | Last name:
24 |
25 |
26 |
27 | ```
28 |
--------------------------------------------------------------------------------
/docs/demo/src/content/tutorial/1-forms-css/3-fieldset/2-a-legend/_files/index.html:
--------------------------------------------------------------------------------
1 |
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 | First name:
18 |
19 |
20 |
21 | Last name:
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 | Required
16 | {values ? 'Values' : 'Type'}
17 | Inherited
18 |
19 |
20 |
21 |
22 | {required ? 'yes' : 'no'}
23 | {values ? values : type}
24 | {inherited ? 'yes' : 'no'}
25 |
26 |
27 |
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 |
31 | Delete File
32 |
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 |
14 | {confirmText}
15 |
16 | ,
17 | document.body,
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/e2e/src/components/TopBar.astro:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 | Custom Top Bar Mounted
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
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 ;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/astro/src/default/components/ThemeSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '@nanostores/react';
2 | import { memo, useEffect, useState } from 'react';
3 | import { themeStore, toggleTheme } from '../stores/theme-store';
4 |
5 | export const ThemeSwitch = memo(() => {
6 | const theme = useStore(themeStore);
7 | const [domLoaded, setDomLoaded] = useState(false);
8 |
9 | useEffect(() => {
10 | setDomLoaded(true);
11 | }, []);
12 |
13 | return (
14 | domLoaded && (
15 | toggleTheme()}
19 | >
20 |
21 |
22 | )
23 | );
24 | });
25 |
--------------------------------------------------------------------------------
/packages/astro/src/default/components/TopBar.astro:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/packages/astro/src/default/components/WorkspacePanelWrapper.tsx:
--------------------------------------------------------------------------------
1 | import { useStore } from '@nanostores/react';
2 | import { WorkspacePanel } from '@tutorialkit/react';
3 | import type { Lesson } from '@tutorialkit/types';
4 | import { useEffect } from 'react';
5 | import { Dialog } from 'tutorialkit:override-components';
6 | import { themeStore } from '../stores/theme-store.js';
7 | import { tutorialStore } from './webcontainer.js';
8 |
9 | interface Props {
10 | lesson: Lesson;
11 | }
12 |
13 | export function WorkspacePanelWrapper({ lesson }: Props) {
14 | const theme = useStore(themeStore);
15 |
16 | useEffect(() => {
17 | tutorialStore.setLesson(lesson);
18 | }, [lesson]);
19 |
20 | if (import.meta.env.SSR || !tutorialStore.lesson) {
21 | tutorialStore.setLesson(lesson, { ssr: import.meta.env.SSR });
22 | }
23 |
24 | return ;
25 | }
26 |
--------------------------------------------------------------------------------
/packages/astro/src/default/components/setup.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This code must be executed before WebContainer boots and be executed as soon as possible.
3 | * This ensures that when the authentication flow is complete in a popup, the popup is closed quickly.
4 | */
5 | import { auth } from '@webcontainer/api';
6 | import { authStore } from '../stores/auth-store.js';
7 |
8 | const authConfig = __WC_CONFIG__;
9 |
10 | export const useAuth = __ENTERPRISE__ && !!authConfig;
11 |
12 | // this condition is here to make sure the branch is removed by esbuild if it evaluates to false
13 | if (__ENTERPRISE__) {
14 | if (authConfig && !import.meta.env.SSR) {
15 | authStore.set(auth.init(authConfig));
16 |
17 | auth.on('auth-failed', (reason) => authStore.set({ status: 'auth-failed', ...reason }));
18 | auth.on('logged-out', () => authStore.set({ status: 'need-auth' }));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/astro/src/default/env-default.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | interface WebContainerConfig {
5 | editorOrigin: string;
6 | clientId: string;
7 | scope: string;
8 | }
9 |
10 | declare module 'tutorialkit:override-components' {
11 | const topBar: typeof import('./src/default/components/TopBar.astro').default;
12 | const headTags: typeof import('./src/default/components/HeadTags.astro').default;
13 | const dialog: typeof import('@tutorialkit/react/dialog').default;
14 |
15 | export { topBar as TopBar, dialog as Dialog, headTags as HeadTags };
16 | }
17 |
18 | declare const __ENTERPRISE__: boolean;
19 | declare const __WC_CONFIG__: WebContainerConfig | undefined;
20 |
--------------------------------------------------------------------------------
/packages/astro/src/default/pages/index.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import MetaTags from '../components/MetaTags.astro';
3 | import { getTutorial } from '../utils/content';
4 | import { joinPaths } from '../utils/url';
5 |
6 | const tutorial = await getTutorial();
7 |
8 | const lesson = tutorial.lessons[0];
9 | const part = lesson.part && tutorial.parts[lesson.part.id];
10 | const chapter = lesson.chapter && part?.chapters[lesson.chapter.id];
11 |
12 | const slug = [part?.slug, chapter?.slug, lesson.slug].filter(Boolean).join('/');
13 | const meta = lesson.data?.meta ?? {};
14 |
15 | meta.title ??= [lesson.part?.title, lesson.chapter?.title, lesson.data.title].filter(Boolean).join(' / ');
16 | meta.description ??= 'A TutorialKit interactive lesson';
17 |
18 | const redirect = joinPaths(import.meta.env.BASE_URL, `/${slug}`);
19 | ---
20 |
21 |
22 |
23 | Redirecting to {redirect}
24 |
25 |
26 |
--------------------------------------------------------------------------------
/packages/astro/src/default/stores/auth-store.ts:
--------------------------------------------------------------------------------
1 | import type { AuthAPI } from '@webcontainer/api';
2 | import { atom } from 'nanostores';
3 |
4 | type AuthStore = ReturnType | { status: 'no-auth' };
5 |
6 | export const authStore = atom({ status: 'no-auth' });
7 |
--------------------------------------------------------------------------------
/packages/astro/src/default/stores/theme-store.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'nanostores';
2 |
3 | export type Theme = 'dark' | 'light';
4 |
5 | export const kTheme = 'tk_theme';
6 |
7 | export function themeIsDark() {
8 | return themeStore.get() === 'dark';
9 | }
10 |
11 | export const themeStore = atom(initStore());
12 |
13 | function initStore() {
14 | if (!import.meta.env.SSR) {
15 | const themeAttribute = document.querySelector('html')?.getAttribute('data-theme');
16 |
17 | return (themeAttribute as Theme) ?? 'light';
18 | }
19 |
20 | return 'light';
21 | }
22 |
23 | export function toggleTheme() {
24 | const currentTheme = themeStore.get();
25 | const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
26 |
27 | themeStore.set(newTheme);
28 |
29 | localStorage.setItem(kTheme, newTheme);
30 |
31 | document.querySelector('html')?.setAttribute('data-theme', newTheme);
32 | }
33 |
--------------------------------------------------------------------------------
/packages/astro/src/default/stores/view-store.ts:
--------------------------------------------------------------------------------
1 | import { atom } from 'nanostores';
2 |
3 | export type View = 'content' | 'editor';
4 |
5 | export const viewStore = atom('content');
6 |
--------------------------------------------------------------------------------
/packages/astro/src/default/styles/base.css:
--------------------------------------------------------------------------------
1 | @import './variables.css';
2 | @import './markdown.css';
3 | @import './panel.css';
4 |
5 | body {
6 | font-family: 'Inter', sans-serif;
7 | font-optical-sizing: auto;
8 | font-weight: 400;
9 | font-style: normal;
10 | font-variation-settings: 'slnt' 0;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/astro/src/default/styles/panel.css:
--------------------------------------------------------------------------------
1 | .panel-button .panel-button-icon {
2 | --at-apply: text-tk-elements-panel-headerButton-iconColor;
3 | }
4 |
5 | .panel-button:hover .panel-button-icon {
6 | --at-apply: text-tk-elements-panel-headerButton-iconColorHover;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/astro/src/default/utils/__snapshots__/single-lesson-no-part.json:
--------------------------------------------------------------------------------
1 | {
2 | "parts": {},
3 | "lessons": [
4 | {
5 | "data": {
6 | "type": "lesson",
7 | "title": "Welcome to TutorialKit",
8 | "template": "default",
9 | "i18n": {
10 | "mocked": "default localization"
11 | },
12 | "openInStackBlitz": true,
13 | "downloadAsZip": false
14 | },
15 | "id": "1-lesson",
16 | "filepath": "1-lesson/content.md",
17 | "order": 0,
18 | "Markdown": "Markdown for tutorial",
19 | "slug": "lesson-slug",
20 | "files": [
21 | "1-lesson-files.json",
22 | []
23 | ],
24 | "solution": [
25 | "1-lesson-solution.json",
26 | []
27 | ]
28 | }
29 | ]
30 | }
--------------------------------------------------------------------------------
/packages/astro/src/default/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const RESIZABLE_PANELS = {
2 | Main: 'main',
3 | } as const;
4 | export const IGNORED_FILES = ['**/.DS_Store', '**/*.swp'];
5 |
6 | export const LOGO_EXTENSIONS = ['svg', 'png', 'jpeg', 'jpg'];
7 |
--------------------------------------------------------------------------------
/packages/astro/src/default/utils/content/files-ref.spec.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from 'vitest';
2 |
3 | import { getFilesRefList } from './files-ref';
4 |
5 | test('getFilesRefList returns files', async () => {
6 | const files = await getFilesRefList('test/fixtures/files', '');
7 |
8 | expect(files).toMatchInlineSnapshot(`
9 | [
10 | "test-fixtures-files.json",
11 | [
12 | "/first.js",
13 | "/nested/directory/second.ts",
14 | ],
15 | ]
16 | `);
17 | });
18 |
--------------------------------------------------------------------------------
/packages/astro/src/default/utils/content/files-ref.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import type { FilesRefList } from '@tutorialkit/types';
3 | import { folderPathToFilesRef } from '@tutorialkit/types';
4 | import glob from 'fast-glob';
5 | import { IGNORED_FILES } from '../constants';
6 |
7 | const CONTENT_DIR = path.join(process.cwd(), 'src/content/tutorial');
8 |
9 | export async function getFilesRefList(pathToFolder: string, base = CONTENT_DIR): Promise {
10 | const root = path.join(base, pathToFolder);
11 |
12 | const filePaths = (
13 | await glob(`${glob.convertPathToPattern(root)}/**/*`, {
14 | onlyFiles: true,
15 | ignore: IGNORED_FILES,
16 | dot: true,
17 | })
18 | ).map((filePath) => `/${path.relative(root, filePath).replaceAll(path.sep, '/')}`);
19 |
20 | filePaths.sort();
21 |
22 | const filesRef = folderPathToFilesRef(pathToFolder);
23 |
24 | return [filesRef, filePaths];
25 | }
26 |
--------------------------------------------------------------------------------
/packages/astro/src/default/utils/logo.ts:
--------------------------------------------------------------------------------
1 | import { LOGO_EXTENSIONS } from './constants';
2 | import { readPublicAsset } from './publicAsset';
3 |
4 | export function readLogoFile(logoPrefix: string = 'logo', absolute?: boolean) {
5 | let logo;
6 |
7 | for (const logoExt of LOGO_EXTENSIONS) {
8 | const logoFilename = `${logoPrefix}.${logoExt}`;
9 | logo = readPublicAsset(logoFilename, absolute);
10 |
11 | if (logo) {
12 | break;
13 | }
14 | }
15 |
16 | return logo;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/astro/src/default/utils/publicAsset.ts:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs';
2 | import path from 'node:path';
3 | import { joinPaths } from './url';
4 |
5 | export function readPublicAsset(filename: string, absolute?: boolean) {
6 | let asset;
7 | const exists = fs.existsSync(path.join('public', filename));
8 |
9 | if (!exists) {
10 | return;
11 | }
12 |
13 | asset = joinPaths(import.meta.env.BASE_URL, filename);
14 |
15 | if (absolute) {
16 | const site = import.meta.env.SITE;
17 |
18 | if (!site) {
19 | // the SITE env variable inherits the value from Astro.site configuration
20 | console.warn('Trying to compute an absolute file URL but Astro.site is not set.');
21 | } else {
22 | asset = joinPaths(site, asset);
23 | }
24 | }
25 |
26 | return asset;
27 | }
28 |
--------------------------------------------------------------------------------
/packages/astro/src/default/utils/url.ts:
--------------------------------------------------------------------------------
1 | export function joinPaths(basePath: string, ...paths: string[]): string {
2 | let result = basePath || '/';
3 |
4 | for (const subpath of paths) {
5 | if (subpath.length === 0) {
6 | continue;
7 | }
8 |
9 | const resultEndsWithSlash = result.endsWith('/');
10 | const subpathStartsWithSlash = subpath.startsWith('/');
11 |
12 | if (resultEndsWithSlash && subpathStartsWithSlash) {
13 | result += subpath.slice(1);
14 | } else if (resultEndsWithSlash || subpathStartsWithSlash) {
15 | result += subpath;
16 | } else {
17 | result += `/${subpath}`;
18 | }
19 | }
20 |
21 | return result;
22 | }
23 |
--------------------------------------------------------------------------------
/packages/astro/src/default/utils/workspace.ts:
--------------------------------------------------------------------------------
1 | import type { Lesson } from '@tutorialkit/types';
2 |
3 | /**
4 | * Tests if the provided lesson needs to show the workspace panel or not.
5 | *
6 | * @param lesson The lesson to check the workspace for.
7 | */
8 | export function hasWorkspace(lesson: Lesson) {
9 | if (lesson.data.editor !== false) {
10 | // we have a workspace if the editor is not hidden
11 | return true;
12 | }
13 |
14 | if (lesson.data.previews !== false) {
15 | // we have a workspace if the previews are shown
16 | return true;
17 | }
18 |
19 | if (lesson.data.terminal === false) {
20 | // if the value is explicitly false, it will render nothing
21 | return false;
22 | }
23 |
24 | if (lesson.data.terminal === true || !Array.isArray(lesson.data.terminal?.panels)) {
25 | // if the value is explicitly true, or `panels` is not an array, we have to render the terminal
26 | return true;
27 | }
28 |
29 | // we have a workspace if we have more than 0 terminal panels
30 | return lesson.data.terminal.panels.length > 0;
31 | }
32 |
--------------------------------------------------------------------------------
/packages/astro/src/remark/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import remarkDirective from 'remark-directive';
3 | import type { AstroConfigSetupOptions } from '../types.js';
4 | import { remarkCalloutsPlugin } from './callouts.js';
5 | import { remarkImportFilePlugin } from './import-file.js';
6 |
7 | export function updateMarkdownConfig({ updateConfig }: AstroConfigSetupOptions) {
8 | updateConfig({
9 | markdown: {
10 | remarkPlugins: [
11 | remarkDirective,
12 | remarkCalloutsPlugin(),
13 | remarkImportFilePlugin({ templatesPath: path.join(process.cwd(), 'src/templates') }),
14 | ],
15 | },
16 | });
17 | }
18 |
--------------------------------------------------------------------------------
/packages/astro/src/types.ts:
--------------------------------------------------------------------------------
1 | import type { AstroIntegration } from 'astro';
2 |
3 | export type AstroConfigSetupOptions = Parameters>[0];
4 | export type AstroServerSetupOptions = Parameters['astro:server:setup']>['0'];
5 | export type AstroBuildDoneOptions = Parameters['astro:build:done']>['0'];
6 | export type ViteDevServer = AstroServerSetupOptions['server'];
7 | export type VitePlugin = ViteDevServer['config']['plugins'][0];
8 | export type Files = Record;
9 |
--------------------------------------------------------------------------------
/packages/astro/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function withResolvers(): PromiseWithResolvers {
2 | let resolve!: (value: T | PromiseLike) => void;
3 | let reject!: (reason?: any) => void;
4 |
5 | const promise = new Promise((_resolve, _reject) => {
6 | resolve = _resolve;
7 | reject = _reject;
8 | });
9 |
10 | return {
11 | resolve,
12 | reject,
13 | promise,
14 | };
15 | }
16 |
17 | export function normalizeImportPath(importPath: string): string {
18 | // this is a fix for windows where import path should still use forward slashes
19 | return importPath.replaceAll('\\', '/');
20 | }
21 |
--------------------------------------------------------------------------------
/packages/astro/src/webcontainer-files/constants.ts:
--------------------------------------------------------------------------------
1 | export const IGNORED_FILES = ['**/.DS_Store', '**/*.swp', '**/node_modules/**'];
2 | export const EXTEND_CONFIG_FILEPATH = '/.tk-config.json';
3 | export const FILES_FOLDER_NAME = '_files';
4 | export const SOLUTION_FOLDER_NAME = '_solution';
5 |
--------------------------------------------------------------------------------
/packages/astro/test/fixtures/files/first.js:
--------------------------------------------------------------------------------
1 | export default 'Example JS file';
2 |
--------------------------------------------------------------------------------
/packages/astro/test/fixtures/files/nested/directory/second.ts:
--------------------------------------------------------------------------------
1 | export default 'Example TS file';
2 |
--------------------------------------------------------------------------------
/packages/astro/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["src"],
4 | "exclude": ["src/default"]
5 | }
6 |
--------------------------------------------------------------------------------
/packages/astro/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "jsxImportSource": "react",
6 | "module": "ESNext",
7 | "moduleResolution": "Bundler",
8 | "outDir": "dist"
9 | },
10 | "include": ["src"],
11 | "references": [
12 | { "path": "../runtime/tsconfig.build.json" },
13 | { "path": "../types/tsconfig.build.json" },
14 | { "path": "../react/tsconfig.build.json" },
15 | { "path": "../theme" }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/packages/astro/types.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @blitz/lines-around-comment */
2 |
3 | declare module 'tutorialkit:store' {
4 | const tutorialStore: import('@tutorialkit/runtime').TutorialStore;
5 |
6 | export default tutorialStore;
7 | }
8 |
9 | declare module 'tutorialkit:core' {
10 | /** Promise that resolves to the webcontainer that's running in the current lesson. */
11 | export const webcontainer: Promise;
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/README.md:
--------------------------------------------------------------------------------
1 | # TutorialKit CLI
2 |
3 | ## Creating interactive tutorials powered by WebContainer API
4 |
5 | **With NPM**
6 |
7 | ```bash
8 | npx tutorialkit@latest create my-tutorial
9 | ```
10 |
11 | **With PNPM**
12 |
13 | ```bash
14 | pnpm dlx tutorialkit@latest create my-tutorial
15 | ```
16 |
--------------------------------------------------------------------------------
/packages/cli/_template:
--------------------------------------------------------------------------------
1 | ../template
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/content/tutorial/1-basics/1-introduction/1-welcome/_files/counter.js:
--------------------------------------------------------------------------------
1 | export function setupCounter(element) {
2 | let counter = 0;
3 |
4 | const setCounter = (count) => {
5 | counter = count;
6 | element.innerHTML = `count is ${counter}`;
7 | };
8 |
9 | setCounter(0);
10 | }
11 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/content/tutorial/1-basics/1-introduction/1-welcome/_solution/counter.js:
--------------------------------------------------------------------------------
1 | export function setupCounter(element) {
2 | let counter = 0;
3 |
4 | const setCounter = (count) => {
5 | counter = count;
6 | element.innerHTML = `count is ${counter}`;
7 | };
8 |
9 | element.addEventListener('click', () => setCounter(counter + 1));
10 |
11 | setCounter(0);
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/content/tutorial/1-basics/1-introduction/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: chapter
3 | title: Introduction
4 | ---
5 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/content/tutorial/1-basics/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Basics
4 | ---
5 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/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 | ---
7 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/templates/default/.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 | *.local
12 | .vscode/*
13 | !.vscode/extensions.json
14 | .idea
15 | .DS_Store
16 | *.suo
17 | *.ntvs*
18 | *.njsproj
19 | *.sln
20 | *.sw?
21 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/templates/default/counter.js:
--------------------------------------------------------------------------------
1 | export function setupCounter(element) {
2 | let counter = 0;
3 |
4 | const setCounter = (count) => {
5 | counter = count;
6 | element.innerHTML = `count is ${counter}`;
7 | };
8 |
9 | element.addEventListener('click', () => setCounter(counter + 1));
10 |
11 | setCounter(0);
12 | }
13 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/templates/default/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/templates/default/javascript.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/templates/default/main.js:
--------------------------------------------------------------------------------
1 | import { setupCounter } from './counter.js';
2 | import javascriptLogo from './javascript.svg';
3 | import './style.css';
4 | import viteLogo from '/vite.svg';
5 |
6 | document.querySelector('#app').innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Hello Vite!
15 |
16 |
17 |
18 |
19 | Click on the Vite logo to learn more
20 |
21 |
22 | `;
23 |
24 | setupCounter(document.querySelector('#counter'));
25 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/src/templates/default/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "devDependencies": {
12 | "vite": "^5.2.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/cli/overwrites/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 |
--------------------------------------------------------------------------------
/packages/cli/scripts/_constants.js:
--------------------------------------------------------------------------------
1 | import path from 'node:path';
2 | import { fileURLToPath } from 'node:url';
3 |
4 | const __dirname = path.dirname(fileURLToPath(import.meta.url));
5 |
6 | export const outDir = 'dist';
7 | export const templatePath = path.join(__dirname, '../../template');
8 | export const templateDest = path.join(__dirname, '../template');
9 | export const distFolder = path.join(__dirname, '..', outDir);
10 | export const overwritesFolder = path.join(__dirname, '../overwrites');
11 |
--------------------------------------------------------------------------------
/packages/cli/scripts/build.js:
--------------------------------------------------------------------------------
1 | import fs from 'node:fs';
2 | import esbuild from 'esbuild';
3 | import { nodeExternalsPlugin } from 'esbuild-node-externals';
4 | import { distFolder, outDir } from './_constants.js';
5 |
6 | const isWatch = process.argv.includes('--watch');
7 |
8 | if (!isWatch) {
9 | fs.rmSync(distFolder, { recursive: true, force: true });
10 | }
11 |
12 | await buildJS();
13 |
14 | async function buildJS() {
15 | const context = await esbuild.context({
16 | entryPoints: ['src/index.ts'],
17 | minify: false,
18 | bundle: true,
19 | platform: 'node',
20 | target: 'node18',
21 | format: 'esm',
22 | outdir: outDir,
23 | define: {
24 | 'process.env.TUTORIALKIT_TEMPLATE_PATH': JSON.stringify(process.env.TUTORIALKIT_TEMPLATE_PATH ?? null),
25 | },
26 | plugins: [nodeExternalsPlugin()],
27 | });
28 |
29 | if (isWatch) {
30 | context.watch();
31 | } else {
32 | await context.rebuild();
33 | await context.dispose();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/cli/scripts/logger.js:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 |
3 | export function success(text) {
4 | console.log(`${chalk.bgGreen(chalk.black(' SUCCESS '))} ${text}`);
5 | }
6 |
7 | export function info(text) {
8 | console.log(`${chalk.bgBlue(chalk.black(' INFO '))} ${text}`);
9 | }
10 |
11 | export function warn(text) {
12 | console.log(`${chalk.bgYellow(chalk.black(' WARN '))} ${text}`);
13 | }
14 |
15 | export function error(text) {
16 | console.log(`${chalk.bgRed(chalk.black(' ERROR '))} ${text}`);
17 | }
18 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/create/hosting-config/_headers.txt:
--------------------------------------------------------------------------------
1 | /*
2 | Cross-Origin-Embedder-Policy: require-corp
3 | Cross-Origin-Opener-Policy: same-origin
4 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/create/hosting-config/netlify_toml.txt:
--------------------------------------------------------------------------------
1 | [[headers]]
2 | for = "/*"
3 | [headers.values]
4 | Cross-Origin-Embedder-Policy = "require-corp"
5 | Cross-Origin-Opener-Policy = "same-origin"
6 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/create/hosting-config/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "headers": [
3 | {
4 | "source": "/(.*)",
5 | "headers": [
6 | {
7 | "key": "Cross-Origin-Embedder-Policy",
8 | "value": "require-corp"
9 | },
10 | {
11 | "key": "Cross-Origin-Opener-Policy",
12 | "value": "same-origin"
13 | }
14 | ]
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/create/types.ts:
--------------------------------------------------------------------------------
1 | export interface TutorialKitConfig {
2 | enterprise?: {
3 | editorOrigin: string;
4 | clientId: string;
5 | scope: string;
6 | };
7 | }
8 |
--------------------------------------------------------------------------------
/packages/cli/src/commands/eject/options.ts:
--------------------------------------------------------------------------------
1 | export interface EjectOptions {
2 | _: Array;
3 | force?: boolean;
4 | defaults?: boolean;
5 | }
6 |
7 | export const DEFAULT_VALUES = {
8 | force: false,
9 | defaults: false,
10 | };
11 |
--------------------------------------------------------------------------------
/packages/cli/src/pkg.ts:
--------------------------------------------------------------------------------
1 | import pkg from '../package.json' assert { type: 'json' };
2 |
3 | export { pkg };
4 |
--------------------------------------------------------------------------------
/packages/cli/src/types.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*?raw' {
2 | const content: string;
3 | export default content;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/babel.ts:
--------------------------------------------------------------------------------
1 | import generator from '@babel/generator';
2 | import parser from '@babel/parser';
3 | import traverse from '@babel/traverse';
4 | import * as t from '@babel/types';
5 |
6 | export const visit = traverse.default;
7 | export { t };
8 |
9 | export function generate(ast: t.File) {
10 | const astToText = generator.default;
11 | const { code } = astToText(ast);
12 |
13 | return code;
14 | }
15 |
16 | export const parse = (code: string) => parser.parse(code, { sourceType: 'unambiguous', plugins: ['typescript'] });
17 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/colors.ts:
--------------------------------------------------------------------------------
1 | import chalk from 'chalk';
2 |
3 | export const primaryBlue = chalk.bgHex('#0d6fe8');
4 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/project.ts:
--------------------------------------------------------------------------------
1 | import { randomValueFromArray } from './random.js';
2 | import { adjectives, nouns } from './words.js';
3 |
4 | export function generateProjectName() {
5 | const adjective = randomValueFromArray(adjectives);
6 | const noun = randomValueFromArray(nouns);
7 |
8 | return `${adjective}-${noun}`;
9 | }
10 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/random.ts:
--------------------------------------------------------------------------------
1 | export function randomValueFromArray(array: any[]) {
2 | return array[Math.floor(array.length * Math.random())];
3 | }
4 |
--------------------------------------------------------------------------------
/packages/cli/src/utils/workspace-version.ts:
--------------------------------------------------------------------------------
1 | export function updateWorkspaceVersions(
2 | dependencies: Record,
3 | version: string,
4 | filterDependency: (dependency: string) => boolean = allowAll,
5 | ) {
6 | for (const dependency in dependencies) {
7 | const depVersion = dependencies[dependency];
8 |
9 | if (depVersion === 'workspace:*' && filterDependency(dependency)) {
10 | dependencies[dependency] = version;
11 | }
12 | }
13 | }
14 |
15 | function allowAll() {
16 | return true;
17 | }
18 |
--------------------------------------------------------------------------------
/packages/cli/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "outDir": "./dist",
5 | "declaration": false,
6 | "declarationMap": false,
7 | "module": "nodenext",
8 | "target": "ESNext",
9 | "resolveJsonModule": true,
10 | "skipLibCheck": true,
11 | "verbatimModuleSyntax": true,
12 | "isolatedModules": true,
13 | "esModuleInterop": true,
14 | "noEmit": true,
15 | "allowSyntheticDefaultImports": true,
16 | "moduleResolution": "node16",
17 | "lib": ["ESNext"],
18 | "sourceMap": false
19 | },
20 | "include": ["src"],
21 | "exclude": ["dist", "node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/create-tutorial/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "create-tutorial",
3 | "version": "0.0.3",
4 | "description": "Interactive tutorials powered by WebContainer API",
5 | "author": "StackBlitz Inc.",
6 | "type": "module",
7 | "bugs": "https://github.com/stackblitz/tutorialkit/issues",
8 | "homepage": "https://github.com/stackblitz/tutorialkit",
9 | "license": "MIT",
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/stackblitz/tutorialkit.git",
13 | "directory": "packages/create-tutorial"
14 | },
15 | "bin": {
16 | "create-tutorial": "./dist/index.js"
17 | },
18 | "scripts": {
19 | "build": "node ./scripts/build.js",
20 | "prepublishOnly": "pnpm run build"
21 | },
22 | "files": [
23 | "dist"
24 | ],
25 | "dependencies": {
26 | "@tutorialkit/cli": "latest"
27 | },
28 | "devDependencies": {
29 | "@types/node": "^20.14.6",
30 | "execa": "^9.2.0",
31 | "typescript": "^5.4.5"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/packages/create-tutorial/scripts/build.js:
--------------------------------------------------------------------------------
1 | import { rm } from 'node:fs/promises';
2 | import { execa } from 'execa';
3 |
4 | await rm('dist', { recursive: true, force: true });
5 | await execa('tsc', ['-b'], { stdio: 'inherit', preferLocal: true });
6 |
--------------------------------------------------------------------------------
/packages/create-tutorial/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { spawnSync } from 'node:child_process';
4 | import { createRequire } from 'node:module';
5 |
6 | const require = createRequire(import.meta.url);
7 | const tutorialKitEntryPoint = require.resolve('@tutorialkit/cli');
8 |
9 | spawnSync('node', [tutorialKitEntryPoint, 'create', ...process.argv.slice(2)], { stdio: 'inherit' });
10 |
--------------------------------------------------------------------------------
/packages/create-tutorial/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "declaration": false
6 | },
7 | "include": ["src"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/react/src/core.ts:
--------------------------------------------------------------------------------
1 | export {
2 | CodeMirrorEditor,
3 | type EditorDocument,
4 | type EditorUpdate,
5 | type OnChangeCallback,
6 | type OnScrollCallback,
7 | type ScrollPosition,
8 | } from './core/CodeMirrorEditor/index.js';
9 | export { FileTree } from './core/FileTree.js';
10 | export { Terminal, type TerminalRef, type TerminalProps } from './core/Terminal/index.js';
11 |
--------------------------------------------------------------------------------
/packages/react/src/core/CodeMirrorEditor/BinaryContent.tsx:
--------------------------------------------------------------------------------
1 | export function BinaryContent() {
2 | return (
3 |
4 | File format cannot be displayed.
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/packages/react/src/core/types.ts:
--------------------------------------------------------------------------------
1 | export type Theme = 'dark' | 'light';
2 |
--------------------------------------------------------------------------------
/packages/react/src/css.module.d.ts:
--------------------------------------------------------------------------------
1 | declare module './styles/*.module.css';
2 | declare module '../styles/*.module.css';
3 |
--------------------------------------------------------------------------------
/packages/react/src/hooks/useOutsideClick.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | export function useOutsideClick(ref: React.RefObject, onOutsideClick?: () => void) {
4 | useEffect(() => {
5 | function handleClickOutside(event: MouseEvent) {
6 | if (ref.current && !ref.current.contains(event.target as Node)) {
7 | onOutsideClick?.();
8 | }
9 | }
10 |
11 | document.addEventListener('mousedown', handleClickOutside);
12 |
13 | return () => {
14 | document.removeEventListener('mousedown', handleClickOutside);
15 | };
16 | }, [ref]);
17 | }
18 |
--------------------------------------------------------------------------------
/packages/react/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './BootScreen.js';
2 | export * from './Button.js';
3 | export * from './Nav.js';
4 | export * from './Panels/EditorPanel.js';
5 | export * from './Panels/PreviewPanel.js';
6 | export * from './Panels/TerminalPanel.js';
7 | export * from './Panels/WorkspacePanel.js';
8 | export { default as Dialog } from './core/Dialog.js';
9 | export type * from './core/types.js';
10 | export * from './utils/classnames.js';
11 |
--------------------------------------------------------------------------------
/packages/react/src/styles/resize-panel.module.css:
--------------------------------------------------------------------------------
1 | .PanelResizeHandle {
2 | position: relative;
3 | }
4 |
5 | .PanelResizeHandle[data-panel-group-direction='horizontal']:after {
6 | content: '';
7 | position: absolute;
8 | top: 0;
9 | bottom: 0;
10 | left: -5px;
11 | right: -5px;
12 | z-index: 999;
13 | }
14 |
15 | .PanelResizeHandle[data-panel-group-direction='vertical']:after {
16 | content: '';
17 | position: absolute;
18 | left: 0;
19 | right: 0;
20 | top: -5px;
21 | bottom: -5px;
22 | z-index: 999;
23 | }
24 |
25 | .PanelResizeHandle[data-resize-handle-state='hover']:after,
26 | .PanelResizeHandle[data-resize-handle-state='drag']:after {
27 | background-color: #8882;
28 | }
29 |
--------------------------------------------------------------------------------
/packages/react/src/styles/terminal.css:
--------------------------------------------------------------------------------
1 | .xterm {
2 | --at-apply: h-full;
3 | --at-apply: p-3;
4 | }
5 |
6 | .xterm .xterm-viewport {
7 | --at-apply: transition-theme;
8 | }
9 |
--------------------------------------------------------------------------------
/packages/react/src/types.d.ts:
--------------------------------------------------------------------------------
1 | // https://github.com/micromatch/picomatch?tab=readme-ov-file#api
2 | declare module 'picomatch/posix' {
3 | export { default } from 'picomatch';
4 | }
5 |
--------------------------------------------------------------------------------
/packages/react/src/utils/debounce.ts:
--------------------------------------------------------------------------------
1 | export function debounce(fn: (...args: Args) => void, delay = 100) {
2 | if (delay === 0) {
3 | return fn;
4 | }
5 |
6 | let timer: number | undefined;
7 |
8 | return function (this: U, ...args: Args) {
9 | const context = this;
10 |
11 | clearTimeout(timer);
12 |
13 | timer = window.setTimeout(() => {
14 | fn.apply(context, args);
15 | }, delay);
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/packages/react/src/utils/mobile.ts:
--------------------------------------------------------------------------------
1 | export function isMobile() {
2 | // we use sm: as the breakpoint for mobile. It's currently set to 640px
3 | return globalThis.innerWidth < 640;
4 | }
5 |
--------------------------------------------------------------------------------
/packages/react/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"],
9 | "exclude": ["src/**/*.spec.ts"],
10 | "references": [
11 | { "path": "../runtime/tsconfig.build.json" },
12 | { "path": "../theme" },
13 | { "path": "../types/tsconfig.build.json" }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/packages/react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "jsx": "react-jsx",
5 | "baseUrl": "./",
6 | "jsxImportSource": "react",
7 | "composite": true,
8 | "outDir": "dist",
9 | "rootDir": "src"
10 | },
11 | "include": ["src"],
12 | "references": [
13 | { "path": "../runtime/tsconfig.build.json" },
14 | { "path": "../theme" },
15 | { "path": "../types/tsconfig.build.json" }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/packages/runtime/README.md:
--------------------------------------------------------------------------------
1 | # @tutorialkit/runtime
2 |
3 | A wrapper around the **[WebContainer API][webcontainer-api]** focused on providing the right abstractions to let you focus on building highly interactive tutorials.
4 |
5 | The runtime exposes the following:
6 |
7 | - `TutorialStore`: A store to manage your tutorial content in WebContainer and in your components.
8 |
9 | Only a single instance of `TutorialStore` should be created in your application and its lifetime is bound by the lifetime of the WebContainer instance.
10 |
11 | ## License
12 |
13 | MIT
14 |
15 | Copyright (c) 2024–present [StackBlitz][stackblitz]
16 |
17 | [stackblitz]: https://stackblitz.com/
18 | [webcontainer-api]: https://webcontainers.io
19 |
--------------------------------------------------------------------------------
/packages/runtime/src/index.ts:
--------------------------------------------------------------------------------
1 | export type { Command, Commands, PreviewInfo, Step, Steps } from './webcontainer/index.js';
2 | export { safeBoot } from './webcontainer/index.js';
3 | export { TutorialStore } from './store/index.js';
4 |
--------------------------------------------------------------------------------
/packages/runtime/src/store/editor.spec.ts:
--------------------------------------------------------------------------------
1 | import type { FileDescriptor } from '@tutorialkit/types';
2 | import { expect, test } from 'vitest';
3 |
4 | import { EditorStore } from './editor.js';
5 |
6 | test('empty directories are removed when new content is added', () => {
7 | const store = new EditorStore();
8 | store.setDocuments({
9 | 'src/index.ts': '',
10 | });
11 |
12 | store.addFileOrFolder({ path: 'src/components', type: 'folder' });
13 |
14 | expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(`
15 | [
16 | "src/index.ts",
17 | "src/components",
18 | ]
19 | `);
20 |
21 | store.addFileOrFolder({ path: 'src/components/FileTree', type: 'folder' });
22 |
23 | expect(store.files.get().map(toFilename)).toMatchInlineSnapshot(`
24 | [
25 | "src/index.ts",
26 | "src/components/FileTree",
27 | ]
28 | `);
29 | });
30 |
31 | function toFilename(file: FileDescriptor) {
32 | return file.path;
33 | }
34 |
--------------------------------------------------------------------------------
/packages/runtime/src/types.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // https://github.com/micromatch/picomatch?tab=readme-ov-file#api
4 | declare module 'picomatch/posix.js' {
5 | export { default } from 'picomatch';
6 | }
7 |
--------------------------------------------------------------------------------
/packages/runtime/src/utils/multi-counter.ts:
--------------------------------------------------------------------------------
1 | export class MultiCounter {
2 | private _counts = new Map();
3 |
4 | increment(name: string | string[]) {
5 | if (typeof name === 'string') {
6 | const currentValue = this._counts.get(name) ?? 0;
7 |
8 | this._counts.set(name, currentValue + 1);
9 |
10 | return;
11 | }
12 |
13 | name.forEach((value) => this.increment(value));
14 | }
15 |
16 | decrement(name: string): boolean {
17 | const currentValue = this._counts.get(name) ?? 0;
18 |
19 | if (currentValue === 0) {
20 | return true;
21 | }
22 |
23 | this._counts.set(name, currentValue - 1);
24 |
25 | return currentValue - 1 === 0;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/runtime/src/utils/promises.ts:
--------------------------------------------------------------------------------
1 | export function withResolvers(): PromiseWithResolvers {
2 | if (typeof Promise.withResolvers === 'function') {
3 | return Promise.withResolvers();
4 | }
5 |
6 | let resolve!: (value: T | PromiseLike) => void;
7 | let reject!: (reason?: any) => void;
8 |
9 | const promise = new Promise((_resolve, _reject) => {
10 | resolve = _resolve;
11 | reject = _reject;
12 | });
13 |
14 | return {
15 | resolve,
16 | reject,
17 | promise,
18 | };
19 | }
20 |
21 | export function wait(ms: number): Promise {
22 | return new Promise((resolve) => setTimeout(resolve, ms));
23 | }
24 |
25 | /**
26 | * Simulates a single tick of the event loop.
27 | *
28 | * @returns A promise that resolves after the tick.
29 | */
30 | export function tick() {
31 | return new Promise((resolve) => {
32 | setTimeout(resolve);
33 | });
34 | }
35 |
--------------------------------------------------------------------------------
/packages/runtime/src/utils/terminal.ts:
--------------------------------------------------------------------------------
1 | export interface ITerminal {
2 | readonly cols?: number;
3 | readonly rows?: number;
4 |
5 | reset: () => void;
6 | write: (data: string) => void;
7 | input: (data: string) => void;
8 | onData: (cb: (data: string) => void) => void;
9 | }
10 |
11 | const reset = '\x1b[0m';
12 |
13 | export const escapeCodes = {
14 | reset,
15 | clear: '\x1b[g',
16 | red: (text: string) => `\x1b[1;31m${text}${reset}`,
17 | gray: (text: string) => `\x1b[37m${text}${reset}`,
18 | green: (text: string) => `\x1b[1;32m${text}${reset}`,
19 | magenta: (text: string) => `\x1b[35m${text}${reset}`,
20 | };
21 |
22 | export function clearTerminal(terminal?: ITerminal) {
23 | terminal?.reset();
24 | terminal?.write(escapeCodes.clear);
25 | }
26 |
--------------------------------------------------------------------------------
/packages/runtime/src/webcontainer/index.ts:
--------------------------------------------------------------------------------
1 | export { Command, Commands } from './command.js';
2 | export { safeBoot } from './on-demand-boot.js';
3 | export { PreviewInfo } from './preview-info.js';
4 | export { StepsController, type Step, type Steps } from './steps.js';
5 |
--------------------------------------------------------------------------------
/packages/runtime/src/webcontainer/port-info.ts:
--------------------------------------------------------------------------------
1 | export class PortInfo {
2 | constructor(
3 | readonly port: number,
4 | public origin?: string,
5 | public ready: boolean = false,
6 | ) {}
7 | }
8 |
--------------------------------------------------------------------------------
/packages/runtime/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"],
9 | "exclude": ["src/**/*.spec.ts"],
10 | "references": [{ "path": "../types/tsconfig.build.json" }]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/runtime/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"],
9 | "references": [{ "path": "../types/tsconfig.build.json" }, { "path": "../test-utils" }]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/runtime/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import tsconfigPaths from 'vite-tsconfig-paths';
2 | import { defineConfig } from 'vitest/config';
3 |
4 | export default defineConfig({
5 | plugins: [tsconfigPaths({ projects: ['../../tsconfig.json'] })],
6 | });
7 |
--------------------------------------------------------------------------------
/packages/template/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | .astro
3 | node_modules
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | .env
9 | .env.production
10 | .DS_Store
11 | .idea
12 |
--------------------------------------------------------------------------------
/packages/template/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode", "StackBlitz.tutorialkit", "unifiedjs.vscode-mdx"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/packages/template/.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 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/template/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: [tutorialkit()],
9 | });
10 |
--------------------------------------------------------------------------------
/packages/template/icons/languages/css.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/template/icons/languages/html.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/template/icons/languages/js.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/template/icons/languages/json.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/template/icons/languages/markdown.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/template/icons/languages/sass.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/template/icons/languages/ts.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/template/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tutorialkit-starter",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "astro": "astro",
8 | "build": "astro check && astro build",
9 | "dev": "astro dev",
10 | "preview": "astro preview",
11 | "start": "astro dev"
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/node": "^20.14.6",
25 | "@types/react": "^18.3.3",
26 | "astro": "^4.15.0",
27 | "prettier-plugin-astro": "^0.14.1",
28 | "typescript": "^5.4.5"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/template/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/packages/template/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 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/1-welcome/_files/src/index.js:
--------------------------------------------------------------------------------
1 | console.log('Hello World!');
2 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/1-welcome/_files/src/test/bar.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/1-welcome/_files/src/test/bar.js
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/1-welcome/_solution/src/index.js:
--------------------------------------------------------------------------------
1 | function add(a, b) {
2 | return a + b;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/bar/styles.css:
--------------------------------------------------------------------------------
1 | .foo {
2 | color: red;
3 | }
4 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 |
10 | Windows
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-1.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-1.js
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-10.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-10.ts
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-11.jsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-11.jsx
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-12.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-12.tsx
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-13.cts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-13.cts
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-14.mts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-14.mts
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-15.svg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-15.svg
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-16.vue:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-16.vue
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-2.cjs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-2.cjs
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-3.mjs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-3.mjs
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-4.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-4.css
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-5.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-5.md
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-6.png
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-7.jpg
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-8.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-8.gif
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-9.xyz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/test-9.xyz
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/unicorn.js:
--------------------------------------------------------------------------------
1 | function unicorn() {
2 | return Promise.resolve();
3 | }
4 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/windows_xp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackblitz/tutorialkit/3c608fb92c9499fc1bf9902bd61a970f7ebcb29b/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_files/src/windows_xp.png
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/2-foo/_solution/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 | Solution
10 | Other files should be visible as well
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/3-bar/_files/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Document
7 |
8 |
9 | TEST
10 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/3-bar/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Bar
4 | slug: bar
5 | focus: /src/index.html
6 | previews: [1]
7 | i18n:
8 | startWebContainerText: 'Foobar'
9 | ---
10 |
11 | # Bar
12 |
13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vestibulum ante non neque convallis tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam a iaculis libero. Suspendisse bibendum placerat enim, vitae iaculis sapien tincidunt vel. Nunc nunc dui, varius eget diam vitae, sagittis rutrum ante. Vestibulum ut nulla in ante tincidunt lacinia. Suspendisse ex orci, elementum vitae enim sed, pellentesque fermentum tortor. In bibendum sapien at nisi pharetra tincidunt. Etiam eleifend lacus dictum lorem ornare euismod. Nunc ullamcorper nunc mi, ut volutpat libero gravida id. Pellentesque cursus mi id tortor convallis eleifend.
14 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/1-introduction/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: chapter
3 | title: The first chapter in part 1
4 | slug: custom-chapter-slug
5 | ---
6 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/2-foo/1-welcome/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Welcome to TutorialKit from part 1
4 | ---
5 |
6 | # Bar 2
7 |
8 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce vestibulum ante non neque convallis tempor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam a iaculis libero. Suspendisse bibendum placerat enim, vitae iaculis sapien tincidunt vel. Nunc nunc dui, varius eget diam vitae, sagittis rutrum ante. Vestibulum ut nulla in ante tincidunt lacinia. Suspendisse ex orci, elementum vitae enim sed, pellentesque fermentum tortor. In bibendum sapien at nisi pharetra tincidunt. Etiam eleifend lacus dictum lorem ornare euismod. Nunc ullamcorper nunc mi, ut volutpat libero gravida id. Pellentesque cursus mi id tortor convallis eleifend.
9 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/2-foo/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: chapter
3 | title: The second chapter in part 1
4 | openInStackBlitz: true
5 | downloadAsZip: true
6 | ---
7 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/1-basics/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Basics
4 | filesystem:
5 | watch: true
6 | ---
7 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/2-advanced/1-unicorn/1-welcome/content.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: lesson
3 | title: Welcome to TutorialKit from part 2
4 | ---
5 |
6 | # Heading 1
7 |
8 | ## Heading 2
9 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/2-advanced/1-unicorn/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: chapter
3 | title: The first chatper in part 2
4 | openInStackBlitz: false
5 | downloadAsZip: false
6 | ---
7 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/2-advanced/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: part
3 | title: Advanced
4 | template: vite-app-2
5 | previews:
6 | - [5173, 'Vite App']
7 | ---
8 |
--------------------------------------------------------------------------------
/packages/template/src/content/tutorial/meta.md:
--------------------------------------------------------------------------------
1 | ---
2 | type: tutorial
3 | logoLink: https://tutorialkit.dev
4 | previews:
5 | - 8080
6 | autoReload: true
7 | mainCommand: ['npm start', 'Starting HTTP server']
8 | prepareCommands:
9 | - ['npm install', 'Installing dependencies']
10 | i18n:
11 | partTemplate: ${title}
12 | openInStackBlitz:
13 | projectTitle: Example Title
14 | projectDescription: Example Description
15 | downloadAsZip:
16 | filename: custom-lesson-name-without-extension
17 | ---
18 |
--------------------------------------------------------------------------------
/packages/template/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
--------------------------------------------------------------------------------
/packages/template/src/templates/default/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "default",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "dependencies": {
8 | "servor": "4.0.2"
9 | }
10 | },
11 | "node_modules/servor": {
12 | "version": "4.0.2",
13 | "resolved": "https://registry.npmjs.org/servor/-/servor-4.0.2.tgz",
14 | "integrity": "sha512-MlmQ5Ntv4jDYUN060x/KEmN7emvIqKMZ9OkM+nY8Bf2+KkyLmGsTqWLyAN2cZr5oESAcH00UanUyyrlS1LRjFw==",
15 | "bin": {
16 | "servor": "cli.js"
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/packages/template/src/templates/default/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example-lib",
3 | "private": true,
4 | "scripts": {
5 | "start": "node ./src/index.js"
6 | },
7 | "dependencies": {
8 | "servor": "4.0.2"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/template/src/templates/default/src/index.js:
--------------------------------------------------------------------------------
1 | const { createServer } = require('node:http');
2 | const servor = require('servor');
3 |
4 | createServer((_req, res) => {
5 | res.writeHead(200, { 'Content-Type': 'text/html' });
6 |
7 | res.end(`
8 |
9 |
10 |
11 | Test results
12 | All passed!
13 |
14 |
15 | `);
16 | }).listen(1);
17 |
18 | createServer((req, res) => res.end(`Server 2\n${req.method} ${req.url}`)).listen(2);
19 |
20 | servor({
21 | root: 'src/',
22 | reload: true,
23 | port: 8080,
24 | });
25 |
--------------------------------------------------------------------------------
/packages/template/src/templates/vite-app-2/.tk-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../vite-app"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/template/src/templates/vite-app-2/foo.txt:
--------------------------------------------------------------------------------
1 | test
2 |
--------------------------------------------------------------------------------
/packages/template/src/templates/vite-app/.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 | *.local
12 | .vscode/*
13 | !.vscode/extensions.json
14 | .idea
15 | .DS_Store
16 | *.suo
17 | *.ntvs*
18 | *.njsproj
19 | *.sln
20 | *.sw?
21 |
--------------------------------------------------------------------------------
/packages/template/src/templates/vite-app/counter.js:
--------------------------------------------------------------------------------
1 | export function setupCounter(element) {
2 | let counter = 0;
3 |
4 | const setCounter = (count) => {
5 | counter = count;
6 | element.innerHTML = `count is ${counter}`;
7 | };
8 |
9 | element.addEventListener('click', () => setCounter(counter + 1));
10 |
11 | setCounter(0);
12 | }
13 |
--------------------------------------------------------------------------------
/packages/template/src/templates/vite-app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/packages/template/src/templates/vite-app/javascript.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/packages/template/src/templates/vite-app/main.js:
--------------------------------------------------------------------------------
1 | import { setupCounter } from './counter.js';
2 | import javascriptLogo from './javascript.svg';
3 | import './style.css';
4 | import viteLogo from '/vite.svg';
5 |
6 | document.querySelector('#app').innerHTML = `
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
Hello Vite!
15 |
16 |
17 |
18 |
19 | Click on the Vite logo to learn more
20 |
21 |
22 | `;
23 |
24 | setupCounter(document.querySelector('#counter'));
25 |
--------------------------------------------------------------------------------
/packages/template/src/templates/vite-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "start": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "devDependencies": {
12 | "vite": "^5.2.0"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/packages/template/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 |
--------------------------------------------------------------------------------
/packages/template/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 |
--------------------------------------------------------------------------------
/packages/test-utils/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-utils",
3 | "version": "1.0.0",
4 | "description": "Test utilities for TutorialKit",
5 | "type": "module",
6 | "private": true,
7 | "devDependencies": {
8 | "@webcontainer/api": "1.5.1",
9 | "typescript": "^5.4.5",
10 | "vitest": "^3.0.5"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/test-utils/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/theme/src/index.spec.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url';
2 | import { expect, test } from 'vitest';
3 | import { getInlineContentForPackage } from './index.js';
4 |
5 | const root = fileURLToPath(new URL('../../template', import.meta.url));
6 |
7 | test('getInlineContentForPackage finds files from @tutorialkit/astro', () => {
8 | const content = getInlineContentForPackage({
9 | name: '@tutorialkit/astro',
10 | pattern: '/dist/default/**/*.astro',
11 | root,
12 | });
13 |
14 | expect(content.length).toBeGreaterThan(0);
15 | });
16 |
--------------------------------------------------------------------------------
/packages/theme/src/transition-theme.ts:
--------------------------------------------------------------------------------
1 | // this is a separate module with its own entrypoint so that it can be used in non-Node environment
2 |
3 | export const transitionTheme = {
4 | transitionProperty: 'background-color, border-color, box-shadow',
5 | transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
6 | transitionDuration: '150ms',
7 | };
8 |
--------------------------------------------------------------------------------
/packages/theme/src/utils.ts:
--------------------------------------------------------------------------------
1 | export function toCSSRules(camelCase: Record) {
2 | return Object.fromEntries(
3 | Object.entries(camelCase).map(([key, value]) => [key.replace(/([A-Z])/g, '-$1').toLowerCase(), value]),
4 | );
5 | }
6 |
--------------------------------------------------------------------------------
/packages/theme/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"]
9 | }
10 |
--------------------------------------------------------------------------------
/packages/types/README.md:
--------------------------------------------------------------------------------
1 | # @tutorialkit/types
2 |
3 | Collection of schemas and type definitions used by a TutorialKit project.
4 |
5 | In particular, this contains the schema for TutorialKit's main content format: parts, chapters, and lessons.
6 |
7 | ## License
8 |
9 | MIT
10 |
11 | Copyright (c) 2024–present [StackBlitz][stackblitz]
12 |
13 | [stackblitz]: https://stackblitz.com/
14 |
--------------------------------------------------------------------------------
/packages/types/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@tutorialkit/types",
3 | "version": "1.5.0",
4 | "description": "Types for TutorialKit",
5 | "author": "StackBlitz Inc.",
6 | "type": "module",
7 | "bugs": "https://github.com/stackblitz/tutorialkit/issues",
8 | "homepage": "https://github.com/stackblitz/tutorialkit",
9 | "license": "MIT",
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/stackblitz/tutorialkit.git",
13 | "directory": "packages/types"
14 | },
15 | "types": "./dist/index.d.ts",
16 | "exports": {
17 | ".": "./dist/index.js"
18 | },
19 | "files": [
20 | "dist"
21 | ],
22 | "scripts": {
23 | "build": "tsc -b tsconfig.build.json",
24 | "dev": "pnpm run build --watch --preserveWatchOutput",
25 | "test": "vitest"
26 | },
27 | "dependencies": {
28 | "zod": "3.23.8"
29 | },
30 | "devDependencies": {
31 | "typescript": "^5.4.5",
32 | "vitest": "^3.0.5"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/packages/types/src/default-localization.ts:
--------------------------------------------------------------------------------
1 | import type { Lesson } from './entities/index.js';
2 |
3 | export const DEFAULT_LOCALIZATION = {
4 | partTemplate: 'Part ${index}: ${title}',
5 | noPreviewNorStepsText: 'No preview to run nor steps to show',
6 | startWebContainerText: 'Run this tutorial',
7 | editPageText: 'Edit this page',
8 | webcontainerLinkText: 'Powered by WebContainers',
9 | filesTitleText: 'Files',
10 | fileTreeCreateFileText: 'Create file',
11 | fileTreeCreateFolderText: 'Create folder',
12 | fileTreeActionNotAllowedText: 'This action is not allowed',
13 | fileTreeFileExistsAlreadyText: 'File exists on filesystem already',
14 | fileTreeAllowedPatternsText: 'Created files and folders must match following patterns:',
15 | confirmationText: 'OK',
16 | prepareEnvironmentTitleText: 'Preparing Environment',
17 | defaultPreviewTitleText: 'Preview',
18 | reloadPreviewTitle: 'Reload Preview',
19 | toggleTerminalButtonText: 'Toggle Terminal',
20 | solveButtonText: 'Solve',
21 | resetButtonText: 'Reset',
22 | } satisfies Required;
23 |
--------------------------------------------------------------------------------
/packages/types/src/entities/nav.ts:
--------------------------------------------------------------------------------
1 | export interface NavItem {
2 | id: string;
3 | title: string;
4 | type?: 'part' | 'chapter' | 'lesson';
5 | href?: string;
6 | sections?: NavItem[];
7 | }
8 |
9 | export type NavList = NavItem[];
10 |
--------------------------------------------------------------------------------
/packages/types/src/files-ref.spec.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { folderPathToFilesRef } from './files-ref.js';
3 |
4 | describe('folderPathToFilesRef', () => {
5 | const testCases = [
6 | ['path/to/folder', 'path-to-folder.json'],
7 | ['path/to///folder', 'path-to-folder.json'],
8 | ['files', 'files.json'],
9 | ['path/to/folder_2', 'path-to-folder2.json'],
10 | ['path\\to\\folder', 'path-to-folder.json'],
11 | ['path\\to\\\\\\folder', 'path-to-folder.json'],
12 | ];
13 |
14 | testCases.forEach(([folderPath, expectedFilesRef]) => {
15 | it(`should return the files ref for a given folder path - ${folderPath}`, () => {
16 | const filesRef = folderPathToFilesRef(folderPath);
17 |
18 | expect(filesRef).toBe(expectedFilesRef);
19 | });
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/packages/types/src/files-ref.ts:
--------------------------------------------------------------------------------
1 | export function folderPathToFilesRef(pathToFolder: string): string {
2 | return encodeURIComponent(pathToFolder.replaceAll(/[\/\\]+/g, '-').replaceAll('_', '')) + '.json';
3 | }
4 |
--------------------------------------------------------------------------------
/packages/types/src/index.ts:
--------------------------------------------------------------------------------
1 | export type * from './entities/index.js';
2 | export * from './schemas/index.js';
3 | export * from './files-ref.js';
4 | export { interpolateString } from './utils/interpolation.js';
5 | export { DEFAULT_LOCALIZATION } from './default-localization.js';
6 |
--------------------------------------------------------------------------------
/packages/types/src/schemas/chapter.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 | import { baseSchema } from './common.js';
3 |
4 | export const chapterSchema = baseSchema.extend({
5 | type: z.literal('chapter'),
6 | lessons: z
7 | .array(z.string())
8 | .optional()
9 | .describe(
10 | 'The list of lessons in this chapter. The order of the array defines the order of the lessons. If not specified a folder-based numbering system is used instead.',
11 | ),
12 | });
13 |
14 | export type ChapterSchema = z.infer;
15 |
--------------------------------------------------------------------------------
/packages/types/src/schemas/content.ts:
--------------------------------------------------------------------------------
1 | import type { z } from 'zod';
2 | import { chapterSchema } from './chapter.js';
3 | import { lessonSchema } from './lesson.js';
4 | import { partSchema } from './part.js';
5 | import { tutorialSchema } from './tutorial.js';
6 |
7 | export const contentSchema = tutorialSchema
8 | .strict()
9 | .or(partSchema.strict())
10 | .or(chapterSchema.strict())
11 | .or(lessonSchema.strict());
12 |
13 | export type ContentSchema = z.infer;
14 |
--------------------------------------------------------------------------------
/packages/types/src/schemas/index.ts:
--------------------------------------------------------------------------------
1 | export * from './chapter.js';
2 | export * from './common.js';
3 | export * from './content.js';
4 | export * from './lesson.js';
5 | export * from './part.js';
6 | export * from './tutorial.js';
7 |
--------------------------------------------------------------------------------
/packages/types/src/schemas/lesson.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 | import { baseSchema } from './common.js';
3 |
4 | export const lessonSchema = baseSchema.extend({
5 | type: z.literal('lesson'),
6 | scope: z.string().optional().describe('A prefix that all file paths must match to be visible in the file tree.'),
7 | hideRoot: z
8 | .boolean()
9 | .optional()
10 | .describe('If set to false, `/` is shown at the top of the file tree. Defaults to true.'),
11 | });
12 |
13 | export type LessonSchema = z.infer;
14 |
--------------------------------------------------------------------------------
/packages/types/src/schemas/metatags.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const metaTagsSchema = z.object({
4 | image: z
5 | .string()
6 | .optional()
7 | /**
8 | * Ideally we would want to use `image` from:
9 | * https://docs.astro.build/en/guides/images/#images-in-content-collections .
10 | */
11 | .describe('Image to show on social previews. A relative path is resolved to the public folder.'),
12 | description: z.string().optional().describe('A description for metadata'),
13 | title: z.string().optional().describe('A title to use specifically for metadata'),
14 | });
15 |
16 | export type MetaTagsSchema = z.infer;
17 |
--------------------------------------------------------------------------------
/packages/types/src/schemas/part.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 | import { baseSchema } from './common.js';
3 |
4 | export const partSchema = baseSchema.extend({
5 | type: z.literal('part'),
6 | chapters: z
7 | .array(z.string())
8 | .optional()
9 | .describe(
10 | 'The list of chapters in this part. The order of this array defines the order of the chapters. If not specified a folder-based numbering system is used instead.',
11 | ),
12 | lessons: z
13 | .array(z.string())
14 | .optional()
15 | .describe(
16 | 'The list of lessons in this part. The order of this array defines the order of the lessons. If not specified a folder-based numbering system is used instead.',
17 | ),
18 | });
19 |
20 | export type PartSchema = z.infer;
21 |
--------------------------------------------------------------------------------
/packages/types/src/schemas/tutorial.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 | import { webcontainerSchema } from './common.js';
3 |
4 | export const tutorialSchema = webcontainerSchema.extend({
5 | type: z.literal('tutorial'),
6 | logoLink: z.string().optional(),
7 | parts: z
8 | .array(z.string())
9 | .optional()
10 | .describe(
11 | 'The list of parts in this tutorial. The order of this array defines the order of the parts. If not specified a folder-based numbering system is used instead.',
12 | ),
13 | lessons: z
14 | .array(z.string())
15 | .optional()
16 | .describe(
17 | 'The list of lessons in this tutorial. The order of this array defines the order of the lessons. If not specified a folder-based numbering system is used instead.',
18 | ),
19 | });
20 |
21 | export type TutorialSchema = z.infer;
22 |
--------------------------------------------------------------------------------
/packages/types/src/utils/interpolation.ts:
--------------------------------------------------------------------------------
1 | export function interpolateString(template: string, variables: Record) {
2 | for (const [variable, value] of Object.entries(variables)) {
3 | template = template.replace(`\${${variable}}`, value.toString());
4 | }
5 |
6 | return template;
7 | }
8 |
--------------------------------------------------------------------------------
/packages/types/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"],
9 | "exclude": ["src/**/*.spec.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "src",
6 | "composite": true
7 | },
8 | "include": ["src"]
9 | }
10 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - 'packages/*'
3 | - 'docs/*'
4 | - 'extensions/*'
5 | - 'integration'
6 | - 'e2e'
7 |
--------------------------------------------------------------------------------
/scripts/clean.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | find . -name "*.tsbuildinfo" -type f -delete
4 | find packages -maxdepth 2 -name "dist" -type d -exec rm -rf {} +
5 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "nodenext",
5 | "verbatimModuleSyntax": true,
6 | "moduleResolution": "nodenext",
7 | "forceConsistentCasingInFileNames": true,
8 | "esModuleInterop": true,
9 | "skipLibCheck": true,
10 | "declaration": true,
11 | "noImplicitReturns": true,
12 | "strict": true,
13 | "paths": {
14 | "@tutorialkit/test-utils": ["./packages/test-utils/src/index.ts"]
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/uno.config.js:
--------------------------------------------------------------------------------
1 | // for vscode intellisense
2 | import { defineConfig } from './packages/theme/dist/index.js';
3 |
4 | export default defineConfig({});
5 |
--------------------------------------------------------------------------------