Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cycling-loading-messages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tinacms': patch
---

Cycle the loading copy through a bank of llama-themed messages in a random order, with a short fade between them. The admin loading page, the sidebar loading placeholder, and the TinaCMS wrapper all share the same message list.
5 changes: 5 additions & 0 deletions .changeset/npm-readme-images.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'tinacms': patch
---

Use absolute URLs for the README images so they render on the npm package page.
4 changes: 2 additions & 2 deletions packages/tinacms/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
[![npm version](https://img.shields.io/npm/v/tinacms.svg?style=flat)](https://www.npmjs.com/package/tinacms)
[![Build, Test, Lint for Main](https://github.com/tinacms/tinacms/actions/workflows/main.yml/badge.svg?branch=main&event=push)](https://github.com/tinacms/tinacms/actions/workflows/main.yml)

# [![TINA CMS](./.github/assets/tinacms-logo-small-default.svg)](https://tina.io)
# [![TINA CMS](https://raw.githubusercontent.com/tinacms/tinacms/main/.github/assets/tinacms-logo-small-default.svg)](https://tina.io)

[![Tina Demo](./.github/assets/homepage-demo-2.gif)](https://tina.io/)
[![Tina Demo](https://raw.githubusercontent.com/tinacms/tinacms/main/.github/assets/homepage-demo-2.gif)](https://tina.io/)

Tina is a headless content management system with support for **Markdown**, MDX, JSON, YAML, and more.

Expand Down
7 changes: 3 additions & 4 deletions packages/tinacms/src/admin/components/LoadingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

*/

import { LoadingMessage } from '@toolkit/components/loading-messages';
import React from 'react';

const LoadingPage = () => (
Expand Down Expand Up @@ -91,7 +92,7 @@ const LoadingPage = () => (
/>
</circle>
</svg>
<p
<LoadingMessage
style={{
fontSize: '16px',
color: '#716c7f',
Expand All @@ -100,9 +101,7 @@ const LoadingPage = () => (
fontFamily: "'Inter', sans-serif",
fontWeight: 'normal',
}}
>
Hang tight, TinaCMS is looking for llamas 🦙 🦙 🦙
</p>
/>
</div>
</div>
</>
Expand Down
11 changes: 5 additions & 6 deletions packages/tinacms/src/tina-cms.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { ReactNode, useState } from 'react';
import { TinaCloudProvider } from './auth';
import { LoadingMessage } from './toolkit/components/loading-messages';
import { FontLoader } from './toolkit/styles/font-loader';

import { LocalClient } from './internalClient/index';
import { useDocumentCreatorPlugin } from './hooks/use-content-creator';
import { parseURL } from '@tinacms/schema-tools';
import { useDocumentCreatorPlugin } from './hooks/use-content-creator';
import { LocalClient } from './internalClient/index';
import type { TinaCMSProviderDefaultProps } from './types/cms';

const errorButtonStyles = {
Expand Down Expand Up @@ -303,7 +304,7 @@ const Loader = (props: { children: React.ReactNode }) => {
/>
</circle>
</svg>
<p
<LoadingMessage
style={{
fontSize: '18px',
color: '#252336',
Expand All @@ -312,9 +313,7 @@ const Loader = (props: { children: React.ReactNode }) => {
fontFamily: "'Inter', sans-serif",
fontWeight: 'normal',
}}
>
Hang tight, TinaCMS is looking for llamas 🦙 🦙 🦙
</p>
/>
</div>
</div>
{props.children}
Expand Down
64 changes: 64 additions & 0 deletions packages/tinacms/src/toolkit/components/loading-messages.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { act, render } from '@testing-library/react';
import * as React from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LOADING_MESSAGES, LoadingMessage } from './loading-messages';

const MESSAGES = ['first', 'second', 'third'];

const tick = () =>
act(() => {
vi.advanceTimersByTime(2000);
});

describe('LoadingMessage', () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
});

it('never repeats the message it is replacing', () => {
const { container } = render(<LoadingMessage messages={MESSAGES} />);
const text = () => container.querySelector('p')?.textContent as string;

let previous = text();
for (let i = 0; i < 20; i++) {
tick();
expect(MESSAGES).toContain(text());
expect(text()).not.toBe(previous);
previous = text();
}
});

it('shows a random message once mounted', () => {
const seen = new Set<string>();
for (let i = 0; i < 30; i++) {
const { container, unmount } = render(<LoadingMessage />);
seen.add(container.querySelector('p')?.textContent as string);
unmount();
}
expect(seen.size).toBeGreaterThan(1);
for (const message of seen) {
expect(LOADING_MESSAGES).toContain(message);
}
});

it('fades out before the text changes', () => {
const { container } = render(<LoadingMessage messages={MESSAGES} />);
const opacity = () => container.querySelector('p')?.style.opacity;

expect(opacity()).toBe('1');

act(() => {
vi.advanceTimersByTime(1800);
});
expect(opacity()).toBe('0');

act(() => {
vi.advanceTimersByTime(200);
});
expect(opacity()).toBe('1');
});
});
85 changes: 85 additions & 0 deletions packages/tinacms/src/toolkit/components/loading-messages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React from 'react';

export const LOADING_MESSAGES = [
'Hang tight, TinaCMS is looking for llamas 🦙 🦙 🦙',
'Reticulating llama splines 🦙',
'Herding llamas into collections 🦙',
'Brushing llama fur 🦙 🦙',
'Teaching a llama to write frontmatter 🦙',
'Feeding the GraphQL llama 🦙',
'Counting llamas in your content folder 🦙',
'Asking a llama what changed in git 🦙',
'Llamas are proofreading your schema 🦙',
'Untangling markdown with llama teeth 🦙',
'Waking the llamas in the media library 🦙',
'Alphabetising llamas by collection 🦙',
'Convincing a llama that MDX is fine 🦙',
'Saddling up the content layer 🦙',
'Polishing the sidebar, llama style 🦙',
'One more llama, almost there 🦙',
];

const CYCLE_MS = 2000;
const FADE_MS = 200;

const prefersReducedMotion = () =>
typeof window !== 'undefined' &&
typeof window.matchMedia === 'function' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;

const pickOther = (current: number, length: number) => {
if (length < 2) return 0;
const offset = 1 + Math.floor(Math.random() * (length - 1));
return (current + offset) % length;
};

interface LoadingMessageProps {
className?: string;
style?: React.CSSProperties;
messages?: string[];
}

export const LoadingMessage = ({
className,
style,
messages = LOADING_MESSAGES,
}: LoadingMessageProps) => {
const [index, setIndex] = React.useState(0);
// Server and first client render must agree, so randomise once mounted.
const [visible, setVisible] = React.useState(false);
const [reducedMotion] = React.useState(prefersReducedMotion);

React.useEffect(() => {
setIndex((current) => pickOther(current, messages.length));
setVisible(true);
}, [messages.length]);

React.useEffect(() => {
const fadeOut = setTimeout(() => setVisible(false), CYCLE_MS - FADE_MS);
const next = setTimeout(() => {
setIndex((current) => pickOther(current, messages.length));
setVisible(true);
}, CYCLE_MS);
return () => {
clearTimeout(fadeOut);
clearTimeout(next);
};
}, [index, messages.length]);

return (
<p
className={className}
aria-live='polite'
style={{
...style,
opacity: reducedMotion || visible ? 1 : 0,
transform: reducedMotion || visible ? 'none' : 'translateY(4px)',
transition: reducedMotion
? undefined
: `opacity ${FADE_MS}ms ease-out, transform ${FADE_MS}ms ease-out`,
}}
>
{messages[index]}
</p>
);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LoadingMessage } from '@toolkit/components/loading-messages';
import { LoadingDots } from '@toolkit/form-builder';
import * as React from 'react';

Expand All @@ -13,11 +14,7 @@ export const SidebarLoadingPlaceholder = () => (
animationDuration: '150ms',
}}
>
<p className='block pb-5'>
Hang tight, TinaCMS is
<br />
looking for llamas 🦙 🦙 🦙
</p>
<LoadingMessage className='block pb-5' />
<LoadingDots color={'var(--tina-color-primary)'} />
</div>
);
Loading