Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions hub/src/components/BrandMark.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
// Renders any brand mark correctly in both themes: light/dark pair, the
// invert fallback for mono marks, and the ink chip for white artwork.
// Sizing comes from the caller; the mark data comes from lib/identity.
import type { BrandMark } from '../lib/identity'

interface Props {
mark: BrandMark | undefined
name: string
imgClass?: string
/** tile: letter tile when no mark exists · none: render nothing */
fallback?: 'tile' | 'none'
}

const { mark, name, imgClass = '', fallback = 'tile' } = Astro.props
// A dark-mode variant is its own treatment — only invert mono artwork
// that has no dark pair.
const mono = !mark?.dark && mark?.treatment === 'darkMono'
const monoClass = mono ? 'dark:brightness-0 dark:invert dark:opacity-80' : ''
const lightInk = !mark?.dark && mark?.treatment === 'lightInk'
---
{mark ? (
mark.dark ? (
<>
<img src={mark.light} alt={name} class={`dark:hidden ${imgClass}`} loading="lazy" />
<img src={mark.dark} alt={name} class={`hidden dark:block ${imgClass}`} loading="lazy" />
</>
) : lightInk ? (
<span class="inline-grid place-items-center rounded-md bg-ink p-1.5 dark:bg-transparent dark:p-0">
<img src={mark.light} alt={name} class={imgClass} loading="lazy" />
</span>
) : (
<img src={mark.light} alt={name} class={`${imgClass} ${monoClass}`} loading="lazy" />
)
) : fallback === 'tile' ? (
<span class:list={['logo-tile', imgClass]}>{name.slice(0, 1)}</span>
) : null}
11 changes: 5 additions & 6 deletions hub/src/components/ClientWall.astro
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
---
import { clients } from '../lib/collections'
import { treatmentFor } from '../lib/logoTreatment'
import LogoFigure from './LogoFigure.astro'
import { clientMark } from '../lib/identity'
import BrandMark from './BrandMark.astro'
---
<section class="border-y border-line py-12 dark:border-slate-800" aria-label="Organizations Ribose works with">
<div class="mx-auto max-w-6xl px-4 sm:px-6">
<p class="eyebrow text-center">Trusted by the community</p>
<ul class="mt-7 flex flex-wrap items-center justify-center gap-x-8 gap-y-5" role="list">
{clients().map((client) => (
<li class="transition duration-200 hover:scale-[1.04]">
<LogoFigure
logo={client.logo}
logoDark={client.logo_dark}
<BrandMark
mark={clientMark(client)}
name={client.name}
imgClass={treatmentFor(client.logo).large
imgClass={clientMark(client).treatment === 'large'
? 'h-12 w-auto max-w-[9.5rem] object-contain opacity-70 grayscale transition duration-200 hover:opacity-100 hover:grayscale-0 dark:opacity-90 dark:grayscale-0 dark:hover:opacity-100'
: 'h-9 w-auto max-w-[8.5rem] object-contain opacity-60 grayscale transition duration-200 hover:opacity-100 hover:grayscale-0 dark:opacity-85 dark:grayscale-0 dark:hover:opacity-100'}
/>
Expand Down
23 changes: 0 additions & 23 deletions hub/src/components/LogoFigure.astro

This file was deleted.

19 changes: 0 additions & 19 deletions hub/src/components/NewsCard.astro

This file was deleted.

5 changes: 3 additions & 2 deletions hub/src/components/PlatformCard.astro
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
---
import type { Platform } from '../lib/registry'
import { suitesOfPlatform } from '../lib/registry'
import PlatformLogo from './PlatformLogo.astro'
import BrandMark from './BrandMark.astro'
import { platformMark } from '../lib/identity'
interface Props { platform: Platform }
const { platform } = Astro.props
const members = suitesOfPlatform(platform)
---
<a href={`/platforms/${platform.id}`} class="card group block p-6" transition:name={`platform-${platform.id}`}>
<div class="flex items-center gap-3">
<PlatformLogo platformId={platform.id} imgClass="h-9 w-9 shrink-0" />
<BrandMark mark={platformMark(platform.id)} name={platform.name} imgClass="h-9 w-9 shrink-0" fallback="none" />
<p class="eyebrow text-brand-700 dark:text-brand-400">Platform</p>
</div>
<h3 class="h-display mt-2 font-bold text-2xl group-hover:text-brand-700 dark:group-hover:text-brand-300">{platform.name}</h3>
Expand Down
16 changes: 0 additions & 16 deletions hub/src/components/PlatformLogo.astro

This file was deleted.

5 changes: 3 additions & 2 deletions hub/src/components/SuiteCard.astro
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
---
import type { Suite } from '../lib/registry'
import SuiteLogo from './SuiteLogo.astro'
import BrandMark from './BrandMark.astro'
import { suiteMark } from '../lib/identity'
interface Props { suite: Suite & { deprecated?: boolean }; domainName?: string }
const { suite, domainName } = Astro.props
---
<a href={`/technologies/${suite.id}`} class="card group block p-5" transition:name={`suite-${suite.id}`}>
<div class="flex items-center gap-3">
<SuiteLogo suite={suite} imgClass="h-8 w-8 shrink-0" />
<BrandMark mark={suiteMark(suite.id, suite.name)} name={suite.name} imgClass="h-8 w-8 shrink-0" />
<h3 class="font-semibold text-lg leading-tight group-hover:text-brand-700 dark:group-hover:text-brand-300">{suite.name}</h3>
</div>
<p class="mt-3 text-sm leading-6 text-slate-600 dark:text-slate-400">{suite.tagline}</p>
Expand Down
20 changes: 0 additions & 20 deletions hub/src/components/SuiteLogo.astro

This file was deleted.

33 changes: 33 additions & 0 deletions hub/src/lib/article.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// The deep Article interface — every news source projects onto this.
// The interface IS the test surface: adapters are tested by producing
// Articles, consumers are tested by rendering them. Adding a source
// = writing an adapter, never touching a page.
export interface Article {
/** globally unique id, e.g. urn:ribose:news:2024-08-27:metanorma:commenter */
urn: string
/** site path, e.g. /news/2024-08-27-metanorma-commenter */
href: string
/** ISO date YYYY-MM-DD */
date: string
title: string
subheadline?: string
/** rendered body as HTML (AsciiDoc-converted or XHTML-from-NewsML) */
bodyHtml: string
/** raw source body (AsciiDoc for wire/posts; empty for spoke) */
bodyAdoc?: string
authors: { name: string }[]
/** origin label for chips, e.g. "Metanorma", "ribose.com" */
origin: string
/** absolute canonical URL (points at the spoke for imported articles) */
canonical: string
lang: string
archived?: boolean
media?: { href: string; alt?: string; credit?: string }[]
dateline?: { place?: string; country?: string; date?: string }
}

// The seam: anything that produces Articles.
export interface NewsSource {
id: string
articles(): Article[] | Promise<Article[]>
}
32 changes: 32 additions & 0 deletions hub/src/lib/cachedFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Cache-first HTTP fetch for build-time data imports (wire, spokes).
// Refreshes over the network; falls back to cache when the remote is
// unreachable; returns null when neither exists. Never throws.
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
import { join, dirname } from 'node:path'

const CACHE_ROOT = join(process.cwd(), '.wire-cache')

export const cachedFetch = async (
url: string,
cacheKey: string,
label: string,
): Promise<string | null> => {
const cacheFile = join(CACHE_ROOT, cacheKey)
let cached: string | null = null
if (existsSync(cacheFile)) cached = readFileSync(cacheFile, 'utf8')
try {
const res = await fetch(url)
if (!res.ok) throw new Error(String(res.status))
const text = await res.text()
mkdirSync(dirname(cacheFile), { recursive: true })
writeFileSync(cacheFile, text)
return text
} catch {
if (cached) {
console.warn(`[${label}] using cached ${cacheKey} (fetch failed)`)
return cached
}
console.warn(`[${label}] no cache and fetch failed for ${cacheKey}; skipping`)
return null
}
}
87 changes: 87 additions & 0 deletions hub/src/lib/identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Brand identity — the visual mark of every entity on the site: suites,
// platforms, products (spoke-site origins), clients and partners. One type,
// one resolver per entity kind, one renderer (BrandMark.astro). Adding an
// entity's identity = dropping a file in public/{suites,platforms}/, adding
// a row to PRODUCT_MARKS, or an entry in clients.yaml. Nothing else changes.
import { logoFor, logoDarkFor, platformLogoFor } from './logos'

export interface BrandMark {
name: string
/** path to the light-mode mark */
light: string
/** path to the dark-mode mark (omit if the light mark works on both) */
dark?: string
treatment?: 'darkMono' | 'lightInk' | 'large'
}

// ---- Suites: symbol pairs under public/suites/{id}/symbol* ----

// plurimath's mark is dark artwork with no dark variant — render it as a
// white monochrome mark in dark mode (verified against #0d1117).
const SUITE_DARK_MONO = new Set(['plurimath'])

export const suiteMark = (id: string, name: string): BrandMark | undefined => {
const light = logoFor(id)
if (!light) return undefined
return {
name,
light,
dark: logoDarkFor(id),
...(SUITE_DARK_MONO.has(id) ? { treatment: 'darkMono' as const } : {}),
}
}

// ---- Platforms: symbols and lockups under public/platforms/{id}/ ----

export const platformMark = (
platformId: string,
variant: 'symbol' | 'logo' = 'symbol',
): BrandMark | undefined => {
const pair = platformLogoFor(platformId, variant)
if (!pair) return undefined
return { name: platformId, light: pair.light, dark: pair.dark }
}

// ---- Products: spoke-site origins get their chip mark here ----

const PRODUCT_MARKS: Record<string, BrandMark> = {
Metanorma: {
name: 'Metanorma',
light: '/suites/metanorma/symbol.svg',
dark: '/suites/metanorma/symbol-dark.svg',
},
PubID: {
name: 'PubID',
light: '/logos/pubid.svg',
},
Ribose: {
name: 'Ribose',
light: '/brand/ribose-r.svg',
},
}

export const productMarkFor = (origin: string): BrandMark | undefined =>
PRODUCT_MARKS[origin]

// ---- Clients & partners: logo files under public/logos/ ----

// Verified by rendering every logo on light (#fcfcfa) and dark (#0d1117)
// backgrounds and measuring visibility. Files not listed are legible in
// both modes as-is.
// lightInk: white artwork, invisible on paper — sit on a small ink chip
// in light mode (until a light variant exists).
// large: reads better slightly larger than the wall default.
const LIGHT_INK = new Set(['ngi-zero-pet.svg', 'expresslang.svg'])
const LARGE = new Set(['thunderbird.svg', 'ogc.svg'])

export const clientMark = (client: {
name: string
logo: string
logo_dark?: string
}): BrandMark => ({
name: client.name,
light: `/logos/${client.logo}`,
dark: client.logo_dark ? `/logos/${client.logo_dark}` : undefined,
...(LARGE.has(client.logo) ? { treatment: 'large' as const } : {}),
...(LIGHT_INK.has(client.logo) ? { treatment: 'lightInk' as const } : {}),
})
25 changes: 0 additions & 25 deletions hub/src/lib/logoTreatment.ts

This file was deleted.

8 changes: 0 additions & 8 deletions hub/src/lib/logos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,3 @@ export const platformLogoFor = (platformId: string, variant: 'symbol' | 'logo' =
const dark = variant === 'symbol' ? platformSymbolsDark[platformId] : platformFullsDark[platformId]
return { light, dark }
}

// Verified by rendering each symbol on light (#fcfcfa) and dark (#0d1117)
// backgrounds: plurimath's mark is dark artwork with no dark variant, so it
// renders as a white monochrome mark in dark mode.
const SUITE_DARK_MONO = new Set(['plurimath']) // primmel now ships a real dark variant

export const suiteLogoClass = (id: string): string =>
SUITE_DARK_MONO.has(id) ? 'dark:brightness-0 dark:invert' : ''
Loading
Loading