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,26 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useId } from 'react';
|
|
3
|
+
|
|
4
|
+
/* Slow rays of light behind a focal point. It is SVG, so place it inside an <svg>, before the content it sits behind.
|
|
5
|
+
motion: 'continuous' turns for ever, 'once' fades in and out, 'still' stays put. Reduced motion stops it. */
|
|
6
|
+
export function Sunburst({ x = 0, y = 0, radius = 500, rays = 12, color = '#ffe8ac', opacity = 0.13, duration = 120, motion = 'continuous' }) {
|
|
7
|
+
const id = 'sunburst-' + useId().replace(/:/g, '');
|
|
8
|
+
const count = Math.max(3, Math.min(24, Math.round(rays)));
|
|
9
|
+
const half = Math.PI / count * 0.42;
|
|
10
|
+
const a = Math.sin(half) * radius, b = Math.cos(half) * radius;
|
|
11
|
+
return (
|
|
12
|
+
<g transform={'translate(' + x + ',' + y + ')'} opacity={opacity} aria-hidden="true" pointerEvents="none">
|
|
13
|
+
<defs>
|
|
14
|
+
<radialGradient id={id} gradientUnits="userSpaceOnUse" cx={0} cy={0} r={radius}>
|
|
15
|
+
<stop offset="0" stopColor={color} stopOpacity="0" />
|
|
16
|
+
<stop offset="0.16" stopColor={color} stopOpacity="0.65" />
|
|
17
|
+
<stop offset="0.42" stopColor={color} />
|
|
18
|
+
<stop offset="1" stopColor={color} stopOpacity="0" />
|
|
19
|
+
</radialGradient>
|
|
20
|
+
</defs>
|
|
21
|
+
<g className="mp-sunburst-motion" data-sunburst-motion={motion} style={{ '--sunburst-duration': Math.max(0.1, duration) + 's' }}>
|
|
22
|
+
{Array.from({ length: count }, (_, i) => <path key={i} transform={'rotate(' + (i * 360 / count) + ')'} d={'M0,0 L' + (-a) + ',' + (-b) + ' Q0,' + (-radius) + ' ' + a + ',' + (-b) + ' Z'} fill={'url(#' + id + ')'} />)}
|
|
23
|
+
</g>
|
|
24
|
+
</g>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Sunburst: rays of light behind a focal point, inside an SVG.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<svg viewBox="0 0 400 300">
|
|
5
|
+
<Sunburst x={200} y={150} radius={260} rays={14} opacity={0.34} duration={90} />
|
|
6
|
+
{/* the thing it sits behind */}
|
|
7
|
+
</svg>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- It is a `<g>`, not an element, so it only works inside an `<svg>`. Draw it first so the content sits on top.
|
|
11
|
+
- Keep `opacity` low (0.1 to 0.35). It is a glow, not a pattern.
|
|
12
|
+
- `motion="once"` plays a single fade for a reveal; `still` doesn't move. Reduced motion stops all of it.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Notification dot / count bubble with a white ring. Pin it to a button or tab corner.
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Notification dot + count" viewport="700x120"
|
|
6
|
+
*/
|
|
7
|
+
export interface BadgeProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'style'> {
|
|
8
|
+
/** "!" (default) or a number/short string. */
|
|
9
|
+
count?: React.ReactNode;
|
|
10
|
+
tone?: 'danger' | 'gold' | 'blue' | 'teal' | 'green';
|
|
11
|
+
size?: 'sm' | 'md';
|
|
12
|
+
/** Gentle bob to draw the eye. */
|
|
13
|
+
pulse?: boolean;
|
|
14
|
+
style?: React.CSSProperties;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Badge(props: BadgeProps): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
const TONES = { danger: ['var(--coral-300)', 'var(--coral-500)', 'var(--coral-700)'], gold: ['var(--gold-300)', 'var(--gold-500)', 'var(--gold-700)'],
|
|
5
|
+
blue: ['var(--blue-300)', 'var(--blue-500)', 'var(--blue-700)'], teal: ['var(--teal-300)', 'var(--teal-500)', 'var(--teal-700)'], green: ['var(--green-300)', 'var(--green-500)', 'var(--green-700)'] };
|
|
6
|
+
|
|
7
|
+
export function Badge({ count = '!', tone = 'danger', size = 'md', pulse = false, style, ...rest }) {
|
|
8
|
+
const [top, mid, shade] = TONES[tone] || TONES.danger;
|
|
9
|
+
const d = size === 'sm' ? 18 : 24; const fs = size === 'sm' ? 11 : 13;
|
|
10
|
+
return (
|
|
11
|
+
<span style={{ display: 'inline-grid', placeItems: 'center', minWidth: d, height: d, padding: '0 ' + (d * 0.25) + 'px', boxSizing: 'border-box', borderRadius: 'var(--radius-pill)',
|
|
12
|
+
background: 'linear-gradient(180deg, ' + top + ', ' + mid + ' 55%)', border: '2px solid #fff', boxShadow: '0 2px 0 ' + shade + ', 0 3px 6px oklch(0 0 0 / 0.25)',
|
|
13
|
+
color: tone === 'gold' ? 'var(--gold-900)' : '#fff', fontFamily: 'var(--font-display)', fontSize: fs, lineHeight: 1, letterSpacing: '0.02em',
|
|
14
|
+
animation: pulse ? 'mp-float 1.6s ease-in-out infinite' : 'none', ...style }} {...rest}>{count}</span>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Notification badge: red "!" or count pinned to the top-right corner of a button, tab or card (position it absolutely from the parent).
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<div style={{ position: 'relative' }}>
|
|
5
|
+
<IconButton variant="primary" shape="square"><GameIcon kind="quest" outline={false} shadow={false} /></IconButton>
|
|
6
|
+
<Badge count={3} style={{ position: 'absolute', top: -6, right: -6 }} />
|
|
7
|
+
</div>
|
|
8
|
+
<Badge /> // "!"
|
|
9
|
+
<Badge count="NEW" tone="gold" size="sm" />
|
|
10
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A short coral error line for under a form or field. Renders nothing without a message, and is announced to screen readers when one appears.
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Coral error line, announced" viewport="700x120"
|
|
6
|
+
*/
|
|
7
|
+
export interface ErrorTextProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ErrorText(props: ErrorTextProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* A short error under a form or a field. Renders nothing when there is no message, and is announced when one appears. */
|
|
5
|
+
export function ErrorText({ children, className, ...rest }) {
|
|
6
|
+
if (!children) return null;
|
|
7
|
+
return <p className={['mp-error-text', className].filter(Boolean).join(' ')} role="alert" {...rest}>{children}</p>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Error text: one line that says what went wrong and what to do.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<ErrorText>{error}</ErrorText>
|
|
5
|
+
<ErrorText>That link has expired. Ask for a new one.</ErrorText>
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
- Always render it and pass the message, which may be empty: with nothing to say it disappears.
|
|
9
|
+
- Say how to fix it, not just what broke.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The boot screen: sky, drifting clouds, the title, a striped gold bar that grows and shrinks, and a hill with the road setting off. Fills the window.
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Full-window boot screen" viewport="700x420"
|
|
6
|
+
*/
|
|
7
|
+
export interface LoadingScreenProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
|
|
8
|
+
/** The name shown big above the bar. Left out when empty. */
|
|
9
|
+
title?: string;
|
|
10
|
+
/** The words under the bar. Default "Loading…". */
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function LoadingScreen(props: LoadingScreenProps): React.JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { OutlineText } from '../text/OutlineText';
|
|
4
|
+
|
|
5
|
+
/* The boot screen: sky with a few drifting clouds, the title, a striped gold bar in its track, and a hill with the road setting off.
|
|
6
|
+
Fills the window. Give it a title; the label under the bar is the accessible name. */
|
|
7
|
+
export function LoadingScreen({ title, label = 'Loading…', className, ...rest }) {
|
|
8
|
+
return (
|
|
9
|
+
<div className={['mp-loading', className].filter(Boolean).join(' ')} role="status" aria-label="Loading" {...rest}>
|
|
10
|
+
<span className="mp-loading-cloud mp-loading-cloud-a" aria-hidden="true" />
|
|
11
|
+
<span className="mp-loading-cloud mp-loading-cloud-b" aria-hidden="true" />
|
|
12
|
+
<span className="mp-loading-cloud mp-loading-cloud-c" aria-hidden="true" />
|
|
13
|
+
<div className="mp-loading-stack">
|
|
14
|
+
{title && <OutlineText as="p" size={40} color="gold" stroke="ink" style={{ margin: 0, textAlign: 'center', textWrap: 'balance' }}>{title}</OutlineText>}
|
|
15
|
+
<span className="mp-loading-track" aria-hidden="true"><span className="mp-loading-fill" /></span>
|
|
16
|
+
<OutlineText as="span" size={18} color="white" stroke="ink" style={{ letterSpacing: '0.04em' }}>{label}</OutlineText>
|
|
17
|
+
</div>
|
|
18
|
+
<div className="mp-loading-ground" aria-hidden="true">
|
|
19
|
+
<span className="mp-loading-hill" />
|
|
20
|
+
<span className="mp-loading-road" />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Loading screen: what shows while the app gets ready.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
if (!session) return <LoadingScreen title="The Birthday Adventure" />;
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
- It fills the window (`min-height: 100dvh`), so render it instead of the screen, not inside it. In a smaller box, give it a height.
|
|
8
|
+
- Pass the app's name as `title`. Without one, the bar and label still show.
|
|
9
|
+
- The bar and clouds keep moving until the screen is replaced, and stop for reduced motion.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A card with a round sticker and a message: a quiet note, a warning, or a celebration. It slides up when it appears, and warnings are announced.
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Sticker and message card: info, warn, gold" viewport="700x360"
|
|
6
|
+
*/
|
|
7
|
+
export interface NoticeProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
8
|
+
/** info is the quiet cream card, warn is coral and announced, gold is for celebration. */
|
|
9
|
+
tone?: 'info' | 'warn' | 'gold';
|
|
10
|
+
/** A Phosphor icon name without the prefix. Default "info". */
|
|
11
|
+
icon?: string;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Notice(props: NoticeProps): React.JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Sticker } from './Sticker';
|
|
4
|
+
|
|
5
|
+
const STICKER_TONE = { info: 'blue', warn: 'coral', gold: 'gold' };
|
|
6
|
+
|
|
7
|
+
/* A card with a sticker and a message. Warnings are announced to screen readers when they appear. */
|
|
8
|
+
export function Notice({ tone = 'info', icon = 'info', children, className, ...rest }) {
|
|
9
|
+
const cls = ['mp-notice', tone === 'warn' ? 'mp-notice-warn' : tone === 'gold' ? 'mp-notice-gold' : '', className].filter(Boolean).join(' ');
|
|
10
|
+
return (
|
|
11
|
+
<div className={cls} role={tone === 'warn' ? 'alert' : undefined} {...rest}>
|
|
12
|
+
<Sticker icon={icon} tone={STICKER_TONE[tone] || 'blue'} />
|
|
13
|
+
<div>{children}</div>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Notice: a short message with a sticker beside it.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Notice icon="envelope-simple">Your note is on its way.</Notice>
|
|
5
|
+
<Notice tone="warn" icon="warning">That photo is too large. Try one under 10 MB.</Notice>
|
|
6
|
+
<Notice tone="gold" icon="gift">Everything is in. Nicely done.</Notice>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
- Use `warn` only for things that went wrong. It is announced to screen readers, so don't show one unless something needs attention.
|
|
10
|
+
- Keep it to a sentence or two. For a single line under a field, use `ErrorText`.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The full message behind a failed save: a conflict says what happened and offers Reload, an error says it will retry. Renders nothing otherwise.
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Conflict and error messages for autosave" viewport="700x140"
|
|
6
|
+
*/
|
|
7
|
+
export interface SaveAlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
8
|
+
status?: 'saved' | 'saving' | 'pending' | 'error' | 'conflict';
|
|
9
|
+
/** The reason. For a conflict it is the whole message; for an error it follows "Not saved yet:". */
|
|
10
|
+
error?: string | null;
|
|
11
|
+
/** Shows a Reload button on a conflict. */
|
|
12
|
+
onReload?: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SaveAlert(props: SaveAlertProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* The full message behind a failed save. A conflict is announced as an alert, with a Reload link when onReload is given;
|
|
5
|
+
an error is a status line. Renders nothing when the save is fine. */
|
|
6
|
+
export function SaveAlert({ status, error, onReload, className, ...rest }) {
|
|
7
|
+
const cls = ['mp-save-alert', className].filter(Boolean).join(' ');
|
|
8
|
+
if (status === 'conflict') {
|
|
9
|
+
return (
|
|
10
|
+
<div className={cls} role="alert" {...rest}>
|
|
11
|
+
<i className="ph-fill ph-warning" aria-hidden="true" /> {error}
|
|
12
|
+
{onReload && <button type="button" className="mp-link-button" onClick={onReload}>Reload</button>}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
if (status === 'error') {
|
|
17
|
+
return (
|
|
18
|
+
<div className={cls} role="status" {...rest}>
|
|
19
|
+
<i className="ph-fill ph-cloud-slash" aria-hidden="true" /> Not saved yet: {error ?? 'retrying when you edit'}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Save alert: the plain-words version of a save that didn't go through. Pair it with `SavePill`.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<SavePill status={status} />
|
|
5
|
+
<SaveAlert status={status} error={saveError} onReload={() => location.reload()} />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
- A `conflict` is announced right away and should say that someone else changed this; give it `onReload` so there is a way out.
|
|
9
|
+
- An `error` is a quiet status line. With no reason given it says it will retry when the person edits again.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The autosave state for a HUD, as a round cloud: it bobs while saving, pops once saved, and shows its word for a moment when pressed. Errors show "Not saved" and stay on screen.
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Autosave cloud: saving, saved, not saved" viewport="700x140"
|
|
6
|
+
*/
|
|
7
|
+
export interface SavePillProps extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
|
|
8
|
+
status?: 'saved' | 'saving' | 'pending' | 'error' | 'conflict';
|
|
9
|
+
/** How long the word stays after a press, in ms. Default 2500. */
|
|
10
|
+
wordMs?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function SavePill(props: SavePillProps): React.JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
/* The autosave state for a HUD, as a cloud that sits on its own so nothing crowds the screen. Pressing it shows the word for a moment.
|
|
5
|
+
Errors stay spelled out. status is 'saved', 'saving', 'pending', 'error' or 'conflict'. */
|
|
6
|
+
export function SavePill({ status = 'saved', wordMs = 2500, className, ...rest }) {
|
|
7
|
+
const [word, setWord] = useState(false);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
if (!word) return undefined;
|
|
10
|
+
const t = setTimeout(() => setWord(false), wordMs);
|
|
11
|
+
return () => clearTimeout(t);
|
|
12
|
+
}, [word, wordMs]);
|
|
13
|
+
if (status === 'conflict' || status === 'error') {
|
|
14
|
+
return (
|
|
15
|
+
<span className={['mp-save-pill mp-save-pill-warn', className].filter(Boolean).join(' ')} aria-hidden="true" {...rest}>
|
|
16
|
+
<i className="ph-fill ph-cloud-slash" /> Not saved
|
|
17
|
+
</span>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const saved = status === 'saved';
|
|
21
|
+
const label = saved ? 'Saved' : 'Saving';
|
|
22
|
+
return (
|
|
23
|
+
<button type="button" className={['mp-save-pill', saved ? 'mp-save-pill-pop' : 'mp-save-pill-busy', word ? '' : 'mp-save-pill-icon', className].filter(Boolean).join(' ')}
|
|
24
|
+
onClick={() => setWord(true)} aria-label={label} {...rest}>
|
|
25
|
+
<i key={status} className={'ph-fill ' + (saved ? 'ph-cloud-check' : 'ph-cloud-arrow-up')} aria-hidden="true" />
|
|
26
|
+
<span className={word ? 'mp-save-word' : 'mp-sr'} role="status" aria-live="polite">{label}</span>
|
|
27
|
+
</button>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Save pill: whether the person's changes are safe, in the corner of a HUD.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<SavePill status={saveStatus} />
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
- Pass the editor's own status: `saved`, `saving` or `pending`, `error` or `conflict`. It only draws the state, so it doesn't save anything itself.
|
|
8
|
+
- On `error` and `conflict` it can't be pressed and says "Not saved". Put a `SaveAlert` where there's room, so the reason and a way out are on screen too.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sound on/off pill with glyph AND text, so the state never relies on colour. Lives on the opening screen and in the HUD.
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Sound on / off control" viewport="700x100"
|
|
6
|
+
*/
|
|
7
|
+
export interface SoundToggleProps extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'style'> {
|
|
8
|
+
on?: boolean;
|
|
9
|
+
onChange?: (on: boolean) => void;
|
|
10
|
+
/** dark over the world (default) · light inside panels. */
|
|
11
|
+
tone?: 'dark' | 'light';
|
|
12
|
+
size?: 'sm' | 'md';
|
|
13
|
+
style?: React.CSSProperties;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SoundToggle(props: SoundToggleProps): React.JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
export function SoundToggle({ on = true, onChange, tone = 'dark', size = 'md', style, ...rest }) {
|
|
5
|
+
const h = size === 'sm' ? 36 : 44, dark = tone === 'dark';
|
|
6
|
+
return (
|
|
7
|
+
<button type="button" role="switch" aria-checked={on} onClick={() => onChange && onChange(!on)}
|
|
8
|
+
style={{ display: 'inline-flex', alignItems: 'center', gap: 8, height: h, padding: '0 16px 0 12px', borderRadius: 999, cursor: 'pointer', border: '2px solid ' + (dark ? 'oklch(0.2 0.05 55 / 0.9)' : 'var(--cream-300)'),
|
|
9
|
+
background: dark ? 'var(--surface-hud)' : 'var(--cream-50)', color: dark ? '#fff' : 'var(--ink-900)', boxShadow: dark ? 'inset 0 2px 4px oklch(0 0 0 / 0.35), 0 2px 0 oklch(1 0 0 / 0.15)' : '0 3px 0 var(--cream-300)',
|
|
10
|
+
fontFamily: 'var(--font-display)', fontSize: size === 'sm' ? 13 : 15, letterSpacing: '0.04em', textTransform: 'uppercase', textShadow: dark ? '0 2px 0 oklch(0 0 0 / 0.35)' : 'none', WebkitTapHighlightColor: 'transparent', ...style }} {...rest}>
|
|
11
|
+
<i key={on ? 'on' : 'off'} className={'ph-fill ph-' + (on ? 'speaker-high' : 'speaker-slash')} aria-hidden="true" style={{ fontSize: h * 0.5, lineHeight: 1, color: on ? (dark ? 'var(--teal-300)' : 'var(--teal-700)') : (dark ? 'var(--coral-300)' : 'var(--coral-700)'), animation: 'mp-pop var(--dur-slow) var(--ease-bounce) both' }} />
|
|
12
|
+
<span>{on ? 'Sound on' : 'Sound off'}</span>
|
|
13
|
+
</button>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Sound toggle: sound starts only after a deliberate tap (the opening's Ready button); this control lets the recipient change their mind anywhere.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<SoundToggle on={sound} onChange={setSound} />
|
|
5
|
+
<SoundToggle on={sound} onChange={setSound} tone="light" size="sm" />
|
|
6
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A round bevelled badge holding one white glyph, tinted from a colour family. Decorative, so pair it with words.
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Round bevelled glyph badge in 7 tones" viewport="700x120"
|
|
6
|
+
*/
|
|
7
|
+
export interface StickerProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'style' | 'children'> {
|
|
8
|
+
/** A Phosphor icon name without the prefix, such as "heart" or "warning". */
|
|
9
|
+
icon: string;
|
|
10
|
+
tone?: 'blue' | 'teal' | 'gold' | 'coral' | 'berry' | 'green' | 'stone';
|
|
11
|
+
weight?: 'fill' | 'bold';
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Sticker(props: StickerProps): React.JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/* A round bevelled badge holding one white glyph. Decorative: it is hidden from screen readers, so say the same thing in words beside it. */
|
|
5
|
+
export function Sticker({ icon, tone = 'blue', weight = 'fill', style, ...rest }) {
|
|
6
|
+
const vars = { '--tone-hi': 'var(--' + tone + '-300)', '--tone-face': 'var(--' + tone + '-500)', '--tone-side': 'var(--' + tone + '-700)', ...style };
|
|
7
|
+
return (
|
|
8
|
+
<span className="mp-sticker" style={vars} aria-hidden="true" {...rest}>
|
|
9
|
+
<i className={'ph-' + weight + ' ph-' + icon} />
|
|
10
|
+
</span>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Sticker: a round glyph badge that gives a line of copy a small visual anchor.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Sticker icon="heart" tone="berry" />
|
|
5
|
+
<Sticker icon="warning" tone="coral" weight="bold" />
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
- It is hidden from screen readers, so never let it carry meaning alone.
|
|
9
|
+
- `tone` picks a colour family (its 300, 500 and 700 shades make the highlight, face and side). Prefer `Notice` when the sticker sits beside a message.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Small uppercase label chip: MOST POPULAR, 3X VALUE, ONE-TIME OFFER, NEW. Sits on a card edge or next to a title.
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Uppercase label chip in 8 tones" viewport="700x120"
|
|
6
|
+
*/
|
|
7
|
+
export interface TagProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, 'style'> {
|
|
8
|
+
tone?: 'danger' | 'gold' | 'teal' | 'blue' | 'berry' | 'green' | 'stone' | 'wood';
|
|
9
|
+
size?: 'sm' | 'md';
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
style?: React.CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Tag(props: TagProps): React.JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
const TONES = { danger: ['var(--coral-500)', 'var(--coral-700)', '#fff'], gold: ['var(--gold-500)', 'var(--gold-700)', 'var(--gold-900)'], teal: ['var(--teal-500)', 'var(--teal-700)', '#fff'],
|
|
5
|
+
blue: ['var(--blue-500)', 'var(--blue-700)', '#fff'], berry: ['var(--berry-500)', 'var(--berry-700)', '#fff'], green: ['var(--green-500)', 'var(--green-700)', '#fff'], stone: ['var(--stone-500)', 'var(--stone-700)', '#fff'], wood: ['var(--wood-500)', 'var(--wood-700)', '#fff'] };
|
|
6
|
+
|
|
7
|
+
export function Tag({ tone = 'danger', size = 'md', icon, children, style, ...rest }) {
|
|
8
|
+
const [bg, shade, text] = TONES[tone] || TONES.danger;
|
|
9
|
+
const h = size === 'sm' ? 18 : 24; const fs = size === 'sm' ? 10 : 12;
|
|
10
|
+
return (
|
|
11
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, height: h, padding: '0 ' + (h * 0.4) + 'px', borderRadius: 'var(--radius-xs)', boxSizing: 'border-box',
|
|
12
|
+
background: bg, boxShadow: '0 2px 0 ' + shade + ', inset 0 1px 0 oklch(1 0 0 / 0.45)', color: text, fontFamily: 'var(--font-display)', fontSize: fs, lineHeight: 1, letterSpacing: '0.06em', textTransform: 'uppercase',
|
|
13
|
+
whiteSpace: 'nowrap', ...style }} {...rest}>{icon}{children}</span>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Label chip: merchandising and status labels; 1–3 uppercase words.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Tag>Most popular</Tag>
|
|
5
|
+
<Tag tone="teal">3x the value</Tag>
|
|
6
|
+
<Tag tone="gold" size="sm">New</Tag>
|
|
7
|
+
<Tag tone="stone" icon={<i className="ph-fill ph-lock" />}>Locked</Tag>
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
- danger (coral) for scarcity, teal/green for value, gold for new/featured, berry for premium, stone for locked.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reward / status toast: dark pill that pops in with a bounce. Position it from the parent (usually top-center or above the bottom bar).
|
|
5
|
+
* @startingPoint section="Feedback" subtitle="Bouncing reward toast" viewport="700x140"
|
|
6
|
+
*/
|
|
7
|
+
export interface ToastProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'style'> {
|
|
8
|
+
/** Leading glyph, e.g. <GameIcon kind="coin" size={32} />. */
|
|
9
|
+
icon?: React.ReactNode;
|
|
10
|
+
/** Headline in display type, e.g. "+50". */
|
|
11
|
+
message: React.ReactNode;
|
|
12
|
+
/** Small UI-type line under the headline, e.g. "Coins collected". */
|
|
13
|
+
detail?: React.ReactNode;
|
|
14
|
+
/** Headline color. */
|
|
15
|
+
tone?: 'gold' | 'blue' | 'teal' | 'coral' | 'berry' | 'white';
|
|
16
|
+
/** Renders nothing when false. */
|
|
17
|
+
visible?: boolean;
|
|
18
|
+
style?: React.CSSProperties;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Toast(props: ToastProps): JSX.Element | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
const TONES = { gold: 'var(--gold-300)', blue: 'var(--blue-300)', teal: 'var(--teal-300)', coral: 'var(--coral-300)', berry: 'var(--berry-300)', white: '#fff' };
|
|
5
|
+
|
|
6
|
+
export function Toast({ icon, message, detail, tone = 'gold', visible = true, style, ...rest }) {
|
|
7
|
+
if (!visible) return null;
|
|
8
|
+
return (
|
|
9
|
+
<div role="status" style={{ display: 'inline-flex', alignItems: 'center', gap: 10, height: 52, padding: '0 20px 0 12px', borderRadius: 'var(--radius-pill)', boxSizing: 'border-box',
|
|
10
|
+
background: 'var(--surface-hud)', border: '2px solid oklch(0.20 0.05 55 / 0.9)', boxShadow: 'var(--shadow-drop), inset 0 2px 0 oklch(1 0 0 / 0.12)',
|
|
11
|
+
animation: 'mp-pop var(--dur-slow) var(--ease-bounce) both', ...style }} {...rest}>
|
|
12
|
+
{icon}
|
|
13
|
+
<div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1.05 }}>
|
|
14
|
+
<span style={{ fontFamily: 'var(--font-display)', fontSize: 20, color: TONES[tone] || tone, textShadow: '0 2px 0 oklch(0 0 0 / 0.35)', letterSpacing: '0.02em' }}>{message}</span>
|
|
15
|
+
{detail && <span style={{ fontFamily: 'var(--font-ui)', fontWeight: 700, fontSize: 12, color: 'oklch(1 0 0 / 0.8)' }}>{detail}</span>}
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Reward toast: confirms a pickup or a state change without a dialog; auto-dismiss after ~1.6s from the parent.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Toast icon={<GameIcon kind="coin" size={32} />} message="+50" detail="Coins collected" />
|
|
5
|
+
<Toast icon={<GameIcon kind="heart" size={32} />} message="Life restored" tone="coral" />
|
|
6
|
+
<Toast message="Saved" tone="teal" />
|
|
7
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Volume-style slider: inset dark track, colored fill, chunky bevelled grip knob. Wraps a native range input for accessibility.
|
|
5
|
+
* @startingPoint section="Forms" subtitle="Volume slider with grip knob" viewport="700x140"
|
|
6
|
+
*/
|
|
7
|
+
export interface SliderProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'style' | 'onChange'> {
|
|
8
|
+
value?: number;
|
|
9
|
+
min?: number;
|
|
10
|
+
max?: number;
|
|
11
|
+
step?: number;
|
|
12
|
+
onChange?: (value: number) => void;
|
|
13
|
+
tone?: 'blue' | 'teal' | 'gold' | 'green' | 'berry';
|
|
14
|
+
/** Leading glyph (e.g. <GameIcon kind="music" />). */
|
|
15
|
+
icon?: React.ReactNode;
|
|
16
|
+
/** Total width incl. icon. Default 220. */
|
|
17
|
+
width?: number | string;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** Accessible name. */
|
|
20
|
+
label?: string;
|
|
21
|
+
style?: React.CSSProperties;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function Slider(props: SliderProps): React.JSX.Element;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
export function Slider({ value = 50, min = 0, max = 100, step = 1, onChange, tone = 'blue', icon, width = 220, disabled = false, label, style, ...rest }) {
|
|
5
|
+
const pct = ((value - min) / ((max - min) || 1)) * 100;
|
|
6
|
+
const c = ['var(--' + tone + '-300)', 'var(--' + tone + '-500)', 'var(--' + tone + '-700)'];
|
|
7
|
+
const h = 18, kw = 28, kh = 36;
|
|
8
|
+
return (
|
|
9
|
+
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 12, width, opacity: disabled ? 0.6 : 1, ...style }} {...rest}>
|
|
10
|
+
{icon}
|
|
11
|
+
<div style={{ position: 'relative', flex: 1, height: kh, display: 'flex', alignItems: 'center' }}>
|
|
12
|
+
<div style={{ position: 'absolute', left: 0, right: 0, height: h, borderRadius: 'var(--radius-pill)', background: 'linear-gradient(180deg, var(--stone-700), var(--stone-500))',
|
|
13
|
+
boxShadow: 'inset 0 2px 4px oklch(0 0 0 / 0.4), 0 2px 0 oklch(1 0 0 / 0.5)', overflow: 'hidden' }}>
|
|
14
|
+
<div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 'calc(' + pct + '% + ' + (kw / 2) + 'px)', borderRadius: 'inherit',
|
|
15
|
+
background: 'linear-gradient(180deg, ' + c[0] + ', ' + c[1] + ' 60%, ' + c[2] + ')', boxShadow: 'inset 0 2px 0 oklch(1 0 0 / 0.5)' }} />
|
|
16
|
+
</div>
|
|
17
|
+
<div aria-hidden="true" style={{ position: 'absolute', left: 'calc(' + pct + '% - ' + (pct / 100) * kw + 'px)', width: kw, height: kh, borderRadius: 'var(--radius-sm)',
|
|
18
|
+
background: 'linear-gradient(180deg, ' + c[0] + ', ' + c[1] + ' 50%)', boxShadow: '0 3px 0 ' + c[2] + ', 0 5px 8px oklch(0 0 0 / 0.3), inset 0 2px 0 oklch(1 0 0 / 0.5)',
|
|
19
|
+
display: 'grid', placeItems: 'center', gridAutoFlow: 'column', gap: 3, pointerEvents: 'none' }}>
|
|
20
|
+
<span style={{ width: 3, height: 14, borderRadius: 2, background: c[2], boxShadow: '0 1px 0 oklch(1 0 0 / 0.4)' }} />
|
|
21
|
+
<span style={{ width: 3, height: 14, borderRadius: 2, background: c[2], boxShadow: '0 1px 0 oklch(1 0 0 / 0.4)' }} />
|
|
22
|
+
</div>
|
|
23
|
+
<input type="range" aria-label={label} min={min} max={max} step={step} value={value} disabled={disabled} onChange={e => onChange && onChange(Number(e.target.value))}
|
|
24
|
+
style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', margin: 0, opacity: 0, cursor: disabled ? 'default' : 'pointer' }} />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
Volume slider: music/SFX levels in Settings; also brightness or speed. Always pair with a leading GameIcon.
|
|
2
|
+
|
|
3
|
+
```jsx
|
|
4
|
+
<Slider value={music} onChange={setMusic} icon={<GameIcon kind="music" />} label="Music" />
|
|
5
|
+
<Slider value={70} tone="teal" icon={<GameIcon kind="sound" tone="teal" />} width={260} />
|
|
6
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chunky on/off toggle with an inset track and a bevelled knob that bounces across.
|
|
5
|
+
* @startingPoint section="Forms" subtitle="Inset toggle, teal/blue/gold" viewport="700x120"
|
|
6
|
+
*/
|
|
7
|
+
export interface SwitchProps extends Omit<React.HTMLAttributes<HTMLButtonElement>, 'style' | 'onChange'> {
|
|
8
|
+
checked?: boolean;
|
|
9
|
+
onChange?: (next: boolean) => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
/** On-state color. Default teal. */
|
|
12
|
+
tone?: 'teal' | 'blue' | 'gold' | 'green' | 'berry';
|
|
13
|
+
size?: 'sm' | 'md';
|
|
14
|
+
/** Accessible name (visible label is rendered by the parent row). */
|
|
15
|
+
label?: string;
|
|
16
|
+
style?: React.CSSProperties;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Switch(props: SwitchProps): React.JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
export function Switch({ checked = false, onChange, disabled = false, tone = 'teal', size = 'md', label, style, ...rest }) {
|
|
5
|
+
const w = size === 'sm' ? 44 : 56; const h = size === 'sm' ? 26 : 32; const k = h - 8;
|
|
6
|
+
const on = ['var(--' + tone + '-300)', 'var(--' + tone + '-500)', 'var(--' + tone + '-700)'];
|
|
7
|
+
return (
|
|
8
|
+
<button type="button" role="switch" aria-checked={checked} aria-label={label} disabled={disabled} onClick={() => !disabled && onChange && onChange(!checked)}
|
|
9
|
+
style={{ appearance: 'none', border: 0, padding: 0, margin: 0, cursor: disabled ? 'default' : 'pointer', position: 'relative', width: w, height: h, flex: 'none', borderRadius: 'var(--radius-pill)',
|
|
10
|
+
background: checked ? 'linear-gradient(180deg, ' + on[2] + ', ' + on[1] + ')' : 'linear-gradient(180deg, var(--stone-700), var(--stone-500))',
|
|
11
|
+
boxShadow: 'inset 0 2px 4px oklch(0 0 0 / 0.35), 0 2px 0 oklch(1 0 0 / 0.5)', opacity: disabled ? 0.6 : 1,
|
|
12
|
+
transition: 'background var(--dur-base)', WebkitTapHighlightColor: 'transparent', ...style }} {...rest}>
|
|
13
|
+
<span aria-hidden="true" style={{ position: 'absolute', top: 2, left: checked ? w - k - 4 : 2, width: k, height: k, borderRadius: '50%',
|
|
14
|
+
background: 'linear-gradient(180deg, #fff, var(--cream-100))', boxShadow: '0 2px 0 var(--cream-300), 0 3px 5px oklch(0 0 0 / 0.25)',
|
|
15
|
+
transition: 'left var(--dur-base) var(--ease-bounce)' }}>
|
|
16
|
+
<span style={{ position: 'absolute', inset: '30%', borderRadius: '50%', background: checked ? on[1] : 'var(--stone-300)', transition: 'background var(--dur-base)' }} />
|
|
17
|
+
</span>
|
|
18
|
+
</button>
|
|
19
|
+
);
|
|
20
|
+
}
|