lanre-ds 0.1.0 → 0.2.0

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.
@@ -1,51 +1,29 @@
1
1
  'use client';
2
- import React, { useState } from 'react';
2
+ import React from 'react';
3
3
  import { Avatar } from '../identity/Avatar';
4
4
 
5
+ /* A completed level on the map: the friend on a gold pedestal, with the level number on a plate underneath. Give it
6
+ onClick and it becomes a button that replays the level, with a press, a hover and a keyboard focus ring. The pedestal's
7
+ shadow and the number plate are the same at every size. */
5
8
  export function PortraitMedallion({ name = '', seed, src, number, size = 84, pop = false, onClick, label, style, ...rest }) {
6
- const [down, setDown] = useState(false);
7
- const [hover, setHover] = useState(false);
8
9
  const interactive = typeof onClick === 'function';
9
- const pressed = interactive && down;
10
- const depth = Math.max(4, Math.round(size / 12)), travel = depth - 2;
11
- const plate = { h: Math.round(size * 0.36), min: Math.round(size * 0.43), fs: Math.round(size * 0.2) };
12
10
  const who = name || 'a friend';
13
11
  const accessibleName = label || (interactive
14
12
  ? (number != null ? 'Replay level ' + number + ', ' + who : 'Replay surprise from ' + who)
15
13
  : (number != null ? 'Level ' + number + ', ' + who : name || 'A friend'));
16
- const body = (
17
- <>
18
- <span aria-hidden="true" style={{
19
- display: 'grid', placeItems: 'center', width: size, height: size, borderRadius: '50%',
20
- background: 'radial-gradient(circle at 50% 30%, var(--gold-300), var(--gold-500) 60%, var(--gold-700))',
21
- boxShadow: '0 ' + (pressed ? 2 : depth) + 'px 0 var(--gold-900), 0 ' + (pressed ? 6 : depth * 2) + 'px ' + (pressed ? 10 : 18) + 'px oklch(0.2 0.05 60 / 0.35), inset 0 3px 0 oklch(1 0 0 / 0.6)',
22
- transform: pressed ? 'translateY(' + travel + 'px)' : 'none',
23
- filter: hover && !pressed ? 'brightness(1.06)' : 'none',
24
- transition: 'transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out), filter var(--dur-fast)',
25
- }}>
14
+ const medallion = (
15
+ <span className={'mp-medallion' + (pop ? ' mp-medallion-pop' : '')} style={{ '--m': size + 'px', ...(interactive ? null : style) }}
16
+ {...(interactive ? null : { role: 'img', 'aria-label': accessibleName, ...rest })}>
17
+ <span className="mp-medallion-ring">
26
18
  <Avatar name={name} seed={seed} src={src} size={Math.round(size * 0.8)} alt="" />
27
19
  </span>
28
- {number != null && (
29
- <span aria-hidden="true" style={{
30
- position: 'relative', zIndex: 1, marginTop: -Math.round(size * 0.17), minWidth: plate.min, height: plate.h, padding: '0 ' + Math.round(plate.h / 3) + 'px', boxSizing: 'border-box',
31
- borderRadius: 'var(--radius-pill)', background: 'linear-gradient(180deg, #fff, var(--cream-100))', boxShadow: '0 3px 0 var(--cream-300), 0 6px 10px oklch(0 0 0 / 0.2)',
32
- fontFamily: 'var(--font-display)', fontSize: plate.fs, lineHeight: plate.h + 'px', color: 'var(--ink-900)', textAlign: 'center',
33
- transform: pressed ? 'translateY(' + (travel - 1) + 'px)' : 'none', transition: 'transform var(--dur-fast) var(--ease-out)',
34
- }}>{number}</span>
35
- )}
36
- </>
20
+ {number != null && <span className="mp-medallion-number">{number}</span>}
21
+ </span>
37
22
  );
38
- const frame = {
39
- position: 'relative', display: 'inline-grid', justifyItems: 'center', width: size,
40
- animation: pop ? 'mp-medallion-pop 900ms var(--ease-out) both' : undefined,
41
- };
42
- if (!interactive) return <span role="img" aria-label={accessibleName} style={{ ...frame, ...style }} {...rest}>{body}</span>;
23
+ if (!interactive) return medallion;
43
24
  return (
44
- <button type="button" aria-label={accessibleName} onClick={onClick}
45
- onPointerDown={() => setDown(true)} onPointerUp={() => setDown(false)} onPointerCancel={() => setDown(false)}
46
- onPointerEnter={() => setHover(true)} onPointerLeave={() => { setDown(false); setHover(false); }}
47
- style={{ appearance: 'none', background: 'none', border: 0, margin: 0, padding: 0, cursor: 'pointer', borderRadius: 'var(--radius-pill)', WebkitTapHighlightColor: 'transparent', ...frame, ...style }} {...rest}>
48
- {body}
25
+ <button type="button" className="mp-medallion-button" aria-label={accessibleName} onClick={onClick} style={style} {...rest}>
26
+ {medallion}
49
27
  </button>
50
28
  );
51
29
  }
@@ -11,3 +11,4 @@ Portrait medallion: what a completed level becomes on the map. The revealed frie
11
11
  - Keep the replay label readable: it defaults to "Replay level 4, Ada Okafor". Pass `label` to change the wording, never to remove the name.
12
12
  - `pop` plays the arrival once. Set it only for the level that was just finished.
13
13
  - Same size as the `LevelNode` it replaces (84 by default), so the road doesn't shift.
14
+ - The pedestal's shadow and the number plate are the same at every size, so a small medallion for a discovery has the same bevel as a big one. It is a real button when it has `onClick`, with a hover, a press and a keyboard focus ring.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lanre-ds",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Lanre: a design system for a casual exploration game. React components and CSS tokens.",
5
5
  "license": "MIT",
6
6
  "keywords": [
package/readme.md CHANGED
@@ -119,7 +119,7 @@ Source: the shared build brief (12 Sep 2026). The generic Lanre kit above is the
119
119
  - `tokens/viewer.css`: the styles behind the viewer (`.mp-viewer`, `.mp-cabinet`, `.mp-keys`, `.mp-knob`, `.mp-tray`). It sets sizes for phones, iPad and desktop.
120
120
  - **Package:** `package.json` makes the repo an installable package, `lanre-ds`: `import 'lanre-ds/styles.css'`, then `import { Button } from 'lanre-ds/components/buttons/Button'`. See the docs page *Install it in an app*. It is published on npm and licensed MIT.
121
121
  - `tokens/fx.css`: the styles behind the effects and the boot screen (`.mp-confetti`, `.mp-sparkle-spot`, `.mp-sunburst-motion`, `.mp-loading`).
122
- - `tokens/ui.css`: the styles behind the small pieces (`.mp-heading`, `.mp-sticker`, `.mp-notice`, `.mp-error-text`, `.mp-save-pill`, `.mp-save-alert`, `.mp-avatar-button`, `.mp-seg`, `.mp-recorder`).
122
+ - `tokens/ui.css`: the styles behind the small pieces (`.mp-heading`, `.mp-sticker`, `.mp-notice`, `.mp-error-text`, `.mp-save-pill`, `.mp-save-alert`, `.mp-avatar-button`, `.mp-seg`, `.mp-recorder`, `.mp-medallion`).
123
123
  - `tokens/signs.css`: the styles behind the world signs (`.mp-gate`, `.mp-sign`, `.mp-world-pill`, `.mp-world-option`).
124
124
  - `tokens/media.css`: the styles behind the media components (`.mp-words`, `.mp-album`, `.mp-cassette`, `.mp-film`, `.mp-shimmer`). They use container queries, so each piece fits the box it sits in.
125
125
  - `tokens/worlds.css`, `tokens/gems.css`: world scopes and gem tones; iPad/phone layout tokens in `spacing.css`; memory/letter sizes in `typography.css`; focus ring + reduced motion in `utilities.css`; the night palette (`--night-*`, the summit letter and credits) and step-marker tokens (`--step-*`) in `colors.css`, with the `.mp-step` recipe in `utilities.css`.
package/tokens/ui.css CHANGED
@@ -1,6 +1,6 @@
1
1
  /* Small interface pieces: a heading with its plate, a sticker, a notice and error text, the autosave pill and alert, an avatar
2
- button with a pencil, segmented tabs, and the voice recorder card. Used by Heading, Sticker, Notice, ErrorText, SavePill,
3
- SaveAlert, AvatarButton, SegmentedTabs and RecorderPanel. */
2
+ button with a pencil, segmented tabs, the voice recorder card and the portrait medallion. Used by Heading, Sticker, Notice,
3
+ ErrorText, SavePill, SaveAlert, AvatarButton, SegmentedTabs, RecorderPanel and PortraitMedallion. */
4
4
 
5
5
  /* The friend's avatar in the HUD, with a small pencil to say it can be changed. */
6
6
  .mp-avatar-button {
@@ -352,3 +352,81 @@
352
352
  background: var(--coral-500);
353
353
  animation: mp-sparkle 1.2s infinite;
354
354
  }
355
+
356
+ /* ---------- Portrait medallion ---------- */
357
+
358
+ /* The tappable wrapper: it takes the press, the hover and the keyboard focus for the medallion inside it. */
359
+ .mp-medallion-button {
360
+ appearance: none;
361
+ background: none;
362
+ border: 0;
363
+ padding: 0;
364
+ cursor: pointer;
365
+ -webkit-tap-highlight-color: transparent;
366
+ }
367
+
368
+ /* Every tap answers back: the medallion presses down like a bevelled button. */
369
+ .mp-medallion-button:hover .mp-medallion-ring {
370
+ filter: brightness(1.06);
371
+ }
372
+
373
+ .mp-medallion-button:active .mp-medallion-ring {
374
+ transform: translateY(5px);
375
+ box-shadow: 0 2px 0 var(--gold-900), 0 6px 10px oklch(0.2 0.05 60 / 0.3), inset 0 3px 0 oklch(1 0 0 / 0.6);
376
+ }
377
+
378
+ .mp-medallion-button:active .mp-medallion-number {
379
+ transform: translateY(4px);
380
+ }
381
+
382
+ .mp-medallion-button:focus-visible {
383
+ outline: 4px solid #fff;
384
+ outline-offset: 4px;
385
+ border-radius: 999px;
386
+ }
387
+
388
+ /* Set --m to the medallion's size. The shadow and the number plate stay the same at every size. */
389
+ .mp-medallion {
390
+ position: relative;
391
+ display: inline-grid;
392
+ justify-items: center;
393
+ width: var(--m);
394
+ }
395
+
396
+ .mp-medallion-ring {
397
+ display: grid;
398
+ place-items: center;
399
+ width: var(--m);
400
+ height: var(--m);
401
+ border-radius: 50%;
402
+ background: radial-gradient(circle at 50% 30%, var(--gold-300), var(--gold-500) 60%, var(--gold-700));
403
+ box-shadow: 0 7px 0 var(--gold-900), 0 14px 18px oklch(0.2 0.05 60 / 0.35), inset 0 3px 0 oklch(1 0 0 / 0.6);
404
+ transition: transform var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out), filter var(--dur-fast);
405
+ }
406
+
407
+ .mp-medallion-number {
408
+ margin-top: -14px;
409
+ min-width: 36px;
410
+ height: 30px;
411
+ padding: 0 10px;
412
+ border-radius: 999px;
413
+ background: linear-gradient(180deg, #fff, var(--cream-100));
414
+ box-shadow: 0 3px 0 var(--cream-300), 0 6px 10px oklch(0 0 0 / 0.2);
415
+ font-family: var(--font-display);
416
+ font-size: 17px;
417
+ line-height: 30px;
418
+ color: var(--ink-900);
419
+ text-align: center;
420
+ z-index: 1;
421
+ transition: transform var(--dur-fast) var(--ease-out);
422
+ }
423
+
424
+ .mp-medallion-pop {
425
+ animation: mp-medallion-pop 900ms var(--ease-out) both;
426
+ }
427
+
428
+ @media (prefers-reduced-motion: reduce) {
429
+ .mp-medallion-pop {
430
+ animation: none;
431
+ }
432
+ }