mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
Project tree view and creation (#203)
* Implement creating and importing new projects * Add downloading a zip of a project * Project validation (WIP) * Add project side panel, remove project pages * Project file saving * Add file tree actions to rename and delete * Fix file creation auto focus * Add button to save file from menu * Add project creation * Fix specificity on version switcher button * Update default version to 1.19 * List project files by type, remember project and delete project
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
version: localStorage.getItem('schema_version') || '1.19',
|
||||
locale: localStorage.getItem('language') || 'en',
|
||||
prefers_color_scheme: matchMedia('(prefers-color-scheme: light)').matches ? 'light' : matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'none',
|
||||
tree_view_mode: localStorage.getItem('misode_tree_view_mode') || 'default',
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"@mcschema/java-1.18.2": "^0.1.10",
|
||||
"@mcschema/java-1.19": "^0.1.27",
|
||||
"@mcschema/locales": "^0.1.64",
|
||||
"@zip.js/zip.js": "^2.4.5",
|
||||
"brace": "^0.11.1",
|
||||
"buffer": "^6.0.3",
|
||||
"comment-json": "^4.1.1",
|
||||
@@ -853,6 +854,11 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@zip.js/zip.js": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.4.5.tgz",
|
||||
"integrity": "sha512-woLBVy50a9evpFFxyZiW6Jj4BXROJlQF2dF9Wzk0ZFqhjQBJ9uS21+KQkrz54674r7Ppiy61gXnzpVie+EMkfw=="
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
@@ -5773,6 +5779,11 @@
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@zip.js/zip.js": {
|
||||
"version": "2.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.4.5.tgz",
|
||||
"integrity": "sha512-woLBVy50a9evpFFxyZiW6Jj4BXROJlQF2dF9Wzk0ZFqhjQBJ9uS21+KQkrz54674r7Ppiy61gXnzpVie+EMkfw=="
|
||||
},
|
||||
"acorn": {
|
||||
"version": "7.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@mcschema/java-1.18.2": "^0.1.10",
|
||||
"@mcschema/java-1.19": "^0.1.27",
|
||||
"@mcschema/locales": "^0.1.64",
|
||||
"@zip.js/zip.js": "^2.4.5",
|
||||
"brace": "^0.11.1",
|
||||
"buffer": "^6.0.3",
|
||||
"comment-json": "^4.1.1",
|
||||
|
||||
@@ -104,6 +104,12 @@ export namespace Analytics {
|
||||
})
|
||||
}
|
||||
|
||||
export function setTreeViewMode(tree_view_mode: string) {
|
||||
gtag('set', {
|
||||
tree_view_mode,
|
||||
})
|
||||
}
|
||||
|
||||
export function resetGenerator(file_type: string, history: number, method: Method) {
|
||||
event(ID_GENERATOR, 'reset')
|
||||
gtag('event', 'reset_generator', {
|
||||
@@ -131,16 +137,6 @@ export namespace Analytics {
|
||||
})
|
||||
}
|
||||
|
||||
export function saveProjectFile(file_type: string, project_size: number, projects_count: number, method: Method) {
|
||||
event(ID_GENERATOR, 'save-project-file', legacyMethod(method))
|
||||
gtag('event', 'save_project_file', {
|
||||
file_type,
|
||||
project_size,
|
||||
projects_count,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function loadPreset(file_type: string, file_name: string) {
|
||||
event(ID_GENERATOR, 'load-preset', file_name)
|
||||
gtag('event', 'load_generator_preset', {
|
||||
@@ -222,4 +218,63 @@ export namespace Analytics {
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function showProject(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
event(ID_GENERATOR, 'show-project', legacyMethod(method))
|
||||
gtag('event', 'show_project', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function hideProject(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
event(ID_GENERATOR, 'hide-project', legacyMethod(method))
|
||||
gtag('event', 'hide_project', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function saveProjectFile(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
event(ID_GENERATOR, 'save-project-file', legacyMethod(method))
|
||||
gtag('event', 'save_project_file', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteProjectFile(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
event(ID_GENERATOR, 'delete-project-file', legacyMethod(method))
|
||||
gtag('event', 'delete_project_file', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function renameProjectFile(file_type: string, projects_count: number, project_size: number, method: Method) {
|
||||
event(ID_GENERATOR, 'rename-project-file', legacyMethod(method))
|
||||
gtag('event', 'rename_project_file', {
|
||||
file_type,
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteProject(projects_count: number, project_size: number, method: Method) {
|
||||
event(ID_GENERATOR, 'delete-project', legacyMethod(method))
|
||||
gtag('event', 'delete_project', {
|
||||
projects_count,
|
||||
project_size,
|
||||
method,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import '../styles/global.css'
|
||||
import '../styles/nodes.css'
|
||||
import { Analytics } from './Analytics'
|
||||
import { Header } from './components'
|
||||
import { Category, Changelog, Generator, Guide, Guides, Home, Partners, Project, Sounds, Versions } from './pages'
|
||||
import { Category, Changelog, Generator, Guide, Guides, Home, Partners, Sounds, Versions } from './pages'
|
||||
import { cleanUrl } from './Utils'
|
||||
|
||||
export function App() {
|
||||
@@ -25,7 +25,6 @@ export function App() {
|
||||
<Sounds path="/sounds" />
|
||||
<Changelog path="/changelog" />
|
||||
<Versions path="/versions" />
|
||||
<Project path="/project" />
|
||||
<Guides path="/guides/" />
|
||||
<Guide path="/guides/:id" />
|
||||
<Generator default />
|
||||
|
||||
@@ -13,6 +13,8 @@ export namespace Store {
|
||||
export const ID_SOUNDS_VERSION = 'minecraft_sounds_version'
|
||||
export const ID_PROJECTS = 'misode_projects'
|
||||
export const ID_BACKUPS = 'misode_generator_backups'
|
||||
export const ID_OPEN_PROJECT = 'misode_open_project'
|
||||
export const ID_TREE_VIEW_MODE = 'misode_tree_view_mode'
|
||||
|
||||
export function getLanguage() {
|
||||
return localStorage.getItem(ID_LANGUAGE) ?? 'en'
|
||||
@@ -67,6 +69,14 @@ export namespace Store {
|
||||
return backups[id]
|
||||
}
|
||||
|
||||
export function getOpenProject() {
|
||||
return localStorage.getItem(ID_OPEN_PROJECT) ?? DRAFT_PROJECT.name
|
||||
}
|
||||
|
||||
export function getTreeViewMode() {
|
||||
return localStorage.getItem(ID_TREE_VIEW_MODE) ?? 'resources'
|
||||
}
|
||||
|
||||
export function setLanguage(language: string | undefined) {
|
||||
if (language) localStorage.setItem(ID_LANGUAGE, language)
|
||||
}
|
||||
@@ -108,4 +118,16 @@ export namespace Store {
|
||||
}
|
||||
localStorage.setItem(ID_BACKUPS, JSON.stringify(backups))
|
||||
}
|
||||
|
||||
export function setOpenProject(projectName: string | undefined) {
|
||||
if (projectName === undefined) {
|
||||
localStorage.removeItem(ID_OPEN_PROJECT)
|
||||
} else {
|
||||
localStorage.setItem(ID_OPEN_PROJECT, projectName)
|
||||
}
|
||||
}
|
||||
|
||||
export function setTreeViewMode(mode: string | undefined) {
|
||||
if (mode) localStorage.setItem(ID_TREE_VIEW_MODE, mode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { Path } from '@mcschema/core'
|
||||
import * as zip from '@zip.js/zip.js'
|
||||
import yaml from 'js-yaml'
|
||||
import { route } from 'preact-router'
|
||||
import rfdc from 'rfdc'
|
||||
@@ -285,3 +286,24 @@ export class BiMap<A, B> {
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
export async function readZip(file: File): Promise<[string, string][]> {
|
||||
const buffer = await file.arrayBuffer()
|
||||
const reader = new zip.ZipReader(new zip.BlobReader(new Blob([buffer])))
|
||||
const entries = await reader.getEntries()
|
||||
return await Promise.all(entries
|
||||
.filter(e => !e.directory)
|
||||
.map(async e => {
|
||||
const writer = new zip.TextWriter('utf-8')
|
||||
return [e.filename, await e.getData?.(writer)] as [string, string]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export async function writeZip(entries: [string, string][]): Promise<string> {
|
||||
const writer = new zip.ZipWriter(new zip.Data64URIWriter('application/zip'))
|
||||
await Promise.all(entries.map(async ([name, data]) => {
|
||||
await writer.add(name, new zip.TextReader(data))
|
||||
}))
|
||||
return await writer.close()
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ type BtnProps = {
|
||||
active?: boolean,
|
||||
tooltip?: string,
|
||||
tooltipLoc?: 'se' | 'sw' | 'nw',
|
||||
showTooltip?: boolean,
|
||||
class?: string,
|
||||
onClick?: (event: MouseEvent) => unknown,
|
||||
disabled?: boolean,
|
||||
}
|
||||
export function Btn({ icon, label, active, class: clazz, tooltip, tooltipLoc, onClick }: BtnProps) {
|
||||
return <div class={`btn${active ? ' active' : ''}${clazz ? ` ${clazz}` : ''}${tooltip ? ` tooltipped tip-${tooltipLoc ?? 'sw'}` : ''}${active ? ' tip-shown' : ''}`} onClick={onClick} aria-label={tooltip}>
|
||||
export function Btn({ icon, label, active, class: clazz, tooltip, tooltipLoc, onClick, disabled }: BtnProps) {
|
||||
return <div class={`btn${active ? ' active' : ''}${clazz ? ` ${clazz}` : ''}${tooltip ? ` tooltipped tip-${tooltipLoc ?? 'sw'}` : ''}${disabled ? ' disabled' : ''}${active ? ' tip-shown' : ''}`} onClick={disabled ? undefined : onClick} aria-label={tooltip}>
|
||||
{icon && Octicon[icon]}
|
||||
{label && <span>{label}</span>}
|
||||
</div>
|
||||
|
||||
21
src/app/components/BtnLink.tsx
Normal file
21
src/app/components/BtnLink.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Octicon } from '.'
|
||||
|
||||
interface Props {
|
||||
link?: string,
|
||||
icon?: keyof typeof Octicon,
|
||||
label?: string,
|
||||
tooltip?: string,
|
||||
tooltipLoc?: 'se' | 'sw' | 'nw',
|
||||
swapped?: boolean,
|
||||
}
|
||||
export function BtnLink({ link, icon, label, tooltip, tooltipLoc, swapped }: Props) {
|
||||
return <a {...link ? { href: link } : { disabled: true }} class={`btn btn-link${tooltip ? ` tooltipped tip-${tooltipLoc ?? 'sw'}` : ''}`} aria-label={tooltip}>
|
||||
{swapped ? <>
|
||||
{label && <span>{label}</span>}
|
||||
{icon && Octicon[icon]}
|
||||
</> : <>
|
||||
{icon && Octicon[icon]}
|
||||
{label && <span>{label}</span>}
|
||||
</>}
|
||||
</a>
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export function BtnMenu(props: BtnMenuProps) {
|
||||
const [active, setActive] = useFocus()
|
||||
|
||||
return <div {...props} class={`btn-menu${relative === false ? ' no-relative' : ''} ${props.class}`}>
|
||||
<Btn {...{icon, label, tooltip, tooltipLoc}} onClick={setActive} />
|
||||
<Btn {...{icon, label, tooltip, tooltipLoc}} onClick={() => setActive()} />
|
||||
{active && <div class="btn-group">
|
||||
{children}
|
||||
</div>}
|
||||
|
||||
40
src/app/components/FileUpload.tsx
Normal file
40
src/app/components/FileUpload.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useCallback, useRef } from 'preact/hooks'
|
||||
import { Btn } from '.'
|
||||
import { useLocale } from '../contexts'
|
||||
|
||||
interface Props {
|
||||
value: File | undefined,
|
||||
onChange: (file: File) => unknown,
|
||||
label?: string,
|
||||
accept?: string,
|
||||
}
|
||||
export function FileUpload({ value, onChange, label, accept }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const fileUpload = useRef<HTMLInputElement>(null)
|
||||
|
||||
const onUpload = () => {
|
||||
if (fileUpload.current === null) return
|
||||
for (let i = 0; i < (fileUpload.current.files?.length ?? 0); i++) {
|
||||
const file = fileUpload.current.files![i]
|
||||
onChange(file)
|
||||
}
|
||||
}
|
||||
|
||||
const onDrop = useCallback((e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
if(!e.dataTransfer) return
|
||||
|
||||
for (let i = 0; i < e.dataTransfer.files.length; i++) {
|
||||
const file = e.dataTransfer.files[i]
|
||||
onChange(file)
|
||||
}
|
||||
}, [onChange])
|
||||
|
||||
return <label class="file-upload" onDrop={onDrop} onDragOver={e => e.preventDefault()}>
|
||||
<input ref={fileUpload} type="file" onChange={onUpload} accept={accept} />
|
||||
<Btn label={label ?? locale('choose_file')} />
|
||||
<span>
|
||||
{value ? value.name : locale('no_file_chosen')}
|
||||
</span>
|
||||
</label>
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getCurrentUrl, Link, route } from 'preact-router'
|
||||
import { Btn, BtnMenu, Icons, Octicon } from '.'
|
||||
import config from '../../config.json'
|
||||
import { useLocale, useTheme, useTitle, useVersion } from '../contexts'
|
||||
import { useLocale, useProject, useTheme, useTitle, useVersion } from '../contexts'
|
||||
import { checkVersion } from '../services'
|
||||
import { cleanUrl, getGenerator } from '../Utils'
|
||||
|
||||
@@ -15,8 +15,10 @@ export function Header() {
|
||||
const { lang, locale, changeLocale: changeLanguage } = useLocale()
|
||||
const { theme, changeTheme } = useTheme()
|
||||
const { version } = useVersion()
|
||||
const { projects, project, changeProject } = useProject()
|
||||
const { title } = useTitle()
|
||||
const gen = getGenerator(getCurrentUrl())
|
||||
const url = getCurrentUrl()
|
||||
const gen = getGenerator(url)
|
||||
|
||||
return <header>
|
||||
<div class="title">
|
||||
@@ -29,6 +31,11 @@ export function Header() {
|
||||
<Btn label={locale(g.partner ? `partner.${g.partner}.${g.id}` : g.id)} active={g.id === gen.id} onClick={() => route(cleanUrl(g.url))} />
|
||||
)}
|
||||
</BtnMenu>}
|
||||
{!gen && url.match(/\/?project\/?$/) && <BtnMenu icon="chevron_down" tooltip={locale('switch_project')}>
|
||||
{projects.map(p =>
|
||||
<Btn label={p.name} active={p.name === project.name} onClick={() => changeProject(p.name)} />
|
||||
)}
|
||||
</BtnMenu>}
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
|
||||
38
src/app/components/Modal.tsx
Normal file
38
src/app/components/Modal.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useCallback, useEffect } from 'preact/hooks'
|
||||
import type { JSXInternal } from 'preact/src/jsx'
|
||||
import { LOSE_FOCUS } from '../hooks'
|
||||
|
||||
const MODALS_KEY = 'data-modals'
|
||||
|
||||
interface Props extends JSXInternal.HTMLAttributes<HTMLDivElement> {
|
||||
onDismiss: () => void,
|
||||
}
|
||||
export function Modal(props: Props) {
|
||||
useEffect(() => {
|
||||
addCurrentModals(1)
|
||||
window.addEventListener('click', props.onDismiss)
|
||||
return () => {
|
||||
addCurrentModals(-1)
|
||||
window.removeEventListener('click', props.onDismiss)
|
||||
}
|
||||
})
|
||||
|
||||
const onClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
e.target?.dispatchEvent(new Event(LOSE_FOCUS, { bubbles: true }))
|
||||
}, [])
|
||||
|
||||
return <div {...props} class={`modal ${props.class ?? ''}`} onClick={onClick}>
|
||||
{props.children}
|
||||
</div>
|
||||
}
|
||||
|
||||
function addCurrentModals(diff: number) {
|
||||
const currentModals = parseInt(document.body.getAttribute(MODALS_KEY) ?? '0')
|
||||
const newModals = currentModals + diff
|
||||
if (newModals <= 0) {
|
||||
document.body.removeAttribute(MODALS_KEY)
|
||||
} else {
|
||||
document.body.setAttribute(MODALS_KEY, newModals.toFixed())
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,12 @@ export const Octicon = {
|
||||
arrow_right: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z"></path></svg>,
|
||||
check: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"></path></svg>,
|
||||
chevron_down: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M12.78 6.22a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06 0L3.22 7.28a.75.75 0 011.06-1.06L8 9.94l3.72-3.72a.75.75 0 011.06 0z"></path></svg>,
|
||||
chevron_left: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M9.78 12.78a.75.75 0 01-1.06 0L4.47 8.53a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L6.06 8l3.72 3.72a.75.75 0 010 1.06z"></path></svg>,
|
||||
chevron_right: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"></path></svg>,
|
||||
chevron_up: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.22 9.78a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 0l4.25 4.25a.75.75 0 01-1.06 1.06L8 6.06 4.28 9.78a.75.75 0 01-1.06 0z"></path></svg>,
|
||||
clippy: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path></svg>,
|
||||
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>,
|
||||
codescan_checkmark: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M10.28 6.28a.75.75 0 10-1.06-1.06L6.25 8.19l-.97-.97a.75.75 0 00-1.06 1.06l1.5 1.5a.75.75 0 001.06 0l3.5-3.5z"></path><path fill-rule="evenodd" d="M7.5 15a7.469 7.469 0 004.746-1.693l2.474 2.473a.75.75 0 101.06-1.06l-2.473-2.474A7.5 7.5 0 107.5 15zm0-13.5a6 6 0 104.094 10.386.75.75 0 01.293-.292A6 6 0 007.5 1.5z"></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>,
|
||||
@@ -18,6 +20,7 @@ export const Octicon = {
|
||||
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>,
|
||||
file_zip: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.5 1.75a.25.25 0 01.25-.25h3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h2.086a.25.25 0 01.177.073l2.914 2.914a.25.25 0 01.073.177v8.586a.25.25 0 01-.25.25h-.5a.75.75 0 000 1.5h.5A1.75 1.75 0 0014 13.25V4.664c0-.464-.184-.909-.513-1.237L10.573.513A1.75 1.75 0 009.336 0H3.75A1.75 1.75 0 002 1.75v11.5c0 .649.353 1.214.874 1.515a.75.75 0 10.752-1.298.25.25 0 01-.126-.217V1.75zM8.75 3a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM6 5.25a.75.75 0 01.75-.75h.5a.75.75 0 010 1.5h-.5A.75.75 0 016 5.25zm2 1.5A.75.75 0 018.75 6h.5a.75.75 0 010 1.5h-.5A.75.75 0 018 6.75zm-1.25.75a.75.75 0 000 1.5h.5a.75.75 0 000-1.5h-.5zM8 9.75A.75.75 0 018.75 9h.5a.75.75 0 010 1.5h-.5A.75.75 0 018 9.75zm-.75.75a1.75 1.75 0 00-1.75 1.75v3c0 .414.336.75.75.75h2.5a.75.75 0 00.75-.75v-3a1.75 1.75 0 00-1.75-1.75h-.5zM7 12.25a.25.25 0 01.25-.25h.5a.25.25 0 01.25.25v2.25H7v-2.25z"></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>,
|
||||
heart: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.25 2.5c-1.336 0-2.75 1.164-2.75 3 0 2.15 1.58 4.144 3.365 5.682A20.565 20.565 0 008 13.393a20.561 20.561 0 003.135-2.211C12.92 9.644 14.5 7.65 14.5 5.5c0-1.836-1.414-3-2.75-3-1.373 0-2.609.986-3.029 2.456a.75.75 0 01-1.442 0C6.859 3.486 5.623 2.5 4.25 2.5zM8 14.25l-.345.666-.002-.001-.006-.003-.018-.01a7.643 7.643 0 01-.31-.17 22.075 22.075 0 01-3.434-2.414C2.045 10.731 0 8.35 0 5.5 0 2.836 2.086 1 4.25 1 5.797 1 7.153 1.802 8 3.02 8.847 1.802 10.203 1 11.75 1 13.914 1 16 2.836 16 5.5c0 2.85-2.045 5.231-3.885 6.818a22.08 22.08 0 01-3.744 2.584l-.018.01-.006.003h-.002L8 14.25zm0 0l.345.666a.752.752 0 01-.69 0L8 14.25z"></path></svg>,
|
||||
@@ -30,10 +33,13 @@ export const Octicon = {
|
||||
mark_github: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>,
|
||||
moon: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M9.598 1.591a.75.75 0 01.785-.175 7 7 0 11-8.967 8.967.75.75 0 01.961-.96 5.5 5.5 0 007.046-7.046.75.75 0 01.175-.786zm1.616 1.945a7 7 0 01-7.678 7.678 5.5 5.5 0 107.678-7.678z"></path></svg>,
|
||||
package: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg>,
|
||||
pencil: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25a1.75 1.75 0 01.445-.758l8.61-8.61zm1.414 1.06a.25.25 0 00-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 000-.354l-1.086-1.086zM11.189 6.25L9.75 4.81l-6.286 6.287a.25.25 0 00-.064.108l-.558 1.953 1.953-.558a.249.249 0 00.108-.064l6.286-6.286z"></path></svg>,
|
||||
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>,
|
||||
rocket: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M14.064 0a8.75 8.75 0 00-6.187 2.563l-.459.458c-.314.314-.616.641-.904.979H3.31a1.75 1.75 0 00-1.49.833L.11 7.607a.75.75 0 00.418 1.11l3.102.954c.037.051.079.1.124.145l2.429 2.428c.046.046.094.088.145.125l.954 3.102a.75.75 0 001.11.418l2.774-1.707a1.75 1.75 0 00.833-1.49V9.485c.338-.288.665-.59.979-.904l.458-.459A8.75 8.75 0 0016 1.936V1.75A1.75 1.75 0 0014.25 0h-.186zM10.5 10.625c-.088.06-.177.118-.266.175l-2.35 1.521.548 1.783 1.949-1.2a.25.25 0 00.119-.213v-2.066zM3.678 8.116L5.2 5.766c.058-.09.117-.178.176-.266H3.309a.25.25 0 00-.213.119l-1.2 1.95 1.782.547zm5.26-4.493A7.25 7.25 0 0114.063 1.5h.186a.25.25 0 01.25.25v.186a7.25 7.25 0 01-2.123 5.127l-.459.458a15.21 15.21 0 01-2.499 2.02l-2.317 1.5-2.143-2.143 1.5-2.317a15.25 15.25 0 012.02-2.5l.458-.458h.002zM12 5a1 1 0 11-2 0 1 1 0 012 0zm-8.44 9.56a1.5 1.5 0 10-2.12-2.12c-.734.73-1.047 2.332-1.15 3.003a.23.23 0 00.265.265c.671-.103 2.273-.416 3.005-1.148z"></path></svg>,
|
||||
rows: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M16 2.75A1.75 1.75 0 0014.25 1H1.75A1.75 1.75 0 000 2.75v2.5A1.75 1.75 0 001.75 7h12.5A1.75 1.75 0 0016 5.25v-2.5zm-1.75-.25a.25.25 0 01.25.25v2.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25v-2.5a.25.25 0 01.25-.25h12.5zM16 10.75A1.75 1.75 0 0014.25 9H1.75A1.75 1.75 0 000 10.75v2.5A1.75 1.75 0 001.75 15h12.5A1.75 1.75 0 0016 13.25v-2.5zm-1.75-.25a.25.25 0 01.25.25v2.5a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25v-2.5a.25.25 0 01.25-.25h12.5z"></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>,
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
import { useMemo, useState } from 'preact/hooks'
|
||||
import { Octicon } from '.'
|
||||
import { useFocus } from '../hooks'
|
||||
|
||||
const SEPARATOR = '/'
|
||||
|
||||
export interface EntryAction {
|
||||
icon: keyof typeof Octicon,
|
||||
label: string,
|
||||
onAction: (entry: string) => unknown,
|
||||
}
|
||||
|
||||
export interface EntryError {
|
||||
path: string,
|
||||
message: string,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
entries: string[],
|
||||
onSelect: (entry: string) => unknown,
|
||||
selected?: string,
|
||||
actions?: EntryAction[],
|
||||
errors?: EntryError[],
|
||||
indent?: number,
|
||||
}
|
||||
export function TreeView({ entries, onSelect, indent }: Props) {
|
||||
export function TreeView({ entries, onSelect, selected, actions, errors, indent }: Props) {
|
||||
const roots = useMemo(() => {
|
||||
const groups: Record<string, string[]> = {}
|
||||
for (const entry of entries) {
|
||||
@@ -18,12 +33,16 @@ export function TreeView({ entries, onSelect, indent }: Props) {
|
||||
;(groups[root] ??= []).push(entry.slice(i + 1))
|
||||
}
|
||||
}
|
||||
return Object.entries(groups)
|
||||
}, entries)
|
||||
return Object.entries(groups).map(([r, entries]) => {
|
||||
const rootActions = actions?.map(a => ({ ...a, onAction: (e: string) => a.onAction(r + SEPARATOR + e) }))
|
||||
const rootErrors = errors?.flatMap(e => e.path.startsWith(r + SEPARATOR) ? [{ ...e, path: e.path.slice(r.length + SEPARATOR.length) }] : [])
|
||||
return [r, entries, rootActions, rootErrors] as [string, string[], EntryAction[], EntryError[]]
|
||||
}).sort()
|
||||
}, [entries, actions, errors])
|
||||
|
||||
const leaves = useMemo(() => {
|
||||
return entries.filter(e => !e.includes(SEPARATOR))
|
||||
}, entries)
|
||||
}, [entries])
|
||||
|
||||
const [hidden, setHidden] = useState(new Set<string>())
|
||||
const toggle = (root: string) => {
|
||||
@@ -36,23 +55,42 @@ export function TreeView({ entries, onSelect, indent }: Props) {
|
||||
}
|
||||
|
||||
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)}/>
|
||||
{roots.map(([r, entries, actions, errors]) => <div>
|
||||
<TreeViewEntry icon={hidden.has(r) ? 'chevron_right' : 'chevron_down'} key={r} label={r} onClick={() => toggle(r)} error={(errors?.length ?? 0) > 0} />
|
||||
{!hidden.has(r) &&
|
||||
<TreeView entries={entries} onSelect={e => onSelect(`${r}/${e}`)} indent={(indent ?? 0) + 1} />}
|
||||
<TreeView entries={entries} onSelect={e => onSelect(`${r}${SEPARATOR}${e}`)}
|
||||
selected={selected?.startsWith(r + SEPARATOR) ? selected.substring(r.length + 1) : undefined}
|
||||
actions={actions} errors={errors} indent={(indent ?? 0) + 1} />}
|
||||
</div>)}
|
||||
{leaves.map(e => <TreeViewEntry icon="file" key={e} label={e} onClick={() => onSelect(e)} />)}
|
||||
{leaves.map(e => <TreeViewEntry icon="file" key={e} label={e} active={e === selected} onClick={() => onSelect(e)} actions={actions?.map(a => ({ ...a, onAction: () => a.onAction(e) }))} error={errors?.find(er => er.path === e)?.message} />)}
|
||||
</div>
|
||||
}
|
||||
|
||||
interface TreeViewEntryProps {
|
||||
icon: keyof typeof Octicon,
|
||||
label: string,
|
||||
active?: boolean,
|
||||
onClick?: () => unknown,
|
||||
actions?: EntryAction[],
|
||||
error?: string | boolean,
|
||||
}
|
||||
function TreeViewEntry({ icon, label, onClick }: TreeViewEntryProps) {
|
||||
return <div class="entry" onClick={onClick} >
|
||||
function TreeViewEntry({ icon, label, active, onClick, actions, error }: TreeViewEntryProps) {
|
||||
const [focused, setFocus] = useFocus()
|
||||
const onContextMenu = (evt: MouseEvent) => {
|
||||
evt.preventDefault()
|
||||
if (actions?.length) {
|
||||
setFocus()
|
||||
}
|
||||
}
|
||||
|
||||
return <div class={`entry${error ? ' has-error' : ''}${active ? ' active' : ''}${focused ? ' focused' : ''}`} onClick={onClick} onContextMenu={onContextMenu} >
|
||||
{Octicon[icon]}
|
||||
{label}
|
||||
<span>{label.replaceAll('\u2215', '/')}</span>
|
||||
{typeof error === 'string' && <div class="status-icon danger tooltipped tip-se" aria-label={error}>
|
||||
{Octicon.issue_opened}
|
||||
</div>}
|
||||
{focused && <div class="entry-menu">
|
||||
{actions?.map(a => <div class="action" onClick={e => { a.onAction(''); e.stopPropagation(); setFocus(false) }}>{Octicon[a.icon]}{a.label}</div>)}
|
||||
</div>}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useRef } from 'preact/hooks'
|
||||
import type { JSXInternal } from 'preact/src/jsx'
|
||||
|
||||
type InputProps = JSXInternal.HTMLAttributes<HTMLInputElement>
|
||||
@@ -5,6 +6,7 @@ type InputProps = JSXInternal.HTMLAttributes<HTMLInputElement>
|
||||
type BaseInputProps<T> = Omit<InputProps, 'onChange' | 'type'> & {
|
||||
onChange?: (value: T) => unknown,
|
||||
onEnter?: (value: T) => unknown,
|
||||
onCancel?: () => unknown,
|
||||
}
|
||||
function BaseInput<T>(name: string, type: string, fn: (value: string) => T) {
|
||||
const component = (props: BaseInputProps<T>) => {
|
||||
@@ -16,9 +18,17 @@ function BaseInput<T>(name: string, type: string, fn: (value: string) => T) {
|
||||
if (evt.key === 'Enter') {
|
||||
const value = (evt.target as HTMLInputElement).value
|
||||
props.onEnter?.(fn(value))
|
||||
} else if (evt.key === 'Escape') {
|
||||
props.onCancel?.()
|
||||
}
|
||||
})
|
||||
return <input {...props} {...{ type, onChange, onKeyDown }} />
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => {
|
||||
if (props.autofocus) {
|
||||
ref.current?.select()
|
||||
}
|
||||
}, [props.autofocus])
|
||||
return <input ref={ref} {...props} {...{ type, onChange, onKeyDown }} />
|
||||
}
|
||||
component.displayName = name
|
||||
return component
|
||||
|
||||
31
src/app/components/generator/FileCreation.tsx
Normal file
31
src/app/components/generator/FileCreation.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { Analytics } from '../../Analytics'
|
||||
import { useLocale, useProject } from '../../contexts'
|
||||
import { Btn } from '../Btn'
|
||||
import { TextInput } from '../forms'
|
||||
import { Modal } from '../Modal'
|
||||
|
||||
interface Props {
|
||||
model: DataModel,
|
||||
id: string,
|
||||
method: string,
|
||||
onClose: () => void,
|
||||
}
|
||||
export function FileCreation({ model, id, method, onClose }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, project, updateFile } = useProject()
|
||||
const [fileId, setFileId] = useState('')
|
||||
|
||||
const doSave = () => {
|
||||
Analytics.saveProjectFile(id, projects.length, project.files.length, method as any)
|
||||
updateFile(id, undefined, { type: id, id: fileId, data: DataModel.unwrapLists(model.data) })
|
||||
onClose()
|
||||
}
|
||||
|
||||
return <Modal class="file-modal" onDismiss={onClose}>
|
||||
<p>{locale('project.save_current_file')}</p>
|
||||
<TextInput autofocus class="btn btn-input" value={fileId} onChange={setFileId} onEnter={doSave} onCancel={onClose} placeholder={locale('resource_location')} spellcheck={false} />
|
||||
<Btn icon="file" label={locale('project.save')} onClick={doSave} />
|
||||
</Modal>
|
||||
}
|
||||
29
src/app/components/generator/FileRenaming.tsx
Normal file
29
src/app/components/generator/FileRenaming.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useState } from 'preact/hooks'
|
||||
import { Analytics } from '../../Analytics'
|
||||
import { useLocale, useProject } from '../../contexts'
|
||||
import { Btn } from '../Btn'
|
||||
import { TextInput } from '../forms'
|
||||
import { Modal } from '../Modal'
|
||||
|
||||
interface Props {
|
||||
id: string,
|
||||
name: string,
|
||||
onClose: () => void,
|
||||
}
|
||||
export function FileRenaming({ id, name, onClose }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, project, updateFile } = useProject()
|
||||
const [fileId, setFileId] = useState(name)
|
||||
|
||||
const doSave = () => {
|
||||
Analytics.renameProjectFile(id, projects.length, project.files.length, 'menu')
|
||||
updateFile(id, name, { type: id, id: fileId })
|
||||
onClose()
|
||||
}
|
||||
|
||||
return <Modal class="file-modal" onDismiss={onClose}>
|
||||
<p>{locale('project.rename_file')}</p>
|
||||
<TextInput autofocus class="btn btn-input" value={fileId} onChange={setFileId} onEnter={doSave} placeholder={locale('resource_location')} spellcheck={false} />
|
||||
<Btn icon="pencil" label={locale('project.rename')} onClick={doSave} />
|
||||
</Modal>
|
||||
}
|
||||
96
src/app/components/generator/ProjectCreation.tsx
Normal file
96
src/app/components/generator/ProjectCreation.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { Btn, BtnMenu, FileUpload, Octicon, TextInput } from '..'
|
||||
import config from '../../../config.json'
|
||||
import type { Project } from '../../contexts'
|
||||
import { disectFilePath, useLocale, useProject } from '../../contexts'
|
||||
import type { VersionId } from '../../services'
|
||||
import { DEFAULT_VERSION, parseSource } from '../../services'
|
||||
import { message, readZip } from '../../Utils'
|
||||
import { Modal } from '../Modal'
|
||||
|
||||
interface Props {
|
||||
onClose: () => unknown,
|
||||
}
|
||||
export function ProjectCreation({ onClose }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, createProject, changeProject, updateProject } = useProject()
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [namespace, setNamespace] = useState('')
|
||||
const [version, setVersion] = useState(DEFAULT_VERSION)
|
||||
const [file, setFile] = useState<File | undefined>(undefined)
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
const onUpload = (file: File) => {
|
||||
if (file.type.match(/^application\/(x-)?zip(-compressed)?$/)) {
|
||||
if (name.length === 0) {
|
||||
setName(file.name
|
||||
.replace(/\.zip$/, '')
|
||||
.replaceAll(/[ _-]+/g, ' '))
|
||||
}
|
||||
setFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
const projectUpdater = useRef(updateProject)
|
||||
useEffect(() => {
|
||||
projectUpdater.current = updateProject
|
||||
}, [updateProject])
|
||||
|
||||
const onCreate = () => {
|
||||
setCreating(true)
|
||||
createProject(name, namespace || undefined, version)
|
||||
changeProject(name)
|
||||
if (file) {
|
||||
readZip(file).then(async (entries) => {
|
||||
const project: Partial<Project> = { files: [] }
|
||||
await Promise.all(entries.map(async (entry) => {
|
||||
const file = disectFilePath(entry[0])
|
||||
if (file) {
|
||||
try {
|
||||
const data = await parseSource(entry[1], 'json')
|
||||
project.files!.push({ ...file, data })
|
||||
} catch (e) {
|
||||
console.error(`Failed parsing ${file.type} ${file.id}: ${message(e)}`)
|
||||
}
|
||||
}
|
||||
}))
|
||||
projectUpdater.current(project)
|
||||
onClose()
|
||||
}).catch(() => {
|
||||
onClose()
|
||||
})
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const invalidName = useMemo(() => {
|
||||
return projects.map(p => p.name.trim().toLowerCase()).includes(name.trim().toLowerCase())
|
||||
}, [projects, name])
|
||||
|
||||
const invalidNamespace = useMemo(() => {
|
||||
return !(namespace.length === 0 || namespace.match(/^(?:[a-z0-9._-]+:)?[a-z0-9/._-]+$/))
|
||||
}, [namespace])
|
||||
|
||||
const versions = config.versions.map(v => v.id as VersionId).reverse()
|
||||
|
||||
return <Modal class="project-creation" onDismiss={onClose}>
|
||||
<p>{locale('project.create')}</p>
|
||||
<div class="input-group">
|
||||
<TextInput autofocus class={`btn btn-input${!creating && (invalidName || name.length === 0) ? ' invalid': ''}`} placeholder={locale('project.name')} value={name} onChange={setName} />
|
||||
{!creating && invalidName && <div class="status-icon danger tooltipped tip-e" aria-label={locale('project.name.already_exists')}>{Octicon.issue_opened}</div>}
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<TextInput class={`btn btn-input${!creating && invalidNamespace ? ' invalid' : ''}`} placeholder={locale('project.namespace')} value={namespace} onChange={setNamespace} />
|
||||
{!creating && invalidNamespace && <div class="status-icon danger tooltipped tip-e" aria-label={locale('project.namespace.invalid')}>{Octicon.issue_opened}</div>}
|
||||
</div>
|
||||
<BtnMenu icon="tag" label={version} tooltip={locale('switch_version')} data-cy="version-switcher">
|
||||
{versions.map(v =>
|
||||
<Btn label={v} active={v === version} onClick={() => setVersion(v)} />
|
||||
)}
|
||||
</BtnMenu>
|
||||
<FileUpload value={file} onChange={onUpload} label={locale('choose_zip_file')} accept=".zip"/>
|
||||
<Btn icon="rocket" label="Create!" disabled={creating || invalidName || name.length === 0 || invalidNamespace} onClick={onCreate} />
|
||||
</Modal>
|
||||
}
|
||||
27
src/app/components/generator/ProjectDeletion.tsx
Normal file
27
src/app/components/generator/ProjectDeletion.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Analytics } from '../../Analytics'
|
||||
import { useLocale, useProject } from '../../contexts'
|
||||
import { Btn } from '../Btn'
|
||||
import { Modal } from '../Modal'
|
||||
|
||||
interface Props {
|
||||
onClose: () => void,
|
||||
}
|
||||
export function ProjectDeletion({ onClose }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, project, deleteProject } = useProject()
|
||||
|
||||
const doSave = () => {
|
||||
Analytics.deleteProject(projects.length, project.files.length, 'menu')
|
||||
deleteProject(project.name)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return <Modal class="file-modal" onDismiss={onClose}>
|
||||
<p>{locale('project.delete_confirm.1', project.name)}</p>
|
||||
<p><b>{locale('project.delete_confirm.2')}</b></p>
|
||||
<div class="button-group">
|
||||
<Btn icon="trashcan" label={locale('project.delete')} onClick={doSave} class="danger" />
|
||||
<Btn label={locale('project.cancel')} onClick={onClose} />
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
122
src/app/components/generator/ProjectPanel.tsx
Normal file
122
src/app/components/generator/ProjectPanel.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { DataModel } from '@mcschema/core'
|
||||
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { Analytics } from '../../Analytics'
|
||||
import { disectFilePath, DRAFT_PROJECT, getFilePath, useLocale, useProject } from '../../contexts'
|
||||
import type { VersionId } from '../../services'
|
||||
import { stringifySource } from '../../services'
|
||||
import { Store } from '../../Store'
|
||||
import { writeZip } from '../../Utils'
|
||||
import { Btn } from '../Btn'
|
||||
import { BtnMenu } from '../BtnMenu'
|
||||
import type { EntryAction } from '../TreeView'
|
||||
import { TreeView } from '../TreeView'
|
||||
|
||||
interface Props {
|
||||
model: DataModel | undefined,
|
||||
version: VersionId,
|
||||
id: string,
|
||||
onError: (message: string) => unknown,
|
||||
onRename: (file: { type: string, id: string }) => unknown,
|
||||
onCreate: () => unknown,
|
||||
onDeleteProject: () => unknown,
|
||||
}
|
||||
export function ProjectPanel({ onRename, onCreate, onDeleteProject }: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { projects, project, changeProject, file, openFile, updateFile } = useProject()
|
||||
|
||||
const [treeViewMode, setTreeViewMode] = useState(Store.getTreeViewMode())
|
||||
|
||||
const changeTreeViewMode = useCallback((mode: string) => {
|
||||
Store.setTreeViewMode(mode)
|
||||
Analytics.setTreeViewMode(mode)
|
||||
setTreeViewMode(mode)
|
||||
}, [])
|
||||
|
||||
const disectEntry = useCallback((entry: string) => {
|
||||
if (treeViewMode === 'resources') {
|
||||
const [type, id] = entry.split('/')
|
||||
return {
|
||||
type: type.replaceAll('\u2215', '/'),
|
||||
id: id.replaceAll('\u2215', '/'),
|
||||
}
|
||||
}
|
||||
return disectFilePath(entry)
|
||||
}, [treeViewMode])
|
||||
|
||||
const entries = useMemo(() => project.files.flatMap(f => {
|
||||
const path = getFilePath(f)
|
||||
if (!path) return []
|
||||
if (treeViewMode === 'resources') {
|
||||
return [`${f.type.replaceAll('/', '\u2215')}/${f.id.replaceAll('/', '\u2215')}`]
|
||||
}
|
||||
return [path]
|
||||
}), [treeViewMode, ...project.files])
|
||||
|
||||
const selected = useMemo(() => file && getFilePath(file), [file])
|
||||
|
||||
const selectFile = useCallback((entry: string) => {
|
||||
const file = disectEntry(entry)
|
||||
if (file) {
|
||||
openFile(file.type, file.id)
|
||||
}
|
||||
}, [disectEntry])
|
||||
|
||||
const download = useRef<HTMLAnchorElement>(null)
|
||||
|
||||
const onDownload = async () => {
|
||||
if (!download.current) return
|
||||
const entries = project.files.flatMap(file => {
|
||||
const path = getFilePath(file)
|
||||
if (path === undefined) return []
|
||||
return [[path, stringifySource(file.data)]] as [string, string][]
|
||||
})
|
||||
const url = await writeZip(entries)
|
||||
download.current.setAttribute('href', url)
|
||||
download.current.setAttribute('download', `${project.name.replaceAll(' ', '_')}.zip`)
|
||||
download.current.click()
|
||||
}
|
||||
|
||||
const actions = useMemo<EntryAction[]>(() => [
|
||||
{
|
||||
icon: 'pencil',
|
||||
label: locale('project.rename_file'),
|
||||
onAction: (e) => {
|
||||
const file = disectEntry(e)
|
||||
if (file) {
|
||||
onRename(file)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'trashcan',
|
||||
label: locale('project.delete_file'),
|
||||
onAction: (e) => {
|
||||
const file = disectEntry(e)
|
||||
if (file) {
|
||||
Analytics.deleteProjectFile(file.type, projects.length, project.files.length, 'menu')
|
||||
updateFile(file.type, file.id, {})
|
||||
}
|
||||
},
|
||||
},
|
||||
], [disectEntry, updateFile, onRename])
|
||||
|
||||
return <>
|
||||
<div class="project-controls">
|
||||
<BtnMenu icon="chevron_down" label={project.name} tooltip={locale('switch_project')} tooltipLoc="se">
|
||||
{projects.map(p => <Btn label={p.name} active={p.name === project.name} onClick={() => changeProject(p.name)} />)}
|
||||
</BtnMenu>
|
||||
<BtnMenu icon="kebab_horizontal" >
|
||||
<Btn icon="file_zip" label={locale('project.download')} onClick={onDownload} />
|
||||
<Btn icon="plus_circle" label={locale('project.new')} onClick={onCreate} />
|
||||
<Btn icon={treeViewMode === 'resources' ? 'three_bars' : 'rows'} label={locale(treeViewMode === 'resources' ? 'project.show_file_paths' : 'project.show_resources')} onClick={() => changeTreeViewMode(treeViewMode === 'resources' ? 'files' : 'resources')} />
|
||||
{project.name !== DRAFT_PROJECT.name && <Btn icon="trashcan" label={locale('project.delete')} onClick={onDeleteProject} />}
|
||||
</BtnMenu>
|
||||
</div>
|
||||
<div class="file-view">
|
||||
{entries.length === 0
|
||||
? <span>{locale('project.no_files')}</span>
|
||||
: <TreeView entries={entries} selected={selected} onSelect={selectFile} actions={actions} />}
|
||||
</div>
|
||||
<a ref={download} style="display: none;"></a>
|
||||
</>
|
||||
}
|
||||
@@ -1,48 +1,14 @@
|
||||
import { DataModel } from '@mcschema/core'
|
||||
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 { getOutput } from '../../schema/transformOutput'
|
||||
import type { BlockStateRegistry } from '../../services'
|
||||
import { getSourceFormats, getSourceIndent, getSourceIndents, parseSource, stringifySource } from '../../services'
|
||||
import { Store } from '../../Store'
|
||||
import { message } from '../../Utils'
|
||||
|
||||
|
||||
const INDENT: Record<string, number | string | undefined> = {
|
||||
'2_spaces': 2,
|
||||
'4_spaces': 4,
|
||||
tabs: '\t',
|
||||
minified: undefined,
|
||||
}
|
||||
|
||||
let commentJson: typeof import('comment-json') | null = null
|
||||
|
||||
const FORMATS: Record<string, {
|
||||
parse: (v: string) => Promise<any>,
|
||||
stringify: (v: any, indentation: string | number | undefined) => string,
|
||||
}> = {
|
||||
json: {
|
||||
parse: async (v) => {
|
||||
try {
|
||||
return JSON.parse(v)
|
||||
} catch (e) {
|
||||
commentJson = await import('comment-json')
|
||||
return commentJson.parse(v)
|
||||
}
|
||||
},
|
||||
stringify: (v, i) => (commentJson ?? JSON).stringify(v, null, i) + '\n',
|
||||
},
|
||||
yaml: {
|
||||
parse: async (v) => yaml.load(v),
|
||||
stringify: (v, i) => yaml.dump(v, {
|
||||
flowLevel: i === undefined ? 0 : -1,
|
||||
indent: typeof i === 'string' ? 4 : i,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
interface Editor {
|
||||
getValue(): string
|
||||
setValue(value: string): void
|
||||
@@ -75,7 +41,7 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
|
||||
|
||||
const getSerializedOutput = useCallback((model: DataModel, blockStates: BlockStateRegistry) => {
|
||||
const data = getOutput(model, blockStates)
|
||||
return FORMATS[format].stringify(data, INDENT[indent])
|
||||
return stringifySource(data, format, indent)
|
||||
}, [indent, format])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -102,7 +68,7 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
|
||||
const value = editor.current.getValue()
|
||||
if (value.length === 0) return
|
||||
try {
|
||||
const data = await FORMATS[format].parse(value)
|
||||
const data = await parseSource(value, format)
|
||||
model?.reset(DataModel.wrapLists(data), false)
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
@@ -149,7 +115,7 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
|
||||
},
|
||||
configure(indent, format) {
|
||||
braceEditor.setOption('useSoftTabs', indent !== 'tabs')
|
||||
braceEditor.setOption('tabSize', indent === 'tabs' ? 4 : INDENT[indent])
|
||||
braceEditor.setOption('tabSize', indent === 'tabs' ? 4 : getSourceIndent(indent))
|
||||
braceEditor.getSession().setMode(`ace/mode/${format}`)
|
||||
},
|
||||
select() {
|
||||
@@ -233,12 +199,12 @@ export function SourcePanel({ name, model, blockStates, doCopy, doDownload, doIm
|
||||
return <>
|
||||
<div class="controls source-controls">
|
||||
<BtnMenu icon="gear" tooltip={locale('output_settings')} data-cy="source-controls">
|
||||
{Object.entries(INDENT).map(([key]) =>
|
||||
{getSourceIndents().map(key =>
|
||||
<Btn label={locale(`indentation.${key}`)} active={indent === key}
|
||||
onClick={() => changeIndent(key)}/>
|
||||
)}
|
||||
<hr />
|
||||
{Object.keys(FORMATS).map(key =>
|
||||
{getSourceFormats().map(key =>
|
||||
<Btn label={locale(`format.${key}`)} active={format === key}
|
||||
onClick={() => changeFormat(key)} />)}
|
||||
<hr />
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
export * from './FileCreation'
|
||||
export * from './FileRenaming'
|
||||
export * from './PreviewPanel'
|
||||
export * from './ProjectCreation'
|
||||
export * from './ProjectDeletion'
|
||||
export * from './ProjectPanel'
|
||||
export * from './SourcePanel'
|
||||
export * from './Tree'
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
export * from './Ad'
|
||||
export * from './Btn'
|
||||
export * from './BtnInput'
|
||||
export * from './BtnLink'
|
||||
export * from './BtnMenu'
|
||||
export * from './ErrorPanel'
|
||||
export * from './FileUpload'
|
||||
export * from './Footer'
|
||||
export * from './forms'
|
||||
export * from './generator'
|
||||
@@ -10,6 +12,7 @@ export * from './Giscus'
|
||||
export * from './GuideCard'
|
||||
export * from './Header'
|
||||
export * from './Icons'
|
||||
export * from './Modal'
|
||||
export * from './Octicon'
|
||||
export * from './previews'
|
||||
export * from './sounds'
|
||||
|
||||
@@ -9,7 +9,7 @@ import { cleanUrl } from '../Utils'
|
||||
|
||||
export type Project = {
|
||||
name: string,
|
||||
namespace: string,
|
||||
namespace?: string,
|
||||
version?: VersionId,
|
||||
files: ProjectFile[],
|
||||
}
|
||||
@@ -25,10 +25,19 @@ export type ProjectFile = {
|
||||
data: any,
|
||||
}
|
||||
|
||||
export const FilePatterns = [
|
||||
'worldgen/[a-z_]+',
|
||||
'tags/worldgen/[a-z_]+',
|
||||
'tags/[a-z_]+',
|
||||
'[a-z_]+',
|
||||
].map(e => RegExp(`^data/([a-z0-9._-]+)/(${e})/([a-z0-9/._-]+)$`))
|
||||
|
||||
interface ProjectContext {
|
||||
projects: Project[],
|
||||
project: Project,
|
||||
file?: ProjectFile,
|
||||
createProject: (name: string, namespace?: string, version?: VersionId) => unknown,
|
||||
deleteProject: (name: string) => unknown,
|
||||
changeProject: (name: string) => unknown,
|
||||
updateProject: (project: Partial<Project>) => unknown,
|
||||
updateFile: (type: string, id: string | undefined, file: Partial<ProjectFile>) => boolean,
|
||||
@@ -38,6 +47,8 @@ interface ProjectContext {
|
||||
const Project = createContext<ProjectContext>({
|
||||
projects: [DRAFT_PROJECT],
|
||||
project: DRAFT_PROJECT,
|
||||
createProject: () => {},
|
||||
deleteProject: () => {},
|
||||
changeProject: () => {},
|
||||
updateProject: () => {},
|
||||
updateFile: () => false,
|
||||
@@ -52,7 +63,7 @@ export function useProject() {
|
||||
export function ProjectProvider({ children }: { children: ComponentChildren }) {
|
||||
const [projects, setProjects] = useState<Project[]>(Store.getProjects())
|
||||
|
||||
const [projectName, setProjectName] = useState<string>(DRAFT_PROJECT.name)
|
||||
const [projectName, setProjectName] = useState<string>(Store.getOpenProject())
|
||||
const project = useMemo(() => {
|
||||
return projects.find(p => p.name === projectName) ?? DRAFT_PROJECT
|
||||
}, [projects, projectName])
|
||||
@@ -68,6 +79,20 @@ export function ProjectProvider({ children }: { children: ComponentChildren }) {
|
||||
setProjects(projects)
|
||||
}, [])
|
||||
|
||||
const createProject = useCallback((name: string, namespace?: string, version?: VersionId) => {
|
||||
changeProjects([...projects, { name, namespace, version, files: [] }])
|
||||
}, [projects])
|
||||
|
||||
const deleteProject = useCallback((name: string) => {
|
||||
if (name === DRAFT_PROJECT.name) return
|
||||
changeProjects(projects.filter(p => p.name !== name))
|
||||
}, [projects])
|
||||
|
||||
const changeProject = useCallback((name: string) => {
|
||||
Store.setOpenProject(name)
|
||||
setProjectName(name)
|
||||
}, [])
|
||||
|
||||
const updateProject = useCallback((edits: Partial<Project>) => {
|
||||
changeProjects(projects.map(p => p.name === projectName ? { ...p, ...edits } : p))
|
||||
}, [projects, projectName])
|
||||
@@ -76,7 +101,7 @@ export function ProjectProvider({ children }: { children: ComponentChildren }) {
|
||||
if (!edits.id) { // remove
|
||||
updateProject({ files: project.files.filter(f => f.type !== type || f.id !== id) })
|
||||
} else {
|
||||
const newId = edits.id.includes(':') ? edits.id : `${project.namespace}:${edits.id}`
|
||||
const newId = edits.id.includes(':') ? edits.id : `${project.namespace ?? 'minecraft'}:${edits.id}`
|
||||
const exists = project.files.some(f => f.type === type && f.id === newId)
|
||||
if (!id) { // create
|
||||
if (exists) return false
|
||||
@@ -110,7 +135,9 @@ export function ProjectProvider({ children }: { children: ComponentChildren }) {
|
||||
projects,
|
||||
project,
|
||||
file,
|
||||
changeProject: setProjectName,
|
||||
createProject,
|
||||
changeProject,
|
||||
deleteProject,
|
||||
updateProject,
|
||||
updateFile,
|
||||
openFile,
|
||||
@@ -122,15 +149,34 @@ export function ProjectProvider({ children }: { children: ComponentChildren }) {
|
||||
</Project.Provider>
|
||||
}
|
||||
|
||||
export function getFilePath(file: ProjectFile) {
|
||||
export function getFilePath(file: { id: string, type: string }) {
|
||||
const [namespace, id] = file.id.includes(':') ? file.id.split(':') : ['minecraft', file.id]
|
||||
if (file.type === 'pack_mcmeta') {
|
||||
return 'pack.mcmeta'
|
||||
if (file.id === 'pack') return 'pack.mcmeta'
|
||||
return undefined
|
||||
}
|
||||
const gen = config.generators.find(g => g.id === file.type)
|
||||
if (!gen) {
|
||||
console.error(`Cannot find generator of type ${file.type}`)
|
||||
return undefined
|
||||
}
|
||||
return `data/${namespace}/${gen.path ?? gen.id}/${id}`
|
||||
return `data/${namespace}/${gen.path ?? gen.id}/${id}.json`
|
||||
}
|
||||
|
||||
export function disectFilePath(path: string) {
|
||||
if (path === 'pack.mcmeta') {
|
||||
return { type: 'pack_mcmeta', id: 'pack' }
|
||||
}
|
||||
for (const p of FilePatterns) {
|
||||
const match = path.match(p)
|
||||
if (!match) continue
|
||||
const gen = config.generators.find(g => (g.path ?? g.id) === match[2])
|
||||
if (!gen) continue
|
||||
const namespace = match[1]
|
||||
const name = match[3].replace(/\.[a-z]+$/, '')
|
||||
return {
|
||||
type: gen.id,
|
||||
id: `${namespace}:${name}`,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
|
||||
export function useFocus(): [boolean, () => unknown] {
|
||||
export const LOSE_FOCUS = 'misode-lose-focus'
|
||||
|
||||
export function useFocus(): [boolean, (active?: boolean) => unknown] {
|
||||
const [active, setActive] = useState(false)
|
||||
|
||||
const hider = () => {
|
||||
@@ -11,12 +13,14 @@ export function useFocus(): [boolean, () => unknown] {
|
||||
if (active) {
|
||||
document.body.addEventListener('click', hider)
|
||||
document.body.addEventListener('contextmenu', hider)
|
||||
document.body.addEventListener(LOSE_FOCUS, hider)
|
||||
}
|
||||
return () => {
|
||||
document.body.removeEventListener('click', hider)
|
||||
document.body.removeEventListener('contextmenu', hider)
|
||||
document.body.removeEventListener(LOSE_FOCUS, hider)
|
||||
}
|
||||
}, [active])
|
||||
|
||||
return [active, () => setActive(true)]
|
||||
return [active, (active = true) => setActive(active)]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { getCurrentUrl, route } from 'preact-router'
|
||||
import { useEffect, useErrorBoundary, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Analytics } from '../Analytics'
|
||||
import { Ad, Btn, BtnMenu, ErrorPanel, Footer, HasPreview, Octicon, PreviewPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../components'
|
||||
import { useLocale, useProject, useTitle, useVersion } from '../contexts'
|
||||
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileRenaming, Footer, HasPreview, Octicon, PreviewPanel, ProjectCreation, ProjectDeletion, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../components'
|
||||
import { DRAFT_PROJECT, useLocale, useProject, useTitle, useVersion } from '../contexts'
|
||||
import { AsyncCancel, useActiveTimeout, useAsync, useModel, useSearchParam } from '../hooks'
|
||||
import { getOutput } from '../schema/transformOutput'
|
||||
import type { VersionId } from '../services'
|
||||
@@ -20,7 +20,7 @@ interface Props {
|
||||
export function Generator({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { version, changeVersion, changeTargetVersion } = useVersion()
|
||||
const { projects, project, file, updateFile, openFile, closeFile } = useProject()
|
||||
const { projects, project, file, updateProject, updateFile } = useProject()
|
||||
const [error, setError] = useState<Error | string | null>(null)
|
||||
const [errorBoundary, errorRetry] = useErrorBoundary()
|
||||
if (errorBoundary) {
|
||||
@@ -91,6 +91,12 @@ export function Generator({}: Props) {
|
||||
}
|
||||
Analytics.openSnippet(gen.id, sharedSnippetId, version)
|
||||
data = snippet.data
|
||||
} else if (file) {
|
||||
if (project.version && project.version !== version) {
|
||||
changeVersion(project.version, false)
|
||||
return AsyncCancel
|
||||
}
|
||||
data = file.data
|
||||
}
|
||||
const [model, blockStates] = await Promise.all([
|
||||
getModel(version, gen.id),
|
||||
@@ -102,65 +108,24 @@ export function Generator({}: Props) {
|
||||
}
|
||||
Analytics.setGenerator(gen.id)
|
||||
return { model, blockStates }
|
||||
}, [gen.id, version, sharedSnippetId, currentPreset])
|
||||
}, [gen.id, version, sharedSnippetId, currentPreset, project.name, file?.id])
|
||||
|
||||
const model = value?.model
|
||||
const blockStates = value?.blockStates
|
||||
|
||||
const [dirty, setDirty] = useState(false)
|
||||
useModel(model, () => {
|
||||
useModel(model, model => {
|
||||
if (!ignoreChange.current) {
|
||||
setCurrentPreset(undefined, true)
|
||||
setSharedSnippetId(undefined, true)
|
||||
}
|
||||
ignoreChange.current = false
|
||||
Store.setBackup(gen.id, DataModel.unwrapLists(model?.data))
|
||||
setError(null)
|
||||
setDirty(true)
|
||||
}, [gen.id, setCurrentPreset, setSharedSnippetId])
|
||||
|
||||
const [fileRename, setFileRename] = useState('')
|
||||
const [fileSaved, doSave] = useActiveTimeout()
|
||||
const [fileError, doFileError] = useActiveTimeout()
|
||||
|
||||
const doFileRename = () => {
|
||||
if (fileRename !== file?.id && fileRename && model && blockStates) {
|
||||
if (file && model && blockStates) {
|
||||
const data = getOutput(model, blockStates)
|
||||
const success = updateFile(gen.id, file?.id, { id: fileRename, data })
|
||||
if (success) {
|
||||
doSave()
|
||||
} else {
|
||||
doFileError()
|
||||
if (file) {
|
||||
setFileRename(file?.id)
|
||||
}
|
||||
}
|
||||
} else if (file) {
|
||||
setFileRename(file?.id)
|
||||
updateFile(gen.id, file.id, { id: file.id, data })
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFile = () => {
|
||||
if (file) {
|
||||
updateFile(gen.id, file.id, {})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (file) {
|
||||
setFileRename(file.id)
|
||||
}
|
||||
}, [file])
|
||||
|
||||
useEffect(() => {
|
||||
if (model) {
|
||||
setFileRename(file?.id ?? '')
|
||||
if (file && gen.id === file.type) {
|
||||
model.reset(DataModel.wrapLists(file.data))
|
||||
}
|
||||
setDirty(false)
|
||||
}
|
||||
}, [file, model])
|
||||
ignoreChange.current = false
|
||||
Store.setBackup(gen.id, DataModel.unwrapLists(model.data))
|
||||
setError(null)
|
||||
}, [gen.id, setCurrentPreset, setSharedSnippetId, blockStates, file?.id])
|
||||
|
||||
const reset = () => {
|
||||
Analytics.resetGenerator(gen.id, model?.historyIndex ?? 1, 'menu')
|
||||
@@ -188,14 +153,9 @@ export function Generator({}: Props) {
|
||||
}
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.ctrlKey && e.key === 's') {
|
||||
setFileSaving('hotkey')
|
||||
e.preventDefault()
|
||||
if (model && blockStates && file) {
|
||||
Analytics.saveProjectFile(gen.id, project.files.length, projects.length, 'hotkey')
|
||||
const data = getOutput(model, blockStates)
|
||||
updateFile(gen.id, file?.id, { id: file?.id, data })
|
||||
setDirty(false)
|
||||
doSave()
|
||||
}
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
@@ -242,6 +202,9 @@ export function Generator({}: Props) {
|
||||
const selectVersion = (version: VersionId) => {
|
||||
setSharedSnippetId(undefined, true)
|
||||
changeVersion(version)
|
||||
if (project.name !== DRAFT_PROJECT.name && project.version !== version) {
|
||||
updateProject({ version })
|
||||
}
|
||||
}
|
||||
|
||||
const [shareUrl, setShareUrl] = useState<string | undefined>(undefined)
|
||||
@@ -339,43 +302,43 @@ export function Generator({}: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
const [projectShown, setProjectShown] = useState(window.innerWidth > 600)
|
||||
const [projectCreating, setProjectCreating] = useState(false)
|
||||
const [projectDeleting, setprojectDeleting] = useState(false)
|
||||
const [fileSaving, setFileSaving] = useState<string | undefined>(undefined)
|
||||
const [fileRenaming, setFileRenaming] = useState<{ type: string, id: string } | undefined>(undefined)
|
||||
|
||||
const toggleProject = () => {
|
||||
if (projectShown) {
|
||||
Analytics.hideProject(gen.id, projects.length, project.files.length, 'menu')
|
||||
} else {
|
||||
Analytics.showProject(gen.id, projects.length, project.files.length, 'menu')
|
||||
}
|
||||
setProjectShown(!projectShown)
|
||||
}
|
||||
|
||||
return <>
|
||||
<main class={previewShown ? 'has-preview' : ''}>
|
||||
<main class={`generator${previewShown ? ' has-preview' : ''}${projectShown ? ' has-project' : ''}`}>
|
||||
{!gen.partner && <Ad id="data-pack-generator" type="text" />}
|
||||
<div class="controls">
|
||||
<div class={`project-controls ${file && 'has-file'}`}>
|
||||
<div class="btn-row">
|
||||
<BtnMenu icon="repo" label={project.name} relative={false}>
|
||||
<Btn icon="arrow_left" label={locale('project.go_to')} onClick={() => route('/project')} />
|
||||
{file && <Btn icon="file" label={locale('project.new_file')} onClick={closeFile} />}
|
||||
{backup !== undefined && <Btn icon="history" label={locale('restore_backup')} onClick={loadBackup} />}
|
||||
<SearchList searchPlaceholder={locale(project.name === 'Drafts' ? 'project.search_drafts' : 'project.search')} noResults={locale('project.no_files')} values={project.files.filter(f => f.type === gen.id).map(f => f.id)} onSelect={(id) => openFile(gen.id, id)} />
|
||||
</BtnMenu>
|
||||
<TextInput class="btn btn-input" placeholder={locale('project.unsaved_file')} value={fileRename} onChange={setFileRename} onEnter={doFileRename} onBlur={doFileRename} />
|
||||
{file && <Btn icon="trashcan" tooltip={locale('project.delete_file')} onClick={deleteFile} />}
|
||||
</div>
|
||||
{dirty ? <div class="status-icon">{Octicon.dot_fill}</div>
|
||||
: fileSaved ? <div class="status-icon active">{Octicon.check}</div>
|
||||
: fileError && <div class="status-icon danger">{Octicon.x}</div> }
|
||||
</div>
|
||||
<div class="generator-controls">
|
||||
<Btn icon="upload" label={locale('import')} onClick={importSource} />
|
||||
<BtnMenu icon="archive" label={locale('presets')} relative={false}>
|
||||
<SearchList searchPlaceholder={locale('search')} noResults={locale('no_presets')} values={presets} onSelect={selectPreset}/>
|
||||
</BtnMenu>
|
||||
<VersionSwitcher value={version} onChange={selectVersion} allowed={allowedVersions} />
|
||||
<BtnMenu icon="kebab_horizontal" tooltip={locale('more')}>
|
||||
<Btn icon="history" label={locale('reset')} onClick={reset} />
|
||||
<Btn icon="arrow_left" label={locale('undo')} onClick={undo} />
|
||||
<Btn icon="arrow_right" label={locale('redo')} onClick={redo} />
|
||||
</BtnMenu>
|
||||
</div>
|
||||
<div class="controls generator-controls">
|
||||
<Btn icon="upload" label={locale('import')} onClick={importSource} />
|
||||
<BtnMenu icon="archive" label={locale('presets')} relative={false}>
|
||||
<SearchList searchPlaceholder={locale('search')} noResults={locale('no_presets')} values={presets} onSelect={selectPreset}/>
|
||||
</BtnMenu>
|
||||
<VersionSwitcher value={version} onChange={selectVersion} allowed={allowedVersions} />
|
||||
<BtnMenu icon="kebab_horizontal" tooltip={locale('more')}>
|
||||
<Btn icon="history" label={locale('reset_default')} onClick={reset} />
|
||||
{backup !== undefined && <Btn icon="history" label={locale('restore_backup')} onClick={loadBackup} />}
|
||||
<Btn icon="arrow_left" label={locale('undo')} onClick={undo} />
|
||||
<Btn icon="arrow_right" label={locale('redo')} onClick={redo} />
|
||||
<Btn icon="file" label={locale('project.save')} onClick={() => setFileSaving('menu')} />
|
||||
</BtnMenu>
|
||||
</div>
|
||||
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
|
||||
<Tree {...{model, version, blockStates}} onError={setError} />
|
||||
<Footer donate={!gen.partner} />
|
||||
</main>
|
||||
<div class="popup-actions" style={`--offset: -${8 + actionsShown * 50}px;`}>
|
||||
<div class="popup-actions right-actions" style={`--offset: -${8 + actionsShown * 50}px;`}>
|
||||
<div class={`popup-action action-preview${hasPreview ? ' shown' : ''} tooltipped tip-nw`} aria-label={locale(previewShown ? 'hide_preview' : 'show_preview')} onClick={togglePreview}>
|
||||
{previewShown ? Octicon.x_circle : Octicon.play}
|
||||
</div>
|
||||
@@ -400,7 +363,19 @@ export function Generator({}: Props) {
|
||||
</div>
|
||||
<div class={`popup-share${shareShown ? ' shown' : ''}`}>
|
||||
<TextInput value={shareUrl} readonly />
|
||||
<Btn icon={shareCopyActive ? 'check' : 'clippy'} onClick={copySharedId} tooltip={locale(shareCopyActive ? 'copied' : 'copy_share')} tooltipLoc="nw" active={shareCopyActive} showTooltip={shareCopyActive} />
|
||||
<Btn icon={shareCopyActive ? 'check' : 'clippy'} onClick={copySharedId} tooltip={locale(shareCopyActive ? 'copied' : 'copy_share')} tooltipLoc="nw" active={shareCopyActive} />
|
||||
</div>
|
||||
<div class="popup-actions left-actions" style="--offset: 50px;">
|
||||
<div class={'popup-action action-project shown tooltipped tip-ne'} aria-label={locale(projectShown ? 'hide_project' : 'show_project')} onClick={toggleProject}>
|
||||
{projectShown ? Octicon.chevron_left : Octicon.repo}
|
||||
</div>
|
||||
</div>
|
||||
<div class={`popup-project${projectShown ? ' shown' : ''}`}>
|
||||
<ProjectPanel {...{model, version, id: gen.id}} onError={setError} onDeleteProject={() => setprojectDeleting(true)} onRename={setFileRenaming} onCreate={() => setProjectCreating(true)} />
|
||||
</div>
|
||||
{projectCreating && <ProjectCreation onClose={() => setProjectCreating(false)} />}
|
||||
{projectDeleting && <ProjectDeletion onClose={() => setprojectDeleting(false)} />}
|
||||
{model && fileSaving && <FileCreation id={gen.id} model={model} method={fileSaving} onClose={() => setFileSaving(undefined)} />}
|
||||
{fileRenaming && <FileRenaming id={fileRenaming.type } name={fileRenaming.id} onClose={() => setFileRenaming(undefined)} />}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useCallback, useMemo } from 'preact/hooks'
|
||||
import { Ad, TreeView } from '../components'
|
||||
import { getFilePath, useLocale, useProject, useTitle } from '../contexts'
|
||||
|
||||
interface Props {
|
||||
path?: string,
|
||||
}
|
||||
export function Project({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { project, openFile } = useProject()
|
||||
useTitle(locale('title.project', project.name))
|
||||
const entries = useMemo(() => project.files.flatMap(f => {
|
||||
const path = getFilePath(f)
|
||||
return path ? [path] : []
|
||||
}), project.files)
|
||||
|
||||
const selectFile = useCallback((entry: string) => {
|
||||
const [, namespace, type, ...id] = entry.split('/')
|
||||
openFile(type, `${namespace}:${id}`)
|
||||
}, [openFile])
|
||||
|
||||
return <main>
|
||||
<Ad id="data-pack-project" type="text" />
|
||||
<div class="project">
|
||||
<h2>{project.name}</h2>
|
||||
<div class="file-view">
|
||||
<TreeView entries={entries} onSelect={selectFile}/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Ad, ErrorPanel, Footer, Octicon, VersionDetail, VersionList } from '../components'
|
||||
import { Ad, BtnLink, ErrorPanel, Footer, VersionDetail, VersionList } from '../components'
|
||||
import { useLocale, useTitle } from '../contexts'
|
||||
import { useAsync, useSearchParam } from '../hooks'
|
||||
import type { VersionMeta } from '../services'
|
||||
@@ -29,19 +29,12 @@ export function Versions({}: Props) {
|
||||
{error && <ErrorPanel error={error} />}
|
||||
<div class="versions">
|
||||
{selectedId ? <>
|
||||
<div class="navigation">
|
||||
<a class="btn btn-link" href="/versions/">
|
||||
{Octicon.three_bars}
|
||||
{locale('versions.all')}
|
||||
</a>
|
||||
<a class="btn btn-link" {...previousVersion ? {href: `/versions/?id=${previousVersion.id}`} : {disabled: true}}>
|
||||
{Octicon.arrow_left}
|
||||
{locale('versions.previous')}
|
||||
</a>
|
||||
<a class="btn btn-link" {...nextVersion ? {href: `/versions/?id=${nextVersion.id}`} : {disabled: true}}>
|
||||
{locale('versions.next')}
|
||||
{Octicon.arrow_right}
|
||||
</a>
|
||||
<div class="version-navigation">
|
||||
<BtnLink link="/versions/" icon="three_bars" label={locale('versions.all')} />
|
||||
<BtnLink link={previousVersion ? `/versions/?id=${previousVersion.id}` : undefined}
|
||||
icon="arrow_left" label={locale('versions.previous')} />
|
||||
<BtnLink link={nextVersion ? `/versions/?id=${nextVersion.id}` : undefined}
|
||||
icon="arrow_right" label={locale('versions.next')} swapped />
|
||||
</div>
|
||||
<VersionDetail id={selectedId} version={selected} />
|
||||
</> : <VersionList versions={versions ?? []} link={id => `/versions/?id=${id}`} />}
|
||||
|
||||
@@ -5,6 +5,5 @@ export * from './Guide'
|
||||
export * from './Guides'
|
||||
export * from './Home'
|
||||
export * from './Partners'
|
||||
export * from './Project'
|
||||
export * from './Sounds'
|
||||
export * from './Versions'
|
||||
|
||||
@@ -67,7 +67,6 @@ export class Deepslate {
|
||||
const newCacheState = [settings, `${seed}`, biome]
|
||||
if (!deepEqual(this.cacheState, newCacheState)) {
|
||||
const biomeSource = new this.d.FixedBiome(checkVersion(this.loadedVersion, '1.18.2') ? this.d.Identifier.parse(biome) : biome as any)
|
||||
console.log(this.d)
|
||||
const noiseSettings = this.d.NoiseGeneratorSettings.fromJson(DataModel.unwrapLists(settings))
|
||||
const chunkGenerator = new this.d.NoiseChunkGenerator(seed, biomeSource, noiseSettings)
|
||||
this.settingsCache = noiseSettings.noise
|
||||
|
||||
@@ -565,7 +565,7 @@ function HelpPopup({ lang, path }: { lang: string, path: Path }) {
|
||||
const popupIcon = (type: string, icon: keyof typeof Octicon, popup: string) => {
|
||||
const [active, setActive] = useFocus()
|
||||
|
||||
return <div class={`node-icon ${type}${active ? ' show' : ''}`} onClick={setActive}>
|
||||
return <div class={`node-icon ${type}${active ? ' show' : ''}`} onClick={() => setActive()}>
|
||||
{Octicon[icon]}
|
||||
<span class="icon-popup">{popup}</span>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { fetchData } from './DataFetcher'
|
||||
export const VersionIds = ['1.15', '1.16', '1.17', '1.18', '1.18.2', '1.19'] as const
|
||||
export type VersionId = typeof VersionIds[number]
|
||||
|
||||
export const DEFAULT_VERSION: VersionId = '1.19'
|
||||
|
||||
export type BlockStateRegistry = {
|
||||
[block: string]: {
|
||||
properties?: {
|
||||
@@ -49,6 +51,7 @@ const versionGetter: {
|
||||
export let CachedDecorator: INode<any>
|
||||
export let CachedFeature: INode<any>
|
||||
export let CachedCollections: CollectionRegistry
|
||||
export let CachedSchemas: SchemaRegistry
|
||||
|
||||
async function getVersion(id: VersionId): Promise<VersionData> {
|
||||
if (!Versions[id]) {
|
||||
@@ -121,6 +124,12 @@ export async function getBlockStates(version: VersionId): Promise<BlockStateRegi
|
||||
return versionData.blockStates
|
||||
}
|
||||
|
||||
export async function getSchemas(version: VersionId): Promise<SchemaRegistry> {
|
||||
const versionData = await getVersion(version)
|
||||
CachedSchemas = versionData.schemas
|
||||
return versionData.schemas
|
||||
}
|
||||
|
||||
export function checkVersion(versionId: string, minVersionId: string | undefined, maxVersionId?: string) {
|
||||
const version = config.versions.findIndex(v => v.id === versionId)
|
||||
const minVersion = minVersionId ? config.versions.findIndex(v => v.id === minVersionId) : 0
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function shareSnippet(type: string, version: VersionId, jsonData: a
|
||||
try {
|
||||
const raw = JSON.stringify(jsonData)
|
||||
const data = lz.compressToBase64(raw)
|
||||
console.log('Compression rate', raw.length / raw.length)
|
||||
console.debug('Compression rate', raw.length / raw.length)
|
||||
const body = JSON.stringify({ data, type, version, show_preview })
|
||||
let id = ShareCache.get(body)
|
||||
if (!id) {
|
||||
|
||||
56
src/app/services/Source.ts
Normal file
56
src/app/services/Source.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import yaml from 'js-yaml'
|
||||
import { Store } from '../Store'
|
||||
|
||||
const INDENTS: Record<string, number | string | undefined> = {
|
||||
'2_spaces': 2,
|
||||
'4_spaces': 4,
|
||||
tabs: '\t',
|
||||
minified: undefined,
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
||||
let commentJson: typeof import('comment-json') | null = null
|
||||
|
||||
const FORMATS: Record<string, {
|
||||
parse: (v: string) => Promise<unknown>,
|
||||
stringify: (v: unknown, indentation: string | number | undefined) => string,
|
||||
}> = {
|
||||
json: {
|
||||
parse: async (v) => {
|
||||
try {
|
||||
return JSON.parse(v)
|
||||
} catch (e) {
|
||||
commentJson = await import('comment-json')
|
||||
return commentJson.parse(v)
|
||||
}
|
||||
},
|
||||
stringify: (v, i) => (commentJson ?? JSON).stringify(v, null, i) + '\n',
|
||||
},
|
||||
yaml: {
|
||||
parse: async (v) => yaml.load(v),
|
||||
stringify: (v, i) => yaml.dump(v, {
|
||||
flowLevel: i === undefined ? 0 : -1,
|
||||
indent: typeof i === 'string' ? 4 : i,
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
export function stringifySource(data: unknown, format?: string, indent?: string) {
|
||||
return FORMATS[format ?? Store.getFormat()].stringify(data, INDENTS[indent ?? Store.getIndent()])
|
||||
}
|
||||
|
||||
export async function parseSource(data: string, format: string) {
|
||||
return await FORMATS[format].parse(data)
|
||||
}
|
||||
|
||||
export function getSourceIndent(indent: string) {
|
||||
return INDENTS[indent]
|
||||
}
|
||||
|
||||
export function getSourceIndents() {
|
||||
return Object.keys(INDENTS)
|
||||
}
|
||||
|
||||
export function getSourceFormats() {
|
||||
return Object.keys(FORMATS)
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export * from './Changelogs'
|
||||
export * from './DataFetcher'
|
||||
export * from './Schemas'
|
||||
export * from './Sharing'
|
||||
export * from './Source'
|
||||
|
||||
@@ -125,6 +125,11 @@
|
||||
"schema": "chat_type",
|
||||
"minVersion": "1.19"
|
||||
},
|
||||
{
|
||||
"id": "pack_mcmeta",
|
||||
"url": "pack-mcmeta",
|
||||
"schema": "pack_mcmeta"
|
||||
},
|
||||
{
|
||||
"id": "dimension",
|
||||
"url": "dimension",
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
"advancement": "Advancement",
|
||||
"any_version": "Any",
|
||||
"assets": "Assets",
|
||||
"back": "Back",
|
||||
"block_definition": "Blockstate",
|
||||
"changelog.search": "Search changes",
|
||||
"changelog.no_results": "No changes",
|
||||
"choose_file": "Choose file",
|
||||
"choose_zip_file": "Choose zip file",
|
||||
"chat_type": "Chat Type",
|
||||
"collapse": "Collapse",
|
||||
"collapse_all": "Hold %0% to collapse all",
|
||||
@@ -44,6 +47,7 @@
|
||||
"guides.no_results.query": "No guides for this query",
|
||||
"hide_output": "Hide output",
|
||||
"hide_preview": "Hide preview",
|
||||
"hide_project": "Hide project",
|
||||
"home": "Home",
|
||||
"import": "Import",
|
||||
"indentation.2_spaces": "2 spaces",
|
||||
@@ -69,12 +73,14 @@
|
||||
"move_down": "Move down",
|
||||
"move_up": "Move up",
|
||||
"not_found.description": "The page you were looking for does not exist.",
|
||||
"no_file_chosen": "No file chosen",
|
||||
"no_presets": "No presets",
|
||||
"output_settings": "Output settings",
|
||||
"predicate": "Predicate",
|
||||
"recipe": "Recipe",
|
||||
"redo": "Redo",
|
||||
"reset": "Reset",
|
||||
"reset_default": "Reset to default",
|
||||
"restore_backup": "Restore last backup",
|
||||
"settings": "Settings",
|
||||
"settings.fields.description": "Customize advanced field settings",
|
||||
@@ -99,8 +105,10 @@
|
||||
"title.home": "Data Pack Generators",
|
||||
"title.partners": "Partners",
|
||||
"title.project": "%0% Project",
|
||||
"title.new_project": "Create a new project",
|
||||
"title.sounds": "Sound Explorer",
|
||||
"title.versions": "Versions Explorer",
|
||||
"pack_mcmeta": "Pack.mcmeta",
|
||||
"partner.immersive_weathering": "Immersive Weathering",
|
||||
"partner.immersive_weathering.block_growth": "Block Growth",
|
||||
"presets": "Presets",
|
||||
@@ -113,17 +121,36 @@
|
||||
"preview.offset": "Offset",
|
||||
"preview.peaks": "Peaks",
|
||||
"preview.width": "Width",
|
||||
"project.new": "New project",
|
||||
"project.cancel": "Cancel",
|
||||
"project.create": "Create a new project",
|
||||
"project.delete": "Delete project",
|
||||
"project.delete_confirm.1": "You are about to delete %0%",
|
||||
"project.delete_confirm.2": "This cannot be undone!",
|
||||
"project.delete_file": "Delete file",
|
||||
"project.download": "Download data pack",
|
||||
"project.go_to": "Go to project",
|
||||
"project.new_file": "New file",
|
||||
"project.no_files": "No files",
|
||||
"project.rename": "Rename",
|
||||
"project.rename_file": "Rename file",
|
||||
"project.save": "Save",
|
||||
"project.save_current_file": "Save file to project",
|
||||
"project.search": "Search project",
|
||||
"project.search_drafts": "Search drafts",
|
||||
"project.show_file_paths": "Show file paths",
|
||||
"project.show_resources": "Show resources",
|
||||
"project.unsaved_file": "Unsaved file",
|
||||
"project.name": "Project name",
|
||||
"project.name.already_exists": "There already exists a project with this name",
|
||||
"project.namespace": "Default namespace",
|
||||
"project.namespace.invalid": "Invalid namespace",
|
||||
"remove": "Remove",
|
||||
"resource_location": "Resource location",
|
||||
"search": "Search",
|
||||
"show_output": "Show output",
|
||||
"show_preview": "Show preview",
|
||||
"show_project": "Show project",
|
||||
"sounds.play": "Play",
|
||||
"sounds.play_sound": "Play sound",
|
||||
"sounds.play_all": "Play all",
|
||||
@@ -140,6 +167,7 @@
|
||||
"source_code_on": "Source code on",
|
||||
"source_placeholder": "Paste raw %0% content here",
|
||||
"switch_generator": "Switch generator",
|
||||
"switch_project": "Switch project",
|
||||
"switch_version": "Switch version",
|
||||
"terrain_settings": "Terrain settings",
|
||||
"text_component": "Text Component",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
--nav-faded-hover: #6e6e6e;
|
||||
--selection: #445a9599;
|
||||
--errors-background: #62190f;
|
||||
--errors-background-hover: #57140b;
|
||||
--errors-text: #ffffffcc;
|
||||
--invalid-text: #fd7951;
|
||||
--text-saturation: 60%;
|
||||
@@ -302,9 +303,6 @@ main {
|
||||
}
|
||||
|
||||
main > .controls {
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
position: initial;
|
||||
margin-right: 16px;
|
||||
margin-left: 16px;
|
||||
row-gap: 8px;
|
||||
@@ -321,6 +319,7 @@ main > .controls {
|
||||
.sounds-controls > *:not(:last-child),
|
||||
.preview-controls > *:not(:last-child),
|
||||
.generator-controls > *:not(:last-child),
|
||||
.project-controls > *:not(:last-child),
|
||||
.versions-controls > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -330,41 +329,28 @@ main > .controls {
|
||||
}
|
||||
|
||||
.project-controls {
|
||||
position: relative;
|
||||
margin: 8px;
|
||||
display: flex;
|
||||
width: max-content;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.project-controls > .btn-row > .btn-input {
|
||||
background-color: var(--background-2);
|
||||
.project-controls > :first-child {
|
||||
max-width: calc(200px - 62px);
|
||||
max-width: calc(max(200px, 20vw) - 62px);
|
||||
text-overflow: ellipsis;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.project-controls > .status-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
align-self: center;
|
||||
margin-right: 8px;
|
||||
fill: var(--text-3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.project-controls.has-file > .status-icon {
|
||||
right: 38px;
|
||||
}
|
||||
|
||||
.project-controls > .active {
|
||||
.status-icon.active {
|
||||
fill: var(--accent-success);
|
||||
}
|
||||
|
||||
.project-controls > .danger {
|
||||
.status-icon.danger {
|
||||
fill: var(--accent-danger);
|
||||
}
|
||||
|
||||
.project-controls .btn-menu .btn-group {
|
||||
.project-creation .btn-menu .btn-group,
|
||||
.project-controls .btn-menu:first-child .btn-group {
|
||||
left: 0;
|
||||
right: unset;
|
||||
}
|
||||
@@ -375,6 +361,7 @@ main > .controls {
|
||||
}
|
||||
|
||||
.tree {
|
||||
margin-top: -40px;
|
||||
overflow-x: auto;
|
||||
padding: 8px 16px 50vh;
|
||||
}
|
||||
@@ -503,6 +490,31 @@ main.has-preview {
|
||||
fill: var(--accent-success);
|
||||
}
|
||||
|
||||
.popup-project {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 56px);
|
||||
width: 200px;
|
||||
width: max(200px, 20vw);
|
||||
right: 100%;
|
||||
bottom: 0;
|
||||
z-index: 3;
|
||||
background-color: var(--background-2);
|
||||
box-shadow: 0 0 7px -3px #000;
|
||||
fill: var(--text-2);
|
||||
transition: transform 0.3s, width 0.3s;
|
||||
}
|
||||
|
||||
.popup-project.shown {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
main.has-project {
|
||||
padding-left: 200px;
|
||||
padding-left: max(200px, 20vw);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -529,7 +541,17 @@ main.has-preview {
|
||||
fill: var(--accent-primary);
|
||||
}
|
||||
|
||||
.btn:not(.btn-input):hover {
|
||||
.btn.disabled {
|
||||
cursor: default;
|
||||
background-color: var(--background-2);
|
||||
}
|
||||
|
||||
.btn.invalid {
|
||||
outline: var(--accent-danger) solid 1px;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.btn:not(.btn-input):not(.disabled):hover {
|
||||
background-color: var(--background-5);
|
||||
}
|
||||
|
||||
@@ -537,20 +559,28 @@ main.has-preview {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn svg:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.btn svg:not(:first-child) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.btn span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.btn-link svg {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.btn-link:not([href]) {
|
||||
cursor: default;
|
||||
background-color: var(--background-2) !important;
|
||||
@@ -651,6 +681,14 @@ main.has-preview {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background-color: var(--errors-background);
|
||||
}
|
||||
|
||||
.btn.danger:not(.btn-input):not(.disabled):hover {
|
||||
background-color: var(--errors-background-hover);
|
||||
}
|
||||
|
||||
.btn-menu .result-list {
|
||||
display: block;
|
||||
width: 380px;
|
||||
@@ -671,7 +709,7 @@ main.has-preview {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.version-switcher > .btn:hover {
|
||||
.version-switcher > .btn:not(.btn-input):not(.disabled):hover {
|
||||
background-color: var(--accent-site-2);
|
||||
}
|
||||
|
||||
@@ -694,13 +732,20 @@ main.has-preview {
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.button-group > *:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
bottom: 8px;
|
||||
left: 100%;
|
||||
z-index: 4;
|
||||
padding-right: 16px;
|
||||
background-color: var(--background-4);
|
||||
box-shadow: 0 0 7px -3px #000;
|
||||
user-select: none;
|
||||
@@ -709,17 +754,38 @@ main.has-preview {
|
||||
-ms-user-select: none;
|
||||
transform: translateX(var(--offset));
|
||||
transition: padding 0.1s, transform 0.3s;
|
||||
border-top-left-radius: 24px;
|
||||
border-bottom-left-radius: 24px;
|
||||
}
|
||||
|
||||
.popup-action {
|
||||
padding: 12px;
|
||||
fill: var(--text-3);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.popup-actions.right-actions {
|
||||
left: 100%;
|
||||
padding-right: 16px;
|
||||
border-top-left-radius: 24px;
|
||||
border-bottom-left-radius: 24px;
|
||||
}
|
||||
|
||||
.popup-actions.right-actions .popup-action {
|
||||
padding-left: 16px;
|
||||
border-top-left-radius: 50%;
|
||||
border-bottom-left-radius: 50%;
|
||||
}
|
||||
|
||||
.popup-actions.left-actions {
|
||||
right: 100%;
|
||||
padding-left: 16px;
|
||||
border-top-right-radius: 24px;
|
||||
border-bottom-right-radius: 24px;
|
||||
}
|
||||
|
||||
.popup-actions.left-actions .popup-action {
|
||||
padding-right: 16px;
|
||||
border-top-right-radius: 50%;
|
||||
border-bottom-right-radius: 50%;
|
||||
}
|
||||
|
||||
.popup-action.shown ~ .popup-action {
|
||||
@@ -887,7 +953,39 @@ main.has-preview {
|
||||
color: var(--text-1)
|
||||
}
|
||||
|
||||
.home, .category, .project, .versions, .guides, .guide {
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: var(--background-2);
|
||||
color: var(--text-2);
|
||||
box-shadow: 0 0 18px -2px #000;
|
||||
border-radius: 6px;
|
||||
padding: 24px;
|
||||
z-index: 101;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
[data-modals] .tree {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.file-modal > *:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.file-modal input {
|
||||
background-color: var(--background-1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.home, .category, .versions, .guides, .guide {
|
||||
padding: 16px;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
@@ -1051,21 +1149,78 @@ hr {
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.project h2 {
|
||||
color: var(--text-1);
|
||||
.file-view {
|
||||
background-color: var(--background-2);
|
||||
color: var(--text-2);
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
padding-bottom: 64px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.file-view > span {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.project-creation {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
background-color: var(--background-2);
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
.project-creation > *:not(:last-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.file-view {
|
||||
background-color: var(--background-2);
|
||||
.project-creation label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.project-creation input {
|
||||
background-color: var(--background-1);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-group .status-icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.input-group .status-icon svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
background-color: var(--background-1);
|
||||
}
|
||||
|
||||
.file-upload input[type=file] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload .btn {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.tree-view .entry {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 4px 2px;
|
||||
padding-left: calc(var(--indent, 0) * 24px);
|
||||
padding-left: calc(var(--indent, 0) * 15px + 8px);
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
@@ -1078,12 +1233,76 @@ hr {
|
||||
background-color: var(--background-3);
|
||||
}
|
||||
|
||||
.tree-view .entry.has-error {
|
||||
color: var(--accent-danger);
|
||||
fill: var(--accent-danger);
|
||||
}
|
||||
|
||||
.tree-view .entry.focused {
|
||||
background-color: var(--background-3);
|
||||
outline: 1px solid var(--accent-primary);
|
||||
outline-offset: -1px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tree-view .entry.active {
|
||||
background-color: var(--background-4);
|
||||
}
|
||||
|
||||
.tree-view .entry svg {
|
||||
margin-right: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree-view .entry .status-icon {
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tree-view .entry-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: var(--background-4);
|
||||
z-index: 4;
|
||||
margin-top: 5px;
|
||||
margin-left: 24px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 7px -2px #000;
|
||||
}
|
||||
|
||||
.tree-view .entry-menu::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
margin-left: 6px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent var(--background-4) transparent;
|
||||
}
|
||||
|
||||
.tree-view .entry-menu .action {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.tree-view .entry-menu .action:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
.tree-view .entry-menu .action:last-child {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
.tree-view .entry-menu .action:hover {
|
||||
background-color: var(--background-5);
|
||||
}
|
||||
|
||||
[data-ea-publisher] {
|
||||
margin: 0 16px 8px;
|
||||
min-height: 69.38px;
|
||||
}
|
||||
|
||||
.ea-content {
|
||||
@@ -1860,6 +2079,18 @@ hr {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1300px) {
|
||||
main.has-preview .tree {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
main .tree {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* SMALL */
|
||||
@media screen and (max-width: 580px) {
|
||||
.home {
|
||||
@@ -1883,6 +2114,10 @@ hr {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
main.has-project {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
main .controls {
|
||||
top: 64px
|
||||
}
|
||||
@@ -1917,10 +2152,6 @@ hr {
|
||||
width: calc(100vw - 32px);
|
||||
}
|
||||
|
||||
.generator-picker {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.version-metadata-hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user