Skip to content

Components

Moon card

The painted moon for the phase angle, the lit percentage and the phase name. Eight artworks are chosen by angle band.

Preview

MOON0%
New Moon
New Moon
MOON15%
Waxing Crescent
Waxing Crescent
MOON50%
First Quarter
First Quarter
MOON85%
Waxing Gibbous
Waxing Gibbous
MOON100%
Full Moon
Full Moon
MOON85%
Waning Gibbous
Waning Gibbous
MOON50%
Last Quarter
Last Quarter
MOON15%
Waning Crescent
Waning Crescent

Variants

Pick a variant with the controls. The preview and both code blocks follow.

Cycle
85%
MOON85%
Waxing Gibbous
Waxing Gibbous
Variant.tsx
<AppMoonCard angle={134} illumination={0.85} />

Implementation

The React tab is this site's own component, the same source that draws the preview, styled by the CSS below it. The Flutter tab is the Dart a screen builds it with, from the app's tokens.

Usage
<AppMoonCard angle={135} />
app-widgets.tsx · moonPhase
/** The moon artwork and label for a phase angle, with the app's bands (moon_mini_card.dart). */export function moonPhase(angle: number): { asset: string; label: string } {
app-widgets.tsx · AppMoonCard
/** MoonMiniCard: the painted moon, illumination and phase name. */export function AppMoonCard({ angle, illumination }: { angle: number; illumination?: number }) {  const { asset, label } = moonPhase(angle);  const lit = Math.round((illumination ?? (1 - Math.cos((angle * Math.PI) / 180)) / 2) * 100);  return (    <div className="app-ui app-card is-mini">      <div className="app-card-head">        <span className="app-label-sm">MOON</span>        <span className="app-label-sm" style={{ color: "var(--c-text)" }}>{lit}%</span>      </div>      <div className="app-moon-body">        {/* eslint-disable-next-line @next/next/no-img-element */}        <img className="app-moon-img" src={`/app/${asset}.webp`} alt={label} />      </div>      <span className="app-label-md app-moon-name">{label}</span>    </div>  );}
app-ui.css
.app-card {  position: relative;  box-sizing: border-box;  display: flex;  flex-direction: column;  padding: var(--spacing-f13);  border-radius: var(--radius-f21);  border: var(--spacing-f1) solid var(--hair-strong);  background: var(--sheen), var(--fill);  box-shadow: var(--highlight);  backdrop-filter: blur(var(--spacing-f13));  overflow: hidden;}.app-card.is-mini { width: var(--spacing-f144); height: calc(var(--spacing-f144) + var(--spacing-f21)); flex: none; }.app-card-head { display: flex; justify-content: space-between; align-items: center; gap: var(--spacing-f8); }.app-moon-body { flex: 1; display: grid; place-items: center; }.app-moon-img { width: var(--spacing-f89); height: var(--spacing-f89); object-fit: contain; }

API reference

React propFlutter parameterTypeDefaultMeaning
anglemoonPhaseInfo.phaseAngledegreesrequired0 new, 180 full.
illuminationmoonPhaseInfo.illumination0 to 1derivedThe lit share. Without it the web card computes (1 − cos angle) / 2; the app passes the ephemeris value.

Design tokens

Every value on this component is a system token. The last column is what the app widget uses today, so the gap to close is visible.

RoleCSS tokenDart tokenApp widget today
Size--spacing-f144 by 144 + --spacing-f21AstrSpace.f144164 by 154
Artwork--spacing-f89 squareAstrSpace.f8998 by 92
Name--text-s-1AstrType.sNeg1labelMd 12
Caption--text-s-2AstrType.sNeg2labelSm 10
Surfaceglass cardAstrGlassCardsurfaceOverlay + borderPrimary