mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-24 23:56:51 +00:00
Complete refactor (#123)
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
import { ModelListener, DataModel } from '@mcschema/core';
|
||||
|
||||
export abstract class AbstractView implements ModelListener {
|
||||
model: DataModel
|
||||
|
||||
constructor(model: DataModel) {
|
||||
this.model = model
|
||||
this.model.addListener(this)
|
||||
}
|
||||
|
||||
setModel(model: DataModel) {
|
||||
this.model.removeListener(this)
|
||||
this.model = model
|
||||
this.model.addListener(this)
|
||||
}
|
||||
|
||||
invalidated(model: DataModel): void {}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import {
|
||||
DataModel,
|
||||
Errors,
|
||||
} from '@mcschema/core'
|
||||
import { AbstractView } from './AbstractView'
|
||||
import { locale } from './locales'
|
||||
|
||||
export class ErrorsView extends AbstractView {
|
||||
target: HTMLElement
|
||||
|
||||
constructor(model: DataModel, target: HTMLElement) {
|
||||
super(model)
|
||||
this.target = target
|
||||
}
|
||||
|
||||
errors(errors: Errors): void {
|
||||
this.target.style.display = errors.count() > 0 ? 'flex' : 'none'
|
||||
|
||||
this.target.children[0].innerHTML = errors.getAll().map(err =>
|
||||
`<div class="error">
|
||||
<svg class="error-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="18" height="18"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
|
||||
<span class="error-path">${err.path.toString()}</span>
|
||||
<span>-</span>
|
||||
<span class="error-message">${locale(err.error, err.params)}</span>
|
||||
</div>`
|
||||
).join('')
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,5 @@
|
||||
import { ModelPath } from '@mcschema/core'
|
||||
|
||||
const dec2hex = (dec: number) => ('0' + dec.toString(16)).substr(-2)
|
||||
|
||||
export function hexId(length = 12) {
|
||||
var arr = new Uint8Array(length / 2)
|
||||
window.crypto.getRandomValues(arr)
|
||||
return Array.from(arr, dec2hex).join('')
|
||||
}
|
||||
import { hexId } from './Utils'
|
||||
|
||||
type Registry = {
|
||||
[id: string]: (el: Element) => void
|
||||
@@ -24,7 +17,7 @@ export interface Mounter {
|
||||
registerChange(callback: (el: Element) => void): string
|
||||
registerClick(callback: (el: Element) => void): string
|
||||
nodeInjector(path: ModelPath, mounter: Mounter): string
|
||||
mount(el: HTMLElement): void
|
||||
mount(el: Element): void
|
||||
}
|
||||
|
||||
export class Mounter implements Mounter {
|
||||
@@ -79,7 +72,7 @@ export class Mounter implements Mounter {
|
||||
return this.registerEvent('click', callback)
|
||||
}
|
||||
|
||||
mount(el: HTMLElement): void {
|
||||
mount(el: Element): void {
|
||||
for (const id in this.registry) {
|
||||
const element = el.querySelector(`[data-id="${id}"]`)
|
||||
if (element !== null) this.registry[id](element)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CollectionRegistry } from '@mcschema/core'
|
||||
import { checkVersion } from './App'
|
||||
import config from '../config.json'
|
||||
|
||||
const localStorageCache = (version: string) => `cache_${version}`
|
||||
@@ -20,6 +21,10 @@ export const RegistryFetcher = async (target: CollectionRegistry, versionId: str
|
||||
await Promise.all(config.registries.map(async r => {
|
||||
const id = typeof r === 'string' ? r : r.id
|
||||
|
||||
if (typeof r !== 'string' && r.minVersion) {
|
||||
if (!checkVersion(versionId, r.minVersion)) return
|
||||
}
|
||||
|
||||
if (!cache.registries) {
|
||||
cache.registries = {}
|
||||
}
|
||||
@@ -28,9 +33,9 @@ export const RegistryFetcher = async (target: CollectionRegistry, versionId: str
|
||||
return
|
||||
}
|
||||
|
||||
const url = typeof r === 'string'
|
||||
? mcdata(version.mcdata_ref, r)
|
||||
: `${baseUrl}/${version.mcdata_ref}/${r.path}`
|
||||
const url = typeof r !== 'string' && r.path
|
||||
? `${baseUrl}/${version.mcdata_ref}/${r.path}/data.min.json`
|
||||
: mcdata(version.mcdata_ref, typeof r === 'string' ? r : r.id)
|
||||
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
@@ -40,7 +45,7 @@ export const RegistryFetcher = async (target: CollectionRegistry, versionId: str
|
||||
cache.registries[id] = data.values
|
||||
cacheDirty = true
|
||||
} catch (e) {
|
||||
console.error(`Error occurred while fetching registry "${id}":`, e)
|
||||
console.warn(`Error occurred while fetching registry "${id}":`, e)
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
41
src/app/Router.ts
Normal file
41
src/app/Router.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { App } from './App';
|
||||
import { View } from './views/View';
|
||||
import { Home } from './views/Home'
|
||||
import { Generator } from './views/Generator'
|
||||
import config from '../config.json'
|
||||
|
||||
const categories = config.models.filter(m => m.category === true)
|
||||
|
||||
const router = async () => {
|
||||
const urlParts = location.pathname.split('/').filter(e => e)
|
||||
const target = document.getElementById('app')!
|
||||
const view = new View()
|
||||
|
||||
if (urlParts.length === 0){
|
||||
App.model.set({ id: '', name: 'Data Pack', category: true})
|
||||
target.innerHTML = Home(view)
|
||||
} else if (urlParts.length === 1 && categories.map(m => m.id).includes(urlParts[0])) {
|
||||
App.model.set(categories.find(m => m.id === urlParts[0])!)
|
||||
target.innerHTML = Home(view)
|
||||
} else {
|
||||
App.model.set(config.models.find(m => m.id === urlParts.join('/'))!)
|
||||
target.innerHTML = Generator(view)
|
||||
}
|
||||
view.mounted(target)
|
||||
}
|
||||
|
||||
window.addEventListener("popstate", router);
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.body.addEventListener("click", e => {
|
||||
if (e.target instanceof Element
|
||||
&& e.target.hasAttribute('data-link')
|
||||
&& e.target.hasAttribute('href')
|
||||
) {
|
||||
e.preventDefault();
|
||||
history.pushState(null, '', e.target.getAttribute('href'));
|
||||
router();
|
||||
}
|
||||
});
|
||||
router();
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import { DataModel, Path, ModelPath } from '@mcschema/core'
|
||||
import { AbstractView } from './AbstractView'
|
||||
import { transformOutput } from './hooks/transformOutput'
|
||||
|
||||
type SourceViewOptions = {
|
||||
indentation?: number | string,
|
||||
rows?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON representation view of the model.
|
||||
* Renders the result in a <textarea>.
|
||||
*/
|
||||
export class SourceView extends AbstractView {
|
||||
target: HTMLTextAreaElement
|
||||
options?: SourceViewOptions
|
||||
|
||||
/**
|
||||
* @param model data model this view represents and listens to
|
||||
* @param target DOM element to render the view
|
||||
* @param options optional options for the view
|
||||
*/
|
||||
constructor(model: DataModel, target: HTMLTextAreaElement, options?: SourceViewOptions) {
|
||||
super(model)
|
||||
this.target = target
|
||||
this.options = options
|
||||
this.target.addEventListener('change', evt => this.updateModel())
|
||||
}
|
||||
|
||||
invalidated() {
|
||||
const transformed = this.model.schema.hook(transformOutput, new ModelPath(this.model), this.model.data)
|
||||
this.target.value = JSON.stringify(transformed, null, this.options?.indentation)
|
||||
}
|
||||
|
||||
updateModel() {
|
||||
let parsed = {}
|
||||
try {
|
||||
parsed = JSON.parse(this.target.value)
|
||||
} catch (err) {
|
||||
this.model.error(new Path().push('JSON'), err.message)
|
||||
return
|
||||
}
|
||||
this.model.reset(parsed)
|
||||
}
|
||||
}
|
||||
25
src/app/Tracker.ts
Normal file
25
src/app/Tracker.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
const event = (category: string, action: string, label?: string) =>
|
||||
ga('send', 'event', category, action, label)
|
||||
|
||||
const dimension = (index: number, value: string) =>
|
||||
ga('set', `dimension${index}`, value);
|
||||
|
||||
export const Tracker = {
|
||||
setTheme: (theme: string) => event('Generator', 'set-theme', theme),
|
||||
setVersion: (version: string) => event('Generator', 'set-version', version),
|
||||
setPreview: (name: string) => event('Preview', 'set-preview', name),
|
||||
setLanguage: (language: string) => event('Generator', 'set-language', language),
|
||||
reset: () => event('Generator', 'reset'),
|
||||
undo: (hotkey = false) => event('Generator', 'undo', hotkey ? 'Hotkey' : 'Menu'),
|
||||
redo: (hotkey = false) => event('Generator', 'redo', hotkey ? 'Hotkey' : 'Menu'),
|
||||
copy: () => event('JsonOutput', 'copy'),
|
||||
download: () => event('JsonOutput', 'download'),
|
||||
share: () => event('JsonOutput', 'share'),
|
||||
toggleErrors: (visible: boolean) => event('Errors', 'toggle', visible ? 'visible' : 'hidden'),
|
||||
hidePreview: () => event('Preview', 'hide-preview'),
|
||||
|
||||
dimTheme: (theme: string) => dimension(1, theme),
|
||||
dimLanguage: (language: string) => dimension(3, language),
|
||||
dimVersion: (version: string) => dimension(4, version),
|
||||
dimPreview: (preview: string) => dimension(5, preview),
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { DataModel, ModelPath } from '@mcschema/core'
|
||||
import { AbstractView } from './AbstractView'
|
||||
import { Mounter } from './Mounter'
|
||||
import { renderHtml } from './hooks/renderHtml'
|
||||
|
||||
type Registry = {
|
||||
[id: string]: (el: Element) => void
|
||||
}
|
||||
|
||||
type TreeViewOptions = {
|
||||
showErrors?: boolean
|
||||
observer?: (el: HTMLElement) => void
|
||||
nodeInjector?: (path: ModelPath, mounter: Mounter) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* DOM representation view of the model.
|
||||
*/
|
||||
export class TreeView extends AbstractView {
|
||||
target: HTMLElement
|
||||
registry: Registry = {}
|
||||
showErrors: boolean
|
||||
observer: (el: HTMLElement) => void
|
||||
nodeInjector: (path: ModelPath, mounter: Mounter) => string
|
||||
|
||||
/**
|
||||
* @param model data model this view represents and listens to
|
||||
* @param target DOM element to render the view
|
||||
*/
|
||||
constructor(model: DataModel, target: HTMLElement, options?: TreeViewOptions) {
|
||||
super(model)
|
||||
this.target = target
|
||||
this.showErrors = options?.showErrors ?? false
|
||||
this.observer = options?.observer ?? (() => {})
|
||||
this.nodeInjector = options?.nodeInjector ?? (() => '')
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
invalidated() {
|
||||
const mounter = new Mounter({nodeInjector: this.nodeInjector})
|
||||
const path = new ModelPath(this.model)
|
||||
const rendered = this.model.schema.hook(renderHtml, path, this.model.data, mounter)
|
||||
const category = this.model.schema.category(path)
|
||||
if (rendered[1]) {
|
||||
this.target.innerHTML = `<div class="node ${this.model.schema.type(path)}-node" ${category ? `data-category="${category}"` : ''}>
|
||||
<div class="node-header">${rendered[1]}</div>
|
||||
<div class="node-body">${rendered[2]}</div>
|
||||
</div>`
|
||||
} else {
|
||||
this.target.innerHTML = rendered[2]
|
||||
}
|
||||
mounter.mount(this.target);
|
||||
this.observer(this.target)
|
||||
}
|
||||
}
|
||||
581
src/app/app.ts
581
src/app/app.ts
@@ -1,26 +1,17 @@
|
||||
import Split from 'split.js'
|
||||
import { Base, CollectionRegistry, DataModel, ModelPath, Path, SchemaRegistry } from '@mcschema/core'
|
||||
import { CollectionRegistry, DataModel, ObjectNode, PathError, SchemaRegistry } from '@mcschema/core';
|
||||
import * as java16 from '@mcschema/java-1.16'
|
||||
import * as java17 from '@mcschema/java-1.17'
|
||||
import { VisualizerView } from './visualization/VisualizerView'
|
||||
import { RegistryFetcher } from './RegistryFetcher'
|
||||
import { TreeView } from './TreeView'
|
||||
import { SourceView } from './SourceView'
|
||||
import { ErrorsView } from './ErrorsView'
|
||||
import config from '../config.json'
|
||||
import { BiomeNoiseVisualizer } from './visualization/BiomeNoiseVisualizer'
|
||||
import { Mounter } from './Mounter'
|
||||
import { getLanguage, hasLocale, locale, registerLocale, setLanguage } from './locales'
|
||||
import { LocalStorageProperty } from './state/LocalStorageProperty';
|
||||
import { Property } from './state/Property';
|
||||
import { Preview } from './preview/Preview';
|
||||
import { RegistryFetcher } from './RegistryFetcher';
|
||||
import { BiomeNoisePreview } from './preview/BiomeNoisePreview';
|
||||
import { NoiseSettingsPreview } from './preview/NoiseSettingsPreview';
|
||||
import config from '../config.json';
|
||||
import { locale, Locales } from './Locales';
|
||||
import { Tracker } from './Tracker';
|
||||
|
||||
type ModelConfig = {
|
||||
id: string
|
||||
name: string
|
||||
schema?: string
|
||||
minVersion?: string
|
||||
children?: ModelConfig[]
|
||||
}
|
||||
|
||||
const versionSchemas: {
|
||||
const Versions: {
|
||||
[versionId: string]: {
|
||||
getCollections: () => CollectionRegistry,
|
||||
getSchemas: (collections: CollectionRegistry) => SchemaRegistry,
|
||||
@@ -30,473 +21,109 @@ const versionSchemas: {
|
||||
'1.17': java17
|
||||
}
|
||||
|
||||
const LOCAL_STORAGE_THEME = 'theme'
|
||||
const LOCAL_STORAGE_LANGUAGE = 'language'
|
||||
const LOCAL_STORAGE_VERSION = 'schema_version'
|
||||
|
||||
const publicPath = '/';
|
||||
|
||||
const modelFromPath = (p: string) => p.replace(publicPath, '').replace(/\/$/, '')
|
||||
|
||||
const modelConfig = (id: string): ModelConfig => config.models.find(m => m.id === id) ?? config.models.filter(m => m.children).reduce((acc: any, cur: any) => [...acc, ...cur.children], []).find(m => m.id === id)
|
||||
|
||||
const addChecked = (el: HTMLElement) => {
|
||||
el.classList.add('check')
|
||||
setTimeout(() => {
|
||||
el.classList.remove('check')
|
||||
}, 2000)
|
||||
export const Previews: {
|
||||
[key: string]: Preview
|
||||
} = {
|
||||
'biome_noise': new BiomeNoisePreview(),
|
||||
'noise_settings': new NoiseSettingsPreview()
|
||||
}
|
||||
|
||||
const treeViewObserver = (el: HTMLElement) => {
|
||||
el.querySelectorAll('.node[data-help]').forEach(e => {
|
||||
const div = document.createElement('div')
|
||||
div.className = 'node-icon'
|
||||
div.addEventListener('click', evt => {
|
||||
div.getElementsByTagName('span')[0].classList.add('show')
|
||||
document.body.addEventListener('click', evt => {
|
||||
div.getElementsByTagName('span')[0].classList.remove('show')
|
||||
}, { capture: true, once: true })
|
||||
})
|
||||
div.insertAdjacentHTML('beforeend', `<span class="icon-popup">${e.getAttribute('data-help')}</span><svg class="node-help" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"></path></svg>`)
|
||||
e.querySelector('.node-header')?.appendChild(div)
|
||||
})
|
||||
el.querySelectorAll('.node[data-error]').forEach(e => {
|
||||
const div = document.createElement('div')
|
||||
div.className = 'node-icon'
|
||||
div.addEventListener('click', evt => {
|
||||
div.getElementsByTagName('span')[0].classList.add('show')
|
||||
document.body.addEventListener('click', evt => {
|
||||
div.getElementsByTagName('span')[0].classList.remove('show')
|
||||
}, { capture: true, once: true })
|
||||
})
|
||||
div.insertAdjacentHTML('beforeend', `<span class="icon-popup">${e.getAttribute('data-error')}</span><svg class="node-error" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>`)
|
||||
e.querySelector('.node-header')?.appendChild(div)
|
||||
})
|
||||
el.querySelectorAll('.collapse.closed, button.add').forEach(e => {
|
||||
e.insertAdjacentHTML('afterbegin', `<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>`)
|
||||
})
|
||||
el.querySelectorAll('.collapse.open, button.remove').forEach(e => {
|
||||
e.insertAdjacentHTML('afterbegin', `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>`)
|
||||
})
|
||||
export const Models: {
|
||||
[key: string]: DataModel
|
||||
} = {}
|
||||
|
||||
config.models.filter(m => m.schema)
|
||||
.forEach(m => Models[m.id] = new DataModel(ObjectNode({})))
|
||||
|
||||
export const App = {
|
||||
version: new LocalStorageProperty('schema_version', config.versions[config.versions.length - 1].id)
|
||||
.watch(Tracker.dimVersion),
|
||||
theme: new LocalStorageProperty('theme', 'light')
|
||||
.watch(Tracker.dimTheme),
|
||||
language: new LocalStorageProperty('language', 'en')
|
||||
.watch(Tracker.dimLanguage),
|
||||
model: new Property<typeof config.models[0] | null>(null),
|
||||
errorsVisible: new Property(false),
|
||||
jsonError: new Property<string | null>(null),
|
||||
preview: new Property<Preview | null>(null)
|
||||
.watch(p => Tracker.dimPreview(p?.getName() ?? 'none')),
|
||||
schemasLoaded: new Property(false),
|
||||
localesLoaded: new Property(false),
|
||||
loaded: new Property(false),
|
||||
mobilePanel: new Property('tree'),
|
||||
}
|
||||
|
||||
const treeViewNodeInjector = (path: ModelPath, mounter: Mounter) => {
|
||||
let res = VisualizerView.visualizers
|
||||
.filter(v => v.active(path))
|
||||
.map(v => {
|
||||
const id = mounter.registerClick(() => {
|
||||
ga('send', 'event', 'Preview', 'set-preview', v.getName())
|
||||
views.visualizer.set(v, path)
|
||||
views.visualizer.model.invalidate()
|
||||
})
|
||||
return `<button data-id=${id}>${locale('visualize')} <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></button>`
|
||||
})
|
||||
.join('')
|
||||
if (views.visualizer.active && views.visualizer.visualizer?.getName() === 'biome-noise') {
|
||||
if (path.pop().endsWith(new Path(['generator', 'biome_source', 'biomes']))) {
|
||||
const biomeVisualizer = views.visualizer.visualizer as BiomeNoiseVisualizer
|
||||
const biome = path.push('biome').get()
|
||||
const id = mounter.registerChange(el => {
|
||||
biomeVisualizer.setBiomeColor(biome, (el as HTMLInputElement).value)
|
||||
views.visualizer.visualizer!.state = {}
|
||||
views.visualizer.invalidated()
|
||||
})
|
||||
res += `<input type="color" value="${biomeVisualizer.getBiomeHex(biome)}" data-id=${id}></input>`
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const fetchLocale = async (id: string) => {
|
||||
const response = await fetch(publicPath + `locales/${id}.json`)
|
||||
registerLocale(id, await response.json())
|
||||
}
|
||||
setLanguage(localStorage.getItem(LOCAL_STORAGE_LANGUAGE)?.toLowerCase())
|
||||
|
||||
const homeLink = document.getElementById('home-link')!
|
||||
const homeGenerators = document.getElementById('home-generators')!
|
||||
const categoryGenerators = document.getElementById('category-generators')!
|
||||
const selectedModel = document.getElementById('selected-model')!
|
||||
const languageSelector = document.getElementById('language-selector')!
|
||||
const languageSelectorMenu = document.getElementById('language-selector-menu')!
|
||||
const themeSelector = document.getElementById('theme-selector')!
|
||||
const treeViewEl = document.getElementById('tree-view')!
|
||||
const sourceViewEl = document.getElementById('source-view')!
|
||||
const errorsViewEl = document.getElementById('errors-view')!
|
||||
const homeViewEl = document.getElementById('home-view')!
|
||||
const errorsToggle = document.getElementById('errors-toggle')!
|
||||
const sourceViewOutput = (document.getElementById('source-view-output') as HTMLTextAreaElement)
|
||||
const treeViewOutput = document.getElementById('tree-view-output')!
|
||||
const sourceControlsToggle = document.getElementById('source-controls-toggle')!
|
||||
const sourceControlsMenu = document.getElementById('source-controls-menu')!
|
||||
const sourceControlsCopy = document.getElementById('source-controls-copy')!
|
||||
const sourceControlsDownload = document.getElementById('source-controls-download')!
|
||||
const sourceControlsShare = document.getElementById('source-controls-share')!
|
||||
const sourceToggle = document.getElementById('source-toggle')!
|
||||
const treeControlsToggle = document.getElementById('tree-controls-toggle')!
|
||||
const treeControlsMenu = document.getElementById('tree-controls-menu')!
|
||||
const treeVersionToggle = document.getElementById('tree-version-toggle')!
|
||||
const treeVersionMenu = document.getElementById('tree-version-menu')!
|
||||
const treeVersionLabel = document.getElementById('tree-version-label')!
|
||||
const treeControlsReset = document.getElementById('tree-controls-reset')!
|
||||
const treeControlsUndo = document.getElementById('tree-controls-undo')!
|
||||
const treeControlsRedo = document.getElementById('tree-controls-redo')!
|
||||
const visualizerContent = document.getElementById('visualizer-content')!
|
||||
const githubLink = document.getElementById('github-link')!
|
||||
|
||||
Split([treeViewEl, sourceViewEl], {
|
||||
sizes: [66, 34]
|
||||
App.version.watchRun(async (value) => {
|
||||
App.schemasLoaded.set(false)
|
||||
await updateSchemas(value)
|
||||
App.schemasLoaded.set(true)
|
||||
})
|
||||
|
||||
Split([sourceViewOutput, visualizerContent], {
|
||||
sizes: [60, 40],
|
||||
direction: 'vertical'
|
||||
App.theme.watchRun((value) => document.documentElement.setAttribute('data-theme', value))
|
||||
|
||||
App.language.watchRun(async (value) => {
|
||||
App.localesLoaded.set(false)
|
||||
await updateLocale(value)
|
||||
App.localesLoaded.set(true)
|
||||
})
|
||||
|
||||
const dummyModel = new DataModel(Base)
|
||||
|
||||
const views = {
|
||||
'tree': new TreeView(dummyModel, treeViewOutput, {
|
||||
showErrors: true,
|
||||
observer: treeViewObserver,
|
||||
nodeInjector: treeViewNodeInjector
|
||||
}),
|
||||
'source': new SourceView(dummyModel, sourceViewOutput, {
|
||||
indentation: 2
|
||||
}),
|
||||
'errors': new ErrorsView(dummyModel, errorsViewEl),
|
||||
'visualizer': new VisualizerView(dummyModel, visualizerContent)
|
||||
}
|
||||
|
||||
let version = localStorage.getItem(LOCAL_STORAGE_VERSION) ?? config.versions[0].id
|
||||
treeVersionLabel.textContent = version
|
||||
|
||||
let COLLECTIONS = versionSchemas[version].getCollections()
|
||||
|
||||
Promise.all([
|
||||
fetchLocale(getLanguage()),
|
||||
...(getLanguage() === 'en' ? [] : [fetchLocale('en')]),
|
||||
RegistryFetcher(COLLECTIONS, version)
|
||||
]).then(() => {
|
||||
|
||||
let SCHEMAS = versionSchemas[version].getSchemas(COLLECTIONS)
|
||||
|
||||
let models: { [key: string]: DataModel } = {}
|
||||
const buildModel = (model: any) => {
|
||||
if (model.schema) {
|
||||
const schema = SCHEMAS.get(model.schema)
|
||||
if (schema) {
|
||||
models[model.id] = new DataModel(schema)
|
||||
}
|
||||
} else if (model.children) {
|
||||
model.children.forEach(buildModel)
|
||||
}
|
||||
}
|
||||
config.models.forEach(buildModel)
|
||||
|
||||
let selected = ''
|
||||
Object.values(models).forEach(m => m.validate(true))
|
||||
|
||||
const updateModel = () => {
|
||||
let title = ''
|
||||
if (models[selected] === undefined) {
|
||||
title = locale('title.home')
|
||||
} else {
|
||||
title = locale('title.generator', [locale(selected)])
|
||||
Object.values(views).forEach(v => v.setModel(models[selected]))
|
||||
models[selected].invalidate()
|
||||
}
|
||||
selectedModel.textContent = title
|
||||
document.title = title
|
||||
|
||||
App.localesLoaded.watch((value) => {
|
||||
if (value) {
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
el.textContent = locale(el.attributes.getNamedItem('data-i18n')!.value)
|
||||
})
|
||||
|
||||
treeVersionMenu.innerHTML = ''
|
||||
const m = modelConfig(selected)
|
||||
const minVersion = config.versions.findIndex(v => v.id === (m?.minVersion ?? '1.16'))
|
||||
config.versions.forEach((v, i) => {
|
||||
if (i > minVersion) return
|
||||
const entry = document.createElement('button')
|
||||
entry.classList.add('btn')
|
||||
entry.textContent = v.id
|
||||
entry.addEventListener('click', () => {
|
||||
updateVersion(v.id)
|
||||
ga('send', 'event', 'Generator', 'set-version', version)
|
||||
})
|
||||
treeVersionMenu.append(entry)
|
||||
})
|
||||
}
|
||||
|
||||
const updateLanguage = (id: string, store = false) => {
|
||||
setLanguage(id)
|
||||
ga('set', 'dimension4', id);
|
||||
if (store) {
|
||||
localStorage.setItem(LOCAL_STORAGE_LANGUAGE, id)
|
||||
}
|
||||
|
||||
languageSelectorMenu.innerHTML = ''
|
||||
config.languages.forEach(lang => {
|
||||
languageSelectorMenu.insertAdjacentHTML('beforeend',
|
||||
`<div class="btn${lang.code === getLanguage() ? ' selected' : ''}">${lang.name}</div>`)
|
||||
languageSelectorMenu.lastChild?.addEventListener('click', evt => {
|
||||
updateLanguage(lang.code, true)
|
||||
languageSelectorMenu.style.visibility = 'hidden'
|
||||
ga('send', 'event', 'Generator', 'set-language', lang.code)
|
||||
})
|
||||
})
|
||||
|
||||
if (hasLocale(id)) {
|
||||
updateModel()
|
||||
} else {
|
||||
fetchLocale(id).then(r => {
|
||||
updateModel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateVersion = async (id: string) => {
|
||||
localStorage.setItem(LOCAL_STORAGE_VERSION, id)
|
||||
if (id === version) return
|
||||
|
||||
const newCollections = versionSchemas[id].getCollections()
|
||||
await RegistryFetcher(newCollections, id)
|
||||
const newSchemas = versionSchemas[id].getSchemas(newCollections)
|
||||
|
||||
const fixModel = (model: ModelConfig) => {
|
||||
if (model.schema) {
|
||||
const minVersion = config.versions.findIndex(v => v.id === (model.minVersion ?? '1.16'))
|
||||
const targetVersion = config.versions.findIndex(v => v.id === id)
|
||||
if (minVersion >= targetVersion) {
|
||||
const schema = newSchemas.get(model.schema)
|
||||
if (models[model.id] === undefined) {
|
||||
models[model.id] = new DataModel(schema)
|
||||
} else {
|
||||
models[model.id].schema = schema
|
||||
}
|
||||
models[model.id].validate()
|
||||
models[model.id].invalidate()
|
||||
} else {
|
||||
delete models[model.id]
|
||||
}
|
||||
} else if (model.children) {
|
||||
model.children.forEach(fixModel)
|
||||
}
|
||||
}
|
||||
config.models.forEach(fixModel)
|
||||
treeVersionLabel.textContent = id
|
||||
version = id
|
||||
ga('set', 'dimension3', version);
|
||||
}
|
||||
|
||||
homeLink.addEventListener('click', evt => {
|
||||
reload(publicPath)
|
||||
})
|
||||
|
||||
window.onpopstate = (evt: PopStateEvent) => {
|
||||
reload(location.pathname)
|
||||
}
|
||||
|
||||
sourceToggle.addEventListener('click', evt => {
|
||||
if (sourceViewEl.classList.contains('active')) {
|
||||
sourceViewEl.classList.remove('active')
|
||||
sourceToggle.classList.remove('toggled')
|
||||
} else {
|
||||
sourceViewEl.classList.add('active')
|
||||
sourceToggle.classList.add('toggled')
|
||||
}
|
||||
})
|
||||
|
||||
languageSelector.addEventListener('click', evt => {
|
||||
languageSelectorMenu.style.visibility = 'visible'
|
||||
document.body.addEventListener('click', evt => {
|
||||
languageSelectorMenu.style.visibility = 'hidden'
|
||||
}, { capture: true, once: true })
|
||||
})
|
||||
|
||||
const updateTheme = (theme: string | null) => {
|
||||
ga('set', 'dimension1', theme ?? 'default');
|
||||
if (theme === null) return
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
themeSelector.classList.add('toggled')
|
||||
localStorage.setItem(LOCAL_STORAGE_THEME, 'dark')
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
themeSelector.classList.remove('toggled')
|
||||
localStorage.setItem(LOCAL_STORAGE_THEME, 'light')
|
||||
}
|
||||
}
|
||||
updateTheme(localStorage.getItem(LOCAL_STORAGE_THEME))
|
||||
|
||||
themeSelector.addEventListener('click', evt => {
|
||||
if (document.documentElement.getAttribute('data-theme') === 'dark') {
|
||||
updateTheme('light')
|
||||
} else {
|
||||
updateTheme('dark')
|
||||
}
|
||||
ga('send', 'event', 'Generator', 'set-theme', document.documentElement.getAttribute('data-theme'))
|
||||
})
|
||||
|
||||
sourceControlsToggle.addEventListener('click', evt => {
|
||||
sourceControlsMenu.style.visibility = 'visible'
|
||||
document.body.addEventListener('click', evt => {
|
||||
sourceControlsMenu.style.visibility = 'hidden'
|
||||
}, { capture: true, once: true })
|
||||
})
|
||||
|
||||
sourceControlsCopy.addEventListener('click', evt => {
|
||||
sourceViewOutput.select()
|
||||
document.execCommand('copy');
|
||||
addChecked(sourceControlsCopy)
|
||||
ga('send', 'event', 'JsonOutput', 'copy')
|
||||
})
|
||||
|
||||
sourceControlsDownload.addEventListener('click', evt => {
|
||||
const fileContents = encodeURIComponent(JSON.stringify(models[selected].data, null, 2) + "\n")
|
||||
const dataString = "data:text/json;charset=utf-8," + fileContents
|
||||
const downloadAnchor = document.getElementById('source-controls-download-anchor')!
|
||||
downloadAnchor.setAttribute("href", dataString)
|
||||
downloadAnchor.setAttribute("download", "data.json")
|
||||
downloadAnchor.click()
|
||||
ga('send', 'event', 'JsonOutput', 'download')
|
||||
})
|
||||
|
||||
sourceControlsShare.addEventListener('click', evt => {
|
||||
const data = btoa(JSON.stringify(JSON.parse(views.source.target.value)));
|
||||
const url = window.location.origin + window.location.pathname + '?q=' + data
|
||||
const shareInput = document.getElementById('source-controls-share-input') as HTMLInputElement
|
||||
shareInput.value = url
|
||||
shareInput.style.display = 'inline-block'
|
||||
document.body.addEventListener('click', evt => {
|
||||
shareInput.style.display = 'none'
|
||||
}, { capture: true, once: true })
|
||||
shareInput.select()
|
||||
document.execCommand('copy');
|
||||
ga('send', 'event', 'JsonOutput', 'share')
|
||||
})
|
||||
|
||||
treeControlsToggle.addEventListener('click', evt => {
|
||||
treeControlsMenu.style.visibility = 'visible'
|
||||
document.body.addEventListener('click', evt => {
|
||||
treeControlsMenu.style.visibility = 'hidden'
|
||||
}, { capture: true, once: true })
|
||||
})
|
||||
|
||||
treeVersionToggle.addEventListener('click', evt => {
|
||||
treeVersionMenu.style.visibility = 'visible'
|
||||
document.body.addEventListener('click', evt => {
|
||||
treeVersionMenu.style.visibility = 'hidden'
|
||||
}, { capture: true, once: true })
|
||||
})
|
||||
|
||||
treeControlsReset.addEventListener('click', evt => {
|
||||
models[selected].reset(models[selected].schema.default(), true)
|
||||
addChecked(treeControlsReset)
|
||||
ga('send', 'event', 'Generator', 'reset')
|
||||
})
|
||||
|
||||
treeControlsUndo.addEventListener('click', evt => {
|
||||
models[selected].undo()
|
||||
ga('send', 'event', 'Generator', 'undo', 'Menu')
|
||||
})
|
||||
|
||||
treeControlsRedo.addEventListener('click', evt => {
|
||||
models[selected].redo()
|
||||
ga('send', 'event', 'Generator', 'redo', 'Menu')
|
||||
})
|
||||
|
||||
document.addEventListener('keyup', evt => {
|
||||
if (evt.ctrlKey && evt.key === 'z') {
|
||||
models[selected].undo()
|
||||
ga('send', 'event', 'Generator', 'undo', 'Hotkey')
|
||||
} else if (evt.ctrlKey && evt.key === 'y') {
|
||||
models[selected].redo()
|
||||
ga('send', 'event', 'Generator', 'redo', 'Hotkey')
|
||||
}
|
||||
})
|
||||
|
||||
errorsToggle.addEventListener('click', evt => {
|
||||
if (errorsViewEl.classList.contains('active')) {
|
||||
errorsViewEl.classList.remove('active')
|
||||
errorsToggle.classList.remove('toggled')
|
||||
} else {
|
||||
errorsViewEl.classList.add('active')
|
||||
errorsToggle.classList.add('toggled')
|
||||
}
|
||||
ga('send', 'event', 'Errors', 'toggle', errorsViewEl.classList.contains('active') ? 'visible' : 'hidden')
|
||||
})
|
||||
|
||||
githubLink.addEventListener('click', () => {
|
||||
window.open('https://github.com/misode/misode.github.io', '_blank')
|
||||
})
|
||||
|
||||
const reload = (target: string, track=true) => {
|
||||
if (!target.endsWith('/')) {
|
||||
target = `${target}/`
|
||||
}
|
||||
|
||||
if (target.startsWith('/dev/')) {
|
||||
reload(target.slice(4))
|
||||
return
|
||||
}
|
||||
|
||||
if (track) {
|
||||
ga('set', 'page', target)
|
||||
ga('send', 'pageview');
|
||||
history.pushState(target, 'Change Page', target)
|
||||
}
|
||||
selected = modelFromPath(target) ?? ''
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
const panels = [treeViewEl, sourceViewEl, errorsViewEl]
|
||||
if (['', 'worldgen'].includes(selected)) {
|
||||
homeViewEl.style.display = '';
|
||||
(document.querySelector('.gutter') as HTMLElement).style.display = 'none';
|
||||
(document.querySelector('.content') as HTMLElement).style.overflowY = 'initial'
|
||||
panels.forEach(v => v.style.display = 'none')
|
||||
|
||||
const addGen = (output: HTMLElement) => (m: any) => {
|
||||
output.insertAdjacentHTML('beforeend',
|
||||
`<a class="generators-card${m.id === selected ? ' selected' : ''}" href="${publicPath + m.id}">
|
||||
${locale(m.name)}
|
||||
${m.schema ? '' : '<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>'}
|
||||
</a>`)
|
||||
output.lastChild?.addEventListener('click', evt => {
|
||||
reload(publicPath + m.id)
|
||||
evt.preventDefault()
|
||||
})
|
||||
}
|
||||
|
||||
homeGenerators.innerHTML = ''
|
||||
categoryGenerators.innerHTML = ''
|
||||
config.models.forEach(addGen(homeGenerators))
|
||||
config.models.find(m => m.id === selected)?.children?.forEach(addGen(categoryGenerators))
|
||||
|
||||
} else {
|
||||
homeViewEl.style.display = 'none';
|
||||
(document.querySelector('.gutter') as HTMLElement).style.display = ''
|
||||
panels.forEach(v => v.style.display = '')
|
||||
if (models[selected] === undefined) {
|
||||
const m = modelConfig(selected)
|
||||
const loadedVersion = config.versions.findIndex(v => v.id === version)
|
||||
const minVersion = config.versions.findIndex(v => v.id === m.minVersion)
|
||||
if (minVersion < loadedVersion) {
|
||||
updateVersion(m.minVersion!).then(() => {
|
||||
updateModel()
|
||||
})
|
||||
}
|
||||
}
|
||||
if (params.has('q')) {
|
||||
const data = atob(params.get('q')!)
|
||||
models[selected].reset(JSON.parse(data))
|
||||
}
|
||||
}
|
||||
|
||||
updateLanguage(getLanguage())
|
||||
}
|
||||
reload(location.pathname, false)
|
||||
document.body.style.visibility = 'initial'
|
||||
App.loaded.set(value && App.schemasLoaded.get())
|
||||
})
|
||||
|
||||
App.schemasLoaded.watch((value) => {
|
||||
App.loaded.set(value && App.localesLoaded.get())
|
||||
})
|
||||
|
||||
App.mobilePanel.watchRun((value) => {
|
||||
document.body.setAttribute('data-panel', value)
|
||||
})
|
||||
|
||||
async function updateSchemas(version: string) {
|
||||
const collections = Versions[version].getCollections()
|
||||
await RegistryFetcher(collections, version)
|
||||
const schemas = Versions[version].getSchemas(collections)
|
||||
config.models
|
||||
.filter(m => m.schema)
|
||||
.filter(m => checkVersion(App.version.get(), m.minVersion ?? config.versions[0].id))
|
||||
.forEach(m => {
|
||||
const model = Models[m.id]
|
||||
const schema = schemas.get(m.schema!)
|
||||
if (schema) {
|
||||
model.schema = schema
|
||||
if (JSON.stringify(model.data) === '{}') {
|
||||
model.data = schema.default()
|
||||
model.history = [JSON.stringify(model.data)]
|
||||
model.historyIndex = 0
|
||||
model.silentInvalidate()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function updateLocale(language: string) {
|
||||
if (Locales[language]) return
|
||||
const data = await (await fetch(`/locales/${language}.json`)).json()
|
||||
Locales[language] = data
|
||||
}
|
||||
|
||||
export function checkVersion(versionId: string, minVersionId: string) {
|
||||
const version = config.versions.findIndex(v => v.id === versionId)
|
||||
const minVersion = config.versions.findIndex(v => v.id === minVersionId)
|
||||
return minVersion <= version
|
||||
}
|
||||
|
||||
document.addEventListener('keyup', (evt) => {
|
||||
if (evt.ctrlKey && evt.key === 'z') {
|
||||
Tracker.undo(true)
|
||||
Models[App.model.get()!.id].undo()
|
||||
} else if (evt.ctrlKey && evt.key === 'y') {
|
||||
Tracker.redo(true)
|
||||
Models[App.model.get()!.id].redo()
|
||||
}
|
||||
})
|
||||
|
||||
22
src/app/components/Dropdown.ts
Normal file
22
src/app/components/Dropdown.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Property } from '../state/Property';
|
||||
import { View } from '../views/View';
|
||||
import { Octicon } from './Octicon';
|
||||
|
||||
export const Dropdown = (view: View, icon: keyof typeof Octicon, entries: [string, string][], state: Property<string>, watcher?: (value: string) => void) => {
|
||||
const dropdown = view.register(el => {
|
||||
el.addEventListener('change', () => {
|
||||
state.set((el as HTMLSelectElement).value)
|
||||
})
|
||||
state.watchRun(v => (el as HTMLSelectElement).value = v, 'dropdown')
|
||||
watcher?.(state.get())
|
||||
})
|
||||
return `
|
||||
<div class="dropdown">
|
||||
<select data-id="${dropdown}">
|
||||
${entries.map(e => `
|
||||
<option value=${e[0]}>${e[1]}</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
${Octicon[icon]}
|
||||
</div>`
|
||||
}
|
||||
53
src/app/components/Errors.ts
Normal file
53
src/app/components/Errors.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { DataModel } from '@mcschema/core';
|
||||
import { App } from '../App';
|
||||
import { locale } from '../Locales';
|
||||
import { View } from '../views/View';
|
||||
import { Octicon } from './Octicon';
|
||||
import { Toggle } from './Toggle';
|
||||
import { htmlEncode } from '../Utils'
|
||||
import { Tracker } from '../Tracker';
|
||||
|
||||
export const Errors = (view: View, model: DataModel) => {
|
||||
const getContent = () => {
|
||||
if (App.jsonError.get()) {
|
||||
return `<div class="error-list">
|
||||
<div class="error">
|
||||
${htmlEncode(App.jsonError.get()!)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="toggle" style="cursor: initial;">
|
||||
${Octicon.issue_opened}
|
||||
</div>`
|
||||
}
|
||||
if (model.errors.count() === 0) return ''
|
||||
return `${App.errorsVisible.get() ? `
|
||||
<div class="error-list">
|
||||
${model.errors.getAll().map(e => `
|
||||
<div class="error">
|
||||
<span class="error-path">${e.path.toString()}</span>
|
||||
<span>-</span>
|
||||
<span class="error-message">${htmlEncode(locale(e.error, e.params))}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
${Toggle(view, [[true, 'chevron_down'], [false, 'issue_opened']], App.errorsVisible, Tracker.toggleErrors)}`
|
||||
}
|
||||
const errors = view.register(el => {
|
||||
model.addListener({
|
||||
errors() {
|
||||
view.mount(el, getContent(), false)
|
||||
}
|
||||
})
|
||||
App.jsonError.watch(() => {
|
||||
view.mount(el, getContent(), false)
|
||||
})
|
||||
App.errorsVisible.watch(() => {
|
||||
view.mount(el, getContent(), false)
|
||||
}, 'errors')
|
||||
})
|
||||
return `
|
||||
<div class="errors" data-id="${errors}">
|
||||
${getContent()}
|
||||
</div>`
|
||||
}
|
||||
28
src/app/components/Header.ts
Normal file
28
src/app/components/Header.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { App } from '../App';
|
||||
import { View } from '../views/View';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import { Octicon } from './Octicon';
|
||||
import { Toggle } from './Toggle';
|
||||
import { languages } from '../../config.json'
|
||||
import { Tracker } from '../Tracker';
|
||||
|
||||
export const Header = (view: View, title: string, homeLink = '/', panelToggleVisible = false) => `
|
||||
<header>
|
||||
<div class="header-title">
|
||||
<a data-link href="${homeLink}" class="home-link">${Octicon.three_bars}</a>
|
||||
<h2>${title}</h2>
|
||||
</div>
|
||||
<nav>
|
||||
${panelToggleVisible ? Toggle(view, [['tree', 'code'], ['source', 'note']], App.mobilePanel) : ''}
|
||||
<ul>
|
||||
<li>${Dropdown(view, 'globe', languages.map(l => [l.code, l.name]), App.language, Tracker.setLanguage)}</li>
|
||||
<li>${Toggle(view, [['dark', 'sun'], ['light', 'moon']], App.theme, Tracker.setTheme)}</li>
|
||||
<li class="dimmed">
|
||||
<a href="https://github.com/misode/misode.github.io" target="_blank">
|
||||
${Octicon.mark_github}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
`
|
||||
25
src/app/components/Octicon.ts
Normal file
25
src/app/components/Octicon.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const Octicon = {
|
||||
arrow_left: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"></path></svg>',
|
||||
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>',
|
||||
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_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>',
|
||||
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>',
|
||||
download: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5 0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"></path></svg>',
|
||||
globe: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.543 7.25h2.733c.144-2.074.866-3.756 1.58-4.948.12-.197.237-.381.353-.552a6.506 6.506 0 00-4.666 5.5zm2.733 1.5H1.543a6.506 6.506 0 004.666 5.5 11.13 11.13 0 01-.352-.552c-.715-1.192-1.437-2.874-1.581-4.948zm1.504 0h4.44a9.637 9.637 0 01-1.363 4.177c-.306.51-.612.919-.857 1.215a9.978 9.978 0 01-.857-1.215A9.637 9.637 0 015.78 8.75zm4.44-1.5H5.78a9.637 9.637 0 011.363-4.177c.306-.51.612-.919.857-1.215.245.296.55.705.857 1.215A9.638 9.638 0 0110.22 7.25zm1.504 1.5c-.144 2.074-.866 3.756-1.58 4.948-.12.197-.237.381-.353.552a6.506 6.506 0 004.666-5.5h-2.733zm2.733-1.5h-2.733c-.144-2.074-.866-3.756-1.58-4.948a11.738 11.738 0 00-.353-.552 6.506 6.506 0 014.666 5.5zM8 0a8 8 0 100 16A8 8 0 008 0z"></path></svg>',
|
||||
history: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"></path></svg>',
|
||||
info: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"></path></svg>',
|
||||
issue_opened: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>',
|
||||
kebab_horizontal: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm13 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg>',
|
||||
link: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg>',
|
||||
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>',
|
||||
note: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z"></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_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>',
|
||||
sun: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8 10.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.06zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.06 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z"></path></svg>',
|
||||
tag: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z"></path></svg>',
|
||||
trashcan: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19c.9 0 1.652-.681 1.741-1.576l.66-6.6a.75.75 0 00-1.492-.149l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"></path></svg>',
|
||||
three_bars: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path></svg>',
|
||||
x: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path></svg>',
|
||||
}
|
||||
10
src/app/components/SplitGroup.ts
Normal file
10
src/app/components/SplitGroup.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import Split from 'split.js'
|
||||
import { View } from '../views/View';
|
||||
|
||||
export const SplitGroup = (view: View, options: Split.Options, entries: string[]) => `
|
||||
<div class="split-group ${options.direction ?? 'horizontal'}" data-id=${view.register(el => {
|
||||
Split([].slice.call(el.children), { snapOffset: 0, ...options })
|
||||
})}>
|
||||
${entries.join('')}
|
||||
</div>
|
||||
`
|
||||
16
src/app/components/Toggle.ts
Normal file
16
src/app/components/Toggle.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Property } from '../state/Property';
|
||||
import { View } from '../views/View';
|
||||
import { Octicon } from './Octicon';
|
||||
|
||||
export const Toggle = <T>(view: View, entries: [T, keyof typeof Octicon][], state: Property<T>, watcher?: (value: T) => void) => {
|
||||
const activeOcticon = () => Octicon[(entries.find(e => e[0] === state.get()) ?? entries[0])[1]]
|
||||
const toggle = view.register(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const i = entries.findIndex(e => e[0] === state.get())
|
||||
state.set(entries[(i + 1) % entries.length][0])
|
||||
})
|
||||
state.watch(_ => el.innerHTML = activeOcticon(), 'toggle')
|
||||
watcher?.(state.get())
|
||||
})
|
||||
return `<div class="toggle" data-id="${toggle}">${activeOcticon()}</div>`
|
||||
}
|
||||
64
src/app/components/panels/PreviewPanel.ts
Normal file
64
src/app/components/panels/PreviewPanel.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { DataModel } from '@mcschema/core';
|
||||
import { App, Previews } from '../../App';
|
||||
import { BiomeNoisePreview } from '../../preview/BiomeNoisePreview';
|
||||
import { Tracker } from '../../Tracker';
|
||||
import { View } from '../../views/View';
|
||||
import { Octicon } from '../Octicon';
|
||||
|
||||
export const PreviewPanel = (view: View, model: DataModel) => {
|
||||
const canvas = view.register(el => {
|
||||
const redraw = () => {
|
||||
const preview = App.preview.get()
|
||||
if (preview && preview.path && preview.path.withModel(model).get()) {
|
||||
const ctx = (el as HTMLCanvasElement).getContext('2d')!
|
||||
const img = ctx.createImageData(200, 100)
|
||||
const newState = preview.path.withModel(model).get()
|
||||
preview.state = JSON.parse(JSON.stringify(newState))
|
||||
preview.draw(model, img)
|
||||
ctx.putImageData(img, 0, 0)
|
||||
} else {
|
||||
App.preview.set(null)
|
||||
}
|
||||
}
|
||||
model.addListener({
|
||||
invalidated: redraw
|
||||
})
|
||||
App.preview.watchRun((value) => {
|
||||
if (value) {
|
||||
redraw()
|
||||
}
|
||||
}, 'preview-panel')
|
||||
|
||||
;(Previews.biome_noise as BiomeNoisePreview).biomeColors.watch(() => {
|
||||
if (App.preview.get()?.getName() === 'biome-noise') {
|
||||
redraw()
|
||||
}
|
||||
}, 'preview-panel')
|
||||
|
||||
let dragStart: number[] | undefined
|
||||
(el as HTMLCanvasElement).addEventListener('mousedown', evt => {
|
||||
dragStart = [evt.offsetX, evt.offsetY]
|
||||
})
|
||||
;(el as HTMLCanvasElement).addEventListener('mousemove', evt => {
|
||||
if (dragStart === undefined) return
|
||||
if (App.preview.get()?.onDrag) {
|
||||
App.preview.get()?.onDrag(dragStart[0], dragStart[1], evt.offsetX, evt.offsetY)
|
||||
redraw()
|
||||
}
|
||||
dragStart = [evt.offsetX, evt.offsetY]
|
||||
})
|
||||
;(el as HTMLCanvasElement).addEventListener('mouseup', evt => {
|
||||
dragStart = undefined
|
||||
})
|
||||
})
|
||||
return `<div class="panel preview-panel">
|
||||
<div class="panel-controls">
|
||||
<div class="btn" data-id="${view.onClick(() => {
|
||||
Tracker.hidePreview(); App.preview.set(null)
|
||||
})}">
|
||||
${Octicon.x}
|
||||
</div>
|
||||
</div>
|
||||
<canvas width="200" height="100" data-id="${canvas}">
|
||||
</div>`
|
||||
}
|
||||
78
src/app/components/panels/SourcePanel.ts
Normal file
78
src/app/components/panels/SourcePanel.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { DataModel, ModelPath, Path } from '@mcschema/core';
|
||||
import { Tracker } from '../../Tracker';
|
||||
import { transformOutput } from '../../hooks/transformOutput';
|
||||
import { View } from '../../views/View';
|
||||
import { Octicon } from '../Octicon';
|
||||
import { App } from '../../App';
|
||||
|
||||
export const SourcePanel = (view: View, model: DataModel) => {
|
||||
const updateContent = (el: HTMLTextAreaElement) => {
|
||||
const data = model.schema.hook(transformOutput, new ModelPath(model), model.data);
|
||||
el.value = JSON.stringify(data, null, 2)
|
||||
}
|
||||
const source = view.register(el => {
|
||||
updateContent(el as HTMLTextAreaElement)
|
||||
model.addListener({
|
||||
invalidated() {
|
||||
App.jsonError.set(null)
|
||||
updateContent(el as HTMLTextAreaElement)
|
||||
}
|
||||
})
|
||||
el.addEventListener('change', () => {
|
||||
const rawSource = (el as HTMLTextAreaElement).value
|
||||
try {
|
||||
model.reset(JSON.parse(rawSource))
|
||||
App.jsonError.set(null)
|
||||
} catch (err) {
|
||||
App.jsonError.set(err.message)
|
||||
}
|
||||
})
|
||||
})
|
||||
const copySource = (el: Element) => {
|
||||
el.closest('.panel')?.getElementsByTagName('textarea')[0].select()
|
||||
document.execCommand('copy');
|
||||
Tracker.copy()
|
||||
}
|
||||
const downloadSource = (el: Element) => {
|
||||
const fileContents = encodeURIComponent(JSON.stringify(model.data, null, 2) + "\n")
|
||||
const downloadAnchor = el.lastElementChild as HTMLAnchorElement
|
||||
downloadAnchor.setAttribute('href', 'data:text/json;charset=utf-8,' + fileContents)
|
||||
downloadAnchor.setAttribute("download", "data.json")
|
||||
downloadAnchor.click()
|
||||
Tracker.download()
|
||||
}
|
||||
const shareSource = (el: Element) => {
|
||||
el.closest('.panel-controls')?.querySelector('input')
|
||||
Tracker.share()
|
||||
}
|
||||
const toggleMenu = (el: Element) => {
|
||||
el.classList.toggle('active')
|
||||
document.body.addEventListener('click', evt => {
|
||||
el.classList.remove('active')
|
||||
}, { capture: true, once: true })
|
||||
}
|
||||
return `<div class="panel source-panel">
|
||||
<div class="panel-controls">
|
||||
<input style="display: none;">
|
||||
<div class="btn" data-id="${view.onClick(copySource)}">
|
||||
${Octicon.clippy}
|
||||
<span data-i18n="copy"></span>
|
||||
</div>
|
||||
<div class="panel-menu">
|
||||
<div class="btn" data-id="${view.onClick(toggleMenu)}">
|
||||
${Octicon.kebab_horizontal}
|
||||
</div>
|
||||
<div class="panel-menu-list btn-group">
|
||||
<div class="btn" data-id="${view.onClick(downloadSource)}">
|
||||
${Octicon.download}<span data-i18n="download"></span>
|
||||
<a style="diplay: none;"></a>
|
||||
</div>
|
||||
<div class="btn" data-id="${view.onClick(shareSource)}">
|
||||
${Octicon.link}<span data-i18n="share"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="source" data-id="${source}" spellcheck="false" autocorrect="off" autocapitalize="off"></textarea>
|
||||
</div>`
|
||||
}
|
||||
161
src/app/components/panels/TreePanel.ts
Normal file
161
src/app/components/panels/TreePanel.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { DataModel, ModelPath, Path } from '@mcschema/core';
|
||||
import { App, checkVersion, Previews } from '../../App';
|
||||
import { Tracker } from '../../Tracker'
|
||||
import { View } from '../../views/View';
|
||||
import { Octicon } from '../Octicon';
|
||||
import { Mounter } from '../../Mounter';
|
||||
import { renderHtml } from '../../hooks/renderHtml';
|
||||
import config from '../../../config.json'
|
||||
import { locale } from '../../Locales';
|
||||
import { BiomeNoisePreview } from '../../preview/BiomeNoisePreview';
|
||||
|
||||
const createPopupIcon = (type: string, icon: keyof typeof Octicon, popup: string) => {
|
||||
const div = document.createElement('div')
|
||||
div.className = `node-icon ${type}`
|
||||
div.addEventListener('click', evt => {
|
||||
div.getElementsByTagName('span')[0].classList.add('show')
|
||||
document.body.addEventListener('click', evt => {
|
||||
div.getElementsByTagName('span')[0].classList.remove('show')
|
||||
}, { capture: true, once: true })
|
||||
})
|
||||
div.insertAdjacentHTML('beforeend', `<span class="icon-popup">${popup}</span>${Octicon[icon]}`)
|
||||
return div
|
||||
}
|
||||
|
||||
const treeViewObserver = (el: Element) => {
|
||||
el.querySelectorAll('.node[data-help]').forEach(e => {
|
||||
e.querySelector('.node-header')?.appendChild(
|
||||
createPopupIcon('node-help', 'info', e.getAttribute('data-help') ?? ''))
|
||||
})
|
||||
el.querySelectorAll('.node[data-error]').forEach(e => {
|
||||
e.querySelector('.node-header')?.appendChild(
|
||||
createPopupIcon('node-error', 'issue_opened', e.getAttribute('data-error') ?? ''))
|
||||
})
|
||||
el.querySelectorAll('.collapse.closed, button.add').forEach(e => {
|
||||
e.insertAdjacentHTML('afterbegin', Octicon.plus_circle)
|
||||
})
|
||||
el.querySelectorAll('.collapse.open, button.remove').forEach(e => {
|
||||
e.insertAdjacentHTML('afterbegin', Octicon.trashcan)
|
||||
})
|
||||
}
|
||||
|
||||
const treeViewNodeInjector = (path: ModelPath, mounter: Mounter) => {
|
||||
|
||||
let res = Object.keys(Previews).map(k => Previews[k])
|
||||
.filter(v => v.active(path))
|
||||
.map(v => {
|
||||
const id = mounter.registerClick(() => {
|
||||
Tracker.setPreview(v.getName())
|
||||
v.path = path
|
||||
App.preview.set(v)
|
||||
})
|
||||
return `<button data-id=${id}>${locale('preview')} ${Octicon.play}</button>`
|
||||
}).join('')
|
||||
|
||||
if (path.pop().endsWith(new Path(['generator', 'biome_source', 'biomes']))) {
|
||||
const biomePreview = Previews.biome_noise as BiomeNoisePreview
|
||||
const biome = path.push('biome').get()
|
||||
const id = mounter.registerChange(el => {
|
||||
biomePreview.setBiomeColor(biome, (el as HTMLInputElement).value)
|
||||
biomePreview.state = {}
|
||||
})
|
||||
res += `<input type="color" value="${biomePreview.getBiomeHex(biome)}" data-id=${id}></input>`
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export const TreePanel = (view: View, model: DataModel) => {
|
||||
const mounter = new Mounter({ nodeInjector: treeViewNodeInjector })
|
||||
const getContent = () => {
|
||||
if (App.loaded.get()) {
|
||||
const path = new ModelPath(model)
|
||||
const rendered = model.schema.hook(renderHtml, path, model.data, mounter)
|
||||
const category = model.schema.category(path)
|
||||
if (rendered[1]) {
|
||||
return `<div class="node ${model.schema.type(path)}-node" ${category ? `data-category="${category}"` : ''}>
|
||||
<div class="node-header">${rendered[1]}</div>
|
||||
<div class="node-body">${rendered[2]}</div>
|
||||
</div>`
|
||||
}
|
||||
return rendered[2]
|
||||
}
|
||||
return '<div class="spinner"></div>'
|
||||
}
|
||||
const mountContent = (el: Element) => {
|
||||
el.innerHTML = getContent()
|
||||
treeViewObserver(el)
|
||||
mounter.mount(el)
|
||||
}
|
||||
const tree = view.register(el => {
|
||||
App.loaded.watchRun((value) => {
|
||||
if (!value) {
|
||||
// If loading is taking more than 100 ms, show spinner
|
||||
new Promise(r => setTimeout(r, 100)).then(() => {
|
||||
if (!App.loaded.get()) {
|
||||
mountContent(el)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
mountContent(el)
|
||||
}
|
||||
})
|
||||
model.addListener({
|
||||
invalidated() {
|
||||
mountContent(el)
|
||||
}
|
||||
})
|
||||
;(Previews.biome_noise as BiomeNoisePreview).biomeColors.watch(() => {
|
||||
mountContent(el)
|
||||
}, 'tree-panel')
|
||||
})
|
||||
const toggleMenu = (el: Element) => {
|
||||
el.classList.toggle('active')
|
||||
document.body.addEventListener('click', evt => {
|
||||
el.classList.remove('active')
|
||||
}, { capture: true, once: true })
|
||||
}
|
||||
return `<div class="panel tree-panel">
|
||||
<div class="panel-controls">
|
||||
<div class="btn" data-id="${view.onClick(() => {
|
||||
Tracker.reset(); model.reset(model.schema.default())
|
||||
})}">
|
||||
${Octicon.history}
|
||||
<span data-i18n="reset"></span>
|
||||
</div>
|
||||
<div class="panel-menu">
|
||||
<div class="btn" data-id="${view.onClick(toggleMenu)}">
|
||||
${Octicon.tag}
|
||||
<span data-id="${view.register(el => App.version.watch(v => el.textContent = v, 'tree-controls'))}">
|
||||
${App.version.get()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-menu-list btn-group">
|
||||
${config.versions
|
||||
.filter(v => checkVersion(v.id, App.model.get()!.minVersion ?? '1.16'))
|
||||
.reverse()
|
||||
.map(v => `
|
||||
<div class="btn" data-id="${view.onClick(() => {
|
||||
Tracker.setVersion(v.id); App.version.set(v.id)
|
||||
})}">
|
||||
${v.id}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-menu">
|
||||
<div class="btn" data-id="${view.onClick(toggleMenu)}">
|
||||
${Octicon.kebab_horizontal}
|
||||
</div>
|
||||
<div class="panel-menu-list btn-group">
|
||||
<div class="btn" data-id="${view.onClick(() => {Tracker.undo(); model.undo()})}">
|
||||
${Octicon.arrow_left}<span data-i18n="undo"></span>
|
||||
</div>
|
||||
<div class="btn" data-id="${view.onClick(() => {Tracker.redo(); model.redo()})}">
|
||||
${Octicon.arrow_right}<span data-i18n="redo"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tree" data-id="${tree}"></div>
|
||||
</div>`
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hook, ModelPath, Path, StringHookParams, ValidationOption, EnumOption, INode, DataModel, MapNode, StringNode } from '@mcschema/core'
|
||||
import { locale, pathLocale, segmentedLocale } from '../locales'
|
||||
import { locale, segmentedLocale } from '../Locales'
|
||||
import { Mounter } from '../Mounter'
|
||||
import { hexId } from '../utils'
|
||||
import { hexId, htmlEncode } from '../Utils'
|
||||
|
||||
/**
|
||||
* Secondary model used to remember the keys of a map
|
||||
@@ -248,6 +248,12 @@ function hashString(str: string) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
function pathLocale(path: Path, params?: string[]): string {
|
||||
// return path.getContext().slice(-5).join('.')
|
||||
return segmentedLocale(path.getContext(), params)
|
||||
?? path.getContext()[path.getContext().length - 1] ?? ''
|
||||
}
|
||||
|
||||
function error(p: ModelPath, exact = true) {
|
||||
const errors = p.model.errors.get(p, exact)
|
||||
if (errors.length === 0) return ''
|
||||
@@ -259,8 +265,3 @@ function help(path: ModelPath) {
|
||||
if (message === undefined) return ''
|
||||
return `data-help="${htmlEncode(message)}"`
|
||||
}
|
||||
|
||||
function htmlEncode(str: string) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/')
|
||||
}
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
import { ModelPath, Path } from '@mcschema/core'
|
||||
import English from '../locales/en.json'
|
||||
import { App } from './App'
|
||||
|
||||
interface Locale {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
const Locales: {
|
||||
export const Locales: {
|
||||
[key: string]: Locale
|
||||
} = {}
|
||||
|
||||
let language = 'en'
|
||||
|
||||
export function registerLocale(code: string, locale: Locale) {
|
||||
Locales[code] = locale
|
||||
}
|
||||
|
||||
export function hasLocale(code: string) {
|
||||
return Locales[code] !== undefined
|
||||
}
|
||||
|
||||
export function setLanguage(code: string | undefined) {
|
||||
language = code ?? language
|
||||
}
|
||||
|
||||
export function getLanguage() {
|
||||
return language
|
||||
} = {
|
||||
'en': English
|
||||
}
|
||||
|
||||
export function resolveLocaleParams(value: string, params?: string[]): string | undefined {
|
||||
@@ -34,17 +19,17 @@ export function resolveLocaleParams(value: string, params?: string[]): string |
|
||||
}
|
||||
|
||||
export function locale(key: string, params?: string[]): string {
|
||||
const value: string | undefined = Locales[language][key] ?? Locales.en[key]
|
||||
const value: string | undefined = Locales[App.language.get()]?.[key] ?? Locales.en[key]
|
||||
return resolveLocaleParams(value, params) ?? key
|
||||
}
|
||||
|
||||
export function segmentedLocale(segments: string[], params?: string[], depth = 5, minDepth = 1): string | undefined {
|
||||
return [language, 'en'].reduce((prev: string | undefined, code) => {
|
||||
return [App.language.get(), 'en'].reduce((prev: string | undefined, code) => {
|
||||
if (prev !== undefined) return prev
|
||||
|
||||
const array = segments.slice(-depth);
|
||||
while (array.length >= minDepth) {
|
||||
const locale = resolveLocaleParams(Locales[code][array.join('.')], params)
|
||||
const locale = resolveLocaleParams(Locales[code]?.[array.join('.')], params)
|
||||
if (locale !== undefined) return locale
|
||||
array.shift()
|
||||
}
|
||||
@@ -52,9 +37,3 @@ export function segmentedLocale(segments: string[], params?: string[], depth = 5
|
||||
return undefined
|
||||
}, undefined)
|
||||
}
|
||||
|
||||
export function pathLocale(path: Path, params?: string[]): string {
|
||||
// return path.getContext().slice(-5).join('.')
|
||||
return segmentedLocale(path.getContext(), params)
|
||||
?? path.getContext()[path.getContext().length - 1] ?? ''
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import { DataModel, Path, ModelPath } from "@mcschema/core"
|
||||
import { Property } from "../state/Property"
|
||||
import { NormalNoise } from './NormalNoise'
|
||||
import { Visualizer } from './Visualizer'
|
||||
import { VisualizerView } from './VisualizerView'
|
||||
import { Preview } from './Preview'
|
||||
|
||||
const LOCAL_STORAGE_BIOME_COLORS = 'biome_colors'
|
||||
|
||||
export class BiomeNoiseVisualizer extends Visualizer {
|
||||
export class BiomeNoisePreview extends Preview {
|
||||
static readonly noiseMaps = ['altitude', 'temperature', 'humidity', 'weirdness']
|
||||
private seed: string
|
||||
private noise: NormalNoise[]
|
||||
private offsetX: number = 0
|
||||
private offsetY: number = 0
|
||||
private viewScale: number = 0
|
||||
private biomeColors: { [id: string]: number[] }
|
||||
seed: string
|
||||
offsetX: number = 0
|
||||
offsetY: number = 0
|
||||
viewScale: number = 0
|
||||
biomeColors: Property<{ [id: string]: number[] }>
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.seed = this.hexId()
|
||||
this.biomeColors = JSON.parse(localStorage.getItem(LOCAL_STORAGE_BIOME_COLORS) ?? '{}')
|
||||
this.biomeColors = new Property({})
|
||||
this.biomeColors.set(JSON.parse(localStorage.getItem(LOCAL_STORAGE_BIOME_COLORS) ?? '{}'))
|
||||
this.noise = []
|
||||
}
|
||||
|
||||
@@ -31,7 +32,7 @@ export class BiomeNoiseVisualizer extends Visualizer {
|
||||
}
|
||||
|
||||
draw(model: DataModel, img: ImageData) {
|
||||
this.noise = BiomeNoiseVisualizer.noiseMaps.map((id, i) => {
|
||||
this.noise = BiomeNoisePreview.noiseMaps.map((id, i) => {
|
||||
const config = this.state[`${id}_noise`]
|
||||
return new NormalNoise(this.seed + i, config.firstOctave, config.amplitudes)
|
||||
})
|
||||
@@ -49,7 +50,7 @@ export class BiomeNoiseVisualizer extends Visualizer {
|
||||
const xx = (x - this.offsetX) * s - 100 * s
|
||||
const yy = (y - this.offsetY) * s - 50 * s
|
||||
const b = this.closestBiome(xx, yy)
|
||||
const color = biomeColorCache[b]
|
||||
const color = biomeColorCache[b] ?? [128, 128, 128]
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
@@ -63,20 +64,8 @@ export class BiomeNoiseVisualizer extends Visualizer {
|
||||
this.offsetY += toY - fromY
|
||||
}
|
||||
|
||||
addControls(el: HTMLElement, view: VisualizerView) {
|
||||
el.insertAdjacentHTML('beforeend', `<button class="btn" id="visualizer-controls-toggle"><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></button><button class="btn" id="visualizer-controls-toggle"><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></button>`)
|
||||
el.childNodes[0].addEventListener('click', () => {
|
||||
this.viewScale -= 0.5
|
||||
view.redraw()
|
||||
})
|
||||
el.childNodes[1].addEventListener('click', () => {
|
||||
this.viewScale += 0.5
|
||||
view.redraw()
|
||||
})
|
||||
}
|
||||
|
||||
private closestBiome(x: number, y: number): string {
|
||||
if (!this.state.biomes) return ''
|
||||
if (!this.state.biomes || this.state.biomes.length === 0) return ''
|
||||
const noise = this.noise.map(n => n.getValue(x, y))
|
||||
let minDist = Infinity
|
||||
let minBiome = ''
|
||||
@@ -95,7 +84,7 @@ export class BiomeNoiseVisualizer extends Visualizer {
|
||||
}
|
||||
|
||||
getBiomeColor(biome: string): number[] {
|
||||
const color = this.biomeColors[biome]
|
||||
const color = this.biomeColors.get()[biome]
|
||||
if (color === undefined) {
|
||||
return this.colorFromBiome(biome)
|
||||
}
|
||||
@@ -103,8 +92,9 @@ export class BiomeNoiseVisualizer extends Visualizer {
|
||||
}
|
||||
|
||||
setBiomeColor(biome: string, value: string) {
|
||||
this.biomeColors[biome] = [parseInt(value.slice(1, 3), 16), parseInt(value.slice(3, 5), 16), parseInt(value.slice(5, 7), 16)]
|
||||
localStorage.setItem(LOCAL_STORAGE_BIOME_COLORS, JSON.stringify(this.biomeColors))
|
||||
const color = [parseInt(value.slice(1, 3), 16), parseInt(value.slice(3, 5), 16), parseInt(value.slice(5, 7), 16)]
|
||||
this.biomeColors.set({...this.biomeColors.get(), [biome]: color})
|
||||
localStorage.setItem(LOCAL_STORAGE_BIOME_COLORS, JSON.stringify(this.biomeColors.get()))
|
||||
}
|
||||
|
||||
getBiomeHex(biome: string): string {
|
||||
@@ -1,10 +1,10 @@
|
||||
import SimplexNoise from 'simplex-noise'
|
||||
import { DataModel, Path, ModelPath } from "@mcschema/core"
|
||||
import { Visualizer } from './Visualizer'
|
||||
import { Preview } from './Preview'
|
||||
|
||||
const debug = false
|
||||
|
||||
export class NoiseSettingsVisualizer extends Visualizer {
|
||||
export class NoiseSettingsPreview extends Preview {
|
||||
private noise: SimplexNoise
|
||||
private offsetX: number
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { DataModel, ModelPath } from "@mcschema/core"
|
||||
import { VisualizerView } from "./VisualizerView"
|
||||
|
||||
export abstract class Visualizer {
|
||||
export abstract class Preview {
|
||||
state: any
|
||||
path?: ModelPath
|
||||
|
||||
dirty(path: ModelPath): boolean {
|
||||
return JSON.stringify(this.state) !== JSON.stringify(path.get())
|
||||
@@ -13,6 +13,4 @@ export abstract class Visualizer {
|
||||
abstract draw(model: DataModel, img: ImageData): void
|
||||
|
||||
onDrag(fromX: number, fromY: number, toX: number, toY: number): void {}
|
||||
|
||||
addControls(el: HTMLElement, view: VisualizerView): void {}
|
||||
}
|
||||
14
src/app/state/LocalStorageProperty.ts
Normal file
14
src/app/state/LocalStorageProperty.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Property } from './Property'
|
||||
|
||||
export class LocalStorageProperty extends Property<string> {
|
||||
constructor(private id: string, fallback: string) {
|
||||
super(localStorage.getItem(id) ?? fallback)
|
||||
}
|
||||
set(value: string) {
|
||||
super.set(value)
|
||||
localStorage.setItem(this.id, value)
|
||||
}
|
||||
get(): string {
|
||||
return this.value
|
||||
}
|
||||
}
|
||||
40
src/app/state/Property.ts
Normal file
40
src/app/state/Property.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { hexId } from "../Utils"
|
||||
|
||||
type PropertyWatcher<T> = (value: T, oldValue: T | null) => void
|
||||
type NamedPropertyWatcher<T> = {
|
||||
name: string
|
||||
watcher: PropertyWatcher<T>
|
||||
}
|
||||
|
||||
export class Property<T> {
|
||||
private watchers: NamedPropertyWatcher<T>[] = []
|
||||
|
||||
constructor(public value: T) {}
|
||||
|
||||
set(value: T) {
|
||||
if (this.value === value) return
|
||||
const oldValue = this.value
|
||||
this.value = value
|
||||
this.watchers.forEach(w => w.watcher(this.value, oldValue))
|
||||
}
|
||||
|
||||
get(): T {
|
||||
return this.value
|
||||
}
|
||||
|
||||
watchRun(watcher: PropertyWatcher<T>, name?: string) {
|
||||
watcher(this.value, null)
|
||||
return this.watch(watcher, name)
|
||||
}
|
||||
|
||||
watch(watcher: PropertyWatcher<T>, name?: string) {
|
||||
name = name ?? hexId()
|
||||
const w = this.watchers.find(w => w.name === name)
|
||||
if (w) {
|
||||
w.watcher = watcher
|
||||
} else {
|
||||
this.watchers.push({ name, watcher })
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,8 @@ export function hexId(length = 12) {
|
||||
window.crypto.getRandomValues(arr)
|
||||
return Array.from(arr, dec2hex).join('')
|
||||
}
|
||||
|
||||
export function htmlEncode(str: string) {
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''').replace(/\//g, '/')
|
||||
}
|
||||
|
||||
65
src/app/views/Generator.ts
Normal file
65
src/app/views/Generator.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { App, checkVersion, Models } from '../App'
|
||||
import { View } from './View'
|
||||
import { Header } from '../components/Header'
|
||||
import { SplitGroup } from '../components/SplitGroup'
|
||||
import { Errors } from '../components/Errors'
|
||||
import { TreePanel } from '../components/panels/TreePanel'
|
||||
import { SourcePanel } from '../components/panels/SourcePanel'
|
||||
import { PreviewPanel } from '../components/panels/PreviewPanel'
|
||||
|
||||
export const Generator = (view: View): string => {
|
||||
const model = Models[App.model.get()!.id]
|
||||
model.listeners = []
|
||||
const getSideContent = () => {
|
||||
return App.preview.get() ?
|
||||
SplitGroup(view, { direction: 'vertical', sizes: [60, 40] }, [
|
||||
SourcePanel(view, model),
|
||||
PreviewPanel(view, model)
|
||||
])
|
||||
: SourcePanel(view, model)
|
||||
}
|
||||
const validatePreview = () => {
|
||||
const preview = App.preview.get()
|
||||
const path = preview?.path?.withModel(model)
|
||||
if (!(path && path.get() && preview?.active(path))) {
|
||||
App.preview.set(null)
|
||||
}
|
||||
}
|
||||
model.addListener({
|
||||
invalidated: validatePreview
|
||||
})
|
||||
App.schemasLoaded.watch((value) => {
|
||||
if (value) {
|
||||
model.validate()
|
||||
model.invalidate()
|
||||
validatePreview()
|
||||
}
|
||||
}, 'generator')
|
||||
App.localesLoaded.watch((value) => {
|
||||
if (value && App.schemasLoaded.get()) {
|
||||
model.invalidate()
|
||||
}
|
||||
}, 'generator')
|
||||
App.version.watchRun((value) => {
|
||||
const minVersion = App.model.get()!.minVersion
|
||||
if (minVersion && !checkVersion(value, minVersion)) {
|
||||
App.version.set(minVersion)
|
||||
}
|
||||
}, 'generator')
|
||||
const sideContent = view.register(el => {
|
||||
App.preview.watch((value, oldValue) => {
|
||||
if (!value || !oldValue) {
|
||||
view.mount(el, getSideContent(), false)
|
||||
}
|
||||
}, 'generator')
|
||||
})
|
||||
const homeLink = typeof App.model.get()!.category === 'string' ? `/${App.model.get()!.category}/` : undefined
|
||||
return `${Header(view, `${App.model.get()!.name} Generator`, homeLink, true)}
|
||||
<div class="content">
|
||||
${SplitGroup(view, { direction: "horizontal", sizes: [66, 34] }, [
|
||||
TreePanel(view, model),
|
||||
`<div class="content-output" data-id="${sideContent}">${getSideContent()}</div>`
|
||||
])}
|
||||
</div>
|
||||
${Errors(view, model)}`
|
||||
}
|
||||
34
src/app/views/Home.ts
Normal file
34
src/app/views/Home.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { App } from '../App'
|
||||
import { Header } from '../components/Header'
|
||||
import { View } from './View'
|
||||
import { Octicon } from '../components/Octicon'
|
||||
import config from '../../config.json'
|
||||
|
||||
const GeneratorCard = (url: string, name: string, arrow?: boolean, active?: boolean) => `
|
||||
<li>
|
||||
<a data-link href="/${url}/" class="generators-card${active ? ' selected' : ''}">
|
||||
${name}
|
||||
${arrow ? Octicon.chevron_right : ''}
|
||||
</a>
|
||||
</li>
|
||||
`
|
||||
|
||||
export const Home = (view: View): string => {
|
||||
const filteredModels = config.models.filter(m => m.category === App.model.get()!.id)
|
||||
return `
|
||||
${Header(view, 'Data Pack Generators')}
|
||||
<div class="home">
|
||||
<ul class="generators-list">
|
||||
${config.models
|
||||
.filter(m => typeof m.category !== 'string')
|
||||
.map(m => GeneratorCard(m.id, m.name, m.category === true, App.model.get()!.id === m.id))
|
||||
.join('')}
|
||||
</ul>
|
||||
${filteredModels.length === 0 ? '' : `
|
||||
<ul class="generators-list">
|
||||
${filteredModels.map(m => GeneratorCard(m.id, m.name)).join('')}
|
||||
</ul>
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
54
src/app/views/View.ts
Normal file
54
src/app/views/View.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { locale } from "../Locales"
|
||||
import { hexId } from "../Utils"
|
||||
|
||||
export class View {
|
||||
private registry: { [id: string]: (el: Element) => void } = {}
|
||||
|
||||
render(): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
register(callback: (el: Element) => void): string {
|
||||
const id = hexId()
|
||||
this.registry[id] = callback
|
||||
return id
|
||||
}
|
||||
|
||||
on(type: string, callback: (el: Element) => void): string {
|
||||
return this.register(el => {
|
||||
el.addEventListener(type, evt => {
|
||||
callback(el)
|
||||
evt.stopPropagation()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onChange(callback: (el: Element) => void): string {
|
||||
return this.on('change', callback)
|
||||
}
|
||||
|
||||
onClick(callback: (el: Element) => void): string {
|
||||
return this.on('click', callback)
|
||||
}
|
||||
|
||||
mounted(el: Element, clear = true): void {
|
||||
for (const id in this.registry) {
|
||||
const element = el.querySelector(`[data-id="${id}"]`)
|
||||
if (element !== null) {
|
||||
this.registry[id](element)
|
||||
delete this.registry[id]
|
||||
}
|
||||
}
|
||||
if (clear) {
|
||||
this.registry = {}
|
||||
}
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
el.textContent = locale(el.attributes.getNamedItem('data-i18n')!.value)
|
||||
})
|
||||
}
|
||||
|
||||
mount(el: Element, html: string, clear = true) {
|
||||
el.innerHTML = html
|
||||
this.mounted(el, clear)
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { DataModel, ModelPath } from "@mcschema/core"
|
||||
import { AbstractView } from "../AbstractView"
|
||||
import { BiomeNoiseVisualizer } from "./BiomeNoiseVisualizer"
|
||||
import { NoiseSettingsVisualizer } from "./NoiseSettingsVisualizer"
|
||||
import { Visualizer } from "./Visualizer"
|
||||
|
||||
export class VisualizerView extends AbstractView {
|
||||
ctx: CanvasRenderingContext2D
|
||||
visualizer?: Visualizer
|
||||
active: boolean
|
||||
path?: ModelPath
|
||||
el: HTMLElement
|
||||
canvas: HTMLCanvasElement
|
||||
sourceView: HTMLElement
|
||||
gutter: HTMLElement
|
||||
controls: HTMLElement
|
||||
lastHeight?: string
|
||||
dragStart?: number[]
|
||||
|
||||
constructor(model: DataModel, el: HTMLElement) {
|
||||
super(model)
|
||||
this.el = el
|
||||
this.canvas = el.querySelector('canvas') as HTMLCanvasElement
|
||||
this.ctx = this.canvas.getContext('2d')!
|
||||
this.active = false
|
||||
this.gutter = el.parentElement!.querySelector('.gutter') as HTMLElement
|
||||
this.sourceView = el.parentElement!.getElementsByTagName('textarea')[0] as HTMLElement
|
||||
this.controls = el.querySelector('.visualizer-controls') as HTMLElement
|
||||
|
||||
this.canvas.addEventListener('mousedown', evt => {
|
||||
this.dragStart = [evt.offsetX, evt.offsetY]
|
||||
})
|
||||
this.canvas.addEventListener('mousemove', evt => {
|
||||
if (this.dragStart === undefined) return
|
||||
if (this.visualizer?.onDrag) {
|
||||
this.visualizer.onDrag(this.dragStart[0], this.dragStart[1], evt.offsetX, evt.offsetY)
|
||||
this.redraw()
|
||||
}
|
||||
this.dragStart = [evt.offsetX, evt.offsetY]
|
||||
})
|
||||
this.canvas.addEventListener('mouseup', evt => {
|
||||
this.dragStart = undefined
|
||||
})
|
||||
}
|
||||
|
||||
redraw() {
|
||||
if (this.active && this.visualizer) {
|
||||
this.visualizer.state = {}
|
||||
this.invalidated()
|
||||
}
|
||||
}
|
||||
|
||||
invalidated() {
|
||||
this.path = this.path?.withModel(this.model)
|
||||
let newState: any
|
||||
if (this.active && this.visualizer && this.path
|
||||
&& this.visualizer.active(this.path)
|
||||
&& (newState = this.path.get())) {
|
||||
if (newState && this.visualizer.dirty(this.path)) {
|
||||
const img = this.ctx.createImageData(200, 100)
|
||||
this.visualizer.state = JSON.parse(JSON.stringify(newState))
|
||||
this.visualizer.draw(this.model, img)
|
||||
this.ctx.putImageData(img, 0, 0)
|
||||
}
|
||||
this.el.style.display = 'block'
|
||||
this.gutter.style.display = 'block'
|
||||
if (this.lastHeight) {
|
||||
this.sourceView.style.height = this.lastHeight
|
||||
this.lastHeight = undefined
|
||||
}
|
||||
} else {
|
||||
this.el.style.display = 'none'
|
||||
this.gutter.style.display = 'none'
|
||||
this.lastHeight = this.sourceView.style.height
|
||||
this.sourceView.style.height = '100%'
|
||||
}
|
||||
}
|
||||
|
||||
set(visualizer: Visualizer, path: ModelPath) {
|
||||
this.active = true
|
||||
this.visualizer = visualizer
|
||||
this.path = path
|
||||
this.visualizer.state = undefined
|
||||
this.controls.innerHTML = ''
|
||||
this.visualizer.addControls(this.controls, this)
|
||||
this.redraw()
|
||||
}
|
||||
|
||||
static visualizers: Visualizer[] = [
|
||||
new BiomeNoiseVisualizer(),
|
||||
new NoiseSettingsVisualizer()
|
||||
]
|
||||
}
|
||||
110
src/config.json
110
src/config.json
@@ -42,13 +42,13 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"id": "1.17",
|
||||
"mcdata_ref": "master"
|
||||
},
|
||||
{
|
||||
"id": "1.16",
|
||||
"mcdata_ref": "1.16.4"
|
||||
},
|
||||
{
|
||||
"id": "1.17",
|
||||
"mcdata_ref": "master"
|
||||
}
|
||||
],
|
||||
"models": [
|
||||
@@ -91,48 +91,55 @@
|
||||
{
|
||||
"id": "worldgen",
|
||||
"name": "Worldgen",
|
||||
"children": [
|
||||
{
|
||||
"id": "worldgen/biome",
|
||||
"name": "Biome",
|
||||
"schema": "biome"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/carver",
|
||||
"name": "Carver",
|
||||
"schema": "configured_carver"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/feature",
|
||||
"name": "Feature",
|
||||
"schema": "configured_feature"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/noise-settings",
|
||||
"name": "Noise Settings",
|
||||
"schema": "noise_settings"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/structure-feature",
|
||||
"name": "Structure Feature",
|
||||
"schema": "configured_structure_feature"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/surface-builder",
|
||||
"name": "Surface Builder",
|
||||
"schema": "configured_surface_builder"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/processor-list",
|
||||
"name": "Processor List",
|
||||
"schema": "processor_list"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/template-pool",
|
||||
"name": "Template Pool",
|
||||
"schema": "template_pool"
|
||||
}
|
||||
]
|
||||
"category": true
|
||||
},
|
||||
{
|
||||
"id": "worldgen/biome",
|
||||
"name": "Biome",
|
||||
"category": "worldgen",
|
||||
"schema": "biome"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/carver",
|
||||
"name": "Carver",
|
||||
"category": "worldgen",
|
||||
"schema": "configured_carver"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/feature",
|
||||
"name": "Feature",
|
||||
"category": "worldgen",
|
||||
"schema": "configured_feature"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/noise-settings",
|
||||
"name": "Noise Settings",
|
||||
"category": "worldgen",
|
||||
"schema": "noise_settings"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/structure-feature",
|
||||
"name": "Structure Feature",
|
||||
"category": "worldgen",
|
||||
"schema": "configured_structure_feature"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/surface-builder",
|
||||
"name": "Surface Builder",
|
||||
"category": "worldgen",
|
||||
"schema": "configured_surface_builder"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/processor-list",
|
||||
"name": "Processor List",
|
||||
"category": "worldgen",
|
||||
"schema": "processor_list"
|
||||
},
|
||||
{
|
||||
"id": "worldgen/template-pool",
|
||||
"name": "Template Pool",
|
||||
"category": "worldgen",
|
||||
"schema": "template_pool"
|
||||
}
|
||||
],
|
||||
"registries": [
|
||||
@@ -144,10 +151,10 @@
|
||||
"item",
|
||||
"loot_condition_type",
|
||||
"loot_function_type",
|
||||
"loot_nbt_provider_type",
|
||||
"loot_number_provider_type",
|
||||
{ "id": "loot_nbt_provider_type", "minVersion": "1.17" },
|
||||
{ "id": "loot_number_provider_type", "minVersion": "1.17" },
|
||||
"loot_pool_entry_type",
|
||||
"loot_score_provider_type",
|
||||
{ "id": "loot_score_provider_type", "minVersion": "1.17" },
|
||||
"mob_effect",
|
||||
"rule_test",
|
||||
"pos_rule_test",
|
||||
@@ -168,9 +175,6 @@
|
||||
"worldgen/surface_builder",
|
||||
"worldgen/tree_decorator_type",
|
||||
"worldgen/trunk_placer_type",
|
||||
{
|
||||
"id": "worldgen/biome",
|
||||
"path": "/processed/reports/biomes/data.min.json"
|
||||
}
|
||||
{ "id": "worldgen/biome", "path": "processed/reports/biomes" }
|
||||
]
|
||||
}
|
||||
|
||||
101
src/index.html
101
src/index.html
@@ -8,11 +8,12 @@
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
ga('create', 'UA-73024255-2', 'auto');
|
||||
ga('set', 'page', location.pathname.replace(/^\/dev/, ''));
|
||||
ga('set', 'page', location.pathname);
|
||||
ga('set', 'dimension1', localStorage.getItem('theme') ?? 'default');
|
||||
ga('set', 'dimension2', 'v2');
|
||||
ga('set', 'dimension3', localStorage.getItem('schema_version') ?? '1.17');
|
||||
ga('set', 'dimension4', localStorage.getItem('language') ?? 'en');
|
||||
ga('set', 'dimension5', 'none');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
<!-- End Google Analytics -->
|
||||
@@ -26,101 +27,7 @@
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.publicPath %>styles/global.css">
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.publicPath %>styles/nodes.css">
|
||||
</head>
|
||||
<body style="visibility: hidden;">
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<div id="home-link" class="home-link nav-item">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="32" height="32"><path fill-rule="evenodd" d="M1 2.75A.75.75 0 011.75 2h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 2.75zm0 5A.75.75 0 011.75 7h12.5a.75.75 0 110 1.5H1.75A.75.75 0 011 7.75zM1.75 12a.75.75 0 100 1.5h12.5a.75.75 0 100-1.5H1.75z"></path></svg>
|
||||
</div>
|
||||
<h2 id="selected-model"></h2>
|
||||
</div>
|
||||
<div class="nav">
|
||||
<div id="source-toggle" class="nav-item toggle">
|
||||
<svg id="source-toggle-tree" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0114.25 14H1.75A1.75 1.75 0 010 12.25v-8.5zm1.75-.25a.25.25 0 00-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25v-8.5a.25.25 0 00-.25-.25H1.75zM3.5 6.25a.75.75 0 01.75-.75h7a.75.75 0 010 1.5h-7a.75.75 0 01-.75-.75zm.75 2.25a.75.75 0 000 1.5h4a.75.75 0 000-1.5h-4z"></path></svg>
|
||||
<svg id="source-toggle-source" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><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>
|
||||
</div>
|
||||
<div class="nav-item nav-selector">
|
||||
<svg id="language-selector" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><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>
|
||||
<div class="nav-selector-menu btn-group" id="language-selector-menu"></div>
|
||||
</div>
|
||||
<div id="theme-selector" class="nav-item toggle">
|
||||
<svg id="theme-selection-light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M8 10.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zM8 12a4 4 0 100-8 4 4 0 000 8zM8 0a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0V.75A.75.75 0 018 0zm0 13a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 018 13zM2.343 2.343a.75.75 0 011.061 0l1.06 1.061a.75.75 0 01-1.06 1.06l-1.06-1.06a.75.75 0 010-1.06zm9.193 9.193a.75.75 0 011.06 0l1.061 1.06a.75.75 0 01-1.06 1.061l-1.061-1.06a.75.75 0 010-1.061zM16 8a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0116 8zM3 8a.75.75 0 01-.75.75H.75a.75.75 0 010-1.5h1.5A.75.75 0 013 8zm10.657-5.657a.75.75 0 010 1.061l-1.061 1.06a.75.75 0 11-1.06-1.06l1.06-1.06a.75.75 0 011.06 0zm-9.193 9.193a.75.75 0 010 1.06l-1.06 1.061a.75.75 0 11-1.061-1.06l1.06-1.061a.75.75 0 011.061 0z"></path></svg>
|
||||
<svg id="theme-selection-dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><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>
|
||||
</div>
|
||||
<div id="github-link" class="nav-item">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="tree" id="tree-view">
|
||||
<div class="tree-controls">
|
||||
<button class="btn" id="tree-controls-toggle"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm13 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg></button>
|
||||
<button class="btn" id="tree-version-toggle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 01.25-.25h5.025a.25.25 0 01.177.073l6.25 6.25a.25.25 0 010 .354l-5.025 5.025a.25.25 0 01-.354 0l-6.25-6.25a.25.25 0 01-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 010 2.474l-5.026 5.026a1.75 1.75 0 01-2.474 0l-6.25-6.25A1.75 1.75 0 011 7.775zM6 5a1 1 0 100 2 1 1 0 000-2z"></path></svg>
|
||||
<span id="tree-version-label"></span>
|
||||
</button>
|
||||
<button class="btn" id="tree-controls-reset">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.643 3.143L.427 1.927A.25.25 0 000 2.104V5.75c0 .138.112.25.25.25h3.646a.25.25 0 00.177-.427L2.715 4.215a6.5 6.5 0 11-1.18 4.458.75.75 0 10-1.493.154 8.001 8.001 0 101.6-5.684zM7.75 4a.75.75 0 01.75.75v2.992l2.028.812a.75.75 0 01-.557 1.392l-2.5-1A.75.75 0 017 8.25v-3.5A.75.75 0 017.75 4z"></path></svg>
|
||||
<span data-i18n="reset"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tree-controls-menu btn-group" id="tree-controls-menu">
|
||||
<button class="btn" id="tree-controls-undo">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.78 12.53a.75.75 0 01-1.06 0L2.47 8.28a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L4.81 7h7.44a.75.75 0 010 1.5H4.81l2.97 2.97a.75.75 0 010 1.06z"></path></svg>
|
||||
<span data-i18n="undo"></span>
|
||||
</button>
|
||||
<button class="btn" id="tree-controls-redo">
|
||||
<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>
|
||||
<span data-i18n="redo"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="tree-controls-menu btn-group" id="tree-version-menu"></div>
|
||||
<div class="tree-content">
|
||||
<div id="tree-view-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="source" id="source-view">
|
||||
<div class="source-controls">
|
||||
<button class="btn" id="source-controls-toggle"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M8 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zM1.5 9a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm13 0a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path></svg></button>
|
||||
<button class="btn" id="source-controls-copy">
|
||||
<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>
|
||||
<span data-i18n="copy"></span>
|
||||
</button>
|
||||
<input type="text" id="source-controls-share-input" style="display: none;"></input>
|
||||
</div>
|
||||
<div class="source-controls-menu btn-group" id="source-controls-menu">
|
||||
<button class="btn" id="source-controls-download">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5 0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"></path></svg>
|
||||
<span data-i18n="download"></span>
|
||||
<a id="source-controls-download-anchor" style="display: none;"></a>
|
||||
</button>
|
||||
<button class="btn" id="source-controls-share">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg>
|
||||
<span data-i18n="share"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="source-content">
|
||||
<textarea id="source-view-output" spellcheck="false" autocorrect="off" autocapitalize="off"></textarea>
|
||||
<div class="visualizer-content" id="visualizer-content">
|
||||
<div class="visualizer-controls" id="visualizer-controls"></div>
|
||||
<canvas width="200" height="100" id="visualizer-output"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="home-view" class="home">
|
||||
<div id="home-generators" class="generators-list"></div>
|
||||
<div id="category-generators" class="generators-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="errors-view" class="errors">
|
||||
<div class="error-list"></div>
|
||||
<div id="errors-toggle" class="toggle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="32" height="32"><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>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><path fill-rule="evenodd" d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm9 3a1 1 0 11-2 0 1 1 0 012 0zm-.25-6.25a.75.75 0 00-1.5 0v3.5a.75.75 0 001.5 0v-3.5z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div id="app" class="container"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"worldgen/feature": "Merkmal",
|
||||
"worldgen/carver": "Borer",
|
||||
"worldgen/biome": "Biom",
|
||||
"visualize": "Visualisieren",
|
||||
"preview": "Visualisieren",
|
||||
"title.home": "Datenpaketgeneratoren",
|
||||
"title.generator": "%0%-Generator",
|
||||
"share": "Teilen",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"share": "Share",
|
||||
"title.generator": "%0% Generator",
|
||||
"title.home": "Data Pack Generators",
|
||||
"visualize": "Visualize",
|
||||
"preview": "Visualize",
|
||||
"undo": "Undo",
|
||||
"world": "World Settings",
|
||||
"worldgen/biome": "Biome",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"share": "Partager",
|
||||
"title.generator": "Générateur de %0%",
|
||||
"title.home": "Générateur de data-pack",
|
||||
"visualize": "Visualiser",
|
||||
"preview": "Visualiser",
|
||||
"world": "Paramètres du monde",
|
||||
"worldgen/biome": "Biome",
|
||||
"worldgen/carver": "Sculpteur",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"share": "共有",
|
||||
"title.generator": "%0%ジェネレーター",
|
||||
"title.home": "データパックジェネレーター",
|
||||
"visualize": "可視化",
|
||||
"preview": "可視化",
|
||||
"world": "ワールド設定 (World Settings)",
|
||||
"worldgen/biome": "バイオーム (Biome)",
|
||||
"worldgen/carver": "地形彫刻 (Carver)",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"share": "Podziel się",
|
||||
"title.generator": "Generator %0%",
|
||||
"title.home": "Generatory Data Packów",
|
||||
"visualize": "Wizualizuj",
|
||||
"preview": "Wizualizuj",
|
||||
"world": "Ustawienia Świata",
|
||||
"worldgen/biome": "Biom",
|
||||
"worldgen/carver": "Rzeźbiarz",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"title.generator": "%0% 生成器",
|
||||
"title.home": "数据包生成器",
|
||||
"undo": "撤销",
|
||||
"visualize": "可视化",
|
||||
"preview": "可视化",
|
||||
"world": "世界设置",
|
||||
"worldgen/biome": "生物群系",
|
||||
"worldgen/carver": "地形雕刻器",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"title.generator": "%0% 生成器",
|
||||
"title.home": "資料包生成器",
|
||||
"undo": "復原",
|
||||
"visualize": "可視化",
|
||||
"preview": "可視化",
|
||||
"world": "世界設定",
|
||||
"worldgen/biome": "生態域",
|
||||
"worldgen/carver": "地形雕刻器",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
https://misode.github.io
|
||||
https://misode.github.io/loot-table/
|
||||
https://misode.github.io/predicate/
|
||||
https://misode.github.io/item-modifier/
|
||||
https://misode.github.io/advancement/
|
||||
https://misode.github.io/dimension/
|
||||
https://misode.github.io/dimension-type/
|
||||
|
||||
@@ -38,18 +38,19 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 18px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
overflow-x: hidden;
|
||||
background-color: var(--background);
|
||||
transition: background-color var(--style-transition);
|
||||
}
|
||||
|
||||
.header {
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -65,114 +66,133 @@ body {
|
||||
}
|
||||
|
||||
.header-title h2 {
|
||||
margin-right: 10px;
|
||||
color: var(--nav);
|
||||
transition: color var(--style-transition);
|
||||
}
|
||||
|
||||
.home-link.nav-item {
|
||||
.home-link {
|
||||
margin: 0 8px 0 0;
|
||||
height: 32px;
|
||||
fill: var(--nav);
|
||||
transition: fill var(--style-transition);
|
||||
}
|
||||
|
||||
.home-link svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
nav ul {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
nav > .toggle,
|
||||
nav li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin: 0 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.home-link:hover svg,
|
||||
header .toggle:hover svg,
|
||||
nav li:hover svg {
|
||||
fill: var(--nav-hover);
|
||||
}
|
||||
|
||||
nav > .toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav li.dimmed svg {
|
||||
fill: var(--nav-faded);
|
||||
}
|
||||
|
||||
nav li.dimmed:hover svg {
|
||||
fill: var(--nav-faded-hover);
|
||||
}
|
||||
|
||||
nav > .toggle svg,
|
||||
nav li > *,
|
||||
nav li svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: var(--nav);
|
||||
transition: fill var(--style-transition);
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
fill: var(--nav-hover);
|
||||
nav > .toggle span {
|
||||
color: var(--nav);
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.nav-item span {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.nav-item#source-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-item#github-link {
|
||||
fill: var(--nav-faded);
|
||||
}
|
||||
|
||||
.nav-item#github-link:hover {
|
||||
fill: var(--nav-faded-hover);
|
||||
}
|
||||
|
||||
.toggle:not(.toggled) svg:nth-child(1) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggle.toggled svg:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-selector {
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-selector-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
visibility: hidden;
|
||||
.dropdown > * {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 10px;
|
||||
z-index: 10;
|
||||
border-radius: 3px;
|
||||
background-color: var(--nav-menu);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.nav-selector-menu .btn {
|
||||
white-space: nowrap;
|
||||
.dropdown > *:not(select) {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown select {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dropdown option {
|
||||
color: var(--text);
|
||||
background-color: var(--background);
|
||||
font-size: 130%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
height: calc(100vh - 56px);
|
||||
overflow-y: hidden;
|
||||
color: var(--text);
|
||||
fill: var(--text);
|
||||
}
|
||||
|
||||
.split-group {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.split-group.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tree {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tree-content {
|
||||
overflow-y: auto;
|
||||
display: flow-root;
|
||||
padding: 16px 16px 50vh;
|
||||
height: 100%;
|
||||
padding: 1rem;
|
||||
padding-bottom: 50vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.source {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.source-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.source textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1.3rem 0.4rem;
|
||||
padding: 32px 8px;
|
||||
border: none;
|
||||
white-space: pre;
|
||||
overflow-wrap: normal;
|
||||
@@ -188,39 +208,48 @@ body {
|
||||
transition: background-color var(--style-transition), color var(--style-transition)
|
||||
}
|
||||
|
||||
.source textarea::selection {
|
||||
.source::selection {
|
||||
background-color: var(--selection);
|
||||
}
|
||||
|
||||
.tree-controls,
|
||||
.source-controls,
|
||||
.visualizer-controls {
|
||||
.panel-controls {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
position: absolute;
|
||||
right: 17px;
|
||||
top: 0;
|
||||
padding: 5px;
|
||||
right: 22px;
|
||||
top: 5px;;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tree-controls .btn:not(:first-child),
|
||||
.source-controls .btn:not(:first-child),
|
||||
.visualizer-controls .btn:not(:first-child) {
|
||||
.preview-panel .panel-controls {
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.panel-controls > *:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.tree-controls-menu,
|
||||
.source-controls-menu {
|
||||
display: flex;
|
||||
visibility: hidden;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: 17px;
|
||||
top: 37px;
|
||||
padding: 5px;
|
||||
.panel-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.source-controls input {
|
||||
.panel-menu > .btn {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.panel-menu-list {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.panel-menu .btn.active ~ .panel-menu-list {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.panel-controls input {
|
||||
margin-right: 5px;
|
||||
background: var(--background);
|
||||
color: var(--text);
|
||||
@@ -229,7 +258,7 @@ body {
|
||||
transition: background-color var(--style-transition), color var(--style-transition);
|
||||
}
|
||||
|
||||
.source-controls input::selection {
|
||||
.panel-controls input::selection {
|
||||
background-color: var(--selection);
|
||||
}
|
||||
|
||||
@@ -250,17 +279,7 @@ body {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.visualizer-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.visualizer-controls {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.visualizer-content canvas {
|
||||
.preview-panel canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--nav-faded);
|
||||
@@ -283,6 +302,7 @@ body {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
background-color: var(--btn-background);
|
||||
color: var(--btn-text);
|
||||
fill: var(--btn-text);
|
||||
@@ -315,6 +335,7 @@ body {
|
||||
|
||||
.errors {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
bottom: 17px;
|
||||
right: 17px;
|
||||
margin: 5px;
|
||||
@@ -325,31 +346,27 @@ body {
|
||||
transition: fill var(--style-transition);
|
||||
}
|
||||
|
||||
.errors:not(.active) .error-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.error span {
|
||||
padding-left: 11px;
|
||||
.error span:not(:last-child) {
|
||||
padding-right: 11px;
|
||||
}
|
||||
|
||||
.errors .toggle {
|
||||
padding: 6px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
align-self: flex-end;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.errors.active .toggle {
|
||||
padding: 2px;
|
||||
.errors svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.home {
|
||||
@@ -361,6 +378,7 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 20px;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.generators-card {
|
||||
@@ -369,6 +387,7 @@ body {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
border-radius: 3px;
|
||||
background-color: var(--nav-faded);
|
||||
color: var(--text);
|
||||
@@ -380,6 +399,10 @@ body {
|
||||
transition: margin 0.2s;
|
||||
}
|
||||
|
||||
.generators-card * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.generators-card:hover,
|
||||
.generators-card.selected {
|
||||
background-color: var(--nav-faded-hover);
|
||||
@@ -391,17 +414,54 @@ body {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
margin: 40px auto 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.spinner:after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 8px;
|
||||
border-radius: 50%;
|
||||
border: 6px solid var(--border);
|
||||
border-color: var(--border) transparent var(--border) transparent;
|
||||
animation: spinner 1.2s linear infinite, fadein 0.4s;
|
||||
transition: border-color var(--style-transition);
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 580px) {
|
||||
body {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
header {
|
||||
flex-direction: column;
|
||||
height: 92px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
nav {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
@@ -410,42 +470,36 @@ body {
|
||||
height: calc(100vh - 92px);
|
||||
}
|
||||
|
||||
.source,
|
||||
.gutter.gutter-horizontal {
|
||||
.tree {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
.tree-panel + .gutter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tree {
|
||||
.content-output,
|
||||
.tree-panel {
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-item#source-toggle {
|
||||
nav > .toggle {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.source.active {
|
||||
display: initial;
|
||||
position: absolute;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
body[data-panel="tree"] .content-output {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.source.active .source-controls {
|
||||
right: 17px;
|
||||
top: 0px;
|
||||
body[data-panel="source"] .tree-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.source.active .source-controls-menu {
|
||||
right: 17px;
|
||||
top: 37px;
|
||||
}
|
||||
|
||||
.nav-selector-menu {
|
||||
right: 0;
|
||||
left: initial;
|
||||
.home {
|
||||
padding: 5px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,10 +507,19 @@ body {
|
||||
.header-title h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.generators-list {
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.generators-card {
|
||||
font-size: 14px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 581px) and (max-width: 640px) {
|
||||
.header-title h2 {
|
||||
font-size: 22px;
|
||||
font-size: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,10 +174,6 @@ button.remove {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.node-icon svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.node-icon .icon-popup {
|
||||
visibility: hidden;
|
||||
width: 240px;
|
||||
@@ -209,19 +205,20 @@ button.remove {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.node-icon > svg {
|
||||
.node-icon svg {
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
min-width: 34px;
|
||||
margin-left: 6px;
|
||||
cursor: pointer;
|
||||
transition: fill var(--style-transition);
|
||||
}
|
||||
|
||||
.node-header svg.node-help {
|
||||
.node-icon.node-help svg {
|
||||
fill: var(--node-border);
|
||||
}
|
||||
|
||||
.node-header svg.node-error {
|
||||
.node-icon.node-error svg {
|
||||
fill: var(--node-remove);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user