@welcome-back/brand 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +301 -21
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,28 @@
1
1
  # @welcome-back/brand
2
2
 
3
- WelcomeBack brand assets for React apps. For now: `RunnerLoader`, the isotipo W
4
- running left with its legs crossing under the body.
3
+ WelcomeBack brand assets for React apps.
4
+
5
+ Today the package has one component: **`RunnerLoader`**, the isotipo W running
6
+ left. Its legs swing on their hips and cross under the body while the body bobs.
7
+ Use it for loading moments that mean "on the way to the next step".
8
+
9
+ - React 18 or 19. Works in Next.js (App Router and Pages), Vite and plain React.
10
+ - No CSS import, no Tailwind, no bundler setup: the animation ships inside the SVG.
11
+ - Server-renderable: it shows before JavaScript loads. Works in React Server
12
+ Components (it uses only `useId`).
13
+ - ESM and CommonJS builds, so Jest suites load it with no transform setup.
14
+
15
+ ## Contents
16
+
17
+ - [Install](#install)
18
+ - [Quick start](#quick-start)
19
+ - [API](#api)
20
+ - [Examples](#examples)
21
+ - [Accessibility](#accessibility)
22
+ - [Where to use it, and where not](#where-to-use-it-and-where-not)
23
+ - [Troubleshooting](#troubleshooting)
24
+ - [Release](#release)
25
+ - [Source art](#source-art)
5
26
 
6
27
  ## Install
7
28
 
@@ -9,37 +30,295 @@ running left with its legs crossing under the body.
9
30
  npm i @welcome-back/brand
10
31
  ```
11
32
 
12
- React 18 or 19. No CSS import and no bundler setup: the animation ships inside
13
- the SVG. ESM and CommonJS builds, so Jest suites can load it too.
33
+ The package is public on npm. No token or `.npmrc` is needed.
14
34
 
15
- ## Use
35
+ ## Quick start
16
36
 
17
37
  ```tsx
18
38
  import { RunnerLoader } from "@welcome-back/brand";
19
39
 
20
- // Decorative (aria-hidden) — the surrounding element owns the label:
21
- <div role="status" aria-label={t("loading")}>
22
- <RunnerLoader className="w-28" />
40
+ export function Loading() {
41
+ return <RunnerLoader className="w-24" label="Loading" />;
42
+ }
43
+ ```
44
+
45
+ That is all. Set a width, and the height follows the art.
46
+
47
+ ## API
48
+
49
+ ### Props
50
+
51
+ | Prop | Type | Default | What it does |
52
+ |---|---|---|---|
53
+ | `className` | `string` | — | Added after the built-in `wb-runner` class. Use it for size, color and the CSS variables below. |
54
+ | `label` | `string` | — | Accessible name. With a label the SVG is `role="img"` and screen readers announce it. Without a label it is `aria-hidden`: use that when a parent element already names the loading state. |
55
+
56
+ The component renders one `<svg>`. It takes no other props.
57
+
58
+ ### CSS custom properties
59
+
60
+ You can set these in `className` (Tailwind arbitrary properties), in your own
61
+ CSS class, or on any ancestor element. Values inherit, so one setting on a
62
+ wrapper applies to all runners inside it.
63
+
64
+ | Property | Controls | Default | Notes |
65
+ |---|---|---|---|
66
+ | `color` | Ink of the W, legs and shoes | `currentColor` | Any Tailwind `text-*` class works. |
67
+ | `--wb-runner-pants` | Fill inside the pants | `Canvas` | Set it to the color **behind** the runner. The near leg covers the far leg when they cross, so the pants must be opaque. |
68
+ | `--wb-runner-step` | Duration of one step | `0.32s` | A full stride is two steps (0.64 s). Lower is faster. |
69
+
70
+ `Canvas` is the page background color that the browser uses for the current
71
+ `color-scheme`: white in light mode, near-black in dark mode. On any other
72
+ surface (a card, a tinted panel, a brand color) set `--wb-runner-pants`.
73
+
74
+ ### Size guide
75
+
76
+ | Width | Use |
77
+ |---|---|
78
+ | 96–128 px | Full-screen or full-panel loading |
79
+ | 48–64 px | Loading inside a card or a list area |
80
+ | 32–40 px | Next to text ("Loading…") |
81
+ | under 32 px | Not recommended: the legs become unreadable. Use a plain spinner. |
82
+
83
+ ## Examples
84
+
85
+ ### Tailwind: size, color and speed in one `className`
86
+
87
+ ```tsx
88
+ <RunnerLoader className="w-16 text-foreground" />
89
+
90
+ // Faster (0.2 s per step) and in the muted text color:
91
+ <RunnerLoader className="w-16 text-muted-foreground [--wb-runner-step:0.2s]" />
92
+ ```
93
+
94
+ ### Plain CSS (no Tailwind)
95
+
96
+ ```css
97
+ /* loading.css */
98
+ .page-loader {
99
+ width: 112px;
100
+ color: #171717;
101
+ --wb-runner-pants: #ffffff;
102
+ }
103
+ ```
104
+
105
+ ```tsx
106
+ import "./loading.css";
107
+
108
+ <RunnerLoader className="page-loader" label="Loading" />;
109
+ ```
110
+
111
+ ### Inline style on a wrapper
112
+
113
+ `RunnerLoader` has no `style` prop. The CSS variables inherit, so put inline
114
+ styles on a parent element. TypeScript does not know custom properties, so cast
115
+ the style object:
116
+
117
+ ```tsx
118
+ import type { CSSProperties } from "react";
119
+
120
+ const runnerTheme = {
121
+ color: "#171717",
122
+ "--wb-runner-pants": "#F6F4E9",
123
+ "--wb-runner-step": "0.28s",
124
+ } as CSSProperties;
125
+
126
+ <div style={runnerTheme}>
127
+ <RunnerLoader className="w-24" label="Loading" />
128
+ </div>;
129
+ ```
130
+
131
+ ### Match a shadcn / Tailwind 3 theme token
132
+
133
+ shadcn stores colors as bare HSL values (`--background: 0 0% 100%`), so wrap
134
+ them in `hsl()`:
135
+
136
+ ```tsx
137
+ <div className="bg-background text-foreground [--wb-runner-pants:hsl(var(--background))]">
138
+ <RunnerLoader className="w-24" />
23
139
  </div>
140
+ ```
141
+
142
+ In Tailwind 4 the tokens are full colors, so drop the `hsl()`:
143
+ `[--wb-runner-pants:var(--background)]`.
144
+
145
+ ### On a card or a colored surface
24
146
 
25
- // Standalone, with its own accessible name:
26
- <RunnerLoader className="w-16" label={t("loading")} />
147
+ The pants must match the surface directly behind the runner, not the page:
148
+
149
+ ```tsx
150
+ // On a card
151
+ <div className="rounded-xl bg-card p-8 [--wb-runner-pants:hsl(var(--card))]">
152
+ <RunnerLoader className="w-16" />
153
+ </div>
154
+
155
+ // On a brand-orange disc, with a cream runner
156
+ <div className="grid size-32 place-items-center rounded-full bg-[#FF6E26] text-[#FFFAF0] [--wb-runner-pants:#FF6E26]">
157
+ <RunnerLoader className="w-20" label="Loading" />
158
+ </div>
27
159
  ```
28
160
 
29
- Size it with a width; the height follows the art.
161
+ ### Dark mode
30
162
 
31
- ## Theme
163
+ With the defaults, no extra code is needed on the page background: `Canvas`
164
+ follows `color-scheme`, and the ink follows your text color.
32
165
 
33
- | Set on the runner or an ancestor | Controls | Default |
34
- |---|---|---|
35
- | `color` | Ink | `currentColor` |
36
- | `--wb-runner-pants` | Pants fill: set it to the background behind the runner | `Canvas` (the page background) |
37
- | `--wb-runner-step` | One step; a stride is two steps | `0.32s` |
166
+ ```tsx
167
+ <main className="bg-background text-foreground">
168
+ <RunnerLoader className="w-24" />
169
+ </main>
170
+ ```
171
+
172
+ If your dark mode is a `.dark` class and not `color-scheme`, set the pants from
173
+ the token, as in the shadcn example above.
38
174
 
39
- Example for a shadcn/Tailwind 3 app on a tinted surface: put
40
- `style={{ "--wb-runner-pants": "hsl(var(--background))" }}` on the wrapping element.
175
+ ### Next to text
41
176
 
42
- With `prefers-reduced-motion: reduce`, the runner holds one pose.
177
+ The label is on the wrapper, so the runner stays decorative:
178
+
179
+ ```tsx
180
+ <div role="status" className="flex items-center gap-3 text-sm text-muted-foreground">
181
+ <RunnerLoader className="w-9" />
182
+ <span>Loading your rewards…</span>
183
+ </div>
184
+ ```
185
+
186
+ ### Next.js route loading (`loading.tsx`)
187
+
188
+ ```tsx
189
+ // app/dashboard/loading.tsx
190
+ import { RunnerLoader } from "@welcome-back/brand";
191
+
192
+ export default function Loading() {
193
+ return (
194
+ <div className="grid min-h-[60vh] place-items-center">
195
+ <RunnerLoader className="w-24" label="Loading" />
196
+ </div>
197
+ );
198
+ }
199
+ ```
200
+
201
+ This is a Server Component. It needs no `"use client"`.
202
+
203
+ ### React `Suspense` fallback
204
+
205
+ ```tsx
206
+ <Suspense fallback={<RunnerLoader className="mx-auto w-20" label="Loading" />}>
207
+ <CustomerDetails id={id} />
208
+ </Suspense>
209
+ ```
210
+
211
+ ### Show only if the wait is long
212
+
213
+ On a fast request the runner can flash for a fraction of a stride. Wait before
214
+ you show it:
215
+
216
+ ```tsx
217
+ "use client";
218
+ import { useEffect, useState } from "react";
219
+ import { RunnerLoader } from "@welcome-back/brand";
220
+
221
+ export function DelayedRunner({ delayMs = 200 }: { delayMs?: number }) {
222
+ const [visible, setVisible] = useState(false);
223
+ useEffect(() => {
224
+ const timer = setTimeout(() => setVisible(true), delayMs);
225
+ return () => clearTimeout(timer);
226
+ }, [delayMs]);
227
+ return visible ? <RunnerLoader className="w-16" label="Loading" /> : null;
228
+ }
229
+ ```
230
+
231
+ ### Full-screen splash
232
+
233
+ A server-rendered overlay covers the page before JavaScript runs. Hide it after
234
+ the app hydrates. This is a short version of staff-ui's `AppSplash`, which also
235
+ holds the splash for two strides on the first open of each session.
236
+
237
+ ```tsx
238
+ "use client";
239
+ import { useEffect, useState } from "react";
240
+ import { RunnerLoader } from "@welcome-back/brand";
241
+
242
+ export function Splash() {
243
+ const [leaving, setLeaving] = useState(false);
244
+ const [gone, setGone] = useState(false);
245
+
246
+ useEffect(() => {
247
+ setLeaving(true);
248
+ const timer = setTimeout(() => setGone(true), 300); // fade duration
249
+ return () => clearTimeout(timer);
250
+ }, []);
251
+
252
+ if (gone) return null;
253
+ return (
254
+ <div
255
+ role="status"
256
+ aria-label="Loading"
257
+ className={`fixed inset-0 z-50 grid place-items-center bg-background text-foreground
258
+ [--wb-runner-pants:hsl(var(--background))] transition-opacity duration-300
259
+ ${leaving ? "pointer-events-none opacity-0" : ""}`}
260
+ >
261
+ <RunnerLoader className="w-28" />
262
+ </div>
263
+ );
264
+ }
265
+ ```
266
+
267
+ ### Vite / plain React
268
+
269
+ Same import, no config:
270
+
271
+ ```tsx
272
+ import { RunnerLoader } from "@welcome-back/brand";
273
+
274
+ function App() {
275
+ const { data, isLoading } = useQuery(/* … */);
276
+ if (isLoading) return <RunnerLoader className="loader" label="Loading" />;
277
+ return <Dashboard data={data} />;
278
+ }
279
+ ```
280
+
281
+ ### Jest and Testing Library
282
+
283
+ Jest loads the CommonJS build automatically. The SVG always has the class
284
+ `wb-runner`, so you can find it in tests:
285
+
286
+ ```tsx
287
+ render(<Loading />);
288
+ expect(screen.getByRole("img", { name: "Loading" })).toHaveClass("wb-runner");
289
+ ```
290
+
291
+ ## Accessibility
292
+
293
+ - **Give exactly one accessible name per loading state.** Either put `label` on
294
+ the runner, or put `role="status"` and `aria-label` on its wrapper and leave
295
+ `label` out. Do not do both, or screen readers announce it twice.
296
+ - **Translate the label.** Pass it from your i18n layer, for example
297
+ `label={t("loading")}`.
298
+ - **Reduced motion is built in.** With `prefers-reduced-motion: reduce` the
299
+ runner holds one pose (legs apart, mid-stride), so the loading state is still
300
+ visible.
301
+
302
+ ## Where to use it, and where not
303
+
304
+ | Use it | Do not use it |
305
+ |---|---|
306
+ | App open and splash screens | Inside buttons: at button size the legs cannot be read |
307
+ | Moves to the next step (after a scan, before a customer page) | Pages where the customer sees a business's own brand first (enrollment, card download): our W would lead over their brand |
308
+ | Route and Suspense fallbacks for a full page or panel | Every small wait: if it is everywhere, staff stop noticing it |
309
+
310
+ A skeleton is better when the layout that is coming is known: it shows where
311
+ the content will appear.
312
+
313
+ ## Troubleshooting
314
+
315
+ | Symptom | Cause | Fix |
316
+ |---|---|---|
317
+ | The legs show a light or dark rectangle | `--wb-runner-pants` does not match the surface behind the runner | Set it to that surface color |
318
+ | The runner is invisible | Ink is the same color as the background | Set `color` / a `text-*` class |
319
+ | The runner does not move | The OS asks for reduced motion | Nothing to fix; this is intended |
320
+ | The runner fills the whole container | No width is set | Give it a width (`w-24`, or `width` in CSS) |
321
+ | A Tailwind arbitrary property has no effect | Tailwind does not scan the file | Check the `content` globs in the Tailwind config |
43
322
 
44
323
  ## Release
45
324
 
@@ -50,7 +329,8 @@ With `prefers-reduced-motion: reduce`, the runner holds one pose.
50
329
 
51
330
  Publishing uses npm trusted publishing (GitHub OIDC): no npm token is stored.
52
331
  The trusted publisher is set on npmjs.com → `@welcome-back/brand` → Settings:
53
- GitHub Actions, `WelcomeBackLoyalty/brand`, workflow `publish.yml`.
332
+ GitHub Actions, `WelcomeBackLoyalty/brand`, workflow `publish.yml`, with
333
+ **Allow npm publish** ticked.
54
334
 
55
335
  ## Source art
56
336
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@welcome-back/brand",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "WelcomeBack brand assets for React apps: the running-W loader.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {