Desktop loading indicator (#13182)

* desktop progress bar

* added homepage loading message

* dark mode hourglass in forums

* inline the lotties

* CSS hourglass spinner

* load a default loading state even faster

* everybody wants their own init file now

* no thank you

* Revert "no thank you"

This reverts commit 80fffc1a05.

* fixes

---------

Co-authored-by: Eli Kinsey <eli@ekinsey.dev>
This commit is contained in:
Cory Watilo
2025-10-17 17:28:30 -04:00
committed by GitHub
parent 9565ee6ade
commit 52c533d144
10 changed files with 2797 additions and 2 deletions

83
WARP.md Normal file
View File

@@ -0,0 +1,83 @@
# WARP.md
This file provides guidance to WARP (warp.dev) when working with code in this repository.
## Development Commands
### Core Development
- **Start development server**: `yarn start` (runs on port 8001)
- **Clean and restart**: `yarn clean && yarn && yarn start` (when things get weird)
- **Build production**: `yarn build`
- **Serve production build locally**: `gatsby build && gatsby serve`
- **Clean build cache**: `gatsby clean`
### Code Quality
- **Format code**: `yarn format` (formats all HTML, JS, TS, TSX, JSON, YML, CSS, SCSS)
- **Format docs specifically**: `yarn format:docs` (fixes MDX formatting for changed files)
- **Run tests**: `yarn test-redirects` (tests redirect configuration)
- **Check links after build**: `yarn check-links-post-build`
### Content & Assets
- **Update SVG sprite**: `yarn update-sprite` (for product feature icons)
- **Generate TypeScript types**: `yarn typegen` (for Kea logic)
### Storybook
- **Run Storybook**: `yarn storybook` (port 6006)
- **Build Storybook**: `yarn build-storybook`
## Architecture Overview
### Desktop OS-Style Website
PostHog.com is designed as a desktop operating system experience in the browser:
- **Desktop Environment**: The main interface (`src/components/Desktop/index.tsx`) renders a macOS-style desktop with draggable icons
- **Window Management**: Pages open in draggable, resizable windows (`src/components/AppWindow/index.tsx`) that can be maximized, minimized, or snapped
- **Multi-Window Support**: Users can have multiple windows open simultaneously with a windows panel to manage them
- **Global State**: Window management and app state handled by `src/context/App.tsx`
### Content Architecture
- **Gatsby Static Site**: Built with Gatsby 4.25.9, hosted on Vercel
- **MDX Content**: All content stored in `/contents/` directory (docs, blog, handbook, tutorials, etc.)
- **Multi-Product Structure**: PostHog is multi-product, with product data centralized in hooks (`useProduct.ts`, `useProducts.tsx`)
- **Customer Data**: Customer information centralized in `useCustomers.ts`
- **Navigation**: All navigation menus defined in `src/navs/index.js`
### Key Integrations
- **Squeak Forum**: In-house community forum integration
- **Ashby Jobs**: Job listings via Ashby integration
- **Shopify Merch Store**: Headless Shopify integration for merchandise
- **Algolia Search**: Site-wide search functionality
- **PostHog Analytics**: Self-hosted analytics (naturally)
### Component Structure
- **Radix UI**: Custom Radix components in `/src/components/RadixUI/` with "Radix" prefix for imports
- **OS Components**: Custom-built components prefixed with "OS" (OSButton, OSFieldset, OSIcons, OSTable, OSTabs)
- **App Components**: Desktop "apps" like ReaderView, Wizard, Explorer for different content types
### Styling
- **Tailwind CSS**: Primary styling system with custom color schemes
- **Three Color Schemes**: Primary/secondary/tertiary with light/dark mode support
- **Design System**: Consistent spacing, colors defined in `tailwind.config.js`
- **Custom CSS**: Additional styles in `src/styles/global.css`
## Environment Setup
- **Node**: Version 22.x required (use `nvm use` if available)
- **Package Manager**: Yarn (not npm)
- **Port**: Development server runs on 8001 (not default 8000)
- **Apple Silicon**: May need to install vips (`brew install vips`) and remove node_modules
## Working with Content
- **Content Location**: All written content in `/contents/` directory
- **Product Data**: Check `useProduct.ts` first, then `useProducts.tsx` for product information
- **Customer References**: Use `useCustomers.ts` for customer data (names, logos, quotes)
- **Navigation Changes**: Modify `src/navs/index.js` but note it's shared with live site
## API Integrations
- **Local PostHog API**: Override schema URL with `POSTHOG_OPEN_API_SPEC_URL` env var
- **External Services**: Require API keys for Ashby (jobs), GitHub (contributors), Shopify (merch)
- **Development**: Most features work without API keys, but some require environment variables
## Build & Deployment
- **Vercel Hosting**: Deployed via Vercel with branch previews
- **Build Process**: Gatsby build with custom post-build processing
- **Redirects**: Test redirect configuration with Jest
- **Performance**: Uses Cloudinary for image optimization and lazy loading

View File

@@ -28,13 +28,20 @@ export const wrapPageElement = ({ element, props: { location } }) => {
)
}
export const onRenderBody = function ({ setPreBodyComponents }) {
export const onRenderBody = function ({ setPreBodyComponents, setPostBodyComponents }) {
setPreBodyComponents([
React.createElement('script', {
key: 'dark-mode',
src: '/scripts/theme-init.js',
}),
])
setPostBodyComponents([
React.createElement('script', {
key: 'initial-loader',
src: '/scripts/initial-loader.js',
}),
])
}
export const onPreRenderHTML = ({ getHeadComponents, replaceHeadComponents }) => {

View File

@@ -0,0 +1,90 @@
import React from 'react'
export default function HourglassSpinner({ className = '' }: { className?: string }) {
return (
<svg
className={`hourglass-spinner ${className}`}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<style>
{`
.hourglass-spinner {
animation: hourglass-flip 2.4s ease-in-out infinite;
}
@keyframes hourglass-flip {
0%, 40% {
transform: rotate(0deg);
}
48%, 90% {
transform: rotate(180deg);
}
98%, 100% {
transform: rotate(360deg);
}
}
.sand-top {
transform-origin: center bottom;
animation: sand-drain-top 2.4s ease-in-out infinite;
}
.sand-bottom {
transform-origin: center top;
animation: sand-drain-bottom 2.4s ease-in-out infinite;
}
@keyframes sand-drain-top {
0% {
transform: scaleY(1);
opacity: 0.6;
}
40% {
transform: scaleY(0);
opacity: 0;
}
48%, 100% {
transform: scaleY(0);
opacity: 0;
}
}
@keyframes sand-drain-bottom {
0%, 48% {
transform: scaleY(0);
opacity: 0;
}
50% {
transform: scaleY(1);
opacity: 0.6;
}
90% {
transform: scaleY(0);
opacity: 0;
}
100% {
transform: scaleY(0);
opacity: 0;
}
}
`}
</style>
{/* Hourglass outline */}
<path
d="M6 2h12M6 22h12M18 2v3.5c0 1.5-1 2.5-2.5 4L12 13l-3.5-3.5C7 8 6 7 6 5.5V2M18 22v-3.5c0-1.5-1-2.5-2.5-4L12 11l-3.5 3.5C7 16 6 17 6 18.5V22"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Top sand - triangle filling top chamber */}
<path className="sand-top" d="M8 4 L16 4 L14.5 7 L9.5 7 Z" fill="currentColor" opacity="0.6" />
{/* Bottom sand - triangle filling bottom chamber */}
<path className="sand-bottom" d="M8 20 L16 20 L14.5 17 L9.5 17 Z" fill="currentColor" opacity="0.6" />
</svg>
)
}

View File

@@ -16,7 +16,9 @@ import { motion } from 'framer-motion'
import { DebugContainerQuery } from 'components/DebugContainerQuery'
import HedgeHogModeEmbed from 'components/HedgehogMode'
import ReactConfetti from 'react-confetti'
import ProgressBar from 'components/ProgressBar'
import OSButton from 'components/OSButton'
import HourglassSpinner from './HourglassSpinner'
interface Product {
name: string
@@ -191,6 +193,17 @@ const validateIconPositions = (
return true
}
const LOADING_MESSAGES = [
'Booting the PostHog experience',
'Compiling hedgehog shaders',
'Rebuilding webpack',
'Hydrating the hedgehogs',
'Sourcing and transforming nodes',
<>
Running <code>yarn serve</code>
</>,
]
export default function Desktop() {
const productLinks = useProductLinks()
const {
@@ -209,6 +222,8 @@ export default function Desktop() {
})
const [rendered, setRendered] = useState(false)
const { getWallpaperClasses } = useTheme()
const [currentMessageIndex, setCurrentMessageIndex] = useState(0)
const [isMessageExiting, setIsMessageExiting] = useState(false)
function generateInitialPositions(): IconPositions {
const positions: IconPositions = {}
@@ -265,6 +280,12 @@ export default function Desktop() {
}
useEffect(() => {
// Hide the initial SSR loader once React has hydrated
const initialLoader = document.getElementById('initial-loader')
if (initialLoader) {
initialLoader.style.display = 'none'
}
const savedPositions = localStorage.getItem(STORAGE_KEY)
if (savedPositions) {
try {
@@ -300,6 +321,21 @@ export default function Desktop() {
}
}, [posthogInstance])
useEffect(() => {
if (rendered) return
const interval = setInterval(() => {
setIsMessageExiting(true)
setTimeout(() => {
setCurrentMessageIndex((prev) => (prev + 1) % LOADING_MESSAGES.length)
setIsMessageExiting(false)
}, 300)
}, 1000)
return () => clearInterval(interval)
}, [rendered, currentMessageIndex])
const handlePositionChange = (appLabel: string, position: IconPosition) => {
const newPositions = { ...iconPositions, [appLabel]: position }
setIconPositions(newPositions)
@@ -544,6 +580,26 @@ export default function Desktop() {
/>
</div>
)}
{!rendered && (
<div
data-scheme="secondary"
className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full flex items-center justify-center z-50"
>
<div className="flex items-center justify-center gap-2 w-80 bg-primary/75 backdrop-blur border border-primary rounded p-4">
<HourglassSpinner className="text-primary opacity-75" />
<div className="flex-1 relative">
<strong
key={currentMessageIndex}
className={`font-medium text-secondary block ${
isMessageExiting ? 'animate-slide-up-fade-out' : 'animate-slide-up-fade-in'
}`}
>
{LOADING_MESSAGES[currentMessageIndex]}
</strong>
</div>
</div>
</div>
)}
</>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ import { useUser } from 'hooks/useUser'
import { navigate } from 'gatsby'
import Lottie from 'lottie-react'
import hourglassAnimation from 'images/icons8-hourglass.json'
import hourglassAnimationWhite from 'images/icons8-hourglass-white.json'
import { useInView } from 'react-intersection-observer'
import useTopicsNav from '../../navs/useTopicsNav'
import { useWindow } from '../../context/Window'
@@ -441,7 +442,12 @@ export default function Inbox(props) {
<div className="flex items-center justify-center py-8 h-full">
<Lottie
animationData={hourglassAnimation}
className="size-6 opacity-75"
className="size-6 opacity-75 dark:hidden"
title="Loading questions..."
/>
<Lottie
animationData={hourglassAnimationWhite}
className="size-6 opacity-75 hidden dark:block"
title="Loading questions..."
/>
</div>

View File

@@ -0,0 +1,774 @@
{
"nm": "hourglass",
"h": 30,
"w": 30,
"meta": { "g": "@lottiefiles/toolkit-js 0.33.2" },
"layers": [
{
"ty": 4,
"nm": "middle Outlines",
"sr": 1,
"st": 0,
"op": 28,
"ip": 0,
"hasMask": false,
"ao": 0,
"ks": {
"a": { "a": 0, "k": [1, 5.5, 0] },
"s": { "a": 0, "k": [100, 100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [15, 15.5, 0] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
},
"ef": [],
"shapes": [
{
"ty": "gr",
"nm": "Group 1",
"it": [
{
"ty": "sh",
"nm": "Path 1",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": false,
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[1, 2.188],
[1, 11]
]
}
}
},
{
"ty": "st",
"nm": "Stroke 1",
"lc": 2,
"lj": 2,
"ml": 1,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 2 },
"c": { "a": 0, "k": [1, 1, 1] }
},
{
"ty": "tr",
"a": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [0, 0] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
},
{
"ty": "tm",
"nm": "Trim Paths 1",
"e": {
"a": 1,
"k": [
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [0], "t": 18 },
{ "s": [100], "t": 22 }
]
},
"o": { "a": 0, "k": 0 },
"s": {
"a": 1,
"k": [
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [0], "t": 18 },
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [0], "t": 22 },
{ "s": [100], "t": 25 }
]
},
"m": 1
}
],
"ind": 1
},
{
"ty": 4,
"nm": "hourglass Outlines",
"sr": 1,
"st": 0,
"op": 28,
"ip": 0,
"hasMask": false,
"ao": 0,
"ks": {
"a": { "a": 0, "k": [12, 16, 0] },
"s": {
"a": 1,
"k": [
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [100, 100, 100], "t": 3 },
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [90, 90, 100], "t": 5 },
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [90, 90, 100], "t": 9 },
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [100, 100, 100], "t": 11 },
{ "s": [100, 100, 100], "t": 18 }
]
},
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [15, 15, 0] },
"r": {
"a": 1,
"k": [
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [0], "t": 3 },
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [40], "t": 5 },
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [193], "t": 9 },
{ "o": { "x": 0.333, "y": 0 }, "i": { "x": 0.667, "y": 1 }, "s": [180], "t": 11 },
{ "s": [180], "t": 18 }
]
},
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
},
"ef": [],
"shapes": [
{
"ty": "gr",
"nm": "Group 1",
"it": [
{
"ty": "sh",
"nm": "Path 1",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"i": [
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[-7, -2],
[7, -2],
[7, 2],
[-7, 2]
]
}
}
},
{
"ty": "fl",
"nm": "Fill 1",
"c": { "a": 0, "k": [1, 1, 1] },
"r": 1,
"o": { "a": 0, "k": 100 }
},
{
"ty": "tr",
"a": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [12, 25] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
},
{
"ty": "gr",
"nm": "Group 2",
"it": [
{
"ty": "sh",
"nm": "Path 1",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": true,
"i": [
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[-7, -2],
[7, -2],
[7, 2],
[-7, 2]
]
}
}
},
{
"ty": "fl",
"nm": "Fill 1",
"c": { "a": 0, "k": [1, 1, 1] },
"r": 1,
"o": { "a": 0, "k": 100 }
},
{
"ty": "tr",
"a": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [12, 7] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
},
{
"ty": "gr",
"nm": "Group 3",
"it": [
{
"ty": "sh",
"nm": "Path 1",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": false,
"i": [
[0, 0],
[0, 0],
[0, 0],
[0, 5],
[0, 0]
],
"o": [
[0, 0],
[0, -5],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[2.5, 11],
[2.5, 8],
[-2.5, 0],
[2.5, -8],
[2.5, -11]
]
}
}
},
{
"ty": "st",
"nm": "Stroke 1",
"lc": 2,
"lj": 2,
"ml": 1,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 2 },
"c": { "a": 0, "k": [1, 1, 1] }
},
{
"ty": "tr",
"a": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [16.5, 16.001] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
},
{
"ty": "gr",
"nm": "Group 4",
"it": [
{
"ty": "sh",
"nm": "Path 1",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": false,
"i": [
[0, 0],
[0, 0],
[0, 0],
[0, -5],
[0, 0]
],
"o": [
[0, 0],
[0, 5],
[0, 0],
[0, 0],
[0, 0]
],
"v": [
[-2.5, -11],
[-2.5, -8],
[2.5, 0],
[-2.5, 8],
[-2.5, 11]
]
}
}
},
{
"ty": "st",
"nm": "Stroke 1",
"lc": 2,
"lj": 2,
"ml": 1,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 2 },
"c": { "a": 0, "k": [1, 1, 1] }
},
{
"ty": "tr",
"a": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [7.5, 16] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
},
{
"ty": "gr",
"nm": "Group 5",
"it": [
{
"ty": "sh",
"nm": "Path 1",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": false,
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[3, 27.001],
[21, 27.001]
]
}
}
},
{
"ty": "st",
"nm": "Stroke 1",
"lc": 2,
"lj": 2,
"ml": 1,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 2 },
"c": { "a": 0, "k": [1, 1, 1] }
},
{
"ty": "tr",
"a": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [0, 0] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
},
{
"ty": "gr",
"nm": "Group 6",
"it": [
{
"ty": "sh",
"nm": "Path 1",
"d": 1,
"ks": {
"a": 0,
"k": {
"c": false,
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[3, 5],
[21, 5]
]
}
}
},
{
"ty": "st",
"nm": "Stroke 1",
"lc": 2,
"lj": 2,
"ml": 1,
"o": { "a": 0, "k": 100 },
"w": { "a": 0, "k": 2 },
"c": { "a": 0, "k": [1, 1, 1] }
},
{
"ty": "tr",
"a": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [0, 0] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
}
],
"ind": 2
},
{
"ty": 4,
"nm": "bottom shape",
"sr": 1,
"st": 0,
"op": 23,
"ip": 0,
"hasMask": false,
"ao": 0,
"ks": {
"a": { "a": 0, "k": [4.167, 1.844, 0] },
"s": { "a": 0, "k": [100, 100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [12, 21.594, 0] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
},
"ef": [],
"shapes": [
{
"ty": "gr",
"nm": "Group 1",
"it": [
{
"ty": "sh",
"nm": "Path 1",
"d": 1,
"ks": {
"a": 1,
"k": [
{
"o": { "x": 0.167, "y": 0.167 },
"i": { "x": 0.833, "y": 0.833 },
"s": [
{
"c": true,
"i": [
[0, 0],
[1.931, 0],
[0.376, -1.819]
],
"o": [
[-0.376, -1.819],
[-1.931, 0],
[0, 0]
],
"v": [
[3.917, 1.594],
[0, -1.594],
[-3.917, 1.594]
]
}
],
"t": 3
},
{
"o": { "x": 0.167, "y": 0.167 },
"i": { "x": 0.833, "y": 0.833 },
"s": [
{
"c": true,
"i": [
[0, 0],
[1.931, 0],
[0.376, -1.819]
],
"o": [
[-0.376, -1.819],
[-1.931, 0],
[0, 0]
],
"v": [
[4.008, 2.243],
[0.091, -0.945],
[-3.826, 2.243]
]
}
],
"t": 5
},
{
"o": { "x": 0.167, "y": 0.167 },
"i": { "x": 0.833, "y": 0.833 },
"s": [
{
"c": true,
"i": [
[0, 0],
[1.931, 0],
[0.376, -1.819]
],
"o": [
[-0.376, -1.819],
[-1.931, 0],
[0, 0]
],
"v": [
[3.972, 0.541],
[0.055, -2.647],
[-3.862, 0.541]
]
}
],
"t": 9
},
{
"o": { "x": 0.167, "y": 0.167 },
"i": { "x": 0.833, "y": 0.833 },
"s": [
{
"c": true,
"i": [
[0, 0],
[1.471, 0],
[0.286, -1.386]
],
"o": [
[-0.286, -1.386],
[-1.471, 0],
[0, 0]
],
"v": [
[2.918, -0.656],
[-0.066, -3.085],
[-3.05, -0.656]
]
}
],
"t": 11
},
{
"o": { "x": 0.167, "y": 0.167 },
"i": { "x": 0.833, "y": 0.833 },
"s": [
{
"c": true,
"i": [
[0, 0],
[1.471, 0],
[0.286, -1.386]
],
"o": [
[-0.286, -1.386],
[-1.471, 0],
[0, 0]
],
"v": [
[2.918, -0.656],
[-0.066, -3.085],
[-3.05, -0.656]
]
}
],
"t": 20
},
{
"s": [
{
"c": true,
"i": [
[0, 0],
[0.832, 0],
[0.162, -0.784]
],
"o": [
[-0.162, -0.784],
[-0.832, 0],
[0, 0]
],
"v": [
[1.688, -1.991],
[0, -3.365],
[-1.688, -1.991]
]
}
],
"t": 23
}
]
}
},
{
"ty": "fl",
"nm": "Fill 1",
"c": { "a": 0, "k": [1, 1, 1] },
"r": 1,
"o": { "a": 0, "k": 100 }
},
{
"ty": "tr",
"a": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [4.167, 1.844] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
}
],
"ind": 3,
"parent": 2
},
{
"ty": 4,
"nm": "bottom shape2",
"sr": 1,
"st": 0,
"op": 28,
"ip": 22,
"hasMask": false,
"ao": 0,
"ks": {
"a": { "a": 0, "k": [4.167, 1.844, 0] },
"s": { "a": 0, "k": [100, 100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [15, 20.594, 0] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
},
"ef": [],
"shapes": [
{
"ty": "gr",
"nm": "Group 1",
"it": [
{
"ty": "sh",
"nm": "Path 1",
"d": 1,
"ks": {
"a": 1,
"k": [
{
"o": { "x": 0.333, "y": 0 },
"i": { "x": 0.667, "y": 1 },
"s": [
{
"c": true,
"i": [
[0, 0],
[0.678, 0],
[0.132, -0.639]
],
"o": [
[-0.132, -0.639],
[-0.678, 0],
[0, 0]
],
"v": [
[1.375, 1.56],
[0, 0.44],
[-1.375, 1.56]
]
}
],
"t": 22
},
{
"s": [
{
"c": true,
"i": [
[0, 0],
[1.931, 0],
[0.376, -1.819]
],
"o": [
[-0.376, -1.819],
[-1.931, 0],
[0, 0]
],
"v": [
[3.917, 1.594],
[0, -1.594],
[-3.917, 1.594]
]
}
],
"t": 25
}
]
}
},
{
"ty": "fl",
"nm": "Fill 1",
"c": { "a": 0, "k": [1, 1, 1] },
"r": 1,
"o": { "a": 0, "k": 100 }
},
{
"ty": "tr",
"a": { "a": 0, "k": [0, 0] },
"s": { "a": 0, "k": [100, 100] },
"sk": { "a": 0, "k": 0 },
"p": { "a": 0, "k": [4.167, 1.844] },
"r": { "a": 0, "k": 0 },
"sa": { "a": 0, "k": 0 },
"o": { "a": 0, "k": 100 }
}
]
}
],
"ind": 4
}
],
"v": "5.6.5",
"fr": 24,
"op": 28,
"ip": 0,
"assets": []
}

Binary file not shown.

View File

@@ -0,0 +1,88 @@
(function () {
try {
const body = document.body;
const loadingWrapper = document.createElement('div');
const loadingStyles = document.createElement('style');
loadingStyles.textContent = `
#initial-loader {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
pointer-events: none;
}
#initial-loader-content {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 20rem;
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(8px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.25rem;
padding: 1rem;
}
body.dark #initial-loader-content {
background: rgba(30, 31, 35, 0.75);
border-color: rgba(255, 255, 255, 0.1);
}
.hourglass-spinner {
animation: hourglass-flip 2.4s ease-in-out infinite;
opacity: 0.75;
}
@keyframes hourglass-flip {
0%, 40% { transform: rotate(0deg); }
48%, 90% { transform: rotate(180deg); }
98%, 100% { transform: rotate(360deg); }
}
.sand-top {
transform-origin: center bottom;
animation: sand-drain-top 2.4s ease-in-out infinite;
}
.sand-bottom {
transform-origin: center top;
animation: sand-drain-bottom 2.4s ease-in-out infinite;
}
@keyframes sand-drain-top {
0% { transform: scaleY(1); opacity: 0.6; }
40% { transform: scaleY(0); opacity: 0; }
48%, 100% { transform: scaleY(0); opacity: 0; }
}
@keyframes sand-drain-bottom {
0%, 48% { transform: scaleY(0); opacity: 0; }
50% { transform: scaleY(1); opacity: 0.6; }
90% { transform: scaleY(0); opacity: 0; }
100% { transform: scaleY(0); opacity: 0; }
}
#initial-loader-text {
flex: 1;
font-weight: 500;
color: rgba(0, 0, 0, 0.8);
}
body.dark #initial-loader-text {
color: rgba(255, 255, 255, 0.8);
}
`;
loadingWrapper.id = 'initial-loader';
loadingWrapper.innerHTML = `
<div id="initial-loader-content">
<svg class="hourglass-spinner" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M6 2h12M6 22h12M18 2v3.5c0 1.5-1 2.5-2.5 4L12 13l-3.5-3.5C7 8 6 7 6 5.5V2M18 22v-3.5c0-1.5-1-2.5-2.5-4L12 11l-3.5 3.5C7 16 6 17 6 18.5V22" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path class="sand-top" d="M8 4 L16 4 L14.5 7 L9.5 7 Z" fill="currentColor" opacity="0.6"/>
<path class="sand-bottom" d="M8 20 L16 20 L14.5 17 L9.5 17 Z" fill="currentColor" opacity="0.6"/>
</svg>
<div id="initial-loader-text">Loading PostHog...</div>
</div>
`;
body.appendChild(loadingStyles);
body.appendChild(loadingWrapper);
} catch (error) {
console.error('Failed to initialize loading spinner:', error);
}
})();

View File

@@ -362,6 +362,26 @@ module.exports = {
'0%, 100%': { 'background-position': '0% 50%' },
'50%': { 'background-position': '100% 50%' },
},
slideUpFadeIn: {
from: {
opacity: '0',
transform: 'translateY(10px)',
},
to: {
opacity: '1',
transform: 'translateY(0)',
},
},
slideUpFadeOut: {
from: {
opacity: '1',
transform: 'translateY(0)',
},
to: {
opacity: '0',
transform: 'translateY(-10px)',
},
},
},
animation: {
wiggle: 'wiggle .2s ease-in-out 3',
@@ -381,6 +401,8 @@ module.exports = {
'spin-slow': 'spin-slow 4s linear infinite',
'spin-slow-reverse': 'spin-slow-reverse 4s linear infinite',
'gradient-rotate': 'gradient-rotate 3s ease-in-out infinite',
'slide-up-fade-in': 'slideUpFadeIn 300ms ease-out forwards',
'slide-up-fade-out': 'slideUpFadeOut 300ms ease-in forwards',
},
containers: {
'2xs': '16rem',