lanre-ds 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/components/buttons/Button.d.ts +28 -0
- package/components/buttons/Button.jsx +50 -0
- package/components/buttons/Button.prompt.md +16 -0
- package/components/buttons/IconButton.d.ts +23 -0
- package/components/buttons/IconButton.jsx +53 -0
- package/components/buttons/IconButton.prompt.md +12 -0
- package/components/buttons/SegmentedTabs.d.ts +21 -0
- package/components/buttons/SegmentedTabs.jsx +15 -0
- package/components/buttons/SegmentedTabs.prompt.md +10 -0
- package/components/contributor/AudioRecorder.d.ts +27 -0
- package/components/contributor/AudioRecorder.jsx +79 -0
- package/components/contributor/AudioRecorder.prompt.md +13 -0
- package/components/contributor/RadioCard.d.ts +17 -0
- package/components/contributor/RadioCard.jsx +30 -0
- package/components/contributor/RadioCard.prompt.md +12 -0
- package/components/contributor/RecorderPanel.d.ts +30 -0
- package/components/contributor/RecorderPanel.jsx +49 -0
- package/components/contributor/RecorderPanel.prompt.md +11 -0
- package/components/contributor/StepBar.d.ts +17 -0
- package/components/contributor/StepBar.jsx +55 -0
- package/components/contributor/StepBar.prompt.md +12 -0
- package/components/contributor/TextField.d.ts +25 -0
- package/components/contributor/TextField.jsx +29 -0
- package/components/contributor/TextField.prompt.md +7 -0
- package/components/contributor/UploadTile.d.ts +39 -0
- package/components/contributor/UploadTile.jsx +116 -0
- package/components/contributor/UploadTile.prompt.md +21 -0
- package/components/contributor/WorldChip.d.ts +18 -0
- package/components/contributor/WorldChip.jsx +16 -0
- package/components/contributor/WorldChip.prompt.md +9 -0
- package/components/contributor/WorldOption.d.ts +19 -0
- package/components/contributor/WorldOption.jsx +19 -0
- package/components/contributor/WorldOption.prompt.md +11 -0
- package/components/contributor/WorldSign.d.ts +25 -0
- package/components/contributor/WorldSign.jsx +32 -0
- package/components/contributor/WorldSign.prompt.md +14 -0
- package/components/contributor/WorldSigns.d.ts +13 -0
- package/components/contributor/WorldSigns.jsx +12 -0
- package/components/contributor/WorldSigns.prompt.md +8 -0
- package/components/effects/Confetti.d.ts +14 -0
- package/components/effects/Confetti.jsx +38 -0
- package/components/effects/Confetti.prompt.md +12 -0
- package/components/effects/Sparkles.d.ts +9 -0
- package/components/effects/Sparkles.jsx +13 -0
- package/components/effects/Sparkles.prompt.md +11 -0
- package/components/effects/Sunburst.d.ts +23 -0
- package/components/effects/Sunburst.jsx +26 -0
- package/components/effects/Sunburst.prompt.md +12 -0
- package/components/feedback/Badge.d.ts +17 -0
- package/components/feedback/Badge.jsx +16 -0
- package/components/feedback/Badge.prompt.md +10 -0
- package/components/feedback/ErrorText.d.ts +11 -0
- package/components/feedback/ErrorText.jsx +8 -0
- package/components/feedback/ErrorText.prompt.md +9 -0
- package/components/feedback/LoadingScreen.d.ts +14 -0
- package/components/feedback/LoadingScreen.jsx +24 -0
- package/components/feedback/LoadingScreen.prompt.md +9 -0
- package/components/feedback/Notice.d.ts +15 -0
- package/components/feedback/Notice.jsx +16 -0
- package/components/feedback/Notice.prompt.md +10 -0
- package/components/feedback/SaveAlert.d.ts +15 -0
- package/components/feedback/SaveAlert.jsx +24 -0
- package/components/feedback/SaveAlert.prompt.md +9 -0
- package/components/feedback/SavePill.d.ts +13 -0
- package/components/feedback/SavePill.jsx +29 -0
- package/components/feedback/SavePill.prompt.md +8 -0
- package/components/feedback/SoundToggle.d.ts +16 -0
- package/components/feedback/SoundToggle.jsx +15 -0
- package/components/feedback/SoundToggle.prompt.md +6 -0
- package/components/feedback/Sticker.d.ts +15 -0
- package/components/feedback/Sticker.jsx +12 -0
- package/components/feedback/Sticker.prompt.md +9 -0
- package/components/feedback/Tag.d.ts +15 -0
- package/components/feedback/Tag.jsx +15 -0
- package/components/feedback/Tag.prompt.md +10 -0
- package/components/feedback/Toast.d.ts +21 -0
- package/components/feedback/Toast.jsx +19 -0
- package/components/feedback/Toast.prompt.md +7 -0
- package/components/forms/Slider.d.ts +24 -0
- package/components/forms/Slider.jsx +28 -0
- package/components/forms/Slider.prompt.md +6 -0
- package/components/forms/Switch.d.ts +19 -0
- package/components/forms/Switch.jsx +20 -0
- package/components/forms/Switch.prompt.md +6 -0
- package/components/games/MatchCard.d.ts +18 -0
- package/components/games/MatchCard.jsx +27 -0
- package/components/games/MatchCard.prompt.md +11 -0
- package/components/hud/Countdown.d.ts +33 -0
- package/components/hud/Countdown.jsx +74 -0
- package/components/hud/Countdown.prompt.md +15 -0
- package/components/hud/CurrencyPill.d.ts +24 -0
- package/components/hud/CurrencyPill.jsx +34 -0
- package/components/hud/CurrencyPill.prompt.md +12 -0
- package/components/hud/HudBar.d.ts +18 -0
- package/components/hud/HudBar.jsx +14 -0
- package/components/hud/HudBar.prompt.md +15 -0
- package/components/hud/HudChip.d.ts +25 -0
- package/components/hud/HudChip.jsx +30 -0
- package/components/hud/HudChip.prompt.md +13 -0
- package/components/hud/ProgressBar.d.ts +27 -0
- package/components/hud/ProgressBar.jsx +39 -0
- package/components/hud/ProgressBar.prompt.md +11 -0
- package/components/hud/TimerChip.d.ts +19 -0
- package/components/hud/TimerChip.jsx +17 -0
- package/components/hud/TimerChip.prompt.md +7 -0
- package/components/icons/GameIcon.d.ts +25 -0
- package/components/icons/GameIcon.jsx +43 -0
- package/components/icons/GameIcon.prompt.md +13 -0
- package/components/identity/Avatar.d.ts +26 -0
- package/components/identity/Avatar.jsx +31 -0
- package/components/identity/Avatar.prompt.md +11 -0
- package/components/identity/AvatarButton.d.ts +19 -0
- package/components/identity/AvatarButton.jsx +13 -0
- package/components/identity/AvatarButton.prompt.md +8 -0
- package/components/identity/Gemstone.d.ts +28 -0
- package/components/identity/Gemstone.jsx +46 -0
- package/components/identity/Gemstone.prompt.md +15 -0
- package/components/identity/PersonTile.d.ts +22 -0
- package/components/identity/PersonTile.jsx +27 -0
- package/components/identity/PersonTile.prompt.md +10 -0
- package/components/levels/ChoiceButton.d.ts +20 -0
- package/components/levels/ChoiceButton.jsx +31 -0
- package/components/levels/ChoiceButton.prompt.md +10 -0
- package/components/map/DiscoveryMarker.d.ts +19 -0
- package/components/map/DiscoveryMarker.jsx +31 -0
- package/components/map/DiscoveryMarker.prompt.md +11 -0
- package/components/map/LevelNode.d.ts +19 -0
- package/components/map/LevelNode.jsx +37 -0
- package/components/map/LevelNode.prompt.md +11 -0
- package/components/map/PortraitMedallion.d.ts +27 -0
- package/components/map/PortraitMedallion.jsx +29 -0
- package/components/map/PortraitMedallion.prompt.md +14 -0
- package/components/map/StarRating.d.ts +22 -0
- package/components/map/StarRating.jsx +21 -0
- package/components/map/StarRating.prompt.md +6 -0
- package/components/map/Tile.d.ts +22 -0
- package/components/map/Tile.jsx +32 -0
- package/components/map/Tile.prompt.md +8 -0
- package/components/map/WorldGate.d.ts +21 -0
- package/components/map/WorldGate.jsx +27 -0
- package/components/map/WorldGate.prompt.md +13 -0
- package/components/map/worlds.d.ts +18 -0
- package/components/map/worlds.js +9 -0
- package/components/media/Album.d.ts +33 -0
- package/components/media/Album.jsx +157 -0
- package/components/media/Album.prompt.md +16 -0
- package/components/media/Cassette.d.ts +23 -0
- package/components/media/Cassette.jsx +64 -0
- package/components/media/Cassette.prompt.md +13 -0
- package/components/media/Film.d.ts +20 -0
- package/components/media/Film.jsx +39 -0
- package/components/media/Film.prompt.md +14 -0
- package/components/media/MemoryText.d.ts +20 -0
- package/components/media/MemoryText.jsx +14 -0
- package/components/media/MemoryText.prompt.md +9 -0
- package/components/media/Unavailable.d.ts +13 -0
- package/components/media/Unavailable.jsx +13 -0
- package/components/media/Unavailable.prompt.md +10 -0
- package/components/media/Words.d.ts +21 -0
- package/components/media/Words.jsx +57 -0
- package/components/media/Words.prompt.md +12 -0
- package/components/media/shared.js +19 -0
- package/components/panels/Modal.d.ts +25 -0
- package/components/panels/Modal.jsx +68 -0
- package/components/panels/Modal.prompt.md +20 -0
- package/components/panels/Panel.d.ts +29 -0
- package/components/panels/Panel.jsx +109 -0
- package/components/panels/Panel.prompt.md +13 -0
- package/components/quiz/LeaderboardRow.d.ts +26 -0
- package/components/quiz/LeaderboardRow.jsx +24 -0
- package/components/quiz/LeaderboardRow.prompt.md +10 -0
- package/components/rewards/ItemCell.d.ts +24 -0
- package/components/rewards/ItemCell.jsx +24 -0
- package/components/rewards/ItemCell.prompt.md +9 -0
- package/components/rewards/RewardCard.d.ts +24 -0
- package/components/rewards/RewardCard.jsx +34 -0
- package/components/rewards/RewardCard.prompt.md +8 -0
- package/components/rewards/ShopRow.d.ts +23 -0
- package/components/rewards/ShopRow.jsx +26 -0
- package/components/rewards/ShopRow.prompt.md +7 -0
- package/components/text/Heading.d.ts +19 -0
- package/components/text/Heading.jsx +15 -0
- package/components/text/Heading.prompt.md +10 -0
- package/components/text/OutlineText.d.ts +24 -0
- package/components/text/OutlineText.jsx +20 -0
- package/components/text/OutlineText.prompt.md +11 -0
- package/components/toolbar/FastTravelDock.d.ts +50 -0
- package/components/toolbar/FastTravelDock.jsx +36 -0
- package/components/toolbar/FastTravelDock.prompt.md +12 -0
- package/components/toolbar/Toolbar.d.ts +16 -0
- package/components/toolbar/Toolbar.jsx +8 -0
- package/components/toolbar/Toolbar.prompt.md +14 -0
- package/components/toolbar/ToolbarNext.d.ts +21 -0
- package/components/toolbar/ToolbarNext.jsx +21 -0
- package/components/toolbar/ToolbarNext.prompt.md +13 -0
- package/components/toolbar/ToolbarTab.d.ts +15 -0
- package/components/toolbar/ToolbarTab.jsx +12 -0
- package/components/toolbar/ToolbarTab.prompt.md +9 -0
- package/components/viewer/ClueCard.d.ts +18 -0
- package/components/viewer/ClueCard.jsx +14 -0
- package/components/viewer/ClueCard.prompt.md +9 -0
- package/components/viewer/ClueTab.d.ts +16 -0
- package/components/viewer/ClueTab.jsx +12 -0
- package/components/viewer/ClueTab.prompt.md +8 -0
- package/components/viewer/RevealTray.d.ts +54 -0
- package/components/viewer/RevealTray.jsx +58 -0
- package/components/viewer/RevealTray.prompt.md +21 -0
- package/components/viewer/RoadsideViewer.d.ts +60 -0
- package/components/viewer/RoadsideViewer.jsx +96 -0
- package/components/viewer/RoadsideViewer.prompt.md +20 -0
- package/components/viewer/ViewerKeys.d.ts +26 -0
- package/components/viewer/ViewerKeys.jsx +22 -0
- package/components/viewer/ViewerKeys.prompt.md +14 -0
- package/components/viewer/ViewerKnob.d.ts +20 -0
- package/components/viewer/ViewerKnob.jsx +12 -0
- package/components/viewer/ViewerKnob.prompt.md +11 -0
- package/components/viewer/WhoButton.d.ts +21 -0
- package/components/viewer/WhoButton.jsx +15 -0
- package/components/viewer/WhoButton.prompt.md +10 -0
- package/package.json +62 -4
- package/readme.md +145 -0
- package/styles.css +15 -0
- package/tokens/colors.css +140 -0
- package/tokens/effects.css +40 -0
- package/tokens/fonts.css +3 -0
- package/tokens/fx.css +186 -0
- package/tokens/gems.css +8 -0
- package/tokens/hud.css +510 -0
- package/tokens/icons.css +7 -0
- package/tokens/media.css +774 -0
- package/tokens/signs.css +459 -0
- package/tokens/spacing.css +38 -0
- package/tokens/typography.css +34 -0
- package/tokens/ui.css +432 -0
- package/tokens/utilities.css +34 -0
- package/tokens/viewer.css +1176 -0
- package/tokens/worlds.css +73 -0
- package/tools/ds-loader.js +60 -0
- package/README.md +0 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Reveal tray: the moment of finding out who it was from. It opens centred over the viewer, so the memory itself is never covered up before it's been seen.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
{trayOpen && (
|
|
5
|
+
<RevealTray tone="blue" revealed={!!identity} identity={identity} wobble={wobble} leaving={leaving} onBackdrop={closeTray}
|
|
6
|
+
message="You found them!" actions={<><Button variant="primary" size="lg" block onClick={done}>Continue</Button><TrayLink onClick={closeTray}>Read it again</TrayLink></>}>
|
|
7
|
+
<TrayPrompt>Who sent this?</TrayPrompt>
|
|
8
|
+
<TrayClue>{clue}</TrayClue>
|
|
9
|
+
<TrayChoices>{options.map((o, i) => <ChoiceButton key={o.id} index={i} size="lg" state={stateOf(o)} onClick={() => guess(o)}>{o.name}</ChoiceButton>)}</TrayChoices>
|
|
10
|
+
<TrayLink onClick={tellMe}>Just tell me</TrayLink>
|
|
11
|
+
</RevealTray>
|
|
12
|
+
)}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- Before the reveal the face is the neutral mystery avatar. Never put the sender in the body or the label.
|
|
16
|
+
- Build the body with `TrayPrompt`, `TrayClue`, `TrayChoices` (of `ChoiceButton`s), `TrayHint` and `TrayLink`. "Just tell me" is always there, so nobody is stuck.
|
|
17
|
+
- After a reveal, the face flips, then the name, the relationship, the line and the actions arrive one after the other. The gem is only a stone, with no score, rank or tier text.
|
|
18
|
+
- `tone` is blue for a level and berry for a surprise beside the road. Pass `gold` once the gem has landed, so the tray warms.
|
|
19
|
+
- A memory already opened is `settled`: no flip, no "You found them!", nothing to unwrap.
|
|
20
|
+
- Render it only while it's open. Set `leaving` for a moment before removing it, so it fades.
|
|
21
|
+
- A wrong guess is `wobble`, once.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A memory in a wooden cabinet over the map. A sign on top names the level, and a screen plays one piece of the memory at a time, with a deck underneath of keys and one big button. While it loads, the cabinet is already in place with its screen warming up. Fill the slots with ViewerKeys, ViewerKnob, WhoButton, ClueTab and ClueCard, and open a RevealTray over it.
|
|
5
|
+
* @startingPoint section="Viewer" subtitle="The roadside viewer: sign, screen and deck" viewport="700x760"
|
|
6
|
+
*/
|
|
7
|
+
export interface RoadsideViewerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
8
|
+
/** World id for data-world, so the cabinet's sheet takes that world's colours. */
|
|
9
|
+
world?: 'mango_grove' | 'small_chops' | 'good_energy' | 'japan';
|
|
10
|
+
kind?: 'level' | 'discovery';
|
|
11
|
+
/** Small line on the sign, usually the world's name. */
|
|
12
|
+
signEyebrow?: string;
|
|
13
|
+
/** Big line on the sign, such as "Level 4". */
|
|
14
|
+
signTitle?: string;
|
|
15
|
+
/** id of the sign's heading, which names the dialog. Default "memory-title". */
|
|
16
|
+
titleId?: string;
|
|
17
|
+
/** Leaves on the sign, for a surprise beside the road. */
|
|
18
|
+
leafy?: boolean;
|
|
19
|
+
/** A berry ribbon over the top, such as "Just arrived". */
|
|
20
|
+
ribbon?: string;
|
|
21
|
+
onClose?: () => void;
|
|
22
|
+
/** Accessible name of the close button. Default "Back to the map". */
|
|
23
|
+
closeLabel?: string;
|
|
24
|
+
/** Small line above the title, such as "2 of 3 · Photo". */
|
|
25
|
+
eyebrow?: string;
|
|
26
|
+
/** The memory's own title. */
|
|
27
|
+
title?: string;
|
|
28
|
+
/** The cabinet is in place with the screen off and "Tuning in". */
|
|
29
|
+
loading?: boolean;
|
|
30
|
+
/** Shows the screen off with this message and a retry button. */
|
|
31
|
+
error?: string;
|
|
32
|
+
onRetry?: () => void;
|
|
33
|
+
retryLabel?: string;
|
|
34
|
+
loadingLabel?: string;
|
|
35
|
+
/** Plays the leaving animation. */
|
|
36
|
+
closing?: boolean;
|
|
37
|
+
/** Sits inside a positioned box instead of covering the window. */
|
|
38
|
+
contained?: boolean;
|
|
39
|
+
titleRef?: React.Ref<HTMLHeadingElement>;
|
|
40
|
+
/** A WhoButton. */
|
|
41
|
+
who?: React.ReactNode;
|
|
42
|
+
/** A ClueTab. */
|
|
43
|
+
clue?: React.ReactNode;
|
|
44
|
+
/** A ClueCard, shown over the screen. */
|
|
45
|
+
clueCard?: React.ReactNode;
|
|
46
|
+
/** A ViewerKeys. */
|
|
47
|
+
keys?: React.ReactNode;
|
|
48
|
+
/** A ViewerKnob. */
|
|
49
|
+
knob?: React.ReactNode;
|
|
50
|
+
/** Changes when the piece on screen changes, so the new piece arrives with its own entrance. */
|
|
51
|
+
pieceKey?: React.Key;
|
|
52
|
+
/** An error shown under the cabinet. */
|
|
53
|
+
message?: string;
|
|
54
|
+
/** Layers over the cabinet, such as a RevealTray. */
|
|
55
|
+
layers?: React.ReactNode;
|
|
56
|
+
/** What's on the screen: Words, an Album, a Cassette or a Film. */
|
|
57
|
+
children?: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function RoadsideViewer(props: RoadsideViewerProps): React.JSX.Element;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Button } from '../buttons/Button';
|
|
4
|
+
|
|
5
|
+
/* A memory in a wooden cabinet over the map: a sign on top, a header, a screen that plays one piece at a time, and a
|
|
6
|
+
deck underneath. Fill the slots with the pieces: `who`, `clue`, `clueCard`, `keys`, `knob`, and the screen's content
|
|
7
|
+
as children. While `loading` the cabinet is already in place with its screen switched off and warming up.
|
|
8
|
+
By default it covers the window like a sheet; set `contained` to sit inside a positioned box instead. */
|
|
9
|
+
export function RoadsideViewer({ world, kind = 'level', signEyebrow, signTitle, titleId = 'memory-title', leafy = false, ribbon, onClose, closeLabel = 'Back to the map',
|
|
10
|
+
eyebrow, title, loading = false, error, onRetry, retryLabel = 'Try again', loadingLabel = 'Tuning in', closing = false, contained = false, titleRef,
|
|
11
|
+
who, clue, clueCard, keys, knob, pieceKey, message, layers, children, ...rest }) {
|
|
12
|
+
const ready = !loading && !error;
|
|
13
|
+
return (
|
|
14
|
+
<div className={'mp-viewer' + (closing ? ' mp-viewer-closing' : '') + (contained ? ' mp-viewer-contained' : '')} role="dialog" aria-modal="true" aria-labelledby={titleId} {...rest}>
|
|
15
|
+
<div className="mp-viewer-overlay">
|
|
16
|
+
<div className="mp-viewer-sheet" data-world={world} data-kind={kind}>
|
|
17
|
+
<div className={'mp-viewer-sign'}>
|
|
18
|
+
{leafy && (
|
|
19
|
+
<>
|
|
20
|
+
<i className="ph-fill ph-leaf mp-viewer-leaf mp-viewer-leaf-a" aria-hidden="true" />
|
|
21
|
+
<i className="ph-fill ph-leaf mp-viewer-leaf mp-viewer-leaf-b" aria-hidden="true" />
|
|
22
|
+
<i className="ph-fill ph-leaf mp-viewer-leaf mp-viewer-leaf-c" aria-hidden="true" />
|
|
23
|
+
</>
|
|
24
|
+
)}
|
|
25
|
+
<span className="mp-viewer-sign-eyebrow">{signEyebrow}</span>
|
|
26
|
+
<h2 id={titleId} ref={titleRef} tabIndex={-1} className={'mp-viewer-sign-title' + (leafy ? ' mp-viewer-sign-title-long' : '')}>{signTitle}</h2>
|
|
27
|
+
</div>
|
|
28
|
+
{ribbon && <span className="mp-viewer-ribbon"><i className="ph-fill ph-sparkle" aria-hidden="true" /> {ribbon}</span>}
|
|
29
|
+
<button type="button" className="mp-viewer-close" onClick={onClose} aria-label={closeLabel}>
|
|
30
|
+
<i className="ph-bold ph-x" aria-hidden="true" />
|
|
31
|
+
</button>
|
|
32
|
+
|
|
33
|
+
{!ready && (
|
|
34
|
+
<>
|
|
35
|
+
<header className="mp-viewer-head" aria-hidden="true">
|
|
36
|
+
<span className="mp-placeholder mp-placeholder-eyebrow" />
|
|
37
|
+
<span className="mp-placeholder mp-placeholder-title" />
|
|
38
|
+
</header>
|
|
39
|
+
<div className="mp-cabinet">
|
|
40
|
+
<div className="mp-screen mp-screen-off" role={error ? 'alert' : 'status'}>
|
|
41
|
+
{error ? (
|
|
42
|
+
<div className="mp-screen-off-message">
|
|
43
|
+
<i className="ph-fill ph-cloud-warning" aria-hidden="true" />
|
|
44
|
+
<p>{error}</p>
|
|
45
|
+
<Button variant="play" onClick={onRetry}>{retryLabel}</Button>
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
<>
|
|
49
|
+
<span className="mp-screen-sweep" aria-hidden="true" />
|
|
50
|
+
<div className="mp-screen-off-message">
|
|
51
|
+
<span className="mp-screen-tuning">
|
|
52
|
+
{loadingLabel}
|
|
53
|
+
<span className="mp-tuning-dots" aria-hidden="true"><i /><i /><i /></span>
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
</>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
<div className="mp-deck" aria-hidden="true">
|
|
60
|
+
<div className="mp-keys">
|
|
61
|
+
<span className="mp-placeholder mp-placeholder-key" />
|
|
62
|
+
<span className="mp-placeholder mp-placeholder-key" />
|
|
63
|
+
</div>
|
|
64
|
+
<span className="mp-placeholder mp-placeholder-knob" />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</>
|
|
68
|
+
)}
|
|
69
|
+
|
|
70
|
+
{ready && (
|
|
71
|
+
<>
|
|
72
|
+
<header className="mp-viewer-head">
|
|
73
|
+
{eyebrow && <span className="mp-viewer-eyebrow">{eyebrow}</span>}
|
|
74
|
+
{title && <h3 className="mp-viewer-title">{title}</h3>}
|
|
75
|
+
</header>
|
|
76
|
+
<div className="mp-cabinet">
|
|
77
|
+
{who}
|
|
78
|
+
{clue}
|
|
79
|
+
<div className="mp-screen" role="group" aria-label="The memory">
|
|
80
|
+
<div key={pieceKey} className="mp-screen-piece">{children}</div>
|
|
81
|
+
{clueCard}
|
|
82
|
+
</div>
|
|
83
|
+
<div className="mp-deck">
|
|
84
|
+
{keys}
|
|
85
|
+
{knob}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
{message && <p role="alert" className="mp-viewer-error">{message}</p>}
|
|
89
|
+
</>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
{layers}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Roadside viewer: a memory opened over the map, in a wooden cabinet with its own sign.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<RoadsideViewer world="small_chops" signEyebrow="Small Chops, Big Gist" signTitle="Level 4" eyebrow="2 of 3 · Photo" title="The day at the beach"
|
|
5
|
+
who={<WhoButton text="Ready to guess" onClick={openTray} />}
|
|
6
|
+
keys={<ViewerKeys pieces={pieces} at={at} onSelect={setAt} />}
|
|
7
|
+
knob={<ViewerKnob label="Next: voice note" onClick={next} />}
|
|
8
|
+
pieceKey={at} onClose={back} layers={tray}>
|
|
9
|
+
<Album photos={photos} />
|
|
10
|
+
</RoadsideViewer>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- The memory comes first. Who sent it is a separate moment, in a `RevealTray` that opens over the cabinet.
|
|
14
|
+
- The screen plays one piece at a time: `Words`, `Album`, `Cassette` or `Film`. Change `pieceKey` when the piece changes so the next one arrives with its entrance.
|
|
15
|
+
- The deck's one big button says what happens next: the next piece, then who, then onwards. Pass a `ViewerKnob` for it, and `ViewerKeys` for the keys.
|
|
16
|
+
- Set `loading` while the memory arrives. The cabinet is already there, with its screen switched off and "Tuning in". If it can't load, pass `error` and `onRetry`.
|
|
17
|
+
- A surprise beside the road is `kind="discovery"` and `leafy`, with a `ribbon` like "Just arrived".
|
|
18
|
+
- By default it covers the window like a sheet. Set `contained` to place it in a positioned box.
|
|
19
|
+
- Its layout follows the window, not the box it's in: phones, iPad and desktop each get their own sizes. So `contained` is for a box about as big as the window, such as a docs preview. A small box on a wide window gets the desktop layout and will clip.
|
|
20
|
+
- The sign's heading names the dialog. Keep `titleId` unique if two are ever open.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ViewerPiece {
|
|
4
|
+
/** A Phosphor icon name, such as "image" or "microphone". */
|
|
5
|
+
icon: string;
|
|
6
|
+
label: string;
|
|
7
|
+
/** A short note shown until the piece is seen, such as a length. */
|
|
8
|
+
meta?: string;
|
|
9
|
+
/** Shows a tick, once the viewer has been through it. */
|
|
10
|
+
seen?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The deck's keys, one per piece of the memory, each with its icon and name. A seen key shows a tick. With more than three pieces only the one on screen keeps its name.
|
|
15
|
+
* @startingPoint section="Viewer" subtitle="The keys, one per piece" viewport="700x140"
|
|
16
|
+
*/
|
|
17
|
+
export interface ViewerKeysProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSelect'> {
|
|
18
|
+
pieces: ViewerPiece[];
|
|
19
|
+
/** Index of the piece on screen. */
|
|
20
|
+
at?: number;
|
|
21
|
+
onSelect?: (index: number) => void;
|
|
22
|
+
/** Accessible name of the group. Default "In this memory". */
|
|
23
|
+
label?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ViewerKeys(props: ViewerKeysProps): React.JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* The deck's keys: one per piece of the memory, with its icon and name. A key already seen shows a tick, and one
|
|
5
|
+
not yet seen can show a short note such as a length. With more than three pieces, only the one on screen keeps its name. */
|
|
6
|
+
export function ViewerKeys({ pieces = [], at = 0, onSelect, label = 'In this memory', ...rest }) {
|
|
7
|
+
return (
|
|
8
|
+
<div className={'mp-keys' + (pieces.length > 3 ? ' mp-keys-many' : '')} role="group" aria-label={label} {...rest}>
|
|
9
|
+
{pieces.map((p, i) => (
|
|
10
|
+
<button key={i} type="button" className={'mp-key' + (i === at ? ' mp-key-on' : '')} aria-current={i === at ? 'true' : undefined} onClick={onSelect ? () => onSelect(i) : undefined}>
|
|
11
|
+
<i className={'ph-fill ph-' + p.icon} aria-hidden="true" />
|
|
12
|
+
<span className="mp-key-label">{p.label}</span>
|
|
13
|
+
{i !== at && p.seen ? (
|
|
14
|
+
<span className="mp-key-tick"><i className="ph-bold ph-check" aria-hidden="true" /><span className="mp-sr">, seen</span></span>
|
|
15
|
+
) : (
|
|
16
|
+
i !== at && p.meta && <span className="mp-key-meta">{p.meta}</span>
|
|
17
|
+
)}
|
|
18
|
+
</button>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Viewer keys: the way around a memory with more than one piece.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<ViewerKeys at={at} onSelect={setAt} pieces={[
|
|
5
|
+
{ icon: 'quotes', label: 'Message', seen: true },
|
|
6
|
+
{ icon: 'images', label: '3 photos' },
|
|
7
|
+
{ icon: 'microphone', label: 'Voice note', meta: '0:42' },
|
|
8
|
+
]} />
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- Order is the contributor's order. Nothing advances by itself.
|
|
12
|
+
- A key for a piece already seen shows a tick. Until then it can show a short `meta` note, such as a length.
|
|
13
|
+
- With more than three pieces, only the key on screen keeps its name, so they all fit.
|
|
14
|
+
- Only show the keys when there's more than one piece.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one big button on the deck. It always says what happens next, with a round dial on the right. Blue and nudging while waiting on a guess, green once saved.
|
|
5
|
+
* @startingPoint section="Viewer" subtitle="The big button that says what's next" viewport="700x140"
|
|
6
|
+
*/
|
|
7
|
+
export interface ViewerKnobProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
8
|
+
label: string;
|
|
9
|
+
/** A Phosphor icon name for the dial. Default "caret-right". */
|
|
10
|
+
icon?: string;
|
|
11
|
+
weight?: 'bold' | 'fill';
|
|
12
|
+
/** guess = waiting on a guess. saved = done. */
|
|
13
|
+
tone?: 'guess' | 'saved';
|
|
14
|
+
/** A gentle nudge every few seconds, like the Level button on the map. */
|
|
15
|
+
attract?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
onClick?: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ViewerKnob(props: ViewerKnobProps): React.JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* The one big button on the deck. It always says what happens next, with a dial on the right. */
|
|
5
|
+
export function ViewerKnob({ label, icon = 'caret-right', weight = 'bold', tone, attract = false, disabled = false, onClick, ...rest }) {
|
|
6
|
+
return (
|
|
7
|
+
<button type="button" className={'mp-knob' + (tone ? ' mp-knob-' + tone : '') + (attract ? ' mp-knob-attract' : '')} onClick={onClick} disabled={disabled} {...rest}>
|
|
8
|
+
{label}
|
|
9
|
+
<span className="mp-knob-dial"><i className={'ph-' + weight + ' ph-' + icon} aria-hidden="true" /></span>
|
|
10
|
+
</button>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Viewer knob: the one big button, so there's never any doubt about what to press next.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<ViewerKnob label="Next: voice note" onClick={next} />
|
|
5
|
+
<ViewerKnob label="Who's behind this?" icon="question" tone="guess" attract onClick={openTray} />
|
|
6
|
+
<ViewerKnob label="Saved" icon="check" tone="saved" />
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
- Its words change with the moment: the next piece, then who, then onwards. Say it in the label, never a bare "Next".
|
|
10
|
+
- `attract` gives the blue guess knob a nudge every few seconds. Use it only while waiting on a guess.
|
|
11
|
+
- With reduced motion it stays still.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Who sent it, on the cabinet: a mystery face and "Ready to guess" until they've guessed, then the friend's face and "From" with their name. Pressing it opens the tray either way.
|
|
5
|
+
* @startingPoint section="Viewer" subtitle="Mystery face, then the friend" viewport="700x140"
|
|
6
|
+
*/
|
|
7
|
+
export interface WhoButtonProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
8
|
+
/** True once they know who it was from. */
|
|
9
|
+
found?: boolean;
|
|
10
|
+
/** Berry instead of blue, for a surprise beside the road. */
|
|
11
|
+
berry?: boolean;
|
|
12
|
+
name?: string;
|
|
13
|
+
/** Stable id for the generated avatar. */
|
|
14
|
+
seed?: string;
|
|
15
|
+
portrait?: string;
|
|
16
|
+
/** What it says before the reveal. Default "Ready to guess". */
|
|
17
|
+
text?: string;
|
|
18
|
+
onClick?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function WhoButton(props: WhoButtonProps): React.JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Avatar } from '../identity/Avatar';
|
|
4
|
+
|
|
5
|
+
/* Who sent it: a mystery face until they've guessed, then the friend. Pressing it opens the tray either way. */
|
|
6
|
+
export function WhoButton({ found = false, berry = false, name, seed, portrait, text = 'Ready to guess', onClick, ...rest }) {
|
|
7
|
+
return (
|
|
8
|
+
<button type="button" className={'mp-who' + (found ? ' mp-who-found' : berry ? ' mp-who-berry' : '')} onClick={onClick} {...rest}>
|
|
9
|
+
{found ? <Avatar name={name} seed={seed} src={portrait || undefined} size={40} alt="" /> : <Avatar mystery size={40} />}
|
|
10
|
+
<span className="mp-who-text">
|
|
11
|
+
{found ? <><small>From</small>{name}</> : text}
|
|
12
|
+
</span>
|
|
13
|
+
</button>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Who button: the cabinet's "who" moment. It's there from the start, so who sent it is never a surprise they can't get back to.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<WhoButton text="Ready to guess" onClick={openTray} aria-label="Who's behind this?" />
|
|
5
|
+
<WhoButton found name="Ada Okafor" seed="c-7" onClick={openTray} aria-label="From Ada Okafor. See who it was from again" />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
- Give it an `aria-label` that says what pressing it does. The visible text alone doesn't.
|
|
9
|
+
- Before the reveal it shows the neutral mystery face, never anything about the sender.
|
|
10
|
+
- Use `text` to match how the level works: "Ready to guess", or "See who it's from" when it's a direct reveal.
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lanre-ds",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Lanre: a design system for a casual exploration game. React components and CSS tokens.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"design-system",
|
|
8
|
+
"react",
|
|
9
|
+
"css-tokens",
|
|
10
|
+
"game-ui"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"workspaces": [
|
|
14
|
+
"docs"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"storybook": "storybook dev -p 6006",
|
|
18
|
+
"build-storybook": "storybook build",
|
|
19
|
+
"docs": "npm run dev -w docs",
|
|
20
|
+
"build-docs": "npm run build -w docs",
|
|
21
|
+
"pack:check": "npm pack --dry-run"
|
|
22
|
+
},
|
|
23
|
+
"sideEffects": [
|
|
24
|
+
"**/*.css"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
"./styles.css": "./styles.css",
|
|
28
|
+
"./tokens/*.css": "./tokens/*.css",
|
|
29
|
+
"./components/map/worlds": {
|
|
30
|
+
"types": "./components/map/worlds.d.ts",
|
|
31
|
+
"default": "./components/map/worlds.js"
|
|
32
|
+
},
|
|
33
|
+
"./components/*": {
|
|
34
|
+
"types": "./components/*.d.ts",
|
|
35
|
+
"default": "./components/*.jsx"
|
|
36
|
+
},
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"components",
|
|
41
|
+
"tokens",
|
|
42
|
+
"styles.css",
|
|
43
|
+
"tools/ds-loader.js",
|
|
44
|
+
"readme.md",
|
|
45
|
+
"!**/*.card.html"
|
|
46
|
+
],
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": "^19.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@storybook/addon-a11y": "10.6.3",
|
|
52
|
+
"@storybook/addon-docs": "10.6.3",
|
|
53
|
+
"@storybook/react-vite": "10.6.3",
|
|
54
|
+
"@vitejs/plugin-react": "6.1.2",
|
|
55
|
+
"react": "19.2.8",
|
|
56
|
+
"react-dom": "19.2.8",
|
|
57
|
+
"storybook": "10.6.3",
|
|
58
|
+
"typescript": "6.0.3",
|
|
59
|
+
"vite": "8.3.5"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Lanre: design system for a casual exploration game
|
|
2
|
+
|
|
3
|
+
Lanre is a design system for mobile "exploration" games: a world map with a winding trail of level nodes, expedition boards, a camp/inventory grid, and the layer of panels every casual game needs (level start/complete, shop, daily reward, settings). It is bright, chunky and tactile, painted meadow greens, sky-blue actions, gold rewards, wood frames, and everything pressable has a solid bottom edge.
|
|
4
|
+
|
|
5
|
+
There is no existing product or brand behind it. It was synthesised from nine reference screenshots supplied by the user (casual level maps, an idle farm, dice/board events, a purple-dusk UI kit, a bubbly loading screen, a wood-framed daily reward, a Chinese travel-event board, a cat-eared settings panel and a level-complete dialog). The name, palette, type pairing and component inventory are original.
|
|
6
|
+
|
|
7
|
+
## Product surfaces
|
|
8
|
+
One product: a portrait mobile game (390 × 844 design frame). Surfaces covered by the UI kit (`ui_kits/lanre-game/`): world map, expedition (board) event, camp/inventory, and the dialog layer.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## CONTENT FUNDAMENTALS
|
|
13
|
+
|
|
14
|
+
- **Voice:** warm, short, imperative. The game talks to one player ("Pick your daily gift!"), never about itself. No "we".
|
|
15
|
+
- **Actions are one word in display caps:** CLAIM · OK · PLAY · SPIN · SHOP · GO. Never "Click here", "Submit", "Continue".
|
|
16
|
+
- **Titles are noun phrases ≤ 3 words**: display face, uppercase: LEVEL 23 · DAILY REWARD · COMPLETE · x2 BOOST.
|
|
17
|
+
- **Prompts and rows are sentence case**: UI face, 600–700: "Sound", "Vibration", "Pick your daily gift!", "One-time offer". One exclamation mark per screen at most.
|
|
18
|
+
- **Fine print** is plain sentences at 12–14px in muted ink ("You will still be able to watch rewarded videos.").
|
|
19
|
+
- **Numbers do the talking.** Always show the delta, fraction or countdown rather than describing it: `+500`, `3/6`, `00:02`, `6/20`, `75%`.
|
|
20
|
+
- **Number formatting:** currency uses `k`/`M` suffixes and comma groups (`50,253k`, `1.2M`, `2,720`); scores use thin-space groups (`14 000`); timers are `mm:ss` or `4h 12m`; prices show the currency code, not a symbol (`USD 0.99`).
|
|
21
|
+
- **Places and progress are proper nouns:** Warmful Valley, Pine Ridge, Day 3, Level 22.
|
|
22
|
+
- **Merchandising labels** are 1–3 uppercase words in a Tag: MOST POPULAR · 3X THE VALUE · ONE-TIME OFFER · NEW.
|
|
23
|
+
- **No emoji, ever.** Glyphs are Phosphor icons (a `GameIcon` when they stand alone), not characters.
|
|
24
|
+
- Casing summary: display type is rendered uppercase by CSS (write Title Case in source); UI type is sentence case; small labels (SCORE, REWARD, LANGUAGE) are UI caps with 0.08–0.12em tracking.
|
|
25
|
+
|
|
26
|
+
## VISUAL FOUNDATIONS
|
|
27
|
+
|
|
28
|
+
**Colour.** A painted, saturated, warm-leaning palette in oklch. Every tone is a 5-step ramp (100 → 900) used with one recipe: **500 is the face, 300 the top highlight, 700 the bevel/side, 900 the outline.**
|
|
29
|
+
- *Meadow green*: the environment (grass gradient `--bg-meadow`, bushes, board tiles).
|
|
30
|
+
- *Sky blue*: action and "current": play buttons, the current/complete level node, water.
|
|
31
|
+
- *Gold*: reward and the primary CTA (coins, stars, CLAIM). Gold buttons take dark brown text, never white.
|
|
32
|
+
- *Coral*: danger/urgency, hearts, close buttons. *Teal*: confirm/OK/value. *Berry (magenta)*: premium, boosts, hazards.
|
|
33
|
+
- *Wood*: frames, fences, signs, the bottom bar. *Stone*: anything locked or disabled (replaces the tone wholesale).
|
|
34
|
+
- *Parchment* creams for panel surfaces; *ink* is warm brown (`--ink-900`), never black.
|
|
35
|
+
- Semantic aliases (`--action-*`, `--state-*`, `--currency-*`, `--surface-*`, `--text-*`) are what components read; see `guidelines/colors-semantic.html`.
|
|
36
|
+
|
|
37
|
+
**Type.** Two faces. *Titan One* (display, one weight) for titles, numerals, button labels, always uppercase with +0.02em tracking. *Quicksand* 500/600/700 (UI) for rows, body, captions. Scale: display 44/32/24/18/14; UI 20/16/14/12/11. Text that sits directly on the world is **outlined**: white fill, dark stroke painted behind the fill (`paint-order: stroke fill`, stroke 0.16em; 0.20em under 20px), hard 10% drop. Stroke tone follows the surface (ink on grass, blue on blue level nodes, stone on locked ones, wood on planks). Minimum text size 11px; button labels ≥ 14px.
|
|
38
|
+
|
|
39
|
+
**Spacing & layout.** 4px base (`--space-1…16`). Touch targets ≥ 48px; primary CTA 60px. Fixed 56px HUD strip (level star + XP left, currency pills right, gear); the world scrolls beneath it; 88px wood bottom bar with 4 captioned square icon buttons (the Birthday Adventure has its own three-slot toolbar, see the project layer); side rails hold captioned icon buttons stacked 14–16px apart. Panels are centred, max 340px wide, never edge-to-edge, over a `--bg-scrim` veil. Grids: inventory 4 columns × 72px with 8px gaps; reward calendar 3 columns.
|
|
40
|
+
|
|
41
|
+
**Backgrounds.** Painted world art is the intended ground; until it exists, CSS gradients stand in: `--bg-meadow` (grass), `--bg-sky` (loading/light), `--bg-dusk` (evening/premium). Decorative bushes, rocks and flowers are simple CSS blobs; hero illustrations get a dashed, striped **placeholder slot** with a monospace note (never hand-drawn art in a mockup or prototype). The one exception is the Birthday Adventure's map, whose scenery is a reviewed SVG library (see **Scenery** in the project layer). No textures, no photo imagery, no repeating patterns beyond the diagonal stripe on progress fills.
|
|
42
|
+
|
|
43
|
+
**Depth (the signature).** Everything pressable is a **bevel**: gradient face (300 → 500 to 42%), a hard `0 N px 0` shadow in the 700 tone as the bottom edge (3/5/7px for sm/md/lg), a soft brown drop below, and an inset 2px white line on top. Pressing translates the element down 4px and collapses the edge to 1px (120ms ease-out). Hover only brightens 6%. Level nodes are ellipses with the same recipe plus two white gloss spots. Tracks and cells are **inset** (dark translucent brown or green with an inner shadow). Shadows are brown-tinted (`oklch(0.22–0.25 0.05 60)`), never grey. Glow rings (tone at 50% + 70% bloom) mark the tappable or rewarded thing.
|
|
44
|
+
|
|
45
|
+
**Corners.** Everything is rounded: 6 tags, 10 badges/knobs, 16 buttons/cells/cards, 22 rows/tabs, 28 panels, pill for tracks and HUD pills; level nodes and blobs are 50% ellipses.
|
|
46
|
+
|
|
47
|
+
**Borders.** Panels have a 4px border in the next-darker cream (wood panels 6px in wood-500); cells a 3px translucent white; badges and tokens a 2–3px solid white ring. No 1px hairlines.
|
|
48
|
+
|
|
49
|
+
**Transparency & blur.** Translucent surfaces only over the world (HUD pills at 82% brown, inventory cells at 30% green, scrim at 55%). Surfaces never blur. The one exception is the scrim behind a modal, which softens the world by `--scrim-blur` (3px).
|
|
50
|
+
|
|
51
|
+
**Motion.** Arrivals bounce (`--ease-bounce`, 420ms: stars, toasts, panels via `mp-pop`); presses ease-out at 120ms; claimable things idle-float (`mp-float`, 2.4s); the current level and reachable tile pulse a ring (`mp-pulse-ring`, 1.6s); progress fills animate width over 420ms. No fades-only, no parallax.
|
|
52
|
+
|
|
53
|
+
**States.** Pressed = travel down + collapsed edge. Disabled/locked = stone tones + 85% opacity (never greyscale filters on buttons; RewardCard uses desaturation because it must keep its prize readable). Selected = teal border + glow. Current = blue + pulse. Complete = blue + star row.
|
|
54
|
+
|
|
55
|
+
**Imagery.** Warm, saturated, painterly with dark outlines, the same language as the icons. Photos are never used.
|
|
56
|
+
|
|
57
|
+
## ICONOGRAPHY
|
|
58
|
+
|
|
59
|
+
- **Set:** [Phosphor Icons](https://phosphoricons.com) in the **FILL** and **BOLD** weights. `tokens/icons.css` loads them from a CDN for no-build pages; an app can install `@phosphor-icons/web` and import the same two weights instead, as the birthday app does. This is a substitution: the references use bespoke painted icons; Phosphor's filled weight is the closest CDN match to their chunky silhouettes. Replace with painted PNG/SVG art in `assets/` when available.
|
|
60
|
+
- **Treatment:** a glyph that stands on its own (on the world, on a panel, as a medal or a prize) is a `GameIcon`: a filled glyph in a tone with a dark outline painted behind (`-webkit-text-stroke` + `paint-order: stroke fill`) and a hard drop, so it reads like a sticker. Semantic kinds fix glyph + tone together (coin = gold `coin`, gem = blue `sketch-logo`, ruby/heart = coral, energy = teal `lightning`, gift = berry, lock = stone…).
|
|
61
|
+
- **Inside components:** a glyph in a component's icon slot (`Button`, `IconButton`, `Tag`, a level node) is plain Phosphor (`<i className="ph-fill ph-star" />`), and the component styles it. Never rebuild the sticker look in CSS; if a glyph needs it, it's a `GameIcon`.
|
|
62
|
+
- **Weights:** FILL for pictorial glyphs (coins, stars, hearts, gifts, chests); BOLD for utility strokes (×, +, ✓, carets, gear). Inside a bevelled button the glyph is plain white with no outline/shadow (the button already carries depth).
|
|
63
|
+
- **Sizes:** 20 / 28 / 40 / 56 (`--size-icon-*`). Currency glyphs overlap the left edge of their pill.
|
|
64
|
+
- No icon fonts other than Phosphor, no emoji, no unicode characters as icons.
|
|
65
|
+
- No logo was provided; the name is set in `.mp-display .mp-outline` wherever a mark would go. Nothing was drawn or reconstructed.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## PROJECT LAYER: The Birthday Adventure
|
|
70
|
+
|
|
71
|
+
Source: the shared build brief (12 Sep 2026). The generic Lanre kit above is the base; this layer adds the components and rules that brief needs. Names in every demo are fictional fixtures; no quiz facts, narrator copy or letter text are supplied here.
|
|
72
|
+
|
|
73
|
+
**Two audiences, two surfaces.** The recipient’s experience: iPad-first, phones supported, no hard device block. Flow: opening → map → level → reveal → progress → summit. Contributor flow (Claude): phone-first, no account: onboarding → composer → theme → suitability → preview → submit → optional quiz → gem + leaderboard → private edit link.
|
|
74
|
+
|
|
75
|
+
**Worlds** (`tokens/worlds.css`, `components/map/worlds.js`). `data-world="base_camp | mango_grove | small_chops | good_energy | japan | summit"` scopes `--world-sky/ground/ground-deep/bg`, `--world-accent/-shade` (signs, chips), `--world-path/-edge` (the road) and `--world-stroke` (outlined text), plus scenery: `--world-sky-low` (the horizon) and `--prop-hill/-bush-hi/-bush/-bush-lo` (CSS hills and bushes). `base_camp` is the contributors' world, where friends pack their memories; it's not a stop on the road. Numbering runs continuously across the road's worlds; uneven populations are fine and never padded. Summit is the author’s letter, never a contributor choice.
|
|
76
|
+
|
|
77
|
+
**Gemstones** (`tokens/gems.css`, `Gemstone`, `gemForScore`). Score 0–2 Amber I/II/III · 3–5 Emerald · 6–8 Amethyst · quiz skipped → Pearl keepsake, unranked (not a zero). Tier I plain, II adds a sparkle, III adds a halo (proposed: III keeps the sparkle). Cuts differ per gem (hexagon / octagon / kite / round) so colour is never the only signal. **The recipient’s surfaces show the stone only**, no score, rank or tier text on the reveal card or in Your People; `showTier` and scores are friend-facing.
|
|
78
|
+
|
|
79
|
+
**Identity & anonymity** (`Avatar`, `RevealTray`, `PersonTile`, `guidelines/brand-anonymity.html`). Before a reveal a level may show: the neutral mystery avatar, the level number, the contributor's own clue, neutrally titled media ("Voice note", "Photo 2") and up to three real names. Never: sender name/relationship/portrait/generated avatar, anyone's gem or score, filenames, EXIF/metadata, or sender data merely hidden in the DOM, it must not be in the payload. Portraits are optional; the generated avatar is seeded by contributor id so it is stable. Self-revealing media (face, spoken name) is flagged honestly by the contributor and routed to a matching game or direct reveal, never a false mystery, never a promise of automatic anonymisation.
|
|
80
|
+
|
|
81
|
+
**Level loop** (`RoadsideViewer`, `ViewerKeys`, `MemoryText`, `Words`, `Album`, `Cassette`, `Film`, `ChoiceButton`, `RevealTray`, `MatchCard`). Package items render in the contributor's order; nothing auto-advances, media never cuts off, every player has pause/seek; completion is an explicit Continue and does not require watching everything. Guess: wrong = coral + ×, retry stays open, "Just tell me" is always offered; correct = green + ✓, others dim; then the face on the tray flips to the friend. A memory opens in a `RoadsideViewer`: a wooden cabinet whose screen plays one piece at a time, with a deck of keys (`ViewerKeys`) and one big button (`ViewerKnob`) that says what happens next. Who sent it is a separate moment: the `WhoButton` opens the `RevealTray` over the cabinet, so the memory is never covered before it's been seen. The contributor's clue waits in a `ClueTab` and comes out as a `ClueCard`. The interaction type (guess / match / direct) is chosen once per level and persisted. Completed levels stay replayable.
|
|
82
|
+
|
|
83
|
+
**Map additions** (`WorldGate`, `DiscoveryMarker`). A gate is the sign where a world begins: its name and how many memories it holds ("Surprises only" when it has none on the road). When the author has written a note for a world, a button under the sign opens it, and there's no button otherwise. Discoveries sit beside the road as thematic objects (mango, rice bowl, flower, dumbbell, lantern, gift): `hidden` (glinting bush), `available`, `new` (late arrival after the route froze, never a new numbered level), `opened`. This game has no star scores: use `LevelNode` with `stars={0}`.
|
|
84
|
+
|
|
85
|
+
**Scenery** (drawn in the birthday app, not shipped here). The map's scenery is original SVG drawn in code, so it's sharp on any iPad and needs no image downloads. Each world has its own mix of hills, trees, bushes, rocks, fences and stalls, and the author reviews each piece. It replaces the generic placeholder slots on the map only. The style: light from the upper left; one shade and one highlight per form; a soft contact shadow; no outlines (unlike the icons) and no SVG blur filters. Lettering, level nodes, portraits and buttons stay out of the art, so they stay sharp and tappable. New scenery joins the same library in the same style; never improvise art in a screen. The cloud-curtain transition and the keepsake's illustrated border are built from the same props, so they stay in the app too; the `Sunburst` rays used behind the summit and the letter are in `effects/`.
|
|
86
|
+
|
|
87
|
+
**Toolbar and fast travel** (`Toolbar`, `ToolbarTab`, `ToolbarNext`, `FastTravelDock`). The generic wood bottom bar becomes a floating cream pill with three slots: Your People, a raised blue Next button in the centre, and Menu. The button shows the next level's number, a padlock when nothing is open, and an hourglass while the road rearranges itself; once the letter is ready it turns gold and shows an envelope. A fast-travel dock in the side rail lists the five stops, the summit at the top and the first world at the bottom, as the map reads. Each stop is done, here or ahead, with a count of the surprises still hidden there. On phones the dock folds away behind a compass. The top of the map is a `HudBar` holding `HudChip`s: friends found and surprises found, each a count that is also a shortcut, with a coral dot for something new and a bump each time the count goes up (`useBump`). All three float over the window, and their sizes follow it.
|
|
88
|
+
|
|
89
|
+
**Contributor kit** (`StepBar`, `TextField`, `UploadTile`, `WorldSign`, `WorldSigns`, `WorldChip`, `WorldOption`, `RadioCard`). Tell contributors plainly it is a surprise shown to the recipient; show what stays anonymous vs. revealed. `StepBar` is the trail of step markers at the top of each screen. `WorldSign`s hang over one road, one per world with the summit locked, to choose where a memory belongs. A `WorldChip` pill then shows the choice, and `WorldOption`s change it in a sheet. `RadioCard`s sit in pairs for an honest either/or. Each `UploadTile` is one block of a memory (text, photo, video or voice note) with its upload state; blocks keep order = the recipient’s order; a failed or missing upload blocks submission until fixed or removed; filenames are contributor-side only. One package per contributor is the main-path memory; others become discoveries.
|
|
90
|
+
|
|
91
|
+
**Quiz & leaderboard** (`LeaderboardRow`, `ChoiceButton`). Eight questions, one point each, no timer, first completed attempt counts, ties share rank ("=3"), skipped players hold a pearl and sit unranked. Friends only. Copy celebrates participation.
|
|
92
|
+
|
|
93
|
+
**Access & privacy** (`guidelines/a11y.html`, `tokens/utilities.css`). Targets ≥ 48 (choices 56/64, Continue 60); memory text 20px/1.6 on iPad, 17 on phones; colour-independent states everywhere (glyphs, cuts, spelled-out sound state); `prefers-reduced-motion` collapses all motion (flips become swaps); `:focus-visible` 4px gold outline; real buttons/inputs/switch/radio roles; nothing plays before the deliberate Ready tap and `SoundToggle` is always reachable.
|
|
94
|
+
|
|
95
|
+
**Layout defaults (proposed, not approved).** iPad portrait 820 / landscape 1180, phone 390; reading column ≤ 640; iPad panels ≤ 560; design portrait-first and let landscape place media beside text (`guidelines/spacing-ipad.html`). Orientation, final title, narrator copy, quiz questions, letter, private access mechanism and upload limits remain open inputs.
|
|
96
|
+
|
|
97
|
+
**Voice adjustments for this project.** The author speaks to the recipient in the second person; the tone is warm, playful and brief, never a test: "Just tell me", "Try again", "Keep exploring", "Continue". Do not invent jokes on the author’s behalf or a finished letter. In the recipient’s flow the turquoise (teal) button is the default continue/confirm (per the panel reference); gold is reserved for rewards and the summit.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Index
|
|
102
|
+
|
|
103
|
+
- **Start here**: the docs (`npm run docs`) and Storybook (`npm run storybook`), both described below. They replaced `showcase.html`.
|
|
104
|
+
- `styles.css`: global entry; `@import`s every token file below.
|
|
105
|
+
- `tokens/`: `fonts.css` (Google Fonts import: Titan One, Quicksand), `icons.css` (Phosphor CDN), `colors.css`, `typography.css`, `spacing.css`, `effects.css` (radii, bevels, shadows, glows, motion), `utilities.css` (`.mp-display`, `.mp-ui`, `.mp-label`, `.mp-outline*`, keyframes `mp-float / mp-pulse-ring / mp-pop / mp-sparkle / mp-shimmer`).
|
|
106
|
+
- `guidelines/`: 24 specimen cards (Colors ×9, Type ×4, Spacing ×3, Effects ×4, Brand ×4) shown in the Design System tab.
|
|
107
|
+
- `components/`: 42 React components, each `Name.jsx` + `Name.d.ts` + `Name.prompt.md`, one preview card per group:
|
|
108
|
+
- `buttons/`: **Button** (8 tones × 3 sizes, display/ui typeface, pill, block), **IconButton** (circle/square, badge, outlined caption), **SegmentedTabs** (pill tab switch).
|
|
109
|
+
- `hud/`: **CurrencyPill** (coin/gem/ruby/heart/energy/star/key, + button), **ProgressBar** (striped, cap, label, dark/light track), **TimerChip**, **Countdown** (live: wooden board or ticking chip), **HudBar** (the map's top bar) and **HudChip** (a count that's also a shortcut).
|
|
110
|
+
- `panels/`: **Panel** (banner / ribbon / sign headers with blue·gold·teal·green·berry·coral accents; cream / wood / sky tones; close; footer), **Modal** (the shell around a Panel: scrim, Escape, scroll lock, focus, fade).
|
|
111
|
+
- `map/`: **LevelNode** (locked / current / complete node), **StarRating** (flat / arc / animated), **Tile** (grass / sand / stone / water / reward / hazard / start).
|
|
112
|
+
- `rewards/`: **RewardCard** (claimed / available / locked, featured), **ItemCell** (empty / filled / locked / add, selected, dim), **ShopRow** (default / featured, tag, price).
|
|
113
|
+
- `forms/`: **Switch**, **Slider**.
|
|
114
|
+
- `feedback/`: **Badge**, **Tag**, **Toast**, **Sticker**, **Notice**, **ErrorText**, **SavePill**, **SaveAlert**, **LoadingScreen**.
|
|
115
|
+
- `effects/`: **Confetti** (one-shot burst), **Sparkles**, **Sunburst** (rays of light for SVG).
|
|
116
|
+
- `text/`: **OutlineText**, **Heading** (tag, outlined title and cream plate). `icons/`, **GameIcon**.
|
|
117
|
+
- Adventure layer: `identity/`: **Gemstone**, **Avatar**, **AvatarButton**, **PersonTile**; `levels/`: **ChoiceButton**; `toolbar/`: **Toolbar**, **ToolbarTab**, **ToolbarNext**, **FastTravelDock**; `viewer/`: **RoadsideViewer** (the cabinet), **ViewerKeys**, **ViewerKnob**, **WhoButton**, **ClueTab**, **ClueCard**, **RevealTray** (the guess tray and the reveal); `media/`: **MemoryText**, **Words**, **Album** (with a full-screen lightbox), **Cassette** (voice note), **Film** (video), **Unavailable**; `map/`: **WorldGate**, **DiscoveryMarker**, **PortraitMedallion** (completed levels), `worlds.js`; `games/`: **MatchCard**; `contributor/`: **StepBar**, **TextField**, **UploadTile**, **WorldChip**, **WorldOption**, **WorldSign** and **WorldSigns**, **RadioCard**, **RecorderPanel** (the voice-recorder card, one step at a time), **AudioRecorder** (a working recorder); `quiz/`: **LeaderboardRow**; `feedback/`: **SoundToggle**.
|
|
118
|
+
- `tokens/hud.css`: the styles behind the map's chrome (`.mp-hud`, `.mp-toolbar`, `.mp-next`, `.mp-dock`). They float over the window and size themselves to it.
|
|
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
|
+
- **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
|
+
- `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`, `.mp-medallion`).
|
|
123
|
+
- `tokens/signs.css`: the styles behind the world signs (`.mp-gate`, `.mp-sign`, `.mp-world-pill`, `.mp-world-option`).
|
|
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
|
+
- `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`.
|
|
126
|
+
- `guidelines/worlds.html`, `colors-gems.html`, `spacing-ipad.html`, `a11y.html`, `brand-anonymity.html`: project specimens.
|
|
127
|
+
- `ui_kits/lanre-game/`: interactive mobile game (`index.html`, `App.jsx`, `Hud.jsx`, `MapScreen.jsx`, `BoardScreen.jsx`, `CampScreen.jsx`, `Dialogs.jsx`, `README.md`). Playable in the docs at Playable kit, which copies it in with the files it loads.
|
|
128
|
+
- `tools/ds-loader.js`: preview loader: uses the compiled `_ds_bundle.js` when present, otherwise transpiles component sources in-browser. Cards and the UI kit load through it.
|
|
129
|
+
- `stories/`, `.storybook/`: Storybook workbench (`npm install`, then `npm run storybook`). Each component has a Playground plus stories ported from its preview card; `stories/patterns/` holds the multi-component demos (guess step, matching game, reveal, composer, settings, quiz result). Controls and prop docs are generated from each `Name.d.ts` by `.storybook/dts-argtypes.js`; usage notes come from `Name.prompt.md`. The World toolbar sets `data-world`; Surface changes the ground. Stories live outside `components/` so that folder stays a plain copyable source.
|
|
130
|
+
- `docs/`: Fumadocs handbook (`npm run docs`, port 3000). Foundations with specimens generated from `tokens/`, the Birthday Adventure rules, a page per component with a live example, its `Name.prompt.md` notes and a props table from its `Name.d.ts`, the playable kit, and a guide to using the system without a build. Tokens are scoped to `.lanre-preview` inside the docs so they can't clash with Tailwind's. See `docs/README.md`.
|
|
131
|
+
- `tools/read-props.js`: reads a component's props from its `.d.ts` and defaults from its `.jsx`. Storybook and the docs both use it, so they always agree.
|
|
132
|
+
- `package.json`: dev dependencies for Storybook and the `docs` workspace. The design system itself still needs no build.
|
|
133
|
+
- `assets/`: empty by design (see `assets/README.md`).
|
|
134
|
+
- `SKILL.md`: agent skill wrapper.
|
|
135
|
+
|
|
136
|
+
### Intentional additions
|
|
137
|
+
This is a from-scratch system (no source inventory), so the component set is the standard casual-game kit rather than a copy of any reference: HUD pills, bevelled buttons, level nodes, tiles, reward cells, panels, switches/sliders, badges/tags/toasts, outlined text and the icon wrapper.
|
|
138
|
+
|
|
139
|
+
### Caveats
|
|
140
|
+
- **Fonts** are loaded from Google Fonts (`tokens/fonts.css`), not shipped as files. Titan One and Quicksand are matches for the bubbly display / rounded UI faces seen in the references, not the exact typefaces those games use.
|
|
141
|
+
- **Icons** are Phosphor (CDN, or the `@phosphor-icons/web` package in an app) with a CSS outline treatment standing in for painted icon art.
|
|
142
|
+
- **Imagery**: the generic kit has no illustrations; its screens reserve striped placeholder slots. The Birthday Adventure's map scenery is drawn in code in the app (see **Scenery**).
|
|
143
|
+
- **Bundle**: `_ds_bundle.js` is generated by the compiler; until it exists, previews fall back to in-browser transpilation via `tools/ds-loader.js` (React 18 UMD + Babel standalone pinned to 7.26 for the classic JSX runtime).
|
|
144
|
+
- **Birthday app copy**: lanre-birthday keeps its own copy of `components/` in `src/design/lanre/`. This repo was last synced from it on 23 Oct 2026, so components carry `'use client'` and React 19 types and drop into Next.js unchanged. Fixes made in either copy need carrying across by hand. Exception: `worlds.js` blurbs stay generic here; the app's personal contributor prompts are not copied into this public repo.
|
|
145
|
+
- Isometric (diamond) boards from two references are not modelled; `Tile` is a top-down rounded square.
|
package/styles.css
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@import url("./tokens/fonts.css");
|
|
2
|
+
@import url("./tokens/icons.css");
|
|
3
|
+
@import url("./tokens/colors.css");
|
|
4
|
+
@import url("./tokens/typography.css");
|
|
5
|
+
@import url("./tokens/spacing.css");
|
|
6
|
+
@import url("./tokens/effects.css");
|
|
7
|
+
@import url("./tokens/worlds.css");
|
|
8
|
+
@import url("./tokens/gems.css");
|
|
9
|
+
@import url("./tokens/utilities.css");
|
|
10
|
+
@import url("./tokens/media.css");
|
|
11
|
+
@import url("./tokens/signs.css");
|
|
12
|
+
@import url("./tokens/viewer.css");
|
|
13
|
+
@import url("./tokens/hud.css");
|
|
14
|
+
@import url("./tokens/ui.css");
|
|
15
|
+
@import url("./tokens/fx.css");
|