11import { css , html , nothing } from 'lit' ;
22import { customElement , property } from 'lit/decorators.js' ;
3- import { planetGlyph , signGlyph } from '../tokens/index.js' ;
3+ import { planetGlyph , SIGNS_ORDER , signGlyph } from '../tokens/index.js' ;
44import type {
55 GetMonthlyEphemerisResponse ,
66 GetMonthlyTropicalEphemerisResponse ,
@@ -75,11 +75,21 @@ interface DayRow {
7575 cells : Array < Position | undefined > ;
7676}
7777
78+ /** One line of the sign key: the glyph the table prints and the name it stands for. */
79+ interface SignKeyEntry {
80+ /** Canonical English sign, the glyph lookup key. */
81+ sign : string ;
82+ /** The sign name a reader sees, in the response language. */
83+ label : string ;
84+ glyph : string ;
85+ }
86+
7887interface ViewModel {
7988 year : number ;
8089 month : number ;
8190 bodies : BodyTrack [ ] ;
8291 rows : DayRow [ ] ;
92+ signs : SignKeyEntry [ ] ;
8393}
8494
8595/**
@@ -224,6 +234,29 @@ export class RoxyEphemerisTable extends RoxyDataElement<EphemerisResponse> {
224234 letter-spacing: 0.06em;
225235 color: var(--roxy-muted, #71717a);
226236 }
237+ /* Wraps rather than scrolls: it is a reference a reader scans, and a
238+ * key hidden behind a horizontal scrollbar helps nobody. */
239+ .signkey {
240+ display: flex;
241+ flex-wrap: wrap;
242+ gap: 1px 0;
243+ margin: 0;
244+ padding: 0;
245+ list-style: none;
246+ }
247+ .signkey-item {
248+ display: inline-flex;
249+ align-items: baseline;
250+ gap: 0.3rem;
251+ padding: 2px 10px 2px 0;
252+ font-size: var(--roxy-text-xs, 0.75rem);
253+ color: var(--roxy-muted, #71717a);
254+ white-space: nowrap;
255+ }
256+ .signkey-glyph {
257+ font-size: var(--roxy-text-sm, 0.875rem);
258+ color: var(--roxy-fg, #0a0a0a);
259+ }
227260 .tracks {
228261 display: grid;
229262 grid-template-columns: minmax(0, 1fr);
@@ -402,6 +435,7 @@ export class RoxyEphemerisTable extends RoxyDataElement<EphemerisResponse> {
402435 > < span class ="basis "> ${ SAMPLE_INSTANT } </ span >
403436 </ p >
404437 </ header >
438+ ${ this . renderSignKey ( vm . signs ) }
405439 < section part ="section changes ">
406440 < h3 > ${ this . t ( 'Sign changes and retrograde periods' ) } </ h3 >
407441 < div class ="tracks " role ="list ">
@@ -424,6 +458,34 @@ export class RoxyEphemerisTable extends RoxyDataElement<EphemerisResponse> {
424458 </ div > ` ;
425459 }
426460
461+ /**
462+ * The glyph-to-name key, above the table it explains.
463+ *
464+ * @remarks
465+ * Every position in this component prints its sign as a glyph, which is how a
466+ * printed ephemeris prints one and is unreadable to somebody still learning
467+ * them. The name rides on each cell's `title`, and a title is a hover: it does
468+ * not exist on touch and it cannot be read down a column. A key that names all
469+ * of them once is the form the page actually needed.
470+ *
471+ * `part="section legend"` rather than a name of its own, because a page hiding
472+ * legends should hide this one too and the vocabulary is shared across every
473+ * component.
474+ */
475+ private renderSignKey ( signs : SignKeyEntry [ ] ) {
476+ if ( signs . length === 0 ) return nothing ;
477+ return html `< section part ="section legend ">
478+ < h3 > ${ this . t ( 'Signs in this month' ) } </ h3 >
479+ < ul class ="signkey " role ="list ">
480+ ${ signs . map (
481+ ( s ) => html `< li class ="signkey-item ">
482+ < span class ="signkey-glyph " aria-hidden ="true "> ${ s . glyph } </ span > ${ s . label }
483+ </ li > ` ,
484+ ) }
485+ </ ul >
486+ </ section > ` ;
487+ }
488+
427489 /** One body across the month: where it started, where it ended, and every change in between. */
428490 private renderTrack ( b : BodyTrack ) {
429491 const glyph = planetGlyph ( b . name ) ;
@@ -617,7 +679,48 @@ export class RoxyEphemerisTable extends RoxyDataElement<EphemerisResponse> {
617679 const byName = new Map ( day . positions . map ( ( p ) => [ p . planet , p ] ) ) ;
618680 return { date : day . date , cells : bodies . map ( ( b ) => byName . get ( b . name ) ) } ;
619681 } ) ;
620- return { year : data . year , month : data . month , bodies, rows } ;
682+ return {
683+ year : data . year ,
684+ month : data . month ,
685+ bodies,
686+ rows,
687+ signs : this . toSignKey ( data ) ,
688+ } ;
689+ }
690+
691+ /**
692+ * The signs this month's table actually prints, in zodiacal order.
693+ *
694+ * @remarks
695+ * Read from the response rather than from the twelve-sign constant, so the key
696+ * explains the glyphs ON THIS PAGE and nothing else. In practice the Moon
697+ * crosses every sign in a month, so a full month lists all twelve anyway; a
698+ * partial range lists only what it contains, which is the point.
699+ *
700+ * The label is the API's own localized sign name, taken the same way every
701+ * other sign name in this component is taken. Translating it here instead
702+ * would put a second vocabulary on the page and let the key disagree with the
703+ * cells it explains.
704+ *
705+ * Ordered by {@link SIGNS_ORDER} because first-seen order is the order the
706+ * bodies happen to sit in on the first of the month, which is meaningless to a
707+ * reader learning the glyphs.
708+ */
709+ private toSignKey ( data : EphemerisResponse ) : SignKeyEntry [ ] {
710+ const labels = new Map < string , string > ( ) ;
711+ for ( const day of data . days ) {
712+ for ( const p of day . positions ) {
713+ const key = String ( p . sign ?? '' ) . toLowerCase ( ) ;
714+ if ( key && ! labels . has ( key ) ) labels . set ( key , display ( p , 'sign' ) ) ;
715+ }
716+ }
717+ const out : SignKeyEntry [ ] = [ ] ;
718+ for ( const sign of SIGNS_ORDER ) {
719+ const label = labels . get ( sign . toLowerCase ( ) ) ;
720+ const glyph = signGlyph ( sign ) ;
721+ if ( label && glyph ) out . push ( { sign, label, glyph } ) ;
722+ }
723+ return out ;
621724 }
622725}
623726
0 commit comments