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,8 @@
|
|
|
1
|
+
Reward card: cells of a daily/streak calendar or a prize track. Grid of 3 per row inside a wood Panel; the last day is featured.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<RewardCard title="Day 1" icon={<GameIcon kind="coin" size={40} />} amount="+1M" state="claimed" />
|
|
5
|
+
<RewardCard title="Day 3" icon={<GameIcon kind="gift" size={40} />} state="available" onClick={claim} />
|
|
6
|
+
<RewardCard title="Day 5" icon={<GameIcon kind="gem" size={40} />} amount="+10" state="locked" />
|
|
7
|
+
<RewardCard title="Day 7" featured icon={<GameIcon kind="heart" size={44} />} amount="+50" state="locked" />
|
|
8
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shop line item: prize glyph, title/subtitle, price button; optional corner Tag; featured tone for the hero offer.
|
|
5
|
+
* @startingPoint section="Rewards" subtitle="Shop rows with price buttons + tags" viewport="700x300"
|
|
6
|
+
*/
|
|
7
|
+
export interface ShopRowProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'style' | 'title'> {
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
title: React.ReactNode;
|
|
10
|
+
subtitle?: React.ReactNode;
|
|
11
|
+
/** Price label for the built-in buy button, e.g. "USD 0.99" or a CurrencyPill. */
|
|
12
|
+
price?: React.ReactNode;
|
|
13
|
+
/** Corner label, e.g. <Tag>Most popular</Tag>. */
|
|
14
|
+
tag?: React.ReactNode;
|
|
15
|
+
/** featured = teal gradient hero row with a gold price button. */
|
|
16
|
+
tone?: 'default' | 'featured';
|
|
17
|
+
onBuy?: () => void;
|
|
18
|
+
/** Replaces the built-in price button. */
|
|
19
|
+
action?: React.ReactNode;
|
|
20
|
+
style?: React.CSSProperties;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ShopRow(props: ShopRowProps): React.JSX.Element;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
|
|
4
|
+
export function ShopRow({ icon, title, subtitle, price, tag, tone = 'default', onBuy, action, style, ...rest }) {
|
|
5
|
+
const [down, setDown] = useState(false);
|
|
6
|
+
const featured = tone === 'featured';
|
|
7
|
+
return (
|
|
8
|
+
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 12, minHeight: 76, padding: '10px 10px 10px 12px', boxSizing: 'border-box', borderRadius: 'var(--radius-lg)',
|
|
9
|
+
background: featured ? 'linear-gradient(135deg, var(--teal-500), var(--teal-700))' : 'linear-gradient(180deg, #fff, var(--cream-100))',
|
|
10
|
+
border: '3px solid ' + (featured ? 'var(--teal-300)' : 'var(--cream-300)'), boxShadow: '0 3px 0 ' + (featured ? 'var(--teal-900)' : 'var(--cream-300)') + ', 0 4px 8px oklch(0.2 0.05 60 / 0.12)', ...style }} {...rest}>
|
|
11
|
+
{tag && <span style={{ position: 'absolute', top: -12, right: 12 }}>{tag}</span>}
|
|
12
|
+
<span style={{ display: 'grid', placeItems: 'center', width: 50, height: 50, flex: 'none' }}>{icon}</span>
|
|
13
|
+
<div style={{ flex: 1, minWidth: 0, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
14
|
+
<span style={{ fontFamily: 'var(--font-display)', fontSize: 16, lineHeight: 1.1, color: featured ? '#fff' : 'var(--ink-900)', textShadow: featured ? '0 2px 0 var(--teal-900)' : 'none', letterSpacing: '0.01em' }}>{title}</span>
|
|
15
|
+
{subtitle && <span style={{ fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 12, color: featured ? 'var(--teal-100)' : 'var(--ink-500)', lineHeight: 1.25 }}>{subtitle}</span>}
|
|
16
|
+
</div>
|
|
17
|
+
{action || (price != null && (
|
|
18
|
+
<button type="button" onClick={onBuy} onPointerDown={() => setDown(true)} onPointerUp={() => setDown(false)} onPointerLeave={() => setDown(false)}
|
|
19
|
+
style={{ appearance: 'none', border: 0, flex: 'none', height: 40, padding: '0 14px', borderRadius: 'var(--radius-md)', cursor: 'pointer',
|
|
20
|
+
background: featured ? 'linear-gradient(180deg, var(--gold-300), var(--gold-500) 45%)' : 'linear-gradient(180deg, var(--teal-300), var(--teal-500) 45%)',
|
|
21
|
+
boxShadow: '0 ' + (down ? 1 : 4) + 'px 0 ' + (featured ? 'var(--gold-700)' : 'var(--teal-700)') + ', inset 0 2px 0 oklch(1 0 0 / 0.45)', transform: 'translateY(' + (down ? 3 : 0) + 'px)', transition: 'transform var(--dur-fast), box-shadow var(--dur-fast)',
|
|
22
|
+
fontFamily: 'var(--font-display)', fontSize: 14, letterSpacing: '0.02em', color: featured ? 'var(--gold-900)' : '#fff', textShadow: featured ? 'none' : '0 2px 0 var(--teal-700)', whiteSpace: 'nowrap' }}>{price}</button>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Shop row: one purchasable per row inside the Shop panel; the first row is the featured hero offer.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<ShopRow tone="featured" icon={<GameIcon kind="coins" size={48} />} title="3,000 coins" subtitle="One-time offer" price="USD 3.99" tag={<Tag tone="gold">3x the value</Tag>} onBuy={buy} />
|
|
5
|
+
<ShopRow icon={<GameIcon name="prohibit" tone="berry" size={44} />} title="No ads" subtitle="Rewarded videos still available" price="USD 0.99" />
|
|
6
|
+
<ShopRow icon={<GameIcon kind="coin" size={44} />} title="2,720 coins" price="USD 0.99" tag={<Tag>Most popular</Tag>} />
|
|
7
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A screen heading: a small wood tag, a big outlined title in the world's colour, and a cream plate with one line of explanation.
|
|
5
|
+
* @startingPoint section="Text" subtitle="Eyebrow, outlined title and cream plate" viewport="700x300"
|
|
6
|
+
*/
|
|
7
|
+
export interface HeadingProps extends Omit<React.HTMLAttributes<HTMLElement>, 'title'> {
|
|
8
|
+
/** A short label above the title. */
|
|
9
|
+
eyebrow?: string;
|
|
10
|
+
title: string;
|
|
11
|
+
/** One or two sentences on a cream plate under the title. */
|
|
12
|
+
lede?: React.ReactNode;
|
|
13
|
+
/** Title size in px. Default 34. */
|
|
14
|
+
size?: number;
|
|
15
|
+
/** The title's element. Default h1. */
|
|
16
|
+
as?: 'h1' | 'h2' | 'h3';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Heading(props: HeadingProps): React.JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Tag } from '../feedback/Tag';
|
|
4
|
+
|
|
5
|
+
/* A screen heading: an optional small tag above, an outlined title, and an optional lede on a cream plate. The outline takes
|
|
6
|
+
the colour of the world it's in (--world-stroke). */
|
|
7
|
+
export function Heading({ eyebrow, title, lede, size, as: Title = 'h1', className, ...rest }) {
|
|
8
|
+
return (
|
|
9
|
+
<header className={['mp-heading', className].filter(Boolean).join(' ')} {...rest}>
|
|
10
|
+
{eyebrow && <Tag tone="wood">{eyebrow}</Tag>}
|
|
11
|
+
<Title className="mp-heading-title" style={size ? { fontSize: size } : undefined}>{title}</Title>
|
|
12
|
+
{lede && <p className="mp-plate">{lede}</p>}
|
|
13
|
+
</header>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Heading: the top of a screen.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Heading eyebrow="Step 2 of 4" title="Pick a spot on the road" lede="Choose where your memory should live." />
|
|
5
|
+
<Heading title="All done" size={42} />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
- Titles are uppercase, so keep them to a few words. Long ones wrap evenly; lower `size` for the longest.
|
|
9
|
+
- The outline follows the surrounding `data-world`, so the same heading suits every world.
|
|
10
|
+
- One `h1` per screen. Use `as="h2"` for headings inside a panel.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display text with a dark stroke painted behind the fill, level numbers, panel titles, captions over the world.
|
|
5
|
+
* @startingPoint section="Text" subtitle="Outlined display type in 5 sizes" viewport="700x200"
|
|
6
|
+
*/
|
|
7
|
+
export interface OutlineTextProps extends Omit<React.HTMLAttributes<HTMLElement>, 'style' | 'color'> {
|
|
8
|
+
/** Element tag. Default span. */
|
|
9
|
+
as?: 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'p';
|
|
10
|
+
/** Preset (xl 44 / lg 32 / md 24 / sm 18 / xs 14) or a px number. */
|
|
11
|
+
size?: 'xl' | 'lg' | 'md' | 'sm' | 'xs' | number;
|
|
12
|
+
/** Fill: white (default), gold, cream, blue, green, or any CSS color. */
|
|
13
|
+
color?: 'white' | 'gold' | 'cream' | 'blue' | 'green' | string;
|
|
14
|
+
/** Stroke tone: ink (default), stone (locked), blue (current level), gold, green, coral, berry, wood, or any CSS color. */
|
|
15
|
+
stroke?: 'ink' | 'stone' | 'blue' | 'gold' | 'green' | 'coral' | 'berry' | 'wood' | string;
|
|
16
|
+
/** Uppercase (default true). */
|
|
17
|
+
uppercase?: boolean;
|
|
18
|
+
/** Hard drop shadow under the text (default true). */
|
|
19
|
+
shadow?: boolean;
|
|
20
|
+
children?: React.ReactNode;
|
|
21
|
+
style?: React.CSSProperties;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function OutlineText(props: OutlineTextProps): React.JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
const SIZES = { xl: 44, lg: 32, md: 24, sm: 18, xs: 14 };
|
|
5
|
+
const STROKES = { ink: 'var(--ink-900)', stone: 'var(--stone-700)', blue: 'var(--blue-700)', gold: 'var(--gold-700)', green: 'var(--green-900)', coral: 'var(--coral-900)', berry: 'var(--berry-900)', wood: 'var(--wood-900)' };
|
|
6
|
+
const FILLS = { white: '#fff', gold: 'var(--gold-300)', cream: 'var(--cream-100)', blue: 'var(--blue-300)', green: 'var(--green-300)' };
|
|
7
|
+
|
|
8
|
+
export function OutlineText({ as = 'span', size = 'md', color = 'white', stroke = 'ink', weight, uppercase = true, shadow = true, children, style, ...rest }) {
|
|
9
|
+
const px = typeof size === 'number' ? size : (SIZES[size] || SIZES.md);
|
|
10
|
+
const sw = px < 20 ? 0.2 : 0.16;
|
|
11
|
+
return React.createElement(as, {
|
|
12
|
+
style: {
|
|
13
|
+
fontFamily: 'var(--font-display)', fontWeight: 400, fontSize: px, lineHeight: 1.05, letterSpacing: '0.02em',
|
|
14
|
+
textTransform: uppercase ? 'uppercase' : 'none', color: FILLS[color] || color,
|
|
15
|
+
WebkitTextStroke: (sw * px).toFixed(1) + 'px ' + (STROKES[stroke] || stroke), paintOrder: 'stroke fill',
|
|
16
|
+
textShadow: shadow ? '0 ' + Math.max(2, Math.round(px * 0.1)) + 'px 0 oklch(0 0 0 / 0.22)' : 'none',
|
|
17
|
+
display: 'inline-block', ...style,
|
|
18
|
+
}, ...rest,
|
|
19
|
+
}, children);
|
|
20
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Outlined display text: every title, level number or caption that sits directly on the world (grass, sky, wood) uses this; plain text goes only inside panels.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<OutlineText size="xl">Daily Reward</OutlineText>
|
|
5
|
+
<OutlineText size={40} stroke="blue">7</OutlineText> // number on a blue level node
|
|
6
|
+
<OutlineText size="xs" stroke="stone">Locked</OutlineText>
|
|
7
|
+
<OutlineText size="lg" color="gold" stroke="gold">x2 Boost</OutlineText>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- Stroke matches the surface it sits on: ink over grass/wood, blue on blue level nodes, stone on locked ones.
|
|
11
|
+
- Keep to 1–3 words; it is a label face, not a paragraph face.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface DockStop {
|
|
4
|
+
id: string;
|
|
5
|
+
/** The stop's name, used for its accessible label unless `ariaLabel` is set. */
|
|
6
|
+
name: string;
|
|
7
|
+
/** The short name under the stop. Defaults to `name`. */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** A Phosphor icon name. */
|
|
10
|
+
glyph: string;
|
|
11
|
+
/** done is lit gold, here is blue with a ring, ahead is stone. */
|
|
12
|
+
state: 'done' | 'here' | 'ahead';
|
|
13
|
+
/** How many surprises are still hidden there. */
|
|
14
|
+
hidden?: number;
|
|
15
|
+
/** A full accessible label, such as "Mango Grove, you are here, 2 surprises still hidden". */
|
|
16
|
+
ariaLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The dock down the side of the map: a compass that jumps to what's next, then one stop for each place on the road, from the top. Done stops are lit, the one you're at is ringed, and a count says how many surprises are still hidden. On phones it folds into the compass until it's tapped.
|
|
21
|
+
* @startingPoint section="Toolbar" subtitle="Fast travel down the side" viewport="700x560"
|
|
22
|
+
*/
|
|
23
|
+
export interface FastTravelDockProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
24
|
+
/** In order from the top of the dock. */
|
|
25
|
+
stops: DockStop[];
|
|
26
|
+
/** Folds into the compass until opened. */
|
|
27
|
+
phone?: boolean;
|
|
28
|
+
/** On a phone, whether it's open. */
|
|
29
|
+
open?: boolean;
|
|
30
|
+
/** The count on the folded compass: the surprises still hidden, in all. */
|
|
31
|
+
hiddenTotal?: number;
|
|
32
|
+
/** The word under the compass. Default "Next". */
|
|
33
|
+
nextLabel?: string;
|
|
34
|
+
/** The group's accessible name. Default "Fast travel". */
|
|
35
|
+
label?: string;
|
|
36
|
+
compassLabel?: string;
|
|
37
|
+
openLabel?: string;
|
|
38
|
+
closeLabel?: string;
|
|
39
|
+
/** Sits inside a positioned box instead of covering the window. */
|
|
40
|
+
contained?: boolean;
|
|
41
|
+
/** The compass was pressed. On a phone it opens or closes the dock. */
|
|
42
|
+
onCompass?: () => void;
|
|
43
|
+
/** The "next" stop on an open phone dock was pressed. */
|
|
44
|
+
onNext?: () => void;
|
|
45
|
+
onTravel?: (id: string) => void;
|
|
46
|
+
/** The dim area behind an open phone dock was pressed. */
|
|
47
|
+
onClose?: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function FastTravelDock(props: FastTravelDockProps): React.JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* The dock down the side of the map: one stop for each place on the road, from the top, with the one you're at marked and
|
|
5
|
+
the ones you've finished lit. A compass jumps to what's next. On phones it folds into the compass until it's tapped.
|
|
6
|
+
Set `contained` to place it in a positioned box instead. */
|
|
7
|
+
export function FastTravelDock({ stops = [], phone = false, open = false, hiddenTotal = 0, nextLabel = 'Next', label = 'Fast travel', compassLabel = 'Take me to the next level',
|
|
8
|
+
closeLabel = 'Close fast travel', openLabel = 'Open fast travel', contained = false, onCompass, onNext, onTravel, onClose, ...rest }) {
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
<div className={'mp-dock' + (phone ? ' mp-dock-phone' : '') + (open ? ' mp-dock-open' : '') + (contained ? ' mp-dock-contained' : '')} role="group" aria-label={label} {...rest}>
|
|
12
|
+
<button type="button" className="mp-stop mp-stop-compass" onClick={onCompass} aria-label={phone ? (open ? closeLabel : openLabel) : compassLabel} aria-expanded={phone ? open : undefined}>
|
|
13
|
+
<i className={'ph-bold ' + (phone ? 'ph-compass' : 'ph-arrow-up')} aria-hidden="true" />
|
|
14
|
+
{phone && hiddenTotal > 0 && !open && <span className="mp-stop-count">{hiddenTotal}</span>}
|
|
15
|
+
</button>
|
|
16
|
+
<span className="mp-dock-label" aria-hidden="true">{nextLabel}</span>
|
|
17
|
+
<span className="mp-dock-rule" aria-hidden="true" />
|
|
18
|
+
{phone && (
|
|
19
|
+
<button type="button" className="mp-stop mp-stop-here mp-stop-next" onClick={onNext} aria-label={compassLabel}>
|
|
20
|
+
<i className="ph-bold ph-arrow-up" aria-hidden="true" />
|
|
21
|
+
</button>
|
|
22
|
+
)}
|
|
23
|
+
{stops.map(st => (
|
|
24
|
+
<div key={st.id} className="mp-stop-wrap">
|
|
25
|
+
<button type="button" className={'mp-stop ' + (st.state === 'done' ? 'mp-stop-done' : st.state === 'here' ? 'mp-stop-here' : 'mp-stop-ahead')} onClick={onTravel ? () => onTravel(st.id) : undefined} aria-label={st.ariaLabel || st.name}>
|
|
26
|
+
<i className={'ph-fill ph-' + st.glyph} aria-hidden="true" />
|
|
27
|
+
{st.hidden > 0 && <span className="mp-stop-count" aria-hidden="true">{st.hidden}</span>}
|
|
28
|
+
</button>
|
|
29
|
+
<span className="mp-dock-label" aria-hidden="true">{st.label || st.name}</span>
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
{phone && open && <button type="button" className={'mp-dock-scrim' + (contained ? ' mp-dock-scrim-contained' : '')} aria-label={closeLabel} onClick={onClose} />}
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Fast travel dock: a way to jump along the road without scrolling.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<FastTravelDock stops={stops} onCompass={goToNext} onTravel={travelTo} />
|
|
5
|
+
<FastTravelDock stops={stops} phone open={open} hiddenTotal={total} onCompass={() => setOpen(o => !o)} onNext={goToNext} onTravel={travelTo} onClose={() => setOpen(false)} />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
- List the stops in the order the dock reads: the summit at the top and the first world at the bottom, the way the map reads.
|
|
9
|
+
- Each stop is `done`, `here` or `ahead`, and can show how many surprises are still hidden there. Give each a full `ariaLabel`, since the visible label is only a short word.
|
|
10
|
+
- The compass at the top jumps to what's next.
|
|
11
|
+
- On a phone, pass `phone`. The dock folds into the compass, with a count of the hidden surprises, and opens when it's tapped. Keep `open` in your state, and close it from `onClose`.
|
|
12
|
+
- It sits against the right side of the window, centred, and its layout follows the window. Set `contained` to place it in a positioned box.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The bar along the bottom of the map: two tabs with the big Next button between them, floating over the window, centred.
|
|
5
|
+
* @startingPoint section="Toolbar" subtitle="Two tabs and a big Next button" viewport="700x260"
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolbarProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
|
|
8
|
+
/** The accessible name. Default "Adventure tools". */
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Sits inside a positioned box instead of covering the window. */
|
|
11
|
+
contained?: boolean;
|
|
12
|
+
/** ToolbarTabs on either side of a ToolbarNext. */
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Toolbar(props: ToolbarProps): React.JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* The bar along the bottom of the map: two tabs with a big Next button between them. It floats over the window, centred.
|
|
5
|
+
Set `contained` to place it in a positioned box instead. */
|
|
6
|
+
export function Toolbar({ label = 'Adventure tools', contained = false, children, ...rest }) {
|
|
7
|
+
return <nav className={'mp-toolbar' + (contained ? ' mp-toolbar-contained' : '')} aria-label={label} {...rest}>{children}</nav>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Toolbar: the three things always within reach on the map.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Toolbar>
|
|
5
|
+
<ToolbarTab icon="users-three" label="Your people" onClick={openPeople} />
|
|
6
|
+
<ToolbarNext aria-label="Next, level 7" onClick={openNext}>7</ToolbarNext>
|
|
7
|
+
<ToolbarTab icon="list" weight="bold" label="Menu" onClick={openMenu} />
|
|
8
|
+
</Toolbar>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- Two `ToolbarTab`s and one `ToolbarNext` in the middle. The Next button rises above the bar.
|
|
12
|
+
- It floats at the bottom of the window, centred, and gets smaller on phones. Its layout follows the window, not its box.
|
|
13
|
+
- Set `contained` to place it in a positioned box.
|
|
14
|
+
- The fast-travel dock is separate: see `FastTravelDock`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The big round button in the middle of the toolbar. It shows what's next, such as a level number, a padlock or an envelope. While the road rearranges itself it's busy and can't be pressed, and once the letter is ready it turns gold.
|
|
5
|
+
* @startingPoint section="Toolbar" subtitle="The big Next button, in each of its moods" viewport="700x200"
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolbarNextProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
8
|
+
/** The small word above the value. Default "Next". */
|
|
9
|
+
caption?: string;
|
|
10
|
+
/** Gold, with a glow, for the letter. */
|
|
11
|
+
gold?: boolean;
|
|
12
|
+
/** Cream, for while the road rearranges itself. Not a button then, and hidden from screen readers. */
|
|
13
|
+
busy?: boolean;
|
|
14
|
+
/** Stone, for when there's nothing to open. */
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
onClick?: () => void;
|
|
17
|
+
/** The value: a number, or an icon such as a padlock, an envelope or an hourglass. */
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ToolbarNext(props: ToolbarNextProps): React.JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* The big round button in the middle of the toolbar. It shows what's next, such as a level number, a padlock or an envelope.
|
|
5
|
+
While the road rearranges itself it's `busy` and can't be pressed. Once the letter is ready it turns `gold`. */
|
|
6
|
+
export function ToolbarNext({ caption = 'Next', gold = false, busy = false, disabled = false, onClick, children, ...rest }) {
|
|
7
|
+
if (busy) {
|
|
8
|
+
return (
|
|
9
|
+
<span className="mp-next mp-next-busy" aria-hidden="true" {...rest}>
|
|
10
|
+
<small>{caption}</small>
|
|
11
|
+
<b>{children}</b>
|
|
12
|
+
</span>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return (
|
|
16
|
+
<button type="button" className={'mp-next' + (gold ? ' mp-next-gold' : '')} onClick={onClick} disabled={disabled} {...rest}>
|
|
17
|
+
<small>{caption}</small>
|
|
18
|
+
<b>{children}</b>
|
|
19
|
+
</button>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Toolbar next: the one big button, so the next thing is always obvious.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<ToolbarNext aria-label="Next, level 7" onClick={openNext}>7</ToolbarNext>
|
|
5
|
+
<ToolbarNext caption="Your" gold aria-label="Your letter" onClick={openLetter}><i className="ph-fill ph-envelope-simple" /></ToolbarNext>
|
|
6
|
+
<ToolbarNext busy><i className="ph-fill ph-hourglass" /></ToolbarNext>
|
|
7
|
+
<ToolbarNext disabled aria-label="Nothing to open yet"><i className="ph-fill ph-lock-simple" /></ToolbarNext>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- Give it an `aria-label` that says what it opens. The visible number alone doesn't.
|
|
11
|
+
- It nudges every few seconds to say "this one". The nudge stops when it's pressed, disabled or busy, and with reduced motion.
|
|
12
|
+
- `busy` is for the moments the road rearranges itself, when there's nothing to press. It isn't a button then.
|
|
13
|
+
- `gold` is for the summit letter, once it's ready.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A tab on the toolbar: a round icon and a short label.
|
|
5
|
+
* @startingPoint section="Toolbar" subtitle="A toolbar tab" viewport="700x140"
|
|
6
|
+
*/
|
|
7
|
+
export interface ToolbarTabProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
8
|
+
/** A Phosphor icon name, such as "users-three". */
|
|
9
|
+
icon: string;
|
|
10
|
+
weight?: 'fill' | 'bold';
|
|
11
|
+
label: string;
|
|
12
|
+
onClick?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ToolbarTab(props: ToolbarTabProps): React.JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* A tab on the toolbar: a round icon and a short label. */
|
|
5
|
+
export function ToolbarTab({ icon, weight = 'fill', label, onClick, ...rest }) {
|
|
6
|
+
return (
|
|
7
|
+
<button type="button" className="mp-toolbar-tab" onClick={onClick} {...rest}>
|
|
8
|
+
<span className="mp-toolbar-tab-icon"><i className={'ph-' + weight + ' ph-' + icon} aria-hidden="true" /></span>
|
|
9
|
+
<span>{label}</span>
|
|
10
|
+
</button>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Toolbar tab: a quiet button for something the player may want any time.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<ToolbarTab icon="users-three" label="Your people" onClick={openPeople} />
|
|
5
|
+
<ToolbarTab icon="list" weight="bold" label="Menu" onClick={openMenu} />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
- Keep labels to one or two words. Use a shorter one on phones, such as "People".
|
|
9
|
+
- Pressing it dips it down and tints it, the same way every other pressable moves.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The contributor's clue as a taped note over the screen. Pressing it puts it back.
|
|
5
|
+
* @startingPoint section="Viewer" subtitle="The clue as a taped note" viewport="700x200"
|
|
6
|
+
*/
|
|
7
|
+
export interface ClueCardProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
8
|
+
clue: string;
|
|
9
|
+
/** Small line above the clue. Default "Their clue". */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Line under the clue. Default "Tap to put it back". */
|
|
12
|
+
hint?: string;
|
|
13
|
+
/** Plays the leaving animation. */
|
|
14
|
+
leaving?: boolean;
|
|
15
|
+
onClick?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ClueCard(props: ClueCardProps): React.JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* The clue as a taped note over the screen. Pressing it puts it back. */
|
|
5
|
+
export function ClueCard({ clue, label = 'Their clue', hint = 'Tap to put it back', leaving = false, onClick, ...rest }) {
|
|
6
|
+
return (
|
|
7
|
+
<button type="button" className={'mp-clue-card' + (leaving ? ' mp-clue-card-leaving' : '')} onClick={onClick} {...rest}>
|
|
8
|
+
<span className="mp-clue-tape" aria-hidden="true" />
|
|
9
|
+
<span className="mp-viewer-label">{label}</span>
|
|
10
|
+
<strong>{clue}</strong>
|
|
11
|
+
<em>{hint}</em>
|
|
12
|
+
</button>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Clue card: the clue, taped over the screen.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
{clueOpen && <ClueCard clue={clue} leaving={clueLeaving} onClick={hideClue} />}
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
- Place it in the viewer's `clueCard` slot, so it sits over the screen.
|
|
8
|
+
- Set `leaving` for a moment before you remove it, so it fades instead of vanishing.
|
|
9
|
+
- The clue is the contributor's own words. Show them as written.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The contributor's clue, parked as a tab on the cabinet. Pressing it calls the clue over the screen as a ClueCard. On narrow phones the tab just says "clue".
|
|
5
|
+
* @startingPoint section="Viewer" subtitle="The clue tab" viewport="700x120"
|
|
6
|
+
*/
|
|
7
|
+
export interface ClueTabProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
|
|
8
|
+
/** True while the clue card is out. */
|
|
9
|
+
open?: boolean;
|
|
10
|
+
/** The part that drops away on narrow phones. Default "Their ". */
|
|
11
|
+
prefix?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
onClick?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ClueTab(props: ClueTabProps): React.JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* The contributor's clue, parked as a tab on the cabinet. Pressing it calls the clue over the screen. */
|
|
5
|
+
export function ClueTab({ open = false, prefix = 'Their ', label = 'clue', onClick, ...rest }) {
|
|
6
|
+
return (
|
|
7
|
+
<button type="button" className={'mp-clue-tab' + (open ? ' mp-clue-tab-on' : '')} aria-expanded={open} onClick={onClick} {...rest}>
|
|
8
|
+
<i className="ph-fill ph-push-pin" aria-hidden="true" />
|
|
9
|
+
<span><span className="mp-clue-tab-their">{prefix}</span>{label}</span>
|
|
10
|
+
</button>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Clue tab: where the contributor's clue waits until it's wanted.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
{clue && <ClueTab open={clueOpen} onClick={toggleClue} />}
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
- Only show it when the contributor wrote a clue.
|
|
8
|
+
- It reports `aria-expanded`, so it works as a disclosure for the `ClueCard`.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
export interface RevealIdentity {
|
|
4
|
+
name: string;
|
|
5
|
+
/** e.g. "Sister", "Friend from uni". */
|
|
6
|
+
relationship?: string;
|
|
7
|
+
/** Portrait URL. The avatar is generated when there's none. */
|
|
8
|
+
portrait?: string | null;
|
|
9
|
+
/** Stable id for the generated avatar. */
|
|
10
|
+
seed?: string;
|
|
11
|
+
gem: 'amber' | 'emerald' | 'amethyst' | 'pearl';
|
|
12
|
+
tier?: 1 | 2 | 3;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The guess tray, centred over the viewer: a mystery face that flips to the friend and their gem. Before the reveal the body is yours (a prompt, choices, a way out). After it, the tray shows who it was from, a line, and your actions, arriving one after the other.
|
|
17
|
+
* @startingPoint section="Viewer" subtitle="The guess tray, and the reveal" viewport="700x620"
|
|
18
|
+
*/
|
|
19
|
+
export interface RevealTrayProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
|
|
20
|
+
/** blue for a level, berry for a surprise beside the road, gold once it has been met. */
|
|
21
|
+
tone?: 'blue' | 'berry' | 'gold';
|
|
22
|
+
/** Flips the face to the friend and swaps the body. */
|
|
23
|
+
revealed?: boolean;
|
|
24
|
+
identity?: RevealIdentity;
|
|
25
|
+
/** Shakes the tray once, for a wrong guess. */
|
|
26
|
+
wobble?: boolean;
|
|
27
|
+
/** No flip, no confetti, no "You found them!": everything sits still, for a memory already opened. */
|
|
28
|
+
settled?: boolean;
|
|
29
|
+
/** Fades the tray out before you remove it. */
|
|
30
|
+
leaving?: boolean;
|
|
31
|
+
/** Celebration over the tray, such as confetti. */
|
|
32
|
+
celebrate?: React.ReactNode;
|
|
33
|
+
/** Accessible name. Defaults to "Who sent this?" and, once revealed, "Who it was from". */
|
|
34
|
+
label?: string;
|
|
35
|
+
/** The line after the name, such as "You found them!". Hidden when settled. */
|
|
36
|
+
message?: string;
|
|
37
|
+
/** Buttons shown after the reveal. */
|
|
38
|
+
actions?: React.ReactNode;
|
|
39
|
+
/** An error shown at the bottom of the tray. */
|
|
40
|
+
error?: string;
|
|
41
|
+
/** Called when the dim area around the tray is pressed. */
|
|
42
|
+
onBackdrop?: () => void;
|
|
43
|
+
innerRef?: React.Ref<HTMLElement>;
|
|
44
|
+
/** The body before the reveal. */
|
|
45
|
+
children?: React.ReactNode;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function RevealTray(props: RevealTrayProps): React.JSX.Element;
|
|
49
|
+
|
|
50
|
+
export function TrayPrompt(props: React.HTMLAttributes<HTMLParagraphElement>): React.JSX.Element;
|
|
51
|
+
export function TrayHint(props: React.HTMLAttributes<HTMLParagraphElement>): React.JSX.Element;
|
|
52
|
+
export function TrayChoices(props: React.HTMLAttributes<HTMLDivElement>): React.JSX.Element;
|
|
53
|
+
export function TrayClue(props: React.HTMLAttributes<HTMLDivElement> & { label?: string }): React.JSX.Element;
|
|
54
|
+
export function TrayLink(props: React.ButtonHTMLAttributes<HTMLButtonElement> & { back?: boolean }): React.JSX.Element;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Avatar } from '../identity/Avatar';
|
|
4
|
+
import { Gemstone } from '../identity/Gemstone';
|
|
5
|
+
|
|
6
|
+
/* Small pieces for the tray's body, before the reveal. */
|
|
7
|
+
export const TrayPrompt = ({ children, ...rest }) => <p className="mp-tray-prompt" {...rest}>{children}</p>;
|
|
8
|
+
export const TrayHint = ({ children, ...rest }) => <p className="mp-tray-hint" {...rest}>{children}</p>;
|
|
9
|
+
export const TrayChoices = ({ children, ...rest }) => <div className="mp-tray-choices" {...rest}>{children}</div>;
|
|
10
|
+
export const TrayClue = ({ label = 'Their clue', children, ...rest }) => (
|
|
11
|
+
<div className="mp-tray-clue" {...rest}><span className="mp-clue-tape" aria-hidden="true" /><span className="mp-viewer-label">{label}</span>{children}</div>
|
|
12
|
+
);
|
|
13
|
+
export const TrayLink = ({ back = false, children, ...rest }) => <button type="button" className={'mp-tray-link' + (back ? ' mp-tray-back' : '')} {...rest}>{children}</button>;
|
|
14
|
+
|
|
15
|
+
/* The guess tray, centred over the viewer. A mystery face on top flips over to the friend and their gem. Before the
|
|
16
|
+
reveal the body is yours (a prompt, choices, a way out). After it, the tray shows who it was from, then a line and
|
|
17
|
+
your actions, one after the other. Render it only while it's open; set `leaving` to fade it out before you remove it. */
|
|
18
|
+
export function RevealTray({ tone = 'blue', revealed = false, identity, wobble = false, settled = false, leaving = false, celebrate, label, message, actions, error, onBackdrop, innerRef, children, ...rest }) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={'mp-tray-layer' + (leaving ? ' mp-tray-layer-leaving' : '')} onClick={e => { if (onBackdrop && e.target === e.currentTarget) onBackdrop(); }}>
|
|
21
|
+
{/* The entrance lives on this wrapper, so a wrong-guess wobble on the tray never replays it. */}
|
|
22
|
+
<div className={'mp-tray-in' + (leaving ? ' mp-tray-in-leaving' : '')}>
|
|
23
|
+
<aside ref={innerRef} tabIndex={-1} className={'mp-tray mp-tray-' + tone + (wobble ? ' mp-tray-wobble' : '') + (settled ? ' mp-tray-settled' : '')} aria-live="polite"
|
|
24
|
+
aria-label={label || (revealed ? 'Who it was from' : 'Who sent this?')} {...rest}>
|
|
25
|
+
<div className="mp-tray-face">
|
|
26
|
+
<span className={'mp-tray-flip' + (revealed ? ' mp-tray-flipped' : '')}>
|
|
27
|
+
<span className="mp-tray-face-front"><Avatar mystery size={104} /></span>
|
|
28
|
+
<span className="mp-tray-face-back">
|
|
29
|
+
{revealed && identity && (
|
|
30
|
+
<>
|
|
31
|
+
<Avatar name={identity.name} seed={identity.seed} src={identity.portrait || undefined} size={104} alt="" />
|
|
32
|
+
<Gemstone gem={identity.gem} tier={identity.tier ?? 1} size={44} decorative className="mp-tray-gem" />
|
|
33
|
+
</>
|
|
34
|
+
)}
|
|
35
|
+
</span>
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
{celebrate}
|
|
39
|
+
<div className="mp-tray-body">
|
|
40
|
+
{!revealed && children}
|
|
41
|
+
{revealed && identity && (
|
|
42
|
+
<>
|
|
43
|
+
<div className="mp-tray-whowas">
|
|
44
|
+
<span className="mp-viewer-label mp-beat-1">From</span>
|
|
45
|
+
<span className="mp-tray-name mp-beat-1">{identity.name}</span>
|
|
46
|
+
{identity.relationship && <span className="mp-tray-relationship mp-beat-2">{identity.relationship}</span>}
|
|
47
|
+
</div>
|
|
48
|
+
{message && !settled && <p className="mp-tray-prompt mp-beat-3">{message}</p>}
|
|
49
|
+
{actions && <div className="mp-tray-actions mp-beat-4">{actions}</div>}
|
|
50
|
+
</>
|
|
51
|
+
)}
|
|
52
|
+
{error && <p role="alert" className="mp-viewer-error">{error}</p>}
|
|
53
|
+
</div>
|
|
54
|
+
</aside>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|