1212 */
1313
1414import type { EmailPlacement } from './email-clicks'
15+ import { brailleSpark , SPARK_BARS , sparkCount , sparkLevels , sparkWeek } from '#shared/braille-spark'
1516import { githubAvatarProxyUrl } from '#shared/image-proxy'
1617import { canonicalRepoSkillPath } from '#shared/skill-routes'
1718import { countedEmailUrl } from './email-clicks'
@@ -77,6 +78,12 @@ const PALETTE: Record<WeeklyTheme, Tokens> = {
7778
7879const SANS = `'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif`
7980const MONO = `'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace`
81+ /**
82+ * Faces that carry braille, one per platform: Apple Mail and iOS, Windows,
83+ * Android, then Linux. IBM Plex Mono has none, and a mail client rarely loads
84+ * it anyway.
85+ */
86+ const BRAILLE = `'Apple Braille','Segoe UI Symbol','Noto Sans Symbols 2','DejaVu Sans Mono',ui-monospace,monospace`
8087
8188/**
8289 * Why a skill is on the trending list, as one value rather than a bag of
@@ -90,17 +97,25 @@ const MONO = `'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospa
9097 * reason.
9198 */
9299export type WeeklyReason
93- = { _tag : 'named' , authorCount : number , mentionCount : number , latestAt : number }
94- | { _tag : 'stars' , gain : number , day : number }
95- | {
96- _tag : 'named-and-stars'
97- authorCount : number
98- mentionCount : number
99- latestAt : number
100- gain : number
101- day : number
102- }
103- | { _tag : 'popular' , stars : number }
100+ = {
101+ _tag : 'named'
102+ authorCount : number
103+ mentionCount : number
104+ latestAt : number
105+ /** Counted mentions per rolling day, oldest first, for the braille spark. */
106+ mentionsByDay : number [ ] | null
107+ }
108+ | { _tag : 'stars' , gain : number , day : number }
109+ | {
110+ _tag : 'named-and-stars'
111+ authorCount : number
112+ mentionCount : number
113+ latestAt : number
114+ mentionsByDay : number [ ] | null
115+ gain : number
116+ day : number
117+ }
118+ | { _tag : 'popular' , stars : number }
104119
105120export interface WeeklyEvidence {
106121 url : string
@@ -307,6 +322,43 @@ function trendingMeta(skill: WeeklyTrendingSkill, now: number): string {
307322 return joinMeta ( [ `${ skill . owner } /${ skill . repo } ` , reasonLine ( skill . reason , now ) , stars ] )
308323}
309324
325+ /**
326+ * The seven days a row's spark draws, or null when it draws none.
327+ *
328+ * Only a row somebody posted about has days to draw. A week of zeros means
329+ * every counted post is older than the spark, and seven blank cells beside
330+ * "0 mentions" say nothing, so that row draws none, as on the board.
331+ */
332+ function sparkDays ( reason : WeeklyReason ) : number [ ] | null {
333+ if ( reason . _tag !== 'named' && reason . _tag !== 'named-and-stars' )
334+ return null
335+ const week = sparkWeek ( reason . mentionsByDay ?? [ ] )
336+ return week . some ( count => count > 0 ) ? week : null
337+ }
338+
339+ function sparkTotal ( days : readonly number [ ] ) : string {
340+ return sparkCount ( days . reduce ( ( sum , count ) => sum + count , 0 ) )
341+ }
342+
343+ /**
344+ * The braille spark for the HTML half, in stone.
345+ *
346+ * The site draws today in rose. The email spends its rose on the button and
347+ * the footer rule, so seven rose cells would break the one-rose budget. A day
348+ * with no count draws the lowest bar faintly as a track, as the site does, so
349+ * a lone bar still reads as one day of seven.
350+ *
351+ * The label carries the numbers and the bars are hidden from screen readers,
352+ * because a braille cell read aloud is noise.
353+ */
354+ function sparkHtml ( t : Tokens , days : readonly number [ ] ) : string {
355+ const label = `${ sparkTotal ( days ) } in 7 days. Per day, oldest first: ${ days . join ( ', ' ) } .`
356+ const bars = sparkLevels ( days )
357+ . map ( level => level === 0 ? `<span style="color:${ t . borderStrong } ;">${ SPARK_BARS [ 1 ] } </span>` : SPARK_BARS [ level ] )
358+ . join ( '' )
359+ return `<span role="img" aria-label="${ esc ( label ) } " style="white-space:nowrap;font-family:${ MONO } ;font-size:14px;line-height:1.45;color:${ t . muted } ;font-variant-numeric:tabular-nums;"><span aria-hidden="true" style="font-family:${ BRAILLE } ;">${ bars } </span> <span aria-hidden="true">${ esc ( sparkTotal ( days ) ) } </span></span>`
360+ }
361+
310362/**
311363 * A skill description cut to something a person reads in one glance.
312364 *
@@ -449,6 +501,8 @@ function trendingRow(t: Tokens, options: {
449501 description : string | null
450502 meta : string
451503 evidence : WeeklyEvidence | null
504+ /** Seven days of mentions, or null for a row nobody posted about. */
505+ spark : number [ ] | null
452506} ) : string {
453507 const description = trimDescription ( options . description )
454508 const evidence = options . evidence
@@ -463,7 +517,12 @@ function trendingRow(t: Tokens, options: {
463517 style="width:28px;height:28px;border-radius:14px;display:block;border:1px solid ${ t . border } ;background:${ t . quote } ;" />
464518 </td>
465519 <td valign="top">
466- <a href="${ esc ( options . href ) } " style="display:inline-block;padding:0 0 3px;font-family:${ MONO } ;font-size:14px;line-height:1.45;font-weight:600;color:${ t . text } ;text-decoration:underline;text-decoration-color:${ t . borderStrong } ;text-underline-offset:3px;">${ esc ( options . title ) } </a>
520+ <table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%">
521+ <tr>
522+ <td valign="top"><a href="${ esc ( options . href ) } " style="display:inline-block;padding:0 0 3px;font-family:${ MONO } ;font-size:14px;line-height:1.45;font-weight:600;color:${ t . text } ;text-decoration:underline;text-decoration-color:${ t . borderStrong } ;text-underline-offset:3px;">${ esc ( options . title ) } </a></td>
523+ ${ options . spark ? `<td valign="top" align="right" style="padding-left:12px;white-space:nowrap;">${ sparkHtml ( t , options . spark ) } </td>` : '' }
524+ </tr>
525+ </table>
467526 ${ description ? `<div style="margin-top:3px;font-family:${ SANS } ;font-size:14px;line-height:1.55;color:${ t . body } ;">${ esc ( description ) } </div>` : '' }
468527 <div style="margin-top:3px;font-family:${ MONO } ;font-size:14px;line-height:1.45;color:${ t . muted } ;font-variant-numeric:tabular-nums;">${ esc ( options . meta ) } ${ evidence } </div>
469528 </td>
@@ -715,6 +774,7 @@ export function renderWeekly(input: WeeklyRenderInput): WeeklyRender {
715774 description : skill . description ,
716775 meta : trendingMeta ( skill , now ) ,
717776 evidence : skill . evidence ,
777+ spark : sparkDays ( skill . reason ) ,
718778 } )
719779 } ) . join ( '' )
720780
@@ -843,7 +903,8 @@ export function renderWeeklyText(input: WeeklyRenderInput): string {
843903 if ( input . trending . length ) {
844904 lines . push ( '' , 'TRENDING THIS WEEK' , '' )
845905 for ( const skill of input . trending ) {
846- lines . push ( `- ${ skill . owner } /${ skill . repo } ${ skill . canonicalName } ` )
906+ const spark = sparkDays ( skill . reason )
907+ lines . push ( `- ${ skill . owner } /${ skill . repo } ${ skill . canonicalName } ${ spark ? ` ${ brailleSpark ( spark ) } ${ sparkTotal ( spark ) } ` : '' } ` )
847908 const description = trimDescription ( skill . description )
848909 if ( description )
849910 lines . push ( ` ${ description } ` )
0 commit comments