Add versions explorer page

This commit is contained in:
Misode
2022-03-02 02:34:41 +01:00
parent fd46bc4360
commit cb24e61cf0
20 changed files with 526 additions and 115 deletions

View File

@@ -4,7 +4,7 @@ import '../styles/global.css'
import '../styles/nodes.css'
import { Analytics } from './Analytics'
import { Header } from './components'
import { Category, Changelog, Generator, Home, Project, Sounds } from './pages'
import { Category, Changelog, Generator, Home, Project, Sounds, Versions } from './pages'
import { cleanUrl } from './Utils'
export function App() {
@@ -21,6 +21,7 @@ export function App() {
<Category path="/assets" category="assets" />
<Sounds path="/sounds" />
<Changelog path="/changelog" />
<Versions path="/versions" />
<Project path="/project" />
<Generator default />
</Router>

View File

@@ -0,0 +1,14 @@
import { hexId } from '../../Utils'
interface Props {
label: string,
value: boolean,
onChange: (value: boolean) => unknown,
}
export function Checkbox({ label, value, onChange }: Props) {
const id = hexId()
return <label class="checkbox">
<input id={id} type="checkbox" checked={value} onClick={() => onChange(!value)} />
{label}
</label>
}

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
import { marked } from 'marked'
import { ChangelogTag } from '.'
import type { Change, ChangelogVersion } from '../../services'
type Props = {
change: Change,
activeTags?: string[],
toggleTag?: (tag: string) => unknown,
}
export function ChangelogEntry({ change, activeTags, toggleTag }: Props) {
return <div class="changelog-entry">
<div class="changelog-version">
<ArticleLink {...change.version}/>
<ArticleLink {...change.group}/>
</div>
<div class="changelog-tags">
{change.tags.map(tag => <ChangelogTag label={tag} onClick={toggleTag ? () => toggleTag(tag) : undefined} active={activeTags?.includes(tag)} />)}
</div>
<div class="changelog-content" dangerouslySetInnerHTML={{ __html: marked(change.content) }} />
</div>
}
function ArticleLink({ id, article }: ChangelogVersion) {
return article === null
? <span>{id}</span>
: <a href={`https://www.minecraft.net/en-us/article/${article}`} target="_blank">{id}</a>
}

View File

@@ -0,0 +1,66 @@
import { useMemo, useState } from 'preact/hooks'
import { Btn, TextInput } from '..'
import { useLocale } from '../../contexts'
import type { Change } from '../../services'
import { ChangelogEntry } from './ChangelogEntry'
import { ChangelogTag } from './ChangelogTag'
interface Props {
changes: Change[] | undefined,
defaultOrder: 'asc' | 'desc',
}
export function ChangelogList({ changes, defaultOrder }: Props) {
const { locale } = useLocale()
const [search, setSearch] = useState('')
const [tags, setTags] = useState<string[]>([])
const toggleTag = (tag: string) => {
if (!tags.includes(tag)) {
setTags([...tags, tag])
} else {
setTags(tags.filter(t => t !== tag))
}
}
const filteredChangelogs = useMemo(() => {
const query = search.split(' ').map(q => q.trim().toLowerCase()).filter(q => q.length > 0)
if (query.length === 0 && tags.length === 0) return changes
return changes?.filter(change => {
if (!tags.every(tag => change.tags.includes(tag))) {
return false
}
const content = change.tags.join(' ') + ' ' + change.content.toLowerCase()
return query.every(q => {
if (q.startsWith('!')) {
return q.length === 1 || !content.includes(q.slice(1))
}
return content.includes(q)
})
})
}, [changes, search, tags])
const [sort, setSort] = useState(defaultOrder === 'desc')
const sortedChangelogs = useMemo(() => {
return filteredChangelogs?.sort((a, b) => sort ? b.order - a.order : a.order - b.order)
}, [filteredChangelogs, sort])
return <>
<div class="changelog-query">
<TextInput class="btn btn-input changelog-search" list="sound-list" placeholder={locale('changelog.search')}
value={search} onChange={setSearch} />
<Btn icon={sort ? 'sort_desc' : 'sort_asc'} label={sort ? 'Newest first' : 'Oldest first'} onClick={() => setSort(!sort)} />
</div>
{tags.length > 0 && <div class="changelog-tags">
{tags.map(tag => <ChangelogTag label={tag} onClick={() => setTags(tags.filter(t => t !== tag))} />)}
</div>}
<div class="changelog-list">
{sortedChangelogs === undefined
? <span>{locale('loading')}</span>
: sortedChangelogs.length === 0
? <span>{locale('changelog.no_results')}</span>
: sortedChangelogs.map(change =>
<ChangelogEntry change={change} activeTags={tags} toggleTag={toggleTag} />)}
</div>
</>
}

View File

@@ -0,0 +1,15 @@
import { Octicon } from '..'
import { hashString } from '../../Utils'
type TagProps = {
label: string,
active?: boolean,
onClick?: () => unknown,
}
export function ChangelogTag({ label, active, onClick }: TagProps) {
const color = label === 'breaking' ? 5 : hashString(label) % 360
return <div class={`changelog-tag${active ? ' active' : ''}${onClick ? ' clickable' : ''}`} style={`--tint: ${color}`} onClick={onClick}>
{label === 'breaking' && Octicon.alert}
{label}
</div>
}

View File

@@ -0,0 +1,48 @@
import { useEffect, useMemo, useState } from 'preact/hooks'
import { VersionMetaData } from '.'
import { useLocale } from '../../contexts'
import type { Change, VersionMeta } from '../../services'
import { getChangelogs } from '../../services'
import { ChangelogList } from './ChangelogList'
interface Props {
version: VersionMeta
}
export function VersionDetail({ version }: Props) {
const { locale } = useLocale()
const [changelogs, setChangelogs] = useState<Change[] | undefined>(undefined)
useEffect(() => {
getChangelogs()
.then(changelogs => setChangelogs(
changelogs.map(c => ({ ...c, tags: c.tags.filter(t => t !== c.group.id) }))
))
.catch(e => console.error(e))
}, [])
const filteredChangelogs = useMemo(() =>
changelogs?.filter(c => c.version.id === version.id || c.group.id === version.id),
[version.id, changelogs])
return <>
<div class="version-detail">
<h2>{version.name}</h2>
<div class="version-info">
<VersionMetaData label={locale('versions.released')} value={releaseDate(version)} />
<VersionMetaData label={locale('versions.release_target')} value={version.release_target} link={version.id !== version.release_target ? `/versions/?id=${version.release_target}` : undefined} />
<VersionMetaData label={locale('versions.data_version')} value={version.data_version} />
<VersionMetaData label={locale('versions.protocol_version')} value={version.protocol_version} />
<VersionMetaData label={locale('versions.data_pack_format')} value={version.data_pack_version} />
<VersionMetaData label={locale('versions.resource_pack_format')} value={version.resource_pack_version} />
</div>
<h3>{locale('versions.technical_changes')}</h3>
<div class="version-changes">
<ChangelogList changes={filteredChangelogs} defaultOrder="asc" />
</div>
</div>
</>
}
export function releaseDate(version: VersionMeta) {
return new Date(version.release_time).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })
}

