xs-dev 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +4 -0
  2. package/build/commands/scan.js +13 -6
  3. package/build/toolbox/setup/esp32.js +2 -2
  4. package/build/toolbox/update/esp32.js +2 -2
  5. package/docs/astro.config.mjs +13 -0
  6. package/docs/public/favicon.ico +0 -0
  7. package/docs/public/make-scrollable-code-focusable.js +3 -0
  8. package/docs/src/components/Footer/AvatarList.astro +149 -0
  9. package/docs/src/components/Footer/Footer.astro +16 -0
  10. package/docs/src/components/HeadCommon.astro +41 -0
  11. package/docs/src/components/HeadSEO.astro +34 -0
  12. package/docs/src/components/Header/Header.astro +133 -0
  13. package/docs/src/components/Header/LanguageSelect.css +47 -0
  14. package/docs/src/components/Header/LanguageSelect.tsx +49 -0
  15. package/docs/src/components/Header/Search.css +39 -0
  16. package/docs/src/components/Header/Search.tsx +65 -0
  17. package/docs/src/components/Header/SidebarToggle.tsx +44 -0
  18. package/docs/src/components/Header/SkipToContent.astro +22 -0
  19. package/docs/src/components/LeftSidebar/LeftSidebar.astro +118 -0
  20. package/docs/src/components/PageContent/PageContent.astro +41 -0
  21. package/docs/src/components/RightSidebar/MoreMenu.astro +70 -0
  22. package/docs/src/components/RightSidebar/RightSidebar.astro +27 -0
  23. package/docs/src/components/RightSidebar/TableOfContents.tsx +49 -0
  24. package/docs/src/components/RightSidebar/ThemeToggleButton.css +37 -0
  25. package/docs/src/components/RightSidebar/ThemeToggleButton.tsx +83 -0
  26. package/docs/src/config.ts +43 -0
  27. package/docs/src/languages.ts +10 -0
  28. package/docs/src/layouts/MainLayout.astro +122 -0
  29. package/docs/src/pages/en/features/include.md +48 -0
  30. package/docs/src/pages/en/features/init.md +57 -0
  31. package/docs/src/pages/en/features/run.md +63 -0
  32. package/docs/src/pages/en/features/scan.md +28 -0
  33. package/docs/src/pages/en/features/setup.md +55 -0
  34. package/docs/src/pages/en/features/teardown.md +13 -0
  35. package/docs/src/pages/en/features/update.md +21 -0
  36. package/docs/src/pages/en/introduction.md +68 -0
  37. package/docs/src/pages/index.astro +5 -0
  38. package/docs/src/pages/search-index.json.ts +18 -0
  39. package/docs/src/styles/index.css +382 -0
  40. package/docs/src/styles/theme.css +125 -0
  41. package/docs/tailwind.config.cjs +7 -0
  42. package/docs/tsconfig.json +15 -0
  43. package/package.json +20 -6
  44. package/docs/commands.md +0 -3
  45. package/docs/plugins.md +0 -47
