* Add file save UI and drafts project

* Fix build

* Create SearchList component as abstraction

* Add project page and file tree view

* Create Locale context

* Create Theme context

* Create Version context

* Create Title context

* Create Project context

* Store current file in project context

* Fix issues when renaming file and implement deleting

* Style improvements

* Make all project strings translatable

* Fix z-index
This commit is contained in:
Misode
2022-01-18 01:02:19 +01:00
committed by GitHub
parent cd318dc795
commit c6c52ca41a
39 changed files with 958 additions and 373 deletions

View File

@@ -8,14 +8,15 @@ interface BtnMenuProps extends JSX.HTMLAttributes<HTMLDivElement> {
label?: string,
relative?: boolean,
tooltip?: string,
tooltipLoc?: 'se' | 'sw' | 'nw',
children: ComponentChildren,
}
export function BtnMenu(props: BtnMenuProps) {
const { icon, label, relative, tooltip, children } = props
const { icon, label, relative, tooltip, tooltipLoc, children } = props
const [active, setActive] = useFocus()
return <div class={`btn-menu${relative === false ? ' no-relative' : ''}`} {...props}>
<Btn {...{icon, label, tooltip}} onClick={setActive} />
<Btn {...{icon, label, tooltip, tooltipLoc}} onClick={setActive} />
{active && <div class="btn-group">
{children}
</div>}

View File

@@ -1,8 +1,7 @@
import { getCurrentUrl, Link, route } from 'preact-router'
import { Btn, BtnMenu, Icons, Octicon } from '.'
import config from '../../config.json'
import { locale } from '../Locales'
import type { VersionId } from '../services'
import { useLocale, useTheme, useTitle, useVersion } from '../contexts'
import { checkVersion } from '../services'
import { cleanUrl, getGenerator } from '../Utils'
@@ -12,51 +11,45 @@ const Themes: Record<string, keyof typeof Octicon> = {
light: 'sun',
}
type HeaderProps = {
lang: string,
title: string,
version: VersionId,
theme: string,
changeTheme: (theme: string) => unknown,
language: string,
changeLanguage: (language: string) => unknown,
}
export function Header({ lang, title, version, theme, changeTheme, language, changeLanguage }: HeaderProps) {
const loc = locale.bind(null, lang)
export function Header() {
const { lang, locale, changeLanguage } = useLocale()
const { theme, changeTheme } = useTheme()
const { version } = useVersion()
const { title } = useTitle()
const gen = getGenerator(getCurrentUrl())
return <header>
<div class="title">
<Link class="home-link" href="/" aria-label={loc('home')} data-cy="home-link">{Icons.home}</Link>
<Link class="home-link" href="/" aria-label={locale('home')} data-cy="home-link">{Icons.home}</Link>
<h1>{title}</h1>
{gen && <BtnMenu icon="chevron_down" tooltip={loc('switch_generator')} data-cy="generator-switcher">
{gen && <BtnMenu icon="chevron_down" tooltip={locale('switch_generator')} data-cy="generator-switcher">
{config.generators
.filter(g => g.category === gen?.category && checkVersion(version, g.minVersion))
.map(g =>
<Btn label={loc(g.id)} active={g.id === gen.id} onClick={() => route(cleanUrl(g.url))} />
<Btn label={locale(g.id)} active={g.id === gen.id} onClick={() => route(cleanUrl(g.url))} />
)}
</BtnMenu>}
</div>
<nav>
<ul>
<li data-cy="language-switcher">
<BtnMenu icon="globe" tooltip={loc('language')}>
<BtnMenu icon="globe" tooltip={locale('language')}>
{config.languages.map(({ code, name }) =>
<Btn label={name} active={code === language}
<Btn label={name} active={code === lang}
onClick={() => changeLanguage(code)} />
)}
</BtnMenu>
</li>
<li data-cy="theme-switcher">
<BtnMenu icon={Themes[theme]} tooltip={loc('theme')}>
<BtnMenu icon={Themes[theme]} tooltip={locale('theme')}>
{Object.entries(Themes).map(([th, icon]) =>
<Btn icon={icon} label={loc(`theme.${th}`)} active={th === theme}
<Btn icon={icon} label={locale(`theme.${th}`)} active={th === theme}
onClick={() => changeTheme(th)} />
)}
</BtnMenu>
</li>
<li class="dimmed">
<a href="https://github.com/misode/misode.github.io" target="_blank" rel="noreferrer" class="tooltipped tip-sw" aria-label={loc('github')}>
<a href="https://github.com/misode/misode.github.io" target="_blank" rel="noreferrer" class="tooltipped tip-sw" aria-label={locale('github')}>
{Octicon.mark_github}
</a>
</li>

View File

@@ -11,10 +11,13 @@ export const Octicon = {
code: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path></svg>,
dash: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2 8a.75.75 0 01.75-.75h10.5a.75.75 0 010 1.5H2.75A.75.75 0 012 8z"></path></svg>,
device_desktop: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 2.5h12.5a.25.25 0 01.25.25v7.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25v-7.5a.25.25 0 01.25-.25zM14.25 1H1.75A1.75 1.75 0 000 2.75v7.5C0 11.216.784 12 1.75 12h3.727c-.1 1.041-.52 1.872-1.292 2.757A.75.75 0 004.75 16h6.5a.75.75 0 00.565-1.243c-.772-.885-1.193-1.716-1.292-2.757h3.727A1.75 1.75 0 0016 10.25v-7.5A1.75 1.75 0 0014.25 1zM9.018 12H6.982a5.72 5.72 0 01-.765 2.5h3.566a5.72 5.72 0 01-.765-2.5z"></path></svg>,
dot_fill: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8z"></path></svg>,
download: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5 0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"></path></svg>,
duplicate: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M10.5 3a.75.75 0 01.75.75v1h1a.75.75 0 010 1.5h-1v1a.75.75 0 01-1.5 0v-1h-1a.75.75 0 010-1.5h1v-1A.75.75 0 0110.5 3z"></path><path fill-rule="evenodd" d="M6.75 0A1.75 1.75 0 005 1.75v7.5c0 .966.784 1.75 1.75 1.75h7.5A1.75 1.75 0 0016 9.25v-7.5A1.75 1.75 0 0014.25 0h-7.5zM6.5 1.75a.25.25 0 01.25-.25h7.5a.25.25 0 01.25.25v7.5a.25.25 0 01-.25.25h-7.5a.25.25 0 01-.25-.25v-7.5z"></path><path d="M1.75 5A1.75 1.75 0 000 6.75v7.5C0 15.216.784 16 1.75 16h7.5A1.75 1.75 0 0011 14.25v-1.5a.75.75 0 00-1.5 0v1.5a.25.25 0 01-.25.25h-7.5a.25.25 0 01-.25-.25v-7.5a.25.25 0 01.25-.25h1.5a.75.75 0 000-1.5h-1.5z"></path></svg>,
eye: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.679 7.932c.412-.621 1.242-1.75 2.366-2.717C5.175 4.242 6.527 3.5 8 3.5c1.473 0 2.824.742 3.955 1.715 1.124.967 1.954 2.096 2.366 2.717a.119.119 0 010 .136c-.412.621-1.242 1.75-2.366 2.717C10.825 11.758 9.473 12.5 8 12.5c-1.473 0-2.824-.742-3.955-1.715C2.92 9.818 2.09 8.69 1.679 8.068a.119.119 0 010-.136zM8 2c-1.981 0-3.67.992-4.933 2.078C1.797 5.169.88 6.423.43 7.1a1.619 1.619 0 000 1.798c.45.678 1.367 1.932 2.637 3.024C4.329 13.008 6.019 14 8 14c1.981 0 3.67-.992 4.933-2.078 1.27-1.091 2.187-2.345 2.637-3.023a1.619 1.619 0 000-1.798c-.45-.678-1.367-1.932-2.637-3.023C11.671 2.992 9.981 2 8 2zm0 8a2 2 0 100-4 2 2 0 000 4z"></path></svg>,
eye_closed: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M.143 2.31a.75.75 0 011.047-.167l14.5 10.5a.75.75 0 11-.88 1.214l-2.248-1.628C11.346 13.19 9.792 14 8 14c-1.981 0-3.67-.992-4.933-2.078C1.797 10.832.88 9.577.43 8.9a1.618 1.618 0 010-1.797c.353-.533.995-1.42 1.868-2.305L.31 3.357A.75.75 0 01.143 2.31zm3.386 3.378a14.21 14.21 0 00-1.85 2.244.12.12 0 00-.022.068c0 .021.006.045.022.068.412.621 1.242 1.75 2.366 2.717C5.175 11.758 6.527 12.5 8 12.5c1.195 0 2.31-.488 3.29-1.191L9.063 9.695A2 2 0 016.058 7.52l-2.53-1.832zM8 3.5c-.516 0-1.017.09-1.499.251a.75.75 0 11-.473-1.423A6.23 6.23 0 018 2c1.981 0 3.67.992 4.933 2.078 1.27 1.091 2.187 2.345 2.637 3.023a1.619 1.619 0 010 1.798c-.11.166-.248.365-.41.587a.75.75 0 11-1.21-.887c.148-.201.272-.382.371-.53a.119.119 0 000-.137c-.412-.621-1.242-1.75-2.366-2.717C10.825 4.242 9.473 3.5 8 3.5z"></path></svg>,
file: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.75 1.5a.25.25 0 00-.25.25v11.5c0 .138.112.25.25.25h8.5a.25.25 0 00.25-.25V6H9.75A1.75 1.75 0 018 4.25V1.5H3.75zm5.75.56v2.19c0 .138.112.25.25.25h2.19L9.5 2.06zM2 1.75C2 .784 2.784 0 3.75 0h5.086c.464 0 .909.184 1.237.513l3.414 3.414c.329.328.513.773.513 1.237v8.086A1.75 1.75 0 0112.25 15h-8.5A1.75 1.75 0 012 13.25V1.75z"></path></svg>,
file_directory: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 1A1.75 1.75 0 000 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25v-8.5A1.75 1.75 0 0014.25 3h-6.5a.25.25 0 01-.2-.1l-.9-1.2c-.33-.44-.85-.7-1.4-.7h-3.5z"></path></svg>,
gear: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.429 1.525a6.593 6.593 0 011.142 0c.036.003.108.036.137.146l.289 1.105c.147.56.55.967.997 1.189.174.086.341.183.501.29.417.278.97.423 1.53.27l1.102-.303c.11-.03.175.016.195.046.219.31.41.641.573.989.014.031.022.11-.059.19l-.815.806c-.411.406-.562.957-.53 1.456a4.588 4.588 0 010 .582c-.032.499.119 1.05.53 1.456l.815.806c.08.08.073.159.059.19a6.494 6.494 0 01-.573.99c-.02.029-.086.074-.195.045l-1.103-.303c-.559-.153-1.112-.008-1.529.27-.16.107-.327.204-.5.29-.449.222-.851.628-.998 1.189l-.289 1.105c-.029.11-.101.143-.137.146a6.613 6.613 0 01-1.142 0c-.036-.003-.108-.037-.137-.146l-.289-1.105c-.147-.56-.55-.967-.997-1.189a4.502 4.502 0 01-.501-.29c-.417-.278-.97-.423-1.53-.27l-1.102.303c-.11.03-.175-.016-.195-.046a6.492 6.492 0 01-.573-.989c-.014-.031-.022-.11.059-.19l.815-.806c.411-.406.562-.957.53-1.456a4.587 4.587 0 010-.582c.032-.499-.119-1.05-.53-1.456l-.815-.806c-.08-.08-.073-.159-.059-.19a6.44 6.44 0 01.573-.99c.02-.029.086-.075.195-.045l1.103.303c.559.153 1.112.008 1.529-.27.16-.107.327-.204.5-.29.449-.222.851-.628.998-1.189l.289-1.105c.029-.11.101-.143.137-.146zM8 0c-.236 0-.47.01-.701.03-.743.065-1.29.615-1.458 1.261l-.29 1.106c-.017.066-.078.158-.211.224a5.994 5.994 0 00-.668.386c-.123.082-.233.09-.3.071L3.27 2.776c-.644-.177-1.392.02-1.82.63a7.977 7.977 0 00-.704 1.217c-.315.675-.111 1.422.363 1.891l.815.806c.05.048.098.147.088.294a6.084 6.084 0 000 .772c.01.147-.038.246-.088.294l-.815.806c-.474.469-.678 1.216-.363 1.891.2.428.436.835.704 1.218.428.609 1.176.806 1.82.63l1.103-.303c.066-.019.176-.011.299.071.213.143.436.272.668.386.133.066.194.158.212.224l.289 1.106c.169.646.715 1.196 1.458 1.26a8.094 8.094 0 001.402 0c.743-.064 1.29-.614 1.458-1.26l.29-1.106c.017-.066.078-.158.211-.224a5.98 5.98 0 00.668-.386c.123-.082.233-.09.3-.071l1.102.302c.644.177 1.392-.02 1.82-.63.268-.382.505-.789.704-1.217.315-.675.111-1.422-.364-1.891l-.814-.806c-.05-.048-.098-.147-.088-.294a6.1 6.1 0 000-.772c-.01-.147.039-.246.088-.294l.814-.806c.475-.469.679-1.216.364-1.891a7.992 7.992 0 00-.704-1.218c-.428-.609-1.176-.806-1.82-.63l-1.103.303c-.066.019-.176.011-.299-.071a5.991 5.991 0 00-.668-.386c-.133-.066-.194-.158-.212-.224L10.16 1.29C9.99.645 9.444.095 8.701.031A8.094 8.094 0 008 0zm1.5 8a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM11 8a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>,
globe: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.543 7.25h2.733c.144-2.074.866-3.756 1.58-4.948.12-.197.237-.381.353-.552a6.506 6.506 0 00-4.666 5.5zm2.733 1.5H1.543a6.506 6.506 0 004.666 5.5 11.13 11.13 0 01-.352-.552c-.715-1.192-1.437-2.874-1.581-4.948zm1.504 0h4.44a9.637 9.637 0 01-1.363 4.177c-.306.51-.612.919-.857 1.215a9.978 9.978 0 01-.857-1.215A9.637 9.637 0 015.78 8.75zm4.44-1.5H5.78a9.637 9.637 0 011.363-4.177c.306-.51.612-.919.857-1.215.245.296.55.705.857 1.215A9.638 9.638 0 0110.22 7.25zm1.504 1.5c-.144 2.074-.866 3.756-1.58 4.948-.12.197-.237.381-.353.552a6.506 6.506 0 004.666-5.5h-2.733zm2.733-1.5h-2.733c-.144-2.074-.866-3.756-1.58-4.948a11.738 11.738 0 00-.353-.552 6.506 6.506 0 014.666 5.5zM8 0a8 8 0 100 16A8 8 0 008 0z"></path></svg>,
history: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"></path></svg>,
@@ -28,6 +31,7 @@ export const Octicon = {
play: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zM6.379 5.227A.25.25 0 006 5.442v5.117a.25.25 0 00.379.214l4.264-2.559a.25.25 0 000-.428L6.379 5.227z"></path></svg>,
plus: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 2a.75.75 0 01.75.75v4.5h4.5a.75.75 0 010 1.5h-4.5v4.5a.75.75 0 01-1.5 0v-4.5h-4.5a.75.75 0 010-1.5h4.5v-4.5A.75.75 0 018 2z"></path></svg>,
plus_circle: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zM8 0a8 8 0 100 16A8 8 0 008 0zm.75 4.75a.75.75 0 00-1.5 0v2.5h-2.5a.75.75 0 000 1.5h2.5v2.5a.75.75 0 001.5 0v-2.5h2.5a.75.75 0 000-1.5h-2.5v-2.5z"></path></svg>,
repo: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8zM5 12.25v3.25a.25.25 0 00.4.2l1.45-1.087a.25.25 0 01.3 0L8.6 15.7a.25.25 0 00.4-.2v-3.25a.25.25 0 00-.25-.25h-3.5a.25.25 0 00-.25.25z"></path></svg>,
search: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.5 7a4.499 4.499 0 11-8.998 0A4.499 4.499 0 0111.5 7zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z"></path></svg>,
sort_asc: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 4.25a.75.75 0 01.75-.75h2.5a.75.75 0 010 1.5H.75A.75.75 0 010 4.25zm0 4a.75.75 0 01.75-.75h4.5a.75.75 0 010 1.5H.75A.75.75 0 010 8.25zm0 4a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5H.75a.75.75 0 01-.75-.75zm12.927-9.677a.25.25 0 00-.354 0l-3 3A.25.25 0 009.75 6H12v6.75a.75.75 0 001.5 0V6h2.25a.25.25 0 00.177-.427l-3-3z"></path></svg>,
sort_desc: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 4.25a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5H.75A.75.75 0 010 4.25zm0 4a.75.75 0 01.75-.75h4.5a.75.75 0 010 1.5H.75A.75.75 0 010 8.25zm0 4a.75.75 0 01.75-.75h2.5a.75.75 0 010 1.5H.75a.75.75 0 01-.75-.75z"></path><path d="M13.5 10h2.25a.25.25 0 01.177.427l-3 3a.25.25 0 01-.354 0l-3-3A.25.25 0 019.75 10H12V3.75a.75.75 0 011.5 0V10z"></path></svg>,

View File

@@ -0,0 +1,58 @@
import { useMemo, useState } from 'preact/hooks'
import { Octicon } from '.'
const SEPARATOR = '/'
interface Props {
entries: string[],
onSelect: (entry: string) => unknown,
indent?: number,
}
export function TreeView({ entries, onSelect, indent }: Props) {
const roots = useMemo(() => {
const groups: Record<string, string[]> = {}
for (const entry of entries) {
const i = entry.indexOf(SEPARATOR)
if (i >= 0) {
const root = entry.slice(0, i)
;(groups[root] ??= []).push(entry.slice(i + 1))
}
}
return Object.entries(groups)
}, entries)
const leaves = useMemo(() => {
return entries.filter(e => !e.includes(SEPARATOR))
}, entries)
const [hidden, setHidden] = useState(new Set<string>())
const toggle = (root: string) => {
if (hidden.has(root)) {
hidden.delete(root)
} else {
hidden.add(root)
}
setHidden(new Set(hidden))
}
return <div class="tree-view" style={`--indent: ${indent ?? 0};`}>
{roots.map(([r, entries]) => <div>
<TreeViewEntry icon={hidden.has(r) ? 'chevron_right' : 'chevron_down'} key={r} label={r} onClick={() => toggle(r)}/>
{!hidden.has(r) &&
<TreeView entries={entries} onSelect={e => onSelect(`${r}/${e}`)} indent={(indent ?? 0) + 1} />}
</div>)}
{leaves.map(e => <TreeViewEntry icon="file" key={e} label={e} onClick={() => onSelect(e)} />)}
</div>
}
interface TreeViewEntryProps {
icon: keyof typeof Octicon,
label: string,
onClick?: () => unknown,
}
function TreeViewEntry({ icon, label, onClick }: TreeViewEntryProps) {
return <div class="entry" onClick={onClick} >
{Octicon[icon]}
{label}
</div>
}

View File

@@ -0,0 +1,24 @@
import { useMemo, useState } from 'preact/hooks'
import { Btn, BtnInput } from '..'
interface Props {
values?: string[],
onSelect?: (value: string) => unknown,
searchPlaceholder?: string,
noResults?: string,
}
export function SearchList({ values, onSelect, searchPlaceholder, noResults }: Props) {
const [search, setSearch] = useState('')
const results = useMemo(() => {
const terms = search.trim().split(' ')
return values?.filter(v => terms.every(t => v.includes(t))) ?? []
}, [values, search])
return <>
<BtnInput icon="search" large value={search} onChange={setSearch} doSelect={1} placeholder={searchPlaceholder ?? 'Search'} />
<div class="result-list">
{results.map(v => <Btn key={v} label={v} onClick={() => onSelect?.(v)} />)}
{results.length === 0 && <Btn label={noResults ?? 'No results'}/>}
</div>
</>
}

View File

@@ -1 +1,2 @@
export * from './Input'
export * from './SearchList'

View File

@@ -8,14 +8,13 @@ import { BiomeSourcePreview, DecoratorPreview, NoisePreview, NoiseSettingsPrevie
export const HasPreview = ['dimension', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature']
type PreviewPanelProps = {
lang: string,
model: DataModel | null,
version: VersionId,
id: string,
shown: boolean,
onError: (message: string) => unknown,
}
export function PreviewPanel({ lang, model, version, id, shown }: PreviewPanelProps) {
export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) {
const [, setCount] = useState(0)
useModel(model, () => {
@@ -24,22 +23,22 @@ export function PreviewPanel({ lang, model, version, id, shown }: PreviewPanelPr
if (id === 'dimension' && model?.get(new Path(['generator', 'type']))?.endsWith('noise')) {
const data = model.get(new Path(['generator', 'biome_source']))
if (data) return <BiomeSourcePreview {...{ lang, model, version, shown, data }} />
if (data) return <BiomeSourcePreview {...{ model, version, shown, data }} />
}
if (id === 'worldgen/noise' && model) {
const data = model.get(new Path([]))
if (data) return <NoisePreview {...{ lang, model, version, shown, data }} />
if (data) return <NoisePreview {...{ model, version, shown, data }} />
}
if (id === 'worldgen/noise_settings' && model) {
const data = model.get(new Path([]))
if (data) return <NoiseSettingsPreview {...{ lang, model, version, shown, data }} />
if (data) return <NoiseSettingsPreview {...{ model, version, shown, data }} />
}
if (id === 'worldgen/configured_feature' && model) {
const data = model.get(new Path([]))
if (data) return <DecoratorPreview {...{ lang, model, version, shown, data }} />
if (data) return <DecoratorPreview {...{ model, version, shown, data }} />
}
return <></>

View File

@@ -1,11 +1,11 @@
import { DataModel, ModelPath } from '@mcschema/core'
import { DataModel } from '@mcschema/core'
import json from 'comment-json'
import yaml from 'js-yaml'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
import { Btn, BtnMenu } from '..'
import { useLocale } from '../../contexts'
import { useModel } from '../../hooks'
import { locale } from '../../Locales'
import { transformOutput } from '../../schema/transformOutput'
import { getOutput } from '../../schema/transformOutput'
import type { BlockStateRegistry } from '../../services'
import { Store } from '../../Store'
import { message } from '../../Utils'
@@ -37,7 +37,6 @@ const FORMATS: Record<string, {
}
type SourcePanelProps = {
lang: string,
name: string,
model: DataModel | null,
blockStates: BlockStateRegistry | null,
@@ -47,16 +46,16 @@ type SourcePanelProps = {
copySuccess: () => unknown,
onError: (message: string) => unknown,
}
export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) {
const loc = locale.bind(null, lang)
export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doImport, copySuccess, onError }: SourcePanelProps) {
const { locale } = useLocale()
const [indent, setIndent] = useState(Store.getIndent())
const [format, setFormat] = useState(Store.getFormat())
const source = useRef<HTMLTextAreaElement>(null)
const download = useRef<HTMLAnchorElement>(null)
const retransform = useRef<Function>()
const getOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => {
const data = model.schema.hook(transformOutput, new ModelPath(model), model.data, { blockStates })
const getSerializedOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => {
const data = getOutput(model, blockStates)
return FORMATS[format].stringify(data, INDENT[indent])
}, [indent, format])
@@ -64,7 +63,7 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
retransform.current = () => {
if (!model || !blockStates) return
try {
const output = getOutput(model, blockStates)
const output = getSerializedOutput(model, blockStates)
if (output.length >= OUTPUT_CHARS_LIMIT) {
source.current.value = output.slice(0, OUTPUT_CHARS_LIMIT) + `\n\nOutput is too large to display (+${OUTPUT_CHARS_LIMIT} chars)\nExport to view complete output\n\n`
} else {
@@ -102,7 +101,7 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
useEffect(() => {
if (doCopy && model && blockStates) {
navigator.clipboard.writeText(getOutput(model, blockStates)).then(() => {
navigator.clipboard.writeText(getSerializedOutput(model, blockStates)).then(() => {
copySuccess()
})
}
@@ -110,7 +109,7 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
useEffect(() => {
if (doDownload && model && blockStates && download.current) {
const content = encodeURIComponent(getOutput(model, blockStates))
const content = encodeURIComponent(getSerializedOutput(model, blockStates))
download.current.setAttribute('href', `data:text/json;charset=utf-8,${content}`)
download.current.setAttribute('download', `${name}.${format}`)
download.current.click()
@@ -136,18 +135,18 @@ export function SourcePanel({ lang, name, model, blockStates, doCopy, doDownload
return <>
<div class="controls">
<BtnMenu icon="gear" tooltip={loc('output_settings')} data-cy="source-controls">
<BtnMenu icon="gear" tooltip={locale('output_settings')} data-cy="source-controls">
{Object.entries(INDENT).map(([key]) =>
<Btn label={loc(`indentation.${key}`)} active={indent === key}
<Btn label={locale(`indentation.${key}`)} active={indent === key}
onClick={() => changeIndent(key)}/>
)}
<hr />
{Object.keys(FORMATS).map(key =>
<Btn label={loc(`format.${key}`)} active={format === key}
<Btn label={locale(`format.${key}`)} active={format === key}
onClick={() => changeFormat(key)} />)}
</BtnMenu>
</div>
<textarea ref={source} class="source" onBlur={onImport} spellcheck={false} autocorrect="off" placeholder={loc('source_placeholder')} data-cy="import-area"></textarea>
<textarea ref={source} class="source" onBlur={onImport} spellcheck={false} autocorrect="off" placeholder={locale('source_placeholder', format.toUpperCase())} data-cy="import-area"></textarea>
<a ref={download} style="display: none;"></a>
</>
}

View File

@@ -1,17 +1,18 @@
import type { DataModel } from '@mcschema/core'
import { useErrorBoundary, useState } from 'preact/hooks'
import { useLocale } from '../../contexts'
import { useModel } from '../../hooks'
import { FullNode } from '../../schema/renderHtml'
import type { BlockStateRegistry, VersionId } from '../../services'
type TreePanelProps = {
lang: string,
version: VersionId,
model: DataModel | null,
blockStates: BlockStateRegistry | null,
onError: (message: string) => unknown,
}
export function Tree({ lang, version, model, blockStates, onError }: TreePanelProps) {
export function Tree({ version, model, blockStates, onError }: TreePanelProps) {
const { lang } = useLocale()
if (!model || !blockStates || lang === 'none') return <></>
const [error] = useErrorBoundary(e => {

View File

@@ -11,3 +11,4 @@ export * from './Octicon'
export * from './previews'
export * from './sounds'
export * from './ToolCard'
export * from './TreeView'

View File

@@ -4,8 +4,8 @@ import { NoiseGeneratorSettings, TerrainShaper } from 'deepslate'
import { useEffect, useRef, useState } from 'preact/hooks'
import type { PreviewProps } from '.'
import { Btn, BtnMenu } from '..'
import { useLocale } from '../../contexts'
import { useCanvas } from '../../hooks'
import { locale } from '../../Locales'
import { biomeMap, getBiome } from '../../previews'
import { newSeed } from '../../Utils'
@@ -13,7 +13,8 @@ const LAYERS = ['biomes', 'temperature', 'humidity', 'continentalness', 'erosion
const OverworldShaper = TerrainShaper.overworld()
export const BiomeSourcePreview = ({ model, data, shown, lang, version }: PreviewProps) => {
export const BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps) => {
const { locale } = useLocale()
const [scale, setScale] = useState(2)
const [focused, setFocused] = useState<string | undefined>(undefined)
const [layers, setLayers] = useState(new Set<typeof LAYERS[number]>(['biomes']))
@@ -76,12 +77,12 @@ export const BiomeSourcePreview = ({ model, data, shown, lang, version }: Previe
<div class="controls">
{focused && <Btn label={focused} class="no-pointer" />}
{type === 'multi_noise' &&
<BtnMenu icon="stack" tooltip={locale(lang, 'configure_layers')}>
<BtnMenu icon="stack" tooltip={locale('configure_layers')}>
{LAYERS.map(name => {
const enabled = layers.has(name)
return <Btn label={locale(lang, `layer.${name}`)}
return <Btn label={locale(`layer.${name}`)}
active={enabled}
tooltip={enabled ? locale(lang, 'enabled') : locale(lang, 'disabled')}
tooltip={enabled ? locale('enabled') : locale('disabled')}
onClick={(e) => {
setLayers(new Set([name]))
e.stopPropagation()
@@ -89,13 +90,13 @@ export const BiomeSourcePreview = ({ model, data, shown, lang, version }: Previe
})}
</BtnMenu>}
{(type === 'multi_noise' || type === 'checkerboard') && <>
<Btn icon="dash" tooltip={locale(lang, 'zoom_out')}
<Btn icon="dash" tooltip={locale('zoom_out')}
onClick={() => changeScale(scale * 1.5)} />
<Btn icon="plus" tooltip={locale(lang, 'zoom_in')}
<Btn icon="plus" tooltip={locale('zoom_in')}
onClick={() => changeScale(scale / 1.5)} />
</>}
{type === 'multi_noise' &&
<Btn icon="sync" tooltip={locale(lang, 'generate_new_seed')}
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => newSeed(model)} />}
</div>
<canvas ref={canvas} width="200" height="200"></canvas>

View File

@@ -1,12 +1,13 @@
import { useEffect, useState } from 'preact/hooks'
import type { PreviewProps } from '.'
import { Btn } from '..'
import { useLocale } from '../../contexts'
import { useCanvas } from '../../hooks'
import { locale } from '../../Locales'
import { decorator } from '../../previews'
import { randomSeed } from '../../Utils'
export const DecoratorPreview = ({ data, version, shown, lang }: PreviewProps) => {
export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => {
const { locale } = useLocale()
const [scale, setScale] = useState(4)
const [seed, setSeed] = useState(randomSeed())
@@ -28,11 +29,11 @@ export const DecoratorPreview = ({ data, version, shown, lang }: PreviewProps) =
return <>
<div class="controls">
<Btn icon="dash" tooltip={locale(lang, 'zoom_out')}
<Btn icon="dash" tooltip={locale('zoom_out')}
onClick={() => setScale(Math.min(16, scale + 1))} />
<Btn icon="plus" tooltip={locale(lang, 'zoom_in')}
<Btn icon="plus" tooltip={locale('zoom_in')}
onClick={() => setScale(Math.max(1, scale - 1))} />
<Btn icon="sync" tooltip={locale(lang, 'generate_new_seed')}
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</div>
<canvas ref={canvas} width="64" height="64"></canvas>

View File

@@ -1,12 +1,13 @@
import { useEffect, useRef, useState } from 'preact/hooks'
import type { PreviewProps } from '.'
import { Btn } from '..'
import { useLocale } from '../../contexts'
import { useCanvas } from '../../hooks'
import { locale } from '../../Locales'
import { normalNoise } from '../../previews'
import { randomSeed } from '../../Utils'
export const NoisePreview = ({ lang, data, shown, version }: PreviewProps) => {
export const NoisePreview = ({ data, shown, version }: PreviewProps) => {
const { locale } = useLocale()
const [seed, setSeed] = useState(randomSeed())
const [scale, setScale] = useState(2)
const offset = useRef<[number, number]>([0, 0])
@@ -41,11 +42,11 @@ export const NoisePreview = ({ lang, data, shown, version }: PreviewProps) => {
return <>
<div class="controls">
<Btn icon="dash" tooltip={locale(lang, 'zoom_out')}
<Btn icon="dash" tooltip={locale('zoom_out')}
onClick={() => changeScale(scale * 1.5)} />
<Btn icon="plus" tooltip={locale(lang, 'zoom_in')}
<Btn icon="plus" tooltip={locale('zoom_in')}
onClick={() => changeScale(scale / 1.5)} />
<Btn icon="sync" tooltip={locale(lang, 'generate_new_seed')}
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</div>
<canvas ref={canvas} width="256" height="256"></canvas>

View File

@@ -1,14 +1,14 @@
import { useEffect, useRef, useState } from 'preact/hooks'
import type { PreviewProps } from '.'
import { Btn, BtnInput, BtnMenu } from '..'
import { useLocale } from '../../contexts'
import { useCanvas } from '../../hooks'
import { locale } from '../../Locales'
import { noiseSettings } from '../../previews'
import { checkVersion } from '../../services'
import { randomSeed } from '../../Utils'
export const NoiseSettingsPreview = ({ lang, data, shown, version }: PreviewProps) => {
const loc = locale.bind(null, lang)
export const NoiseSettingsPreview = ({ data, shown, version }: PreviewProps) => {
const { locale } = useLocale()
const [seed, setSeed] = useState(randomSeed())
const [biomeScale, setBiomeScale] = useState(0.2)
const [biomeDepth, setBiomeDepth] = useState(0.1)
@@ -48,12 +48,12 @@ export const NoiseSettingsPreview = ({ lang, data, shown, version }: PreviewProp
<div class="controls">
{focused && <Btn label={`Y = ${focused}`} class="no-pointer" />}
{checkVersion(version, undefined, '1.17') &&
<BtnMenu icon="gear" tooltip={locale(lang, 'terrain_settings')}>
<BtnInput label={loc('preview.scale')} value={`${biomeScale}`} onChange={v => setBiomeScale(Number(v))} />
<BtnInput label={loc('preview.depth')} value={`${biomeDepth}`} onChange={v => setBiomeDepth(Number(v))} />
<BtnMenu icon="gear" tooltip={locale('terrain_settings')}>
<BtnInput label={locale('preview.scale')} value={`${biomeScale}`} onChange={v => setBiomeScale(Number(v))} />
<BtnInput label={locale('preview.depth')} value={`${biomeDepth}`} onChange={v => setBiomeDepth(Number(v))} />
</BtnMenu>
}
<Btn icon="sync" tooltip={locale(lang, 'generate_new_seed')}
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</div>
<canvas ref={canvas} width={size} height={size}></canvas>

View File

@@ -7,7 +7,6 @@ export * from './NoisePreview'
export * from './NoiseSettingsPreview'
export type PreviewProps = {
lang: string,
model: DataModel,
data: any,
shown: boolean,

View File

@@ -1,7 +1,7 @@
import { Howl } from 'howler'
import { useEffect, useRef, useState } from 'preact/hooks'
import { Btn, NumberInput, RangeInput, TextInput } from '..'
import { locale } from '../../Locales'
import { useLocale } from '../../contexts'
import type { SoundEvents, VersionAssets } from '../../services'
import { getResourceUrl } from '../../services'
@@ -13,15 +13,14 @@ export interface SoundConfig {
volume: number,
}
type SoundConfigProps = SoundConfig & {
lang: string,
assets: VersionAssets,
sounds: SoundEvents,
onEdit: (changes: Partial<SoundConfig>) => unknown,
onDelete: () => unknown,
delayedPlay?: number,
}
export function SoundConfig({ lang, assets, sounds, sound, delay, pitch, volume, onEdit, onDelete, delayedPlay }: SoundConfigProps) {
const loc = locale.bind(null, lang)
export function SoundConfig({ assets, sounds, sound, delay, pitch, volume, onEdit, onDelete, delayedPlay }: SoundConfigProps) {
const { locale } = useLocale()
const [loading, setLoading] = useState(true)
const [playing, setPlaying] = useState(false)
const [invalid, setInvalid] = useState(false)
@@ -100,23 +99,23 @@ export function SoundConfig({ lang, assets, sounds, sound, delay, pitch, volume,
}
return <div class={`sound-config${loading ? ' loading' : playing ? ' playing' : ''}${invalid ? ' invalid' : ''}`}>
<Btn class="play" icon={invalid ? 'alert' : loading ? 'sync' : 'play'} label={loc('sounds.play')} onClick={play} tooltip={invalid ? loc('sounds.unknown_sound') : loading ? loc('sounds.loading_sound') : loc('sounds.play_sound')} tooltipLoc="se" />
<Btn class="play" icon={invalid ? 'alert' : loading ? 'sync' : 'play'} label={locale('sounds.play')} onClick={play} tooltip={invalid ? locale('sounds.unknown_sound') : loading ? locale('sounds.loading_sound') : locale('sounds.play_sound')} tooltipLoc="se" />
<TextInput class="btn btn-input sound" list="sound-list" spellcheck={false}
value={sound} onChange={sound => onEdit({ sound })} />
<label class="delay-label">{loc('sounds.delay')}: </label>
<label class="delay-label">{locale('sounds.delay')}: </label>
<NumberInput class="btn btn-input delay" min={0}
value={delay} onChange={delay => onEdit({ delay })} />
<label class="pitch-label">{loc('sounds.pitch')}: </label>
<label class="pitch-label">{locale('sounds.pitch')}: </label>
<RangeInput class="pitch tooltipped tip-s" min={0.5} max={2} step={0.01}
aria-label={pitch.toFixed(2)} style={`--x: ${(pitch - 0.5) * (100 / 1.5)}%`}
value={pitch} onChange={pitch => onEdit({ pitch })} />
<label class="volume-label">{loc('sounds.volume')}: </label>
<label class="volume-label">{locale('sounds.volume')}: </label>
<RangeInput class="volume tooltipped tip-s" min={0} max={1} step={0.01}
aria-label={volume.toFixed(2)} style={`--x: ${volume * 100}%`}
value={volume} onChange={volume => onEdit({ volume })} />
<Btn class={`copy${copyActive ? ' active' : ''}`} icon={copyActive ? 'check' : 'terminal'} label={loc('copy')} tooltip={copyActive ? loc('copied') : loc('sounds.copy_command')}
<Btn class={`copy${copyActive ? ' active' : ''}`} icon={copyActive ? 'check' : 'terminal'} label={locale('copy')} tooltip={copyActive ? locale('copied') : locale('sounds.copy_command')}
onClick={copy} />
<Btn class="remove" icon="trashcan" tooltip={loc('sounds.remove_sound')}
<Btn class="remove" icon="trashcan" tooltip={locale('sounds.remove_sound')}
onClick={() => {onDelete(); stop()}} />
</div>
}