View File

@@ -0,0 +1,18 @@
import { releaseDate, VersionMetaData } from '.'
import { useLocale } from '../../contexts'
import type { VersionMeta } from '../../services'
interface Props {
version: VersionMeta,
link?: string,
}
export function VersionEntry({ version, link }: Props) {
const { locale } = useLocale()
return <a class="version-entry" href={link}>
<span class="version-id">{version.id}</span>
<VersionMetaData label={locale('versions.released')} value={releaseDate(version)} compact />
<VersionMetaData label={locale('versions.data_version')} value={version.data_version} optional />
<VersionMetaData label={locale('versions.pack_format')} value={version.data_pack_version} optional />
</a>
}

View File

@@ -0,0 +1,36 @@
import { useMemo, useState } from 'preact/hooks'
import { Checkbox, TextInput } from '..'
import { useLocale } from '../../contexts'
import type { VersionMeta } from '../../services'
import { VersionEntry } from './VersionEntry'
interface Props {
versions: VersionMeta[]
link?: (id: string) => string
}
export function VersionList({ versions, link }: Props) {
const { locale } = useLocale()
const [snapshots, setSnapshots] = useState(true)
const [search, setSearch] = useState('')
const filteredVersions = useMemo(() => versions.filter(v => {
if (v.type === 'snapshot' && !snapshots) return false
return v.id.includes(search)
}), [versions, snapshots, search])
return <>
<div class="versions-controls">
<TextInput class="btn btn-input version-search" list="sound-list" placeholder={locale('versions.search')}
value={search} onChange={setSearch} />
<Checkbox label="Include snapshots" value={snapshots} onChange={setSnapshots} />
</div>
<div class="version-list">
{filteredVersions.map(v => <VersionEntry version={v} link={link?.(v.id)} />)}
{filteredVersions.length === 0 && <span>
{locale('versions.no_results')}
</span>}
</div>
</>
}

