Frontend Forever App
We have a mobile app for you to download and use. And you can unlock many features in the app.
Get it now
Intall Later
Run
HTML
CSS
Javascript
Output
Document
@charset "UTF-8"; @import url(https://fonts.googleapis.com/css?family=Nunito+Sans:300,400,600,700,800); *, :after, :before { box-sizing: border-box; padding: 0; margin: 0; } * { border: 0; box-sizing: border-box; margin: 0; padding: 0; } :root { --hue: 223; --bg: hsl(var(--hue),10%,90%); --fg: hsl(var(--hue),10%,10%); --gray1: hsl(var(--hue),10%,60%); --gray2: hsl(var(--hue),10%,40%); --neutral: hsl(var(--hue),10%,50%); --success: hsl(153,90%,35%); --warning: hsl(33,90%,50%); --trans-dur: 0.3s; --trans-timing: cubic-bezier(0.65,0,0.35,1); font-size: clamp(1rem,0.95rem + 0.25vw,1.25rem); } body { background-color: var(--bg); color: var(--fg); display: flex; font: 1em/1.5 "DM Sans", sans-serif; height: 100vh; transition: background-color var(--trans-dur), color var(--trans-dur); } main { display: flex; overflow-x: hidden; width: 100vw; height: 100vh; } svg { polyline { transition: stroke var(--trans-dur); } } .icon { display: block; overflow: visible; width: 1.5em; height: 1.5em; transition: color var(--trans-dur); &--neutral { color: var(--neutral); } &--success { color: var(--success); } &--warning { color: var(--warning); } } .loading { display: flex; overflow: hidden; height: 100%; &--done { overflow: visible; height: 26.25em; } &, &__step, &__steps { width: 100%; } &, &__steps { margin: auto; } &, &__step { display: flex; } &__ellipsis { display: inline-flex; &-dot { --dot-dur: 2s; animation: ellipsis-dot-1 var(--dot-dur) steps(1,end) infinite; visibility: hidden; @for $d from 2 through 3 { &:nth-child(#{$d}) { animation-name: ellipsis-dot-#{$d}; } } } } &__step { gap: 1em; padding: 0 1.5em; position: absolute; top: 0; left: 0; height: 5.25em; transition: opacity var(--trans-dur), transform var(--trans-dur) var(--trans-timing); &-info { color: var(--gray2); font-size: 0.75em; line-height: 1.333; opacity: 0; transition: color var(--trans-dur), opacity var(--trans-dur); } &--in &-info { opacity: 1; } &-title { font-size: 1.25em; font-weight: 500; line-height: 1.2; margin-bottom: 0.25rem; } } &__steps { position: relative; height: 2.75em; max-width: 27em; } } /* Dark theme */ @media (prefers-color-scheme: dark) { :root { --bg: hsl(var(--hue),10%,10%); --fg: hsl(var(--hue),10%,90%); } .loading { &__step { &-info { color: var(--gray1); } } } } /* Animations */ @keyframes ellipsis-dot-1 { from { visibility: hidden; } 25%, to { visibility: visible; } } @keyframes ellipsis-dot-2 { from, 25% { visibility: hidden; } 50%, to { visibility: visible; } } @keyframes ellipsis-dot-3 { from, 50% { visibility: hidden; } 75%, to { visibility: visible; } }
console.log("Event Fired") import React, { StrictMode, useEffect, useRef, useState } from "https://esm.sh/react"; import { createRoot } from "https://esm.sh/react-dom/client"; createRoot(document.getElementById("root")!).render(
); function Icon({ icon, color }: IconProps) { const colorClass = color ? ` icon--${color}` : ""; return (
); } function IconSprites() { const viewBox = "0 0 16 16"; const emptyCircleRectAngles = []; const emptyCircleRectAngleCount = 16; for (let r = 0; r < emptyCircleRectAngleCount; ++r) { emptyCircleRectAngles.push(360 / emptyCircleRectAngleCount * r); } return (
{ emptyCircleRectAngles.map((rotation,i) =>
) }
) } function Loading() { const [step, setStep] = useState(0); const progressFrame = useRef(0); const steps: LoadingStep[] = [ { id: 0, state: "waiting", title: "Preparing" }, { id: 1, state: "waiting", title: "Downloading", filesPreparedMax: 50 }, { id: 2, state: "waiting", title: "Analyzing", filesPreparedMax: 50 }, { id: 3, state: "waiting", title: "Creating" }, { id: 4, state: "waiting", title: "Finalizing" }, ]; const stepCount = useRef(steps.length); const [stepObjects, setStepObjects] = useState(steps); const allStepsDone = step === stepCount.current; /** * Increment file preparation or set the state of a loading step. * @param item Loading step */ function updatedItem(item: LoadingStep) { const { id, state, start, filesPrepared, filesPreparedMax } = item; const updated: LoadingStep = { id, state }; if (!start) { updated.start = new Date(); updated.state = "progress"; // don’t proceed to file preparation if not applicable if (!filesPreparedMax) return updated; } if (filesPreparedMax) { // increment prepared files const prepared = filesPrepared === undefined ? -1 : filesPrepared; const preparedInc = 1; updated.filesPrepared = Math.min(prepared + preparedInc,filesPreparedMax); } if (!filesPreparedMax || updated.filesPrepared === filesPreparedMax) { // mark it done if no files to prepare or all files are prepared updated.finish = new Date(); updated.state = "done"; } return updated; }; useEffect(() => { const updatePromise = async (delay: number = 0) => await new Promise((resolve) => { progressFrame.current = setTimeout(resolve,delay); }).then(() => { setStepObjects((prev) => prev.map((item) => { if (item.id !== step) return item; const updated = updatedItem(item); if (updated.state === "done") { clearTimeout(progressFrame.current); setStep((step) => step + 1); } return { ...item, ...updated }; })); // loop if (step === 1 || step === 2) updatePromise(50); else if (step < stepCount.current) updatePromise(1500); }); updatePromise(); return () => clearTimeout(progressFrame.current); }, [step]); return (
{stepObjects.map((s,i) => { const { state, title, start, finish, filesPrepared, filesPreparedMax } = s; let distance = i; if (allStepsDone) { // keep the middle step in the center distance -= Math.floor(stepCount.current / 2); } else { // allow waiting items to be slightly closer to each other let moveBy = step; if (i > step + 1) { const stepHeight = 5.25; moveBy += (i - (step + 1)) * (1.5 / stepHeight); } distance -= moveBy; } const fade = allStepsDone ? 0 : Math.abs(i - step); return
})}
); } function LoadingEllipsis() { return (
.
.
.
); } function LoadingStepBlock({ state, title = "", distance = 0, fade = 0, start, finish, filesPrepared, filesPreparedMax = 0 }: LoadingStepBlockProps) { const stateMap = { waiting: "empty-circle", progress: "half-circle", done: "check-circle" }; const colorMap = { waiting: "neutral", progress: "warning", done: "success" }; const style = { opacity: 1 - fade * 0.225, transform: `translateY(${100 * distance}%)` }; /** * Get a friendly-formatted date. * @param date Date */ function dateFormatted(date: Date) { return new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeStyle: "medium", }).format(date); } return (
{title}
{start ? `${dateFormatted(start)} — ` : ""} {finish ? dateFormatted(finish) : (state === "progress" ?
: "")}
{filesPrepared !== undefined ? `${filesPrepared} of ${filesPreparedMax} files prepared` : ""}
); } type IconProps = { icon: string; color?: string; }; type LoadingState = "waiting" | "progress" | "done"; type LoadingStepBlockProps = { state: LoadingState; title?: string; distance?: number; fade?: number; start?: Date; finish?: Date; filesPrepared?: number; filesPreparedMax?: number; } interface LoadingStep { id: number; state: LoadingState; title?: string; start?: Date; finish?: Date; filesPrepared?: number; filesPreparedMax?: number; }