@@ -0,0 +1,65 @@
1
+ import type { FunctionalComponent } from 'preact'
2
+ import { h, Fragment } from 'preact'
3
+ import { useEffect, useRef, useState } from 'preact/hooks'
4
+ import Fuse from 'fuse.js'
5
+ import './Search.css'
6
+
7
+ const SearchForm: FunctionalComponent = () => {
8
+ const fuse = useRef(null)
9
+ const [results, setResults] = useState([])
10
+
11
+ const searchContent = (event: SubmitEvent) => {
12
+ event.preventDefault()
13
+ const data = Object.fromEntries(
14
+ new FormData(event.currentTarget as HTMLFormElement)
15
+ )
16
+ const result = fuse.current.search(data.search)
17
+ setResults(result.map(({ item }) => item))
18
+ }
19
+
20
+ useEffect(() => {
21
+ fetch('/xs-dev/search-index.json')
22
+ .then((res) => res.json())
23
+ .then((content) => {
24
+ const options = { keys: ['title', 'description', 'content'] }
25
+ fuse.current = new Fuse(content, options)
26
+ })
27
+ }, [])
28
+
29
+ return (
30
+ <div class="flex flex-column SearchContainer">
31
+ <form method="GET" id="search-form" onSubmit={searchContent}>
32
+ <div role="search" class="flex flex-row">
33
+ <input
34
+ type="search"
35
+ id="search"
36
+ name="search"
37
+ aria-label="search documentation content"
38
+ placeholder="search documentation content"
39
+ class="SearchContainer-input"
40
+ />
41
+ <button type="submit" class="SearchContainer-button">
42
+ Search
43
+ </button>
44
+ </div>
45
+ </form>
46
+ <ul
47
+ class={`SearchContainer-list ${
48
+ results.length > 0 ? 'has-results' : undefined
49
+ }`}
50
+ aria-live="assertive"
51
+ aria-atomic="true"
52
+ >
53
+ {results.map((result) => (
54
+ <li class="SearchContainer-list_item">
55
+ <a href={result.url} class="SearchContainer-link">
56
+ {result.title}
57
+ </a>
58
+ </li>
59
+ ))}
60
+ </ul>
61
+ </div>
62
+ )
63
+ }
64
+
65
+ export default SearchForm
@@ -0,0 +1,44 @@
1
+ import type { FunctionalComponent } from 'preact';
2
+ import { h, Fragment } from 'preact';
3
+ import { useState, useEffect } from 'preact/hooks';
4
+
5
+ const MenuToggle: FunctionalComponent = () => {
6
+ const [sidebarShown, setSidebarShown] = useState(false);
7
+
8
+ useEffect(() => {
9
+ const body = document.getElementsByTagName('body')[0];
10
+ if (sidebarShown) {
11
+ body.classList.add('mobile-sidebar-toggle');
12
+ } else {
13
+ body.classList.remove('mobile-sidebar-toggle');
14
+ }
15
+ }, [sidebarShown]);
16
+
17
+ return (
18
+ <button
19
+ type="button"
20
+ aria-pressed={sidebarShown ? 'true' : 'false'}
21
+ id="menu-toggle"
22
+ onClick={() => setSidebarShown(!sidebarShown)}
23
+ >
24
+ <svg
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ width="1em"
27
+ height="1em"
28
+ fill="none"
29
+ viewBox="0 0 24 24"
30
+ stroke="currentColor"
31
+ >
32
+ <path
33
+ stroke-linecap="round"
34
+ stroke-linejoin="round"
35
+ stroke-width="2"
36
+ d="M4 6h16M4 12h16M4 18h16"
37
+ />
38
+ </svg>
39
+ <span className="sr-only">Toggle sidebar</span>
40
+ </button>
41
+ );
42
+ };
43
+
44
+ export default MenuToggle;
@@ -0,0 +1,22 @@
1
+ <a href="#article" class="sr-only focus:not-sr-only skiplink"><span>Skip to Content</span></a>
2
+
3
+ <style>
4
+ .skiplink,
5
+ .skiplink:focus,
6
+ .skiplink:focus-visible {
7
+ position: absolute;
8
+ padding: 0.25em;
9
+ font-size: larger;
10
+ top: 0;
11
+ left: 0;
12
+ right: 0;
13
+ z-index: 9;
14
+ display: block;
15
+ text-align: center;
16
+ background-color: var(--theme-text-accent);
17
+ color: var(--theme-bg);
18
+ border-radius: 0.25em;
19
+ outline: var(--theme-bg) solid 1px;
20
+ outline-offset: 0;
21
+ }
22
+ </style>
@@ -0,0 +1,118 @@
1
+ ---
2
+ import { getLanguageFromURL } from '../../languages.ts';
3
+ import { SIDEBAR } from '../../config.ts';
4
+ const { currentPage } = Astro.props;
5
+ const currentPageMatch = currentPage.slice(1);
6
+ const langCode = getLanguageFromURL(currentPage);
7
+ // SIDEBAR is a flat array. Group it by sections to properly render.
8
+ const sidebarSections = SIDEBAR[langCode].reduce((col, item, i) => {
9
+ // If the first item is not a section header, create a new container section.
10
+ if (i === 0) {
11
+ if (!item.header) {
12
+ const pesudoSection = { text: "" };
13
+ col.push({ ...pesudoSection, children: [] });
14
+ }
15
+ }
16
+ if (item.header) {
17
+ col.push({ ...item, children: [] });
18
+ } else {
19
+ col[col.length - 1].children.push(item);
20
+ }
21
+ return col;
22
+ }, []);
23
+ ---
24
+
25
+ <nav aria-labelledby="grid-left">
26
+ <ul class="nav-groups">
27
+ {sidebarSections.map((section) => (
28
+ <li>
29
+ <div class="nav-group">
30
+ <h2 class="nav-group-title">{section.text}</h2>
31
+ <ul>
32
+ {section.children.map((child) => (
33
+ <li class="nav-link">
34
+ <a href={`${Astro.site.pathname}${child.link}`} aria-current={`${currentPageMatch === child.link ? 'page' : 'false'}`}>
35
+ {child.text}
36
+ </a>
37
+ </li>
38
+ ))}
39
+ </ul>
40
+ </div>
41
+ </li>
42
+ ))}
43
+ </ul>
44
+ </nav>
45
+
46
+ <script is:inline>
47
+ window.addEventListener('DOMContentLoaded', (event) => {
48
+ var target = document.querySelector('[aria-current="page"]');
49
+ if (target && target.offsetTop > window.innerHeight - 100) {
50
+ document.querySelector('.nav-groups').scrollTop = target.offsetTop;
51
+ }
52
+ });
53
+ </script>
54
+
55
+ <style>
56
+ nav {
57
+ width: 100%;
58
+ margin-right: 1rem;
59
+ }
60
+ .nav-groups {
61
+ height: 100%;
62
+ padding: 2rem 0;
63
+ overflow-x: visible;
64
+ overflow-y: auto;
65
+ max-height: 100vh;
66
+ }
67
+
68
+ .nav-groups > li + li {
69
+ margin-top: 2rem;
70
+ }
71
+
72
+ .nav-groups > :first-child {
73
+ padding-top: var(--doc-padding);
74
+ }
75
+
76
+ .nav-groups > :last-child {
77
+ padding-bottom: 2rem;
78
+ margin-bottom: var(--theme-navbar-height);
79
+ }
80
+
81
+ .nav-group-title {
82
+ font-size: 1rem;
83
+ font-weight: 700;
84
+ padding: 0.1rem 1rem;
85
+ text-transform: uppercase;
86
+ margin-bottom: 0.5rem;
87
+ }
88
+
89
+ .nav-link a {
90
+ font-size: 1rem;
91
+ margin: 1px;
92
+ padding: 0.3rem 1rem;
93
+ font: inherit;
94
+ color: inherit;
95
+ text-decoration: none;
96
+ display: block;
97
+ }
98
+ .nav-link a:hover,
99
+ .nav-link a:focus {
100
+ background-color: var(--theme-bg-hover);
101
+ }
102
+
103
+ .nav-link a[aria-current='page'] {
104
+ color: var(--theme-text-accent);
105
+ background-color: var(--theme-bg-accent);
106
+ font-weight: 600;
107
+ }
108
+
109
+ :global(:root.theme-dark) .nav-link a[aria-current='page'] {
110
+ color: hsla(var(--color-base-white), 100%, 1);
111
+ }
112
+
113
+ @media (min-width: 50em) {
114
+ .nav-groups {
115
+ padding: 0;
116
+ }
117
+ }
118
+ </style>
@@ -0,0 +1,41 @@
1
+ ---
2
+ import MoreMenu from '../RightSidebar/MoreMenu.astro';
3
+ import TableOfContents from '../RightSidebar/TableOfContents.tsx';
4
+
5
+ const { content, githubEditUrl } = Astro.props;
6
+ const title = content.title;
7
+ const headers = content.astro.headers;
8
+ ---
9
+
10
+ <article id="article" class="content">
11
+ <section class="main-section">
12
+ <h1 class="content-title" id="overview">{title}</h1>
13
+ <slot />
14
+ </section>
15
+ <nav class="block sm:hidden">
16
+ <MoreMenu editHref={githubEditUrl} />
17
+ </nav>
18
+ </article>
19
+
20
+ <style>
21
+ .content {
22
+ padding: 0;
23
+ max-width: 75ch;
24
+ width: 100%;
25
+ height: 100%;
26
+ display: flex;
27
+ flex-direction: column;
28
+ }
29
+ .content > section {
30
+ margin-bottom: 4rem;
31
+ }
32
+ .block {
33
+ display: block;
34
+ }
35
+
36
+ @media (min-width: 50em) {
37
+ .sm\:hidden {
38
+ display: none;
39
+ }
40
+ }
41
+ </style>
@@ -0,0 +1,70 @@
1
+ ---
2
+ import ThemeToggleButton from './ThemeToggleButton.tsx';
3
+ import * as CONFIG from '../../config';
4
+ const { editHref } = Astro.props;
5
+ const showMoreSection = CONFIG.COMMUNITY_INVITE_URL || editHref;
6
+ ---
7
+
8
+ {showMoreSection && <h2 class="heading">More</h2>}
9
+ <ul>
10
+ {editHref && (
11
+ <li class={`header-link depth-2`}>
12
+ <a class="edit-on-github" href={editHref} target="_blank">
13
+ <svg
14
+ aria-hidden="true"
15
+ focusable="false"
16
+ data-prefix="fas"
17
+ data-icon="pen"
18
+ class="svg-inline--fa fa-pen fa-w-16"
19
+ role="img"
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ viewBox="0 0 512 512"
22
+ height="1em"
23
+ width="1em"
24
+ >
25
+ <path
26
+ fill="currentColor"
27
+ d="M290.74 93.24l128.02 128.02-277.99 277.99-114.14 12.6C11.35 513.54-1.56 500.62.14 485.34l12.7-114.22 277.9-277.88zm207.2-19.06l-60.11-60.11c-18.75-18.75-49.16-18.75-67.91 0l-56.55 56.55 128.02 128.02 56.55-56.55c18.75-18.76 18.75-49.16 0-67.91z"
28
+ ></path>
29
+ </svg>
30
+ <span>Edit this page</span>
31
+ </a>
32
+ </li>
33
+ )}
34
+ {CONFIG.COMMUNITY_INVITE_URL && (
35
+ <li class={`header-link depth-2`}>
36
+ <a href={CONFIG.COMMUNITY_INVITE_URL} target="_blank">
37
+ <svg
38
+ aria-hidden="true"
39
+ focusable="false"
40
+ data-prefix="fas"
41
+ data-icon="comment-alt"
42
+ class="svg-inline--fa fa-comment-alt fa-w-16"
43
+ role="img"
44
+ xmlns="http://www.w3.org/2000/svg"
45
+ viewBox="0 0 512 512"
46
+ height="1em"
47
+ width="1em"
48
+ >
49
+ <path
50
+ fill="currentColor"
51
+ d="M448 0H64C28.7 0 0 28.7 0 64v288c0 35.3 28.7 64 64 64h96v84c0 9.8 11.2 15.5 19.1 9.7L304 416h144c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64z"
52
+ ></path>
53
+ </svg>
54
+ <span>Join our community</span>
55
+ </a>
56
+ </li>
57
+ )}
58
+ </ul>
59
+ <div style="margin: 2rem 0; text-align: center;">
60
+ <ThemeToggleButton client:visible />
61
+ </div>
62
+
63
+ <style>
64
+ .edit-on-github {
65
+ text-decoration: none;
66
+ font: inherit;
67
+ color: inherit;
68
+ font-size: 1rem;
69
+ }
70
+ </style>
@@ -0,0 +1,27 @@
1
+ ---
2
+ import TableOfContents from './TableOfContents.tsx';
3
+ import MoreMenu from './MoreMenu.astro';
4
+ const { content, githubEditUrl } = Astro.props;
5
+ const headers = content.astro.headers;
6
+ ---
7
+
8
+ <nav class="sidebar-nav" aria-labelledby="grid-right">
9
+ <div class="sidebar-nav-inner">
10
+ <TableOfContents client:media="(min-width: 50em)" {headers} />
11
+ <MoreMenu editHref={githubEditUrl} />
12
+ </div>
13
+ </nav>
14
+
15
+ <style>
16
+ .sidebar-nav {
17
+ width: 100%;
18
+ position: sticky;
19
+ top: 0;
20
+ }
21
+ .sidebar-nav-inner {
22
+ height: 100%;
23
+ padding: 0;
24
+ padding-top: var(--doc-padding);
25
+ overflow: auto;
26
+ }
27
+ </style>
@@ -0,0 +1,49 @@
1
+ import type { FunctionalComponent } from 'preact';
2
+ import { h, Fragment } from 'preact';
3
+ import { useState, useEffect, useRef } from 'preact/hooks';
4
+
5
+ const TableOfContents: FunctionalComponent<{ headers: any[] }> = ({ headers = [] }) => {
6
+ const itemOffsets = useRef([]);
7
+ const [activeId, setActiveId] = useState<string>(undefined);
8
+
9
+ useEffect(() => {
10
+ const getItemOffsets = () => {
11
+ const titles = document.querySelectorAll('article :is(h1, h2, h3, h4)');
12
+ itemOffsets.current = Array.from(titles).map((title) => ({
13
+ id: title.id,
14
+ topOffset: title.getBoundingClientRect().top + window.scrollY,
15
+ }));
16
+ };
17
+
18
+ getItemOffsets();
19
+ window.addEventListener('resize', getItemOffsets);
20
+
21
+ return () => {
22
+ window.removeEventListener('resize', getItemOffsets);
23
+ };
24
+ }, []);
25
+
26
+ return (
27
+ <>
28
+ <h2 class="heading">On this page</h2>
29
+ <ul>
30
+ <li class={`header-link depth-2 ${activeId === 'overview' ? 'active' : ''}`.trim()}>
31
+ <a href="#overview">Overview</a>
32
+ </li>
33
+ {headers
34
+ .filter(({ depth }) => depth > 1 && depth < 4)
35
+ .map((header) => (
36
+ <li
37
+ class={`header-link depth-${header.depth} ${
38
+ activeId === header.slug ? 'active' : ''
39
+ }`.trim()}
40
+ >
41
+ <a href={`#${header.slug}`}>{header.text}</a>
42
+ </li>
43
+ ))}
44
+ </ul>
45
+ </>
46
+ );
47
+ };
48
+
49
+ export default TableOfContents;
@@ -0,0 +1,37 @@
1
+ .theme-toggle {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 0.25em;
5
+ padding: 0.33em 0.67em;
6
+ border-radius: 99em;
7
+ background-color: var(--theme-code-inline-bg);
8
+ }
9
+
10
+ .theme-toggle > label:focus-within {
11
+ outline: 2px solid transparent;
12
+ box-shadow: 0 0 0 0.08em var(--theme-accent), 0 0 0 0.12em white;
13
+ }
14
+
15
+ .theme-toggle > label {
16
+ color: var(--theme-code-inline-text);
17
+ position: relative;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ opacity: 0.5;
22
+ }
23
+
24
+ .theme-toggle .checked {
25
+ color: var(--theme-accent);
26
+ opacity: 1;
27
+ }
28
+
29
+ input[name='theme-toggle'] {
30
+ position: absolute;
31
+ opacity: 0;
32
+ top: 0;
33
+ right: 0;
34
+ bottom: 0;
35
+ left: 0;
36
+ z-index: -1;
37
+ }
@@ -0,0 +1,83 @@
1
+ import type { FunctionalComponent } from 'preact';
2
+ import { h, Fragment } from 'preact';
3
+ import { useState, useEffect } from 'preact/hooks';
4
+ import './ThemeToggleButton.css';
5
+
6
+ const themes = ['light', 'dark'];
7
+
8
+ const icons = [
9
+ <svg
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ width="20"
12
+ height="20"
13
+ viewBox="0 0 20 20"
14
+ fill="currentColor"
15
+ >
16
+ <path
17
+ fillRule="evenodd"
18
+ d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
19
+ clipRule="evenodd"
20
+ />
21
+ </svg>,
22
+ <svg
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ width="20"
25
+ height="20"
26
+ viewBox="0 0 20 20"
27
+ fill="currentColor"
28
+ >
29
+ <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
30
+ </svg>,
31
+ ];
32
+
33
+ const ThemeToggle: FunctionalComponent = () => {
34
+ const [theme, setTheme] = useState(() => {
35
+ if (import.meta.env.SSR) {
36
+ return undefined;
37
+ }
38
+ if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
39
+ return localStorage.getItem('theme');
40
+ }
41
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
42
+ return 'dark';
43
+ }
44
+ return 'light';
45
+ });
46
+
47
+ useEffect(() => {
48
+ const root = document.documentElement;
49
+ if (theme === 'light') {
50
+ root.classList.remove('theme-dark');
51
+ } else {
52
+ root.classList.add('theme-dark');
53
+ }
54
+ }, [theme]);
55
+
56
+ return (
57
+ <div class="theme-toggle">
58
+ {themes.map((t, i) => {
59
+ const icon = icons[i];
60
+ const checked = t === theme;
61
+ return (
62
+ <label className={checked ? ' checked' : ''}>
63
+ {icon}
64
+ <input
65
+ type="radio"
66
+ name="theme-toggle"
67
+ checked={checked}
68
+ value={t}
69
+ title={`Use ${t} theme`}
70
+ aria-label={`Use ${t} theme`}
71
+ onChange={() => {
72
+ localStorage.setItem('theme', t);
73
+ setTheme(t);
74
+ }}
75
+ />
76
+ </label>
77
+ );
78
+ })}
79
+ </div>
80
+ );
81
+ };
82
+
83
+ export default ThemeToggle;
@@ -0,0 +1,43 @@
1
+ export const SITE = {
2
+ title: 'xs-dev Documentation',
3
+ description: 'CLI for automating the setup and usage of Moddable XS tools',
4
+ defaultLanguage: 'en_US',
5
+ }
6
+
7
+ export const OPEN_GRAPH = {
8
+ twitter: 'hipsterbrown',
9
+ github: 'hipsterbrown',
10
+ }
11
+
12
+ export const KNOWN_LANGUAGES = {
13
+ English: 'en',
14
+ }
15
+
16
+ // Uncomment this to add an "Edit this page" button to every page of documentation.
17
+ export const GITHUB_EDIT_URL = `https://github.com/hipsterbrown/xs-dev/blob/main/docs/`
18
+
19
+ // Uncomment this to add an "Join our Community" button to every page of documentation.
20
+ // export const COMMUNITY_INVITE_URL = `https://astro.build/chat`;
21
+
22
+ // Uncomment this to enable site search.
23
+ // See "Algolia" section of the README for more information.
24
+ // export const ALGOLIA = {
25
+ // indexName: 'XXXXXXXXXX',
26
+ // appId: 'XXXXXXXXXX',
27
+ // apiKey: 'XXXXXXXXXX',
28
+ // }
29
+
30
+ export const SIDEBAR = {
31
+ en: [
32
+ { text: '', header: true },
33
+ { text: 'Features', header: true },
34
+ { text: 'Introduction', link: 'en/introduction' },
35
+ { text: 'Setup', link: 'en/features/setup' },
36
+ { text: 'Updates', link: 'en/features/update' },
37
+ { text: 'Teardown', link: 'en/features/teardown' },
38
+ { text: 'Device Discovery', link: 'en/features/scan' },
39
+ { text: 'Project Creation', link: 'en/features/init' },
40
+ { text: 'Build & Run', link: 'en/features/run' },
41
+ { text: 'SDK Module Management', link: 'en/features/include' },
42
+ ],
43
+ }
@@ -0,0 +1,10 @@
1
+ import { KNOWN_LANGUAGES } from './config';
2
+
3
+ export { KNOWN_LANGUAGES };
4
+ export const KNOWN_LANGUAGE_CODES = Object.values(KNOWN_LANGUAGES);
5
+ export const langPathRegex = /\/([a-z]{2}-?[A-Z]{0,2})\//;
6
+
7
+ export function getLanguageFromURL(pathname: string) {
8
+ const langCodeMatch = pathname.match(langPathRegex);
9
+ return langCodeMatch ? langCodeMatch[1] : 'en';
10
+ }