Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import { getLocale, i18nReady, setLocale, SUPPORTED_LOCALES, t } from './i18n.mjs'
export const TOOL_NAVIGATION_ITEMS = Object.freeze([
{ id: 'home', href: '', label: 'ホーム', description: 'Webツールの一覧', icon: 'home' },
{
id: 'flash',
href: 'flash/',
label: 'ファームウェア書き込み',
description: 'USBで対応ボードへ書き込む',
icon: 'cpu',
},
{
id: 'preference',
href: 'preference/',
label: '設定',
description: 'BLEで本体の設定を変更する',
icon: 'sliders-horizontal',
},
{
id: 'mod-gallery',
href: 'mod-gallery/',
label: 'MOD Gallery',
description: '公開済みMODを試して編集する',
icon: 'store',
},
{
id: 'mediapipe',
href: 'mediapipe/',
label: 'MediaPipe BLE追従',
description: '顔と手の動きをBLEで送る',
icon: 'scan-face',
},
{
id: 'simulator',
href: 'simulator/',
label: 'シミュレーター',
description: 'WASMと3Dモデルを実行する',
icon: 'box',
},
{
id: 'editor',
href: 'editor/',
label: 'ブロックエディタ',
description: 'BlocklyからMODを作成する',
icon: 'blocks',
},
{
id: 'face-editor',
href: 'face-editor/',
label: 'Shape顔エディタ',
description: 'Shape型Faceを設計する',
icon: 'smile',
},
])
export const GUIDE_NAVIGATION_ITEMS = Object.freeze([
{
id: 'tutorial',
href: 'editor/tutorial.html',
label: 'ブロックエディタ チュートリアル',
description: '作成から実行までの手順',
icon: 'circle-help',
},
])
export function navigationItemForPath(pathname) {
const normalized = String(pathname || '/').replace(/\/index\.html$/, '/')
if (/\/editor\/tutorial\.html$/.test(normalized)) return 'tutorial'
if (/\/mediapipe\/?$/.test(normalized)) return 'mediapipe'
if (/\/face-editor\/?$/.test(normalized)) return 'face-editor'
if (/\/editor\/?$/.test(normalized)) return 'editor'
if (/\/mod-gallery\/?$/.test(normalized)) return 'mod-gallery'
if (/\/simulator\/?$/.test(normalized)) return 'simulator'
if (/\/preference\/?$/.test(normalized)) return 'preference'
if (/\/flash\/?$/.test(normalized)) return 'flash'
return 'home'
}
function navigationLink(item, rootUrl, currentId) {
const anchor = document.createElement('a')
anchor.className = 'tool-drawer-link'
anchor.dataset.toolId = item.id
anchor.href = new URL(item.href, rootUrl).href
if (item.id === currentId) anchor.setAttribute('aria-current', 'page')
const icon = document.createElement('i')
icon.dataset.lucide = item.icon
icon.setAttribute('aria-hidden', 'true')
const copy = document.createElement('span')
const label = document.createElement('strong')
label.textContent = t(item.label)
const description = document.createElement('small')
description.textContent = t(item.description)
copy.append(label, description)
anchor.append(icon, copy)
return anchor
}
function navigationSection(title, items, rootUrl, currentId) {
const section = document.createElement('section')
section.className = 'tool-drawer-section'
const heading = document.createElement('h2')
heading.textContent = t(title)
const nav = document.createElement('nav')
nav.setAttribute('aria-label', t(title))
for (const item of items) nav.append(navigationLink(item, rootUrl, currentId))
section.append(heading, nav)
return section
}
function languageSection() {
const section = document.createElement('section')
section.className = 'tool-drawer-section tool-drawer-language'
const label = document.createElement('label')
label.htmlFor = 'tool-language-select'
label.textContent = t('表示言語')
const select = document.createElement('select')
select.id = 'tool-language-select'
select.setAttribute('aria-label', t('表示言語'))
const names = new Map([
['ja', '日本語'],
['en', 'English'],
['zh-CN', '简体中文'],
])
for (const locale of SUPPORTED_LOCALES) {
const option = document.createElement('option')
option.value = locale
option.textContent = names.get(locale)
option.setAttribute('translate', 'no')
select.append(option)
}
select.value = getLocale()
select.addEventListener('change', async () => {
select.disabled = true
await setLocale(select.value)
location.reload()
})
section.append(label, select)
return section
}
export function installToolNavigation({
topbar = document.querySelector('.topbar'),
pathname = location.pathname,
rootUrl = new URL(document.querySelector('.brand-link')?.getAttribute('href') ?? './', document.baseURI),
} = {}) {
if (!topbar || topbar.querySelector('.tool-menu-button')) return null
topbar.querySelector('.tool-nav')?.remove()
const button = document.createElement('button')
button.className = 'icon-button tool-menu-button'
button.type = 'button'
button.title = t('ツールメニュー')
button.setAttribute('aria-label', t('ツールメニューを開く'))
button.setAttribute('aria-haspopup', 'dialog')
button.setAttribute('aria-controls', 'tool-drawer')
button.innerHTML = '<i data-lucide="menu" aria-hidden="true"></i>'
topbar.prepend(button)
const dialog = document.createElement('dialog')
dialog.id = 'tool-drawer'
dialog.className = 'tool-drawer'
dialog.setAttribute('aria-labelledby', 'tool-drawer-title')
const panel = document.createElement('div')
panel.className = 'tool-drawer-panel'
const header = document.createElement('header')
const titleGroup = document.createElement('div')
const eyebrow = document.createElement('span')
eyebrow.className = 'tool-drawer-eyebrow'
eyebrow.textContent = 'Stack-chan'
const title = document.createElement('h1')
title.id = 'tool-drawer-title'
title.textContent = t('Webツール')
titleGroup.append(eyebrow, title)
const close = document.createElement('button')
close.className = 'icon-button'
close.type = 'button'
close.setAttribute('aria-label', t('ツールメニューを閉じる'))
close.innerHTML = '<i data-lucide="x" aria-hidden="true"></i>'
header.append(titleGroup, close)
const currentId = navigationItemForPath(pathname)
const content = document.createElement('div')
content.className = 'tool-drawer-content'
content.append(
navigationSection('ツール', TOOL_NAVIGATION_ITEMS, rootUrl, currentId),
navigationSection('ガイド', GUIDE_NAVIGATION_ITEMS, rootUrl, currentId),
languageSection()
)
panel.append(header, content)
dialog.append(panel)
document.body.append(dialog)
const open = () => {
dialog.showModal()
dialog.querySelector('[aria-current="page"]')?.focus()
}
const closeDialog = () => dialog.close()
button.addEventListener('click', open)
close.addEventListener('click', closeDialog)
dialog.addEventListener('click', (event) => {
if (event.target === dialog) closeDialog()
})
dialog.addEventListener('close', () => button.focus())
globalThis.lucide?.createIcons()
return { button, dialog, open, close: closeDialog }
}
if (typeof document !== 'undefined') {
await i18nReady
installToolNavigation()
}
3adb53fb9This file is pinned to the selected release. Download the original to open it in your design tool.