View File

@@ -0,0 +1,16 @@
import { Octicon } from '..'
interface Props {
label: string,
value: string | number,
link?: string,
compact?: boolean,
optional?: boolean,
}
export function VersionMetaData({ label, value, link, compact, optional }: Props) {
return <div class={`version-metadata${optional ? ' version-metadata-hide' : ''}`}>
<span class={compact ? 'version-metadata-hide' : undefined}>{label}: </span>
<span class="version-metadata-value">{value}</span>
{link && <a href={link} class="version-metadata-link">{Octicon.link_external}</a>}
</div>
}

View File

@@ -0,0 +1,7 @@
export * from './ChangelogEntry'
export * from './ChangelogList'
export * from './ChangelogTag'
export * from './VersionDetail'
export * from './VersionEntry'
export * from './VersionList'
export * from './VersionMetaData'

View File

@@ -1,10 +1,8 @@
import { marked } from 'marked'
import { useEffect, useMemo, useState } from 'preact/hooks'
import { Ad, Btn, ErrorPanel, Octicon, TextInput } from '../components'
import { useEffect, useState } from 'preact/hooks'
import { Ad, ChangelogList, ErrorPanel } from '../components'
import { useLocale, useTitle } from '../contexts'
import type { ChangelogEntry, ChangelogVersion } from '../services'
import type { Change } from '../services'
import { getChangelogs } from '../services'
import { hashString } from '../Utils'
interface Props {
path?: string,
@@ -14,99 +12,19 @@ export function Changelog({}: Props) {
const [error, setError] = useState<string | null>(null)
useTitle(locale('title.changelog'))
const [changelogs, setChangelogs] = useState<ChangelogEntry[]>([])
const [changelogs, setChangelogs] = useState<Change[]>([])
useEffect(() => {
getChangelogs()
.then(changelogs => setChangelogs(changelogs))
.catch(e => { console.error(e); setError(e) })
}, [])
const [search, setSearch] = useState('')
const [tags, setTags] = useState<string[]>([])
const toggleTag = (tag: string) => {
if (!tags.includes(tag)) {
setTags([...tags, tag])
} else {
setTags(tags.filter(t => t !== tag))
}
}
const filteredChangelogs = useMemo(() => {
const query = search.split(' ').map(q => q.trim().toLowerCase()).filter(q => q.length > 0)
if (query.length === 0 && tags.length === 0) return changelogs
return changelogs.filter(change => {
if (!tags.every(tag => change.tags.includes(tag))) {
return false
}
const content = change.tags.join(' ') + ' ' + change.content.toLowerCase()
return query.every(q => {
if (q.startsWith('!')) {
return q.length === 1 || !content.includes(q.slice(1))
}
return content.includes(q)
})
})
}, [changelogs, search, tags])
const [sort, setSort] = useState(true)
const sortedChangelogs = useMemo(() => {
return filteredChangelogs.sort((a, b) => sort ? b.order - a.order : a.order - b.order)
}, [filteredChangelogs, sort])
return <main>
<Ad type="text" id="changelog" />
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
<div class="changelog-controls">
<div class="changelog-query">
<TextInput class="btn btn-input changelog-search" list="sound-list" placeholder={locale('changelog.search')}
value={search} onChange={setSearch} />
<Btn icon={sort ? 'sort_desc' : 'sort_asc'} label={sort ? 'Newest first' : 'Oldest first'} onClick={() => setSort(!sort)} />
</div>
{tags.length > 0 && <div class="changelog-tags">
{tags.map(tag => <Tag label={tag} onClick={() => setTags(tags.filter(t => t !== tag))} />)}
</div>}
</div>
<div class="changelog">
{sortedChangelogs.map(change =>
<Change change={change} activeTags={tags} toggleTag={toggleTag} />)}
<ChangelogList changes={changelogs} defaultOrder="desc" />
</div>
</main>
}
type ChangeProps = {
change: ChangelogEntry,
activeTags: string[],
toggleTag: (tag: string) => unknown,
}
function Change({ change, activeTags, toggleTag }: ChangeProps) {
return <div class="changelog-entry">
<div class="changelog-version">
<ArticleLink {...change.version}/>
<ArticleLink {...change.group}/>
</div>
<div class="changelog-tags">
{change.tags.map(tag => <Tag label={tag} onClick={() => toggleTag(tag)} active={activeTags.includes(tag)} />)}
</div>
<div class="changelog-content" dangerouslySetInnerHTML={{ __html: marked(change.content) }} />
</div>
}
function ArticleLink({ id, article }: ChangelogVersion) {
return article === null
? <span>{id}</span>
: <a href={`https://www.minecraft.net/en-us/article/${article}`} target="_blank">{id}</a>
}
type TagProps = {
label: string,
active?: boolean,
onClick?: () => unknown,
}
function Tag({ label, active, onClick }: TagProps) {
const color = label === 'breaking' ? 5 : hashString(label) % 360
return <div class={`changelog-tag${active ? ' active' : ''}${onClick ? ' clickable' : ''}`} style={`--tint: ${color}`} onClick={onClick}>
{label === 'breaking' && Octicon.alert}
{label}
</div>
}

View File

@@ -32,6 +32,7 @@ export function Home({}: Props) {
link="https://misode.github.io/upgrader/"
desc="Convert your data packs from 1.16 to 1.17 to 1.18" />
<ToolCard title="Technical Changelog" link="/changelog/" />
<ToolCard title="Minecraft Versions" link="/versions/" />
</div>
</main>
}

View File

@@ -0,0 +1,64 @@
import { getCurrentUrl } from 'preact-router'
import { useEffect, useState } from 'preact/hooks'
import { Ad, ErrorPanel, Octicon, VersionDetail, VersionList } from '../components'
import { useLocale, useTitle } from '../contexts'
import type { VersionMeta } from '../services'
import { fetchVersions } from '../services'
import { getSearchParams } from '../Utils'
interface Props {
path?: string,
}
export function Versions({}: Props) {
const { locale } = useLocale()
const [error, setError] = useState<string | null>(null)
useTitle(locale('title.versions'))
const [versions, setVersions] = useState<VersionMeta[]>([])
useEffect(() => {
fetchVersions()
.then(versions => setVersions(versions))
.catch(e => { console.error(e); setError(e) })
}, [])
const selectedId = getSearchParams(getCurrentUrl()).get('id')
const selected = versions.find(v => v.id === selectedId)
useTitle(selected ? selected.name : 'Versions Explorer', selected ? [] : undefined)
const nextVersion = selected && getOffsetVersion(versions, selected, -1)
const previousVersion = selected && getOffsetVersion(versions, selected, 1)
return <main>
<Ad type="text" id="versions" />
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
<div class="versions">
{selected ? <>
<div class="version-navigation">
<a class="btn btn-link" href="/versions/">
{Octicon.three_bars}
{locale('versions.all')}
</a>
<a class="btn btn-link" {...previousVersion ? {href: `/versions/?id=${previousVersion.id}`} : {disabled: true}}>
{Octicon.arrow_left}
{locale('versions.previous')}
</a>
<a class="btn btn-link" {...nextVersion ? {href: `/versions/?id=${nextVersion.id}`} : {disabled: true}}>
{locale('versions.next')}
{Octicon.arrow_right}
</a>
</div>
<VersionDetail version={selected} />
</> : <VersionList versions={versions} link={id => `/versions/?id=${id}`} />}
</div>
</main>
}
function getOffsetVersion(versions: VersionMeta[], current: VersionMeta, offset: number) {
const currentIndex = versions.findIndex(v => v.id === current.id)
const offsetIndex = currentIndex + offset
if (offsetIndex < 0 || offsetIndex >= versions.length) {
return undefined
}
return versions[offsetIndex]
}

View File

@@ -4,3 +4,4 @@ export * from './Generator'
export * from './Home'
export * from './Project'
export * from './Sounds'
export * from './Versions'

View File

@@ -2,7 +2,7 @@ import { isObject } from '../Utils'
const repo = 'https://raw.githubusercontent.com/misode/technical-changes/main'
export type ChangelogEntry = {
export type Change = {
group: ChangelogVersion,
version: ChangelogVersion,
order: number,
@@ -15,14 +15,14 @@ export type ChangelogVersion = {
article: string | null,
}
let Changelogs: ChangelogEntry[] | Promise<ChangelogEntry[]> | null = null
let Changelogs: Change[] | Promise<Change[]> | null = null
export async function getChangelogs() {
if (!Changelogs) {
const index = await (await fetch(`${repo}/index.json`)).json() as string[]
Changelogs = (await Promise.all(
index.map((group, i) => fetchGroup(parseVersion(group), i))
)).flat().map<ChangelogEntry>(change => ({
)).flat().map<Change>(change => ({
...change,
tags: [change.group.id, ...change.tags],
}))

View File

@@ -1,7 +1,7 @@
import type { CollectionRegistry } from '@mcschema/core';
import config from '../../config.json';
import { message } from '../Utils';
import type { BlockStateRegistry, VersionId } from './Schemas';
import type { CollectionRegistry } from '@mcschema/core'
import config from '../../config.json'
import { message } from '../Utils'
import type { BlockStateRegistry, VersionId } from './Schemas'
// Cleanup old caches
['1.15', '1.16', '1.17'].forEach(v => localStorage.removeItem(`cache_${v}`));
@@ -23,10 +23,20 @@ const mcmetaUrl = 'https://raw.githubusercontent.com/misode/mcmeta'
type McmetaTypes = 'summary' | 'data' | 'assets' | 'registries'
function mcmeta(version: Version, type: McmetaTypes) {
function mcmeta(version: { dynamic: true } | { dynamic?: false, ref?: string}, type: McmetaTypes) {
return `${mcmetaUrl}/${version.dynamic ? type : `${version.ref}-${type}`}`
}
async function validateCache(version: Version) {
if (version.dynamic) {
if (localStorage.getItem(CACHE_LATEST_VERSION) !== latestVersion) {
await deleteMatching(url => url.startsWith(`${mcmetaUrl}/summary/`) || url.startsWith(`${mcmetaUrl}/data/`))
localStorage.setItem(CACHE_LATEST_VERSION, latestVersion)
}
version.ref = latestVersion
}
}
export async function fetchData(versionId: string, collectionTarget: CollectionRegistry, blockStateTarget: BlockStateRegistry) {
const version = config.versions.find(v => v.id === versionId) as Version | undefined
if (!version) {
@@ -34,13 +44,7 @@ export async function fetchData(versionId: string, collectionTarget: CollectionR
return
}
if (version.dynamic) {
if (localStorage.getItem(CACHE_LATEST_VERSION) !== latestVersion) {
await deleteMatching(url => url.startsWith(`${mcmetaUrl}/summary/`) || url.startsWith(`${mcmetaUrl}/data/`))
localStorage.setItem(CACHE_LATEST_VERSION, latestVersion)
}
version.ref = latestVersion
}
await validateCache(version)
await Promise.all([
fetchRegistries(version, collectionTarget),
@@ -91,6 +95,7 @@ export async function fetchPreset(versionId: VersionId, registry: string, id: st
export async function fetchAllPresets(versionId: VersionId, registry: string) {
console.debug(`[fetchAllPresets] ${versionId} ${registry}`)
const version = config.versions.find(v => v.id === versionId)!
await validateCache(version)
try {
const entries = await getData(`${mcmeta(version, 'registries')}/${registry}/data.min.json`)
return new Map<string, unknown>(await Promise.all(
@@ -109,6 +114,7 @@ export type SoundEvents = {
}
export async function fetchSounds(versionId: VersionId): Promise<SoundEvents> {
const version = config.versions.find(v => v.id === versionId)!
await validateCache(version)
try {
const url = `${mcmeta(version, 'summary')}/sounds/data.min.json`
return await getData(url)
@@ -122,6 +128,30 @@ export function getSoundUrl(versionId: VersionId, path: string) {
return `${mcmeta(version, 'assets')}/assets/minecraft/sounds/${path}.ogg`
}
export type VersionMeta = {
id: string,
name: string,
release_target: string,
type: 'snapshot' | 'release',
stable: boolean,
data_version: number,
protocol_version: number,
data_pack_version: number,
resource_pack_version: number,
build_time: string,
release_time: string,
sha1: string,
}
export async function fetchVersions(): Promise<VersionMeta[]> {
const version = config.versions[config.versions.length - 1]
await validateCache(version)
try {
return getData(`${mcmeta(version, 'summary')}/versions/data.min.json`)
} catch (e) {
throw new Error(`Error occured while fetching versions: ${message(e)}`)
}
}
async function getData<T = any>(url: string, fn: (v: any) => T = (v: any) => v): Promise<T> {
try {
const cache = await caches.open(CACHE_NAME)

View File

@@ -6,6 +6,7 @@
"assets": "Assets",
"block_definition": "Blockstate",
"changelog.search": "Search changes",
"changelog.no_results": "No changes",
"collapse": "Collapse",
"collapse_all": "Hold %0% to collapse all",
"configure_layers": "Configure layers",
@@ -51,6 +52,7 @@
"layer.factor": "Factor",
"layer.jaggedness": "Jaggedness",
"highlighting": "Highlighting",
"loading": "Loading...",
"loot_table": "Loot Table",
"model": "Model",
"more": "More",
@@ -78,6 +80,7 @@
"title.home": "Data Pack Generators",
"title.project": "%0% Project",
"title.sounds": "Sound Explorer",
"title.versions": "Versions Explorer",
"presets": "Presets",
"preview": "Visualize",
"preview.auto_scroll": "Auto scroll",
@@ -117,6 +120,19 @@
"switch_version": "Switch version",
"terrain_settings": "Terrain settings",
"undo": "Undo",
"versions.search": "Search versions",
"versions.no_results": "No results",
"versions.all": "All versions",
"versions.previous": "Previous",
"versions.next": "Next",
"versions.released": "Released",
"versions.release_target": "Release target",
"versions.data_version": "Data version",
"versions.protocol_version": "Protocol version",
"versions.pack_format": "Pack format",
"versions.data_pack_format": "Data pack format",
"versions.resource_pack_format": "Resource pack format",
"versions.technical_changes": "Technical changes",
"world": "World Settings",
"worldgen": "Worldgen",
"worldgen/biome": "Biome",

View File

@@ -273,7 +273,8 @@ main > .controls {
.sounds-controls > *:not(:last-child),
.preview-controls > *:not(:last-child),
.generator-controls > *:not(:last-child) {
.generator-controls > *:not(:last-child),
.versions-controls > *:not(:last-child) {
margin-right: 8px;
}
@@ -458,6 +459,21 @@ main.has-preview {
margin-right: 5px;
}
.btn-link {
text-decoration: none;
display: inline-flex;
}
.btn-link svg {
margin-left: 4px;
margin-right: 4px;
}
.btn-link:not([href]) {
cursor: default;
background-color: var(--background-2) !important;
}
.btn-menu:not(.no-relative) {
position: relative;
}
@@ -749,7 +765,7 @@ main.has-preview {
color: var(--text-1)
}
.home, .category, .project {
.home, .category, .project, .versions {
padding: 16px;
max-width: 960px;
margin: 0 auto;
@@ -1230,6 +1246,10 @@ hr {
text-decoration: underline;
}
.changelog-content {
word-wrap: break-word;
}
.changelog-content ul {
padding-left: 24px;
}
@@ -1241,14 +1261,9 @@ hr {
color: var(--text-1);
}
.changelog-controls {
display: flex;
flex-direction: column;
padding: 0 16px;
}
.changelog-query {
display: flex;
margin-bottom: 8px;
}
.changelog-query > *:not(:first-child) {
@@ -1257,13 +1272,121 @@ hr {
.changelog-search {
flex-basis: 100%;
padding: 8px;
background-color: var(--background-2);
border-radius: 6px;
}
.changelog-controls .changelog-tags {
margin: 8px 0 0;
.changelog-tags {
margin-bottom: 8px;
}
.versions-controls {
display: flex;
padding-bottom: 16px;
}
.version-search {
min-width: 0px;
background-color: var(--background-2);
}
.checkbox {
display: flex;
align-items: center;
padding: 7px 11px;
border-radius: 6px;
height: 32px;
font-size: 1rem;
color: var(--text-2);
background-color: var(--background-2);
box-shadow: 0 1px 7px -2px #000;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.checkbox input {
margin-right: 8px;
}
.versions-controls .checkbox {
white-space: nowrap;
}
.version-list {
display: flex;
flex-direction: column;
}
.version-entry {
display: grid;
grid-template-columns: 0.8fr 1.2fr 1fr 0.8fr;
gap: 8px;
background: var(--background-2);
border-radius: 4px;
margin-bottom: 8px;
padding: 8px;
text-decoration: none;
}
.version-entry:hover {
background: var(--background-3);
}
.version-entry > .version-metadata {
font-size: 1rem;
align-self: center;
}
.version-entry > .version-id {
color: var(--text-1);
font-size: 1.1rem;
}
.version-navigation {
display: flex;
}
.version-navigation > *:not(:last-child) {
margin-right: 8px;
}
.version-detail {
color: var(--text-3);
}
.version-detail h2,
.version-detail h3,
.version-detail h4 {
color: var(--text-2);
margin-top: 24px;
margin-bottom: 8px;
}
.version-info {
background: var(--background-2);
border-radius: 6px;
padding: 7px 11px;
box-shadow: 0 1px 5px -2px #000;
}
.version-metadata {
color: var(--text-3);
font-size: 1.2rem;
}
.version-metadata-value {
color: var(--text-1);
}
.version-metadata-link {
fill: var(--text-2);
vertical-align: middle;
margin-left: 8px;
}
.version-metadata-link:hover {
fill: var(--accent-primary);
}
.ace_editor,
@@ -1348,6 +1471,10 @@ hr {
.sound-config .volume { grid-area: volume; }
.sound-config .copy { grid-area: copy; }
.sound-config .remove { grid-area: remove; }
.version-entry {
grid-template-columns: 1fr 1fr;
}
}
@keyframes spinning {
@@ -1428,4 +1555,8 @@ hr {
.generator-picker {
justify-content: center;
}
.version-metadata-hide {
display: none;
}
}