@welcome-back/brand 0.1.1 → 0.1.3

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 +437 -27
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,7 +1,29 @@
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
+ - [Develop](#develop)
25
+ - [Publish a change](#publish-a-change)
26
+ - [Source art](#source-art)
5
27
 
6
28
  ## Install
7
29
 
@@ -9,48 +31,436 @@ running left with its legs crossing under the body.
9
31
  npm i @welcome-back/brand
10
32
  ```
11
33
 
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.
34
+ The package is public on npm. No token or `.npmrc` is needed.
14
35
 
15
- ## Use
36
+ ## Quick start
16
37
 
17
38
  ```tsx
18
39
  import { RunnerLoader } from "@welcome-back/brand";
19
40
 
20
- // Decorative (aria-hidden) — the surrounding element owns the label:
21
- <div role="status" aria-label={t("loading")}>
22
- <RunnerLoader className="w-28" />
41
+ export function Loading() {
42
+ return <RunnerLoader className="w-24" label="Loading" />;
43
+ }
44
+ ```
45
+
46
+ That is all. Set a width, and the height follows the art.
47
+
48
+ ## API
49
+
50
+ ### Props
51
+
52
+ | Prop | Type | Default | What it does |
53
+ |---|---|---|---|
54
+ | `className` | `string` | — | Added after the built-in `wb-runner` class. Use it for size, color and the CSS variables below. |
55
+ | `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. |
56
+
57
+ The component renders one `<svg>`. It takes no other props.
58
+
59
+ ### CSS custom properties
60
+
61
+ You can set these in `className` (Tailwind arbitrary properties), in your own
62
+ CSS class, or on any ancestor element. Values inherit, so one setting on a
63
+ wrapper applies to all runners inside it.
64
+
65
+ | Property | Controls | Default | Notes |
66
+ |---|---|---|---|
67
+ | `color` | Ink of the W, legs and shoes | `currentColor` | Any Tailwind `text-*` class works. |
68
+ | `--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. |
69
+ | `--wb-runner-step` | Duration of one step | `0.32s` | A full stride is two steps (0.64 s). Lower is faster. |
70
+
71
+ `Canvas` is the page background color that the browser uses for the current
72
+ `color-scheme`: white in light mode, near-black in dark mode. On any other
73
+ surface (a card, a tinted panel, a brand color) set `--wb-runner-pants`.
74
+
75
+ ### Size guide
76
+
77
+ | Width | Use |
78
+ |---|---|
79
+ | 96–128 px | Full-screen or full-panel loading |
80
+ | 48–64 px | Loading inside a card or a list area |
81
+ | 32–40 px | Next to text ("Loading…") |
82
+ | under 32 px | Not recommended: the legs become unreadable. Use a plain spinner. |
83
+
84
+ ## Examples
85
+
86
+ ### Tailwind: size, color and speed in one `className`
87
+
88
+ ```tsx
89
+ <RunnerLoader className="w-16 text-foreground" />
90
+
91
+ // Faster (0.2 s per step) and in the muted text color:
92
+ <RunnerLoader className="w-16 text-muted-foreground [--wb-runner-step:0.2s]" />
93
+ ```
94
+
95
+ ### Plain CSS (no Tailwind)
96
+
97
+ ```css
98
+ /* loading.css */
99
+ .page-loader {
100
+ width: 112px;
101
+ color: #171717;
102
+ --wb-runner-pants: #ffffff;
103
+ }
104
+ ```
105
+
106
+ ```tsx
107
+ import "./loading.css";
108
+
109
+ <RunnerLoader className="page-loader" label="Loading" />;
110
+ ```
111
+
112
+ ### Inline style on a wrapper
113
+
114
+ `RunnerLoader` has no `style` prop. The CSS variables inherit, so put inline
115
+ styles on a parent element. TypeScript does not know custom properties, so cast
116
+ the style object:
117
+
118
+ ```tsx
119
+ import type { CSSProperties } from "react";
120
+
121
+ const runnerTheme = {
122
+ color: "#171717",
123
+ "--wb-runner-pants": "#F6F4E9",
124
+ "--wb-runner-step": "0.28s",
125
+ } as CSSProperties;
126
+
127
+ <div style={runnerTheme}>
128
+ <RunnerLoader className="w-24" label="Loading" />
129
+ </div>;
130
+ ```
131
+
132
+ ### Match a shadcn / Tailwind 3 theme token
133
+
134
+ shadcn stores colors as bare HSL values (`--background: 0 0% 100%`), so wrap
135
+ them in `hsl()`:
136
+
137
+ ```tsx
138
+ <div className="bg-background text-foreground [--wb-runner-pants:hsl(var(--background))]">
139
+ <RunnerLoader className="w-24" />
140
+ </div>
141
+ ```
142
+
143
+ In Tailwind 4 the tokens are full colors, so drop the `hsl()`:
144
+ `[--wb-runner-pants:var(--background)]`.
145
+
146
+ ### On a card or a colored surface
147
+
148
+ The pants must match the surface directly behind the runner, not the page:
149
+
150
+ ```tsx
151
+ // On a card
152
+ <div className="rounded-xl bg-card p-8 [--wb-runner-pants:hsl(var(--card))]">
153
+ <RunnerLoader className="w-16" />
154
+ </div>
155
+
156
+ // On a brand-orange disc, with a cream runner
157
+ <div className="grid size-32 place-items-center rounded-full bg-[#FF6E26] text-[#FFFAF0] [--wb-runner-pants:#FF6E26]">
158
+ <RunnerLoader className="w-20" label="Loading" />
159
+ </div>
160
+ ```
161
+
162
+ ### Dark mode
163
+
164
+ With the defaults, no extra code is needed on the page background: `Canvas`
165
+ follows `color-scheme`, and the ink follows your text color.
166
+
167
+ ```tsx
168
+ <main className="bg-background text-foreground">
169
+ <RunnerLoader className="w-24" />
170
+ </main>
171
+ ```
172
+
173
+ If your dark mode is a `.dark` class and not `color-scheme`, set the pants from
174
+ the token, as in the shadcn example above.
175
+
176
+ ### Next to text
177
+
178
+ The label is on the wrapper, so the runner stays decorative:
179
+
180
+ ```tsx
181
+ <div role="status" className="flex items-center gap-3 text-sm text-muted-foreground">
182
+ <RunnerLoader className="w-9" />
183
+ <span>Loading your rewards…</span>
23
184
  </div>
185
+ ```
186
+
187
+ ### Next.js route loading (`loading.tsx`)
188
+
189
+ ```tsx
190
+ // app/dashboard/loading.tsx
191
+ import { RunnerLoader } from "@welcome-back/brand";
192
+
193
+ export default function Loading() {
194
+ return (
195
+ <div className="grid min-h-[60vh] place-items-center">
196
+ <RunnerLoader className="w-24" label="Loading" />
197
+ </div>
198
+ );
199
+ }
200
+ ```
201
+
202
+ This is a Server Component. It needs no `"use client"`.
203
+
204
+ ### React `Suspense` fallback
205
+
206
+ ```tsx
207
+ <Suspense fallback={<RunnerLoader className="mx-auto w-20" label="Loading" />}>
208
+ <CustomerDetails id={id} />
209
+ </Suspense>
210
+ ```
211
+
212
+ ### Show only if the wait is long
213
+
214
+ On a fast request the runner can flash for a fraction of a stride. Wait before
215
+ you show it:
216
+
217
+ ```tsx
218
+ "use client";
219
+ import { useEffect, useState } from "react";
220
+ import { RunnerLoader } from "@welcome-back/brand";
221
+
222
+ export function DelayedRunner({ delayMs = 200 }: { delayMs?: number }) {
223
+ const [visible, setVisible] = useState(false);
224
+ useEffect(() => {
225
+ const timer = setTimeout(() => setVisible(true), delayMs);
226
+ return () => clearTimeout(timer);
227
+ }, [delayMs]);
228
+ return visible ? <RunnerLoader className="w-16" label="Loading" /> : null;
229
+ }
230
+ ```
231
+
232
+ ### Full-screen splash
233
+
234
+ A server-rendered overlay covers the page before JavaScript runs. Hide it after
235
+ the app hydrates. This is a short version of staff-ui's `AppSplash`, which also
236
+ holds the splash for two strides on the first open of each session.
237
+
238
+ ```tsx
239
+ "use client";
240
+ import { useEffect, useState } from "react";
241
+ import { RunnerLoader } from "@welcome-back/brand";
242
+
243
+ export function Splash() {
244
+ const [leaving, setLeaving] = useState(false);
245
+ const [gone, setGone] = useState(false);
246
+
247
+ useEffect(() => {
248
+ setLeaving(true);
249
+ const timer = setTimeout(() => setGone(true), 300); // fade duration
250
+ return () => clearTimeout(timer);
251
+ }, []);
252
+
253
+ if (gone) return null;
254
+ return (
255
+ <div
256
+ role="status"
257
+ aria-label="Loading"
258
+ className={`fixed inset-0 z-50 grid place-items-center bg-background text-foreground
259
+ [--wb-runner-pants:hsl(var(--background))] transition-opacity duration-300
260
+ ${leaving ? "pointer-events-none opacity-0" : ""}`}
261
+ >
262
+ <RunnerLoader className="w-28" />
263
+ </div>
264
+ );
265
+ }
266
+ ```
267
+
268
+ ### Vite / plain React
269
+
270
+ Same import, no config:
271
+
272
+ ```tsx
273
+ import { RunnerLoader } from "@welcome-back/brand";
274
+
275
+ function App() {
276
+ const { data, isLoading } = useQuery(/* … */);
277
+ if (isLoading) return <RunnerLoader className="loader" label="Loading" />;
278
+ return <Dashboard data={data} />;
279
+ }
280
+ ```
281
+
282
+ ### Jest and Testing Library
283
+
284
+ Jest loads the CommonJS build automatically. The SVG always has the class
285
+ `wb-runner`, so you can find it in tests:
286
+
287
+ ```tsx
288
+ render(<Loading />);
289
+ expect(screen.getByRole("img", { name: "Loading" })).toHaveClass("wb-runner");
290
+ ```
291
+
292
+ ## Accessibility
293
+
294
+ - **Give exactly one accessible name per loading state.** Either put `label` on
295
+ the runner, or put `role="status"` and `aria-label` on its wrapper and leave
296
+ `label` out. Do not do both, or screen readers announce it twice.
297
+ - **Translate the label.** Pass it from your i18n layer, for example
298
+ `label={t("loading")}`.
299
+ - **Reduced motion is built in.** With `prefers-reduced-motion: reduce` the
300
+ runner holds one pose (legs apart, mid-stride), so the loading state is still
301
+ visible.
302
+
303
+ ## Where to use it, and where not
304
+
305
+ | Use it | Do not use it |
306
+ |---|---|
307
+ | App open and splash screens | Inside buttons: at button size the legs cannot be read |
308
+ | 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 |
309
+ | Route and Suspense fallbacks for a full page or panel | Every small wait: if it is everywhere, staff stop noticing it |
310
+
311
+ A skeleton is better when the layout that is coming is known: it shows where
312
+ the content will appear.
313
+
314
+ ## Troubleshooting
315
+
316
+ | Symptom | Cause | Fix |
317
+ |---|---|---|
318
+ | 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 |
319
+ | The runner is invisible | Ink is the same color as the background | Set `color` / a `text-*` class |
320
+ | The runner does not move | The OS asks for reduced motion | Nothing to fix; this is intended |
321
+ | The runner fills the whole container | No width is set | Give it a width (`w-24`, or `width` in CSS) |
322
+ | A Tailwind arbitrary property has no effect | Tailwind does not scan the file | Check the `content` globs in the Tailwind config |
323
+
324
+ ## Develop
325
+
326
+ ### Repo layout
327
+
328
+ | Path | What it holds |
329
+ |---|---|
330
+ | `src/index.ts` | Public exports. Only what is exported here is part of the API. |
331
+ | `src/RunnerLoader.tsx` | The component: SVG structure, clip paths, props |
332
+ | `src/runnerCss.ts` | The animation: keyframes, timing, swing angles, reduced motion |
333
+ | `src/runnerArt.ts` | Vector data: paths, pants fills, clip polygons |
334
+ | `src/*.test.tsx` | Vitest tests (server render, no browser) |
335
+ | `scripts/mark-cjs.mjs` | Marks `dist/cjs` as CommonJS after the build |
336
+ | `.github/workflows/ci.yml` | Runs on every PR and on `main` |
337
+ | `.github/workflows/publish.yml` | Runs on a `v*` tag and publishes to npm |
338
+
339
+ ### Commands
340
+
341
+ Node 22 or later.
24
342
 
25
- // Standalone, with its own accessible name:
26
- <RunnerLoader className="w-16" label={t("loading")} />
343
+ ```bash
344
+ npm ci # install
345
+ npm run typecheck # tsc, no output
346
+ npm test # vitest
347
+ npm run build # dist/esm (ESM + types) and dist/cjs (CommonJS)
348
+ ```
349
+
350
+ CI runs all four, then loads both builds to prove that `import` and `require`
351
+ work.
352
+
353
+ ### Try a change in an app before you publish
354
+
355
+ Pack the package and install the tarball in the app:
356
+
357
+ ```bash
358
+ # in brand/
359
+ npm run build && npm pack # writes welcome-back-brand-<version>.tgz
360
+
361
+ # in the app, for example staff-ui/
362
+ npm i ../brand/welcome-back-brand-<version>.tgz
27
363
  ```
28
364
 
29
- Size it with a width; the height follows the art.
365
+ Undo it in the app with `npm i @welcome-back/brand@latest`. Do not commit the
366
+ tarball path in the app's `package.json`.
30
367
 
31
- ## Theme
368
+ Prefer `npm pack` to `npm link`. A linked package can load a second copy of
369
+ React, and then hooks fail with "Invalid hook call".
32
370
 
33
- | Set on the runner or an ancestor | Controls | Default |
371
+ ### Common changes
372
+
373
+ | To change | Edit | Note |
34
374
  |---|---|---|
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` |
375
+ | Speed | `--wb-runner-step-default` in `src/runnerCss.ts` | Apps can already override it per use with `--wb-runner-step` |
376
+ | Stride length (swing angles) | `@keyframes wb-runner-swing-front` / `-back` in `src/runnerCss.ts` | Angles are measured from each leg drawing's rest pose. The front leg is vertical at `-36deg`, the back leg at `39deg`. Keep forward and back swings equal around those values. |
377
+ | Where the legs join the body | `@keyframes wb-runner-hip-front` / `-back` | Values are in the art's coordinates (the 1500×1500 PDF page) |
378
+ | What is hidden behind the W | `LEGS_CLIP` / `BODY_CLIP` in `src/runnerArt.ts` | See [Source art](#source-art) |
379
+ | A new prop | `src/RunnerLoader.tsx`, plus a test and a row in the props table | |
380
+ | A new asset (logo, icon, token) | A new file in `src/`, exported from `src/index.ts`, plus a test and a README section | |
381
+
382
+ After a visual change, check it in a real app with `npm pack` (above) in light
383
+ mode, dark mode and with reduced motion on.
384
+
385
+ ## Publish a change
386
+
387
+ Every change reaches npm through a PR and a tag. Nobody publishes from a laptop,
388
+ and there is no npm token: GitHub Actions publishes with npm trusted publishing.
389
+
390
+ ### 1. Pick the version
391
+
392
+ The package follows [semver](https://semver.org). Apps install it with `^`, so
393
+ they get minor and patch releases on their next install.
394
+
395
+ | Change | Bump | Example |
396
+ |---|---|---|
397
+ | Docs, tests, or a fix with no visible change | patch | `0.1.2` → `0.1.3` |
398
+ | A new prop, asset or CSS variable; a visual tweak that keeps the API | minor | `0.1.3` → `0.2.0` |
399
+ | A removed or renamed prop, export or CSS variable | major | `0.9.0` → `1.0.0` |
400
+
401
+ While the version is `0.x`, npm treats a minor bump as breaking for `^`
402
+ ranges. Apps then need an explicit update to get it.
403
+
404
+ ### 2. Open a PR with the change and the new version
405
+
406
+ ```bash
407
+ git switch main && git pull
408
+ git switch -c <type>/<short-name>
409
+ # make the change
410
+ npm version <patch|minor|major> --no-git-tag-version # edits package.json and package-lock.json
411
+ git commit -am "<type>: <what changed> (v<new version>)"
412
+ git push -u origin HEAD
413
+ gh pr create --base main
414
+ ```
415
+
416
+ CI must pass before you merge. Squash-merge the PR.
417
+
418
+ ### 3. Tag the merge commit
419
+
420
+ ```bash
421
+ git switch main && git pull
422
+ git tag v<new version> # must equal "version" in package.json
423
+ git push origin v<new version>
424
+ ```
425
+
426
+ The tag starts the Publish workflow. It:
427
+
428
+ 1. checks that the tag equals `version` in `package.json`,
429
+ 2. runs typecheck, tests and build (`prepublishOnly`),
430
+ 3. publishes to npm through trusted publishing.
431
+
432
+ Watch it with `gh run watch`, or in the repo's **Actions** tab.
38
433
 
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.
434
+ ### 4. Check npm and update the apps
41
435
 
42
- With `prefers-reduced-motion: reduce`, the runner holds one pose.
436
+ ```bash
437
+ npm view @welcome-back/brand version # the new version, after a few minutes
438
+ ```
439
+
440
+ A new version can take up to about 3 minutes to show on the registry. Then, in
441
+ each app:
442
+
443
+ ```bash
444
+ npm i @welcome-back/brand@latest
445
+ ```
446
+
447
+ ### If the publish fails
448
+
449
+ | Error in the workflow log | Cause | Fix |
450
+ |---|---|---|
451
+ | `Tag must match package.json version` step fails | The tag and `version` differ | Delete the tag (`git push origin :v<x>` and `git tag -d v<x>`), fix the version in a PR, tag again |
452
+ | `403 … OIDC permission denied` | The trusted publisher on npmjs.com does not allow direct publish, or its fields do not match | npmjs.com → package → Settings → Trusted Publisher: `WelcomeBackLoyalty` / `brand` / `publish.yml`, no environment, **Allow npm publish** ticked |
453
+ | `403 … cannot publish over the previously published versions` | That version is already on npm | Bump the version in a new PR and tag again. npm never allows a republish of a version. |
454
+ | Typecheck or test fails | The tagged commit is broken | Fix it in a PR, bump the version, tag again |
43
455
 
44
- ## Release
456
+ To rerun a failed publish without a new tag: `gh run rerun <run-id> --failed`.
45
457
 
46
- 1. Bump `version` in `package.json` in a PR, and merge it to `main`.
47
- 2. Tag the merge commit: `git tag v0.2.0 && git push origin v0.2.0`.
48
- 3. The Publish workflow checks the tag against the version, runs typecheck,
49
- tests and build, and publishes to npm.
458
+ ### npm settings (for maintainers)
50
459
 
51
- Publishing uses npm trusted publishing (GitHub OIDC): no npm token is stored.
52
- The trusted publisher is set on npmjs.com → `@welcome-back/brand` → Settings:
53
- GitHub Actions, `WelcomeBackLoyalty/brand`, workflow `publish.yml`.
460
+ - Org: `welcome-back`. Package access: public.
461
+ - Trusted publisher: GitHub Actions, `WelcomeBackLoyalty/brand`, `publish.yml`,
462
+ no environment, **Allow npm publish** ticked.
463
+ - No provenance: npm cannot generate it for a private GitHub repo.
54
464
 
55
465
  ## Source art
56
466
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@welcome-back/brand",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "WelcomeBack brand assets for React apps: the running-W loader.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {