Add skeleton loading indicators

This commit is contained in:
Misode
2024-11-20 22:06:29 +01:00
parent 56b2e1a382
commit f3de707224
7 changed files with 109 additions and 41 deletions

View File

@@ -0,0 +1,37 @@
import type { DocAndNode } from '@spyglassmc/core'
import { JsonFileNode } from '@spyglassmc/json'
import { useErrorBoundary } from 'preact/hooks'
import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx'
import { message } from '../../Utils.js'
import { ErrorPanel } from '../ErrorPanel.jsx'
import { JsonFileView } from './JsonFileView.jsx'
type FileViewProps = {
docAndNode: DocAndNode | undefined,
}
export function FileView({ docAndNode: original }: FileViewProps) {
const { serviceLoading } = useSpyglass()
const [error, errorRetry] = useErrorBoundary()
if (error) {
return <ErrorPanel error={`Error viewing the file: ${message(error)}`} onDismiss={errorRetry} />
}
const docAndNode = useDocAndNode(original)
if (!docAndNode || serviceLoading) {
return <div class="file-view flex flex-col gap-1">
<div class="skeleton rounded-md h-[34px] w-[200px]"></div>
<div class="skeleton rounded-md h-[34px] w-[240px]"></div>
<div class="skeleton rounded-md h-[34px] w-[190px] ml-[18px]"></div>
<div class="skeleton rounded-md h-[34px] w-[130px] ml-[18px]"></div>
<div class="skeleton rounded-md h-[34px] w-[290px]"></div>
</div>
}
const fileNode = docAndNode?.node.children[0]
if (JsonFileNode.is(fileNode)) {
return <JsonFileView docAndNode={docAndNode} node={fileNode.children[0]} />
}
return <ErrorPanel error={`Cannot view file ${docAndNode.doc.uri}`} />
}

View File

@@ -2,35 +2,19 @@ import type { DocAndNode, Range } from '@spyglassmc/core'
import { dissectUri } from '@spyglassmc/java-edition/lib/binder/index.js'
import type { JsonNode } from '@spyglassmc/json'
import { JsonFileNode } from '@spyglassmc/json'
import { useCallback, useErrorBoundary, useMemo } from 'preact/hooks'
import { useLocale } from '../../contexts/index.js'
import { useDocAndNode, useSpyglass } from '../../contexts/Spyglass.jsx'
import { useCallback, useMemo } from 'preact/hooks'
import { useSpyglass } from '../../contexts/Spyglass.jsx'
import { getRootType, simplifyType } from './McdocHelpers.js'
import type { McdocContext } from './McdocRenderer.jsx'
import { McdocRoot } from './McdocRenderer.jsx'
type TreePanelProps = {
type JsonFileViewProps = {
docAndNode: DocAndNode,
onError: (message: string) => unknown,
node: JsonNode,
}
export function Tree({ docAndNode: original, onError }: TreePanelProps) {
const { lang } = useLocale()
export function JsonFileView({ docAndNode, node }: JsonFileViewProps) {
const { service } = useSpyglass()
if (lang === 'none') return <></>
const docAndNode = useDocAndNode(original)
const fileChild = docAndNode.node.children[0]
if (!JsonFileNode.is(fileChild)) {
return <></>
}
const [error] = useErrorBoundary(e => {
onError(`Error rendering the tree: ${e.message}`)
console.error(e)
})
if (error) return <></>
const makeEdit = useCallback((edit: (range: Range) => JsonNode | undefined) => {
if (!service) {
return
@@ -62,15 +46,15 @@ export function Tree({ docAndNode: original, onError }: TreePanelProps) {
}, [docAndNode, service, makeEdit])
const resourceType = useMemo(() => {
if (original.doc.uri.endsWith('/pack.mcmeta')) {
if (docAndNode.doc.uri.endsWith('/pack.mcmeta')) {
return 'pack_mcmeta'
}
if (ctx === undefined) {
return undefined
}
const res = dissectUri(original.doc.uri, ctx)
const res = dissectUri(docAndNode.doc.uri, ctx)
return res?.category
}, [original, ctx])
}, [docAndNode, ctx])
const mcdocType = useMemo(() => {
if (!ctx || !resourceType) {
@@ -81,8 +65,8 @@ export function Tree({ docAndNode: original, onError }: TreePanelProps) {
return type
}, [resourceType, ctx])
return <div class="tree node-root" data-category={getCategory(resourceType)}>
{(ctx && mcdocType) && <McdocRoot type={mcdocType} node={fileChild.children[0]} ctx={ctx} />}
return <div class="file-view tree node-root" data-category={getCategory(resourceType)}>
{(ctx && mcdocType) && <McdocRoot type={mcdocType} node={node} ctx={ctx} />}
</div>
}

View File

@@ -144,9 +144,15 @@ export function ProjectPanel() {
{project.name !== DRAFT_PROJECT.name && <Btn icon="trashcan" label={locale('project.delete')} onClick={onDeleteProject} />}
</BtnMenu>
</div>
<div class="file-view">
<div class="project-files">
{entries === undefined
? <></>
? <div class="p-2 flex flex-col gap-2">
<div class="skeleton-2 rounded h-4 w-24"></div>
<div class="skeleton-2 rounded h-4 w-32 ml-4"></div>
<div class="skeleton-2 rounded h-4 w-24 ml-8"></div>
<div class="skeleton-2 rounded h-4 w-36 ml-8"></div>
<div class="skeleton-2 rounded h-4 w-28"></div>
</div>
: entries.length === 0
? <span>{locale('project.no_files')}</span>
: <TreeView entries={entries} split={path => path.split('/')} group={FolderEntry} leaf={FileEntry} />}

View File

@@ -13,7 +13,7 @@ import { checkVersion, fetchDependencyMcdoc, fetchPreset, fetchRegistries, getSn
import { DEPENDENCY_URI } from '../../services/Spyglass.js'
import { Store } from '../../Store.js'
import { cleanUrl, genPath } from '../../Utils.js'
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, Footer, HasPreview, Octicon, PreviewPanel, ProjectPanel, SearchList, SourcePanel, TextInput, Tree, VersionSwitcher } from '../index.js'
import { Ad, Btn, BtnMenu, ErrorPanel, FileCreation, FileView, Footer, HasPreview, Octicon, PreviewPanel, ProjectPanel, SearchList, SourcePanel, TextInput, VersionSwitcher } from '../index.js'
import { getRootDefault } from './McdocHelpers.js'
export const SHARE_KEY = 'share'
@@ -59,7 +59,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
const [sharedSnippetId, setSharedSnippetId] = useSearchParam(SHARE_KEY)
const ignoreChange = useRef(false)
const { value: docAndNode } = useAsync(async () => {
const { value: docAndNode, loading: docLoading } = useAsync(async () => {
let text: string | undefined = undefined
if (currentPreset && sharedSnippetId) {
setSharedSnippetId(undefined)
@@ -386,7 +386,7 @@ export function SchemaGenerator({ gen, allowedVersions }: Props) {
</BtnMenu>
</div>
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
{docAndNode && <Tree docAndNode={docAndNode} onError={setError} />}
<FileView docAndNode={docLoading ? undefined : docAndNode} />
<Footer donate={!gen.tags?.includes('partners')} />
</main>
<div class="popup-actions right-actions" style={`--offset: -${8 + actionsShown * 50}px;`}>

View File

@@ -1,10 +1,11 @@
export * from './FileCreation.js'
export * from './FileRenaming.js'
export * from './FileView.jsx'
export * from './GeneratorCard.jsx'
export * from './GeneratorList.jsx'
export * from './JsonFileView.jsx'
export * from './PreviewPanel.js'
export * from './ProjectCreation.js'
export * from './ProjectDeletion.js'
export * from './ProjectPanel.js'
export * from './SourcePanel.js'
export * from './Tree.js'

View File

@@ -11,6 +11,7 @@ import { useVersion } from './Version.jsx'
interface SpyglassContext {
client: SpyglassClient
service: SpyglassService | undefined
serviceLoading: boolean
}
const SpyglassContext = createContext<SpyglassContext | undefined>(undefined)
@@ -59,13 +60,14 @@ export function SpyglassProvider({ children }: { children: ComponentChildren })
const { version } = useVersion()
const [client] = useState(new SpyglassClient())
const { value: service } = useAsync(() => {
const { value: service, loading: serviceLoading } = useAsync(() => {
return client.createService(version)
}, [client, version])
const value: SpyglassContext = {
client,
service,
serviceLoading,
}
return <SpyglassContext.Provider value={value}>

View File

@@ -1,7 +1,9 @@
:root {
--background-1: #fafafa;
--background-2: #e2e2e2;
--background-2-shimmer: #d9d9d9;
--background-3: #d4d3d3;
--background-3-shimmer: #cbcbcb;
--background-4: #b8b8b8;
--background-5: #bdbdbd;
--background-6: #cecece;
@@ -53,8 +55,10 @@
:root.dark {
--background-1: #1b1b1b;
--background-2: #252525;
--background-2-shimmer: #2c2c2c;
--background-3: #222222;
--background-4: #3d3d3d;
--background-4-shimmer: #424242;
--background-5: #383838;
--background-6: #575757;
--text-1: #ffffff;
@@ -361,16 +365,48 @@ main > .controls {
z-index: 10;
}
.tree {
.file-view {
margin-top: -40px;
overflow-x: auto;
padding: 8px 16px 50vh;
}
.error + .tree {
.error + .file-view {
margin-top: 0;
}
.skeleton {
opacity: 1;
background-image: linear-gradient(90deg, var(--background-2) 0px, var(--background-2-shimmer) 38px, var(--background-2-shimmer) 42px, var(--background-2) 80px);
background-size: 600px;
animation: skeleton-shimmer 2.5s infinite linear, skeleton-fade-in 1s forwards linear;
}
.skeleton-2 {
opacity: 1;
background-image: linear-gradient(90deg, var(--background-4) 0px, var(--background-4-shimmer) 38px, var(--background-4-shimmer) 42px, var(--background-4) 80px);
background-size: 600px;
animation: skeleton-shimmer 2.5s infinite linear, skeleton-fade-in 1s forwards linear;
}
@keyframes skeleton-shimmer {
0% {
background-position: -110px;
}
40%, 100% {
background-position: 300px;
}
}
@keyframes skeleton-fade-in {
0%, 20% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.popup-source {
position: fixed;
display: flex;
@@ -1434,7 +1470,7 @@ main.has-project {
font-size: 16px;
}
[data-modals] .tree {
[data-modals] .file-view {
pointer-events: none;
}
@@ -1786,7 +1822,7 @@ hr {
content: '\200b';
}
.file-view {
.project-files {
background-color: var(--background-2);
color: var(--text-2);
overflow: hidden;
@@ -1795,7 +1831,7 @@ hr {
flex-grow: 1;
}
.file-view > span {
.project-files > span {
padding: 4px 8px;
}
@@ -1943,8 +1979,10 @@ hr {
}
.ea-content {
opacity: 1;
margin: 0 !important;
background: var(--background-2) !important;
animation: skeleton-fade-in 0.5s forwards linear;
}
.ea-content span {
@@ -2978,13 +3016,13 @@ hr {
}
@media screen and (max-width: 1300px) {
main.has-preview .tree {
main.has-preview .file-view {
margin-top: 4px;
}
}
@media screen and (max-width: 800px) {
main .tree {
main .file-view {
margin-top: 4px !important;
}
}
@@ -3015,7 +3053,7 @@ hr {
top: 64px
}
.tree {
.file-view {
padding-left: 8px;
padding-right: 8px;
}