File size: 3,474 Bytes
782dc23
 
e63a744
 
 
782dc23
 
e63a744
 
 
 
 
 
 
 
 
 
 
782dc23
e63a744
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
782dc23
 
e63a744
 
 
 
 
 
 
 
 
 
 
 
 
 
782dc23
e63a744
 
 
 
 
 
782dc23
 
e63a744
 
782dc23
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
"use client";

import { useEffect, useState } from "react";
import { cx, Spinner } from "@/components/ui";
import { exitMs, usePresence } from "@/utils/motion";

/**

 * Loading veil over the content area (`absolute`: the nearest positioned ancestor is the episode

 * page's content column, which keeps the same geometry from the data-loading shell to the loaded

 * page, so the spinner holds its spot through both phases while the header and sidebar stay put).

 *

 * - `visible={false}` fades the veil out (with clicks passing through) and then unmounts it, so

 *   the content appears through it instead of popping.

 * - `appear={false}` is for taking over from the previous phase's veil: it shows at once, without

 *   replaying the fade-in (which would read as a blink) or announcing the status a second time.

 *   A later reopen of the same instance fades in again. Taking over with nothing left to load

 *   (`visible={false}` from the start, e.g. a view with no loading phase of its own) plays the

 *   previous veil's fade-out from here instead of letting it vanish.

 */
export default function Loading({

  visible = true,

  appear = true,

}: {

  visible?: boolean;

  appear?: boolean;

} = {}) {
  const presence = usePresence(visible, "--duration-fast", 250);
  const [handoff, setHandoff] = useState(!appear && !visible);
  useEffect(() => {
    if (!handoff) return;
    const t = window.setTimeout(
      () => setHandoff(false),
      exitMs("--duration-fast", 250),
    );
    return () => window.clearTimeout(t);
  }, [handoff]);
  const state = handoff && presence === "closed" ? "closing" : presence;
  const [fresh, setFresh] = useState(appear);
  if (state === "closed" && !fresh) setFresh(true);
  if (state === "closed") return null;
  const closing = state === "closing";
  return (
    <div

      data-state={state}

      inert={closing}

      aria-hidden={closing || undefined}

      role={fresh ? "status" : undefined}

      aria-live={fresh ? "polite" : undefined}

      className={cx(

        "absolute inset-0 z-10 flex flex-col items-center justify-center bg-bg/70 backdrop-blur-md",

        // Opacity only: never animate the blur radius of a layer this size. `starting:` only

        // applies to a veil mounted already closing (the hand-off above): it fades from opaque.

        "transition-opacity duration-(--duration-fast) ease-(--ease-out)",

        closing

          ? "pointer-events-none opacity-0 starting:opacity-100"

          : "opacity-100",

      )}

    >

      <div

        className={cx(

          "flex flex-col items-center gap-3",

          fresh && "animate-fade-in",

        )}

      >

        {/* The rotation lives on this div, not on the SVG: a transformed HTML box runs on the

            compositor, so it keeps spinning while the main thread is busy decoding parquet and

            building charts. Stepped like the shared Spinner. */}

        <div className="h-7 w-7 animate-spin text-fg-secondary [animation-duration:900ms] [animation-timing-function:steps(12)] [will-change:transform]">

          <Spinner className="h-7 w-7 animate-none!" />

        </div>

        <div className="text-center">

          <h1 className="text-13 font-medium text-fg-secondary">Loading</h1>

          <p className="mt-0.5 text-2xs text-fg-secondary">

            Preparing data and videos

          </p>

        </div>

      </div>

    </div>
  );
}