Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3786cee
feat(noodles): add /noodles archive with per-noodle markdown pages
Adebesin-Cell May 20, 2026
1eb92ce
feat(noodles): case-study layout for individual noodle pages
Adebesin-Cell May 22, 2026
b8a9c76
fix(noodles): address CI failures
Adebesin-Cell May 22, 2026
df1bc25
fix(storybook): ignore noodle .md files in storybook build, mirrors b…
Adebesin-Cell May 22, 2026
1fd3721
refactor(noodles): drop detail pages, keep landing only
Adebesin-Cell May 22, 2026
1630205
refactor(noodles): drop detail pages, keep landing only
Adebesin-Cell May 22, 2026
1c4eb7f
feat(noodles): restore artemis, drop query-param trigger, fix bowl mo…
Adebesin-Cell May 22, 2026
fb70a25
feat(noodles): bring back optional detail page + cool 404
Adebesin-Cell May 22, 2026
7115cb3
feat(noodles): split kawaii into original + pride revision, center bo…
Adebesin-Cell May 22, 2026
350494f
feat(noodles): credits, load-more pagination, what-is restored, respo…
Adebesin-Cell May 22, 2026
2336298
fix(noodles): drop container wrapper, blog-style author credits
Adebesin-Cell May 22, 2026
f9a88c2
refactor(noodles): move credits into the metadata tombstone
Adebesin-Cell May 22, 2026
56dd591
fix(noodles): credits to own row in metadata, article padding on mobile
Adebesin-Cell May 22, 2026
7358124
feat(noodles): build-time Bluesky avatar resolution
Adebesin-Cell May 22, 2026
1ad1376
fix(noodles): address CI failures
Adebesin-Cell May 22, 2026
513bb9d
fix(i18n): regenerate schema after adding noodles keys
Adebesin-Cell May 22, 2026
0c10f8c
fix(noodles): throw on missing logo registration instead of silently …
Adebesin-Cell May 22, 2026
4e870d1
fix(noodles): timeout + Readable.fromWeb on bluesky avatar fetches
Adebesin-Cell May 22, 2026
b928860
docs(noodles): clarify in schema doc that occasion is optional
Adebesin-Cell May 22, 2026
d2c66ab
fix(noodles): use arrayBuffer for avatar writes to satisfy type check
Adebesin-Cell May 22, 2026
a397b84
feat(noodles): process carousel, mobile nav link, logo cleanup
Adebesin-Cell May 22, 2026
1b2cf68
fix(noodles): hide landing moon in light mode, restore artemis moon
Adebesin-Cell May 22, 2026
c9203a7
feat(noodles): magnifying-lens carousel on the detail page
Adebesin-Cell May 22, 2026
7f15eeb
feat(noodles): infinite-loop lens carousel + keyboard nav
Adebesin-Cell May 22, 2026
229ed32
a11y(noodles): reduced motion, dot focus ring, live region, 404 fix
Adebesin-Cell May 22, 2026
8efc84c
feat(noodles): per-noodle og images + bigger chevrons + comment trim
Adebesin-Cell May 22, 2026
c0f9839
fix(og): inline brand mark in Noodle.takumi to bypass auto-import issue
Adebesin-Cell May 22, 2026
0cd7e73
fix(og): use moon for artemis poster, not the wordmark
Adebesin-Cell May 22, 2026
3595a6e
fix(og): pin poster column to 360px so it stops bleeding off canvas
Adebesin-Cell May 22, 2026
641b831
fix(og): drop artemis backdrop, keep wordmark only
Adebesin-Cell May 22, 2026
0f169c0
feat(noodles): two-column detail layout + 404 shares same shell
Adebesin-Cell May 22, 2026
3e71ebc
fix(noodles): stretch detail hero on tall screens + raise 2-col break…
Adebesin-Cell May 22, 2026
6b9ca03
fix(noodles): vertically center detail content on tall screens
Adebesin-Cell May 22, 2026
f5798f9
fix(noodles): lens sticks to top of grid, drop vertical centering
Adebesin-Cell May 22, 2026
588c3a9
fix(noodles): sticky lens + drop dummy preview content
Adebesin-Cell May 22, 2026
9cba372
fix(noodles): drop orphaned Carousel.vue + noodles.process i18n key
Adebesin-Cell May 22, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ test-results/
shared/types/lexicons
file-tree-sprite.svg
public/blog/avatar
public/noodle-avatar

**/__screenshots__/**

Expand Down
4 changes: 4 additions & 0 deletions app/components/AppFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ const footerSections = computed<Array<{ label: string; links: FooterLink[] }>>((
name: t('pds.title'),
href: '/pds',
},
{
name: t('noodles.title'),
href: '/noodles',
},
{
name: t('footer.docs'),
href: NPMX_DOCS_SITE,
Expand Down
8 changes: 8 additions & 0 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ const mobileLinks = computed<NavigationConfigWithGroups>(() => [
external: false,
iconClass: 'i-lucide:palette',
},
{
name: 'Noodles',
label: $t('noodles.title'),
to: { name: 'noodles' },
type: 'link',
external: false,
iconClass: 'i-lucide:sparkles',
},
],
},
{
Expand Down
70 changes: 14 additions & 56 deletions app/components/Landing/IntroHeader.vue
Original file line number Diff line number Diff line change
@@ -1,60 +1,31 @@
<script setup lang="ts">
import { ACTIVE_NOODLES, PERMANENT_NOODLES, type Noodle } from '../Noodle'
import { NOODLES, type Noodle } from '../Noodle'

const { env } = useAppConfig().buildInfo

const activeNoodlesData = ACTIVE_NOODLES.map(noodle => ({
const noodlesData = NOODLES.map(noodle => ({
key: noodle.key,
date: noodle.date,
dateTo: noodle.dateTo,
timezone: noodle.timezone,
tagline: noodle.tagline,
}))

const permanentNoodlesData = PERMANENT_NOODLES.map(noodle => ({
key: noodle.key,
tagline: noodle.tagline,
}))

onPrehydrate(el => {
const tagline = el.querySelector<HTMLElement>('#intro-header-tagline')
const defaultLogo = el.querySelector<HTMLElement>('#intro-header-noodle-default')

if (!tagline || !defaultLogo) return

let permanentNoodles
try {
permanentNoodles = JSON.parse(el.dataset.permanentNoodles as string) as Noodle[]
} catch {
return
}
const activePermanentNoodle = permanentNoodles?.find(noodle =>
new URLSearchParams(window.location.search).has(noodle.key),
)

if (activePermanentNoodle) {
const permanentNoodleLogo = el.querySelector<HTMLElement>(
`#intro-header-noodle-${activePermanentNoodle.key}`,
)

if (!permanentNoodleLogo) return

permanentNoodleLogo.style.display = 'block'
defaultLogo.style.display = 'none'
if (activePermanentNoodle.tagline === false) {
tagline.style.display = 'none'
}
return
}

let activeNoodles
let noodles: Noodle[]
try {
activeNoodles = JSON.parse(el.dataset.activeNoodles as string) as Noodle[]
noodles = JSON.parse(el.dataset.noodles as string) as Noodle[]
} catch {
return
}

const currentActiveNoodles = activeNoodles.filter(noodle => {
// A noodle is active iff today falls inside its date window.
const currentNoodles = noodles.filter(noodle => {
const todayDate = new Date()
const todayDateRaw = new Intl.DateTimeFormat('en-US', {
timeZone: noodle.timezone === 'auto' ? undefined : noodle.timezone,
Expand All @@ -63,7 +34,7 @@ onPrehydrate(el => {
year: 'numeric',
}).format(todayDate)

const noodleDateFrom = new Date(noodle.date!)
const noodleDateFrom = new Date(noodle.date)
if (!noodle.dateTo) {
const noodleDateFromRaw = new Intl.DateTimeFormat('en-US', {
timeZone: noodle.timezone === 'auto' ? undefined : noodle.timezone,
Expand All @@ -73,20 +44,18 @@ onPrehydrate(el => {
}).format(noodleDateFrom)
return todayDateRaw === noodleDateFromRaw
}
const noodleDateTo = new Date(noodle.dateTo!)
const noodleDateTo = new Date(noodle.dateTo)
return todayDate >= noodleDateFrom && todayDate <= noodleDateTo
})

if (!currentActiveNoodles.length) return

const roll = Math.floor(Math.random() * currentActiveNoodles.length)
const selectedNoodle = currentActiveNoodles[roll]
if (!currentNoodles.length) return

const roll = Math.floor(Math.random() * currentNoodles.length)
const selectedNoodle = currentNoodles[roll]
if (!selectedNoodle) return

const noodleLogo = el.querySelector<HTMLElement>(`#intro-header-noodle-${selectedNoodle.key}`)

if (!defaultLogo || !noodleLogo || !tagline) return
if (!noodleLogo) return

defaultLogo.style.display = 'none'
noodleLogo.style.display = 'block'
Expand All @@ -97,10 +66,7 @@ onPrehydrate(el => {
</script>

<template>
<div
:data-active-noodles="JSON.stringify(activeNoodlesData)"
:data-permanent-noodles="JSON.stringify(permanentNoodlesData)"
>
<div :data-noodles="JSON.stringify(noodlesData)">
<h1 class="sr-only">
{{ $t('alt_logo') }}
</h1>
Expand All @@ -118,15 +84,7 @@ onPrehydrate(el => {
</span>
</div>
<component
v-for="noodle in PERMANENT_NOODLES"
:key="noodle.key"
:id="`intro-header-noodle-${noodle.key}`"
class="hidden"
aria-hidden="true"
:is="noodle.logo"
/>
<component
v-for="noodle in ACTIVE_NOODLES"
v-for="noodle in NOODLES"
:key="noodle.key"
:id="`intro-header-noodle-${noodle.key}`"
class="hidden"
Expand Down
19 changes: 19 additions & 0 deletions app/components/Noodle/Artemis/Logo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<div class="relative w-80 sm:w-100 max-w-full">
<ColorSchemeImg
width="400"
class="motion-safe:animate-fade-in motion-safe:animate-scale-in relative z-10 w-full"
dark-src="/extra/npmx-dark-artemis.svg"
light-src="/extra/npmx-light-artemis.svg"
:alt="$t('alt_logo')"
/>
<ColorSchemeImg
width="1440"
height="455"
class="absolute -bottom-4 inset-is-0 w-full h-auto mix-blend-lighten light:mix-blend-darken motion-safe:animate-fade-in pointer-events-none select-none"
dark-src="/extra/moon-dark.png"
light-src="/extra/moon-light.png"
alt=""
/>
</div>
</template>
2 changes: 1 addition & 1 deletion app/components/Noodle/Kawaii/Logo.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<img
width="400"
class="mb-8 motion-safe:animate-fade-in motion-safe:animate-scale-in motion-safe:hover:scale-105 motion-safe:transition w-80 sm:w-100"
class="motion-safe:animate-fade-in motion-safe:animate-scale-in w-80 sm:w-100"
src="/extra/npmx-cute.svg"
:alt="$t('alt_logo_kawaii')"
/>
Expand Down
8 changes: 8 additions & 0 deletions app/components/Noodle/KawaiiPride/Logo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<img
width="400"
class="motion-safe:animate-fade-in motion-safe:animate-scale-in w-80 sm:w-100"
src="/extra/npmx-cute-transgender.svg"
:alt="$t('alt_logo_kawaii')"
/>
</template>
36 changes: 36 additions & 0 deletions app/components/Noodle/ListCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { Noodle } from '#shared/schemas/noodle'
import { resolveNoodleLogo } from '../Noodle'

const props = defineProps<{
noodle: Noodle
}>()

const logo = computed(() => resolveNoodleLogo(props.noodle.key))
</script>

<template>
<NuxtLink
:to="`/noodles/${noodle.slug}`"
class="group relative block rounded-xl border border-border bg-bg-elevated overflow-hidden transition-colors hover:border-border-hover decoration-none"
>
<span class="sr-only">{{ noodle.title }}</span>
<div class="aspect-[4/3] flex items-center justify-center bg-bg p-8 overflow-hidden">
<component :is="logo" v-if="logo" class="max-w-full max-h-full" />
<span v-else class="i-lucide:sparkles w-12 h-12 text-fg-subtle" aria-hidden="true" />
</div>
<div class="absolute top-3 inset-ie-3 text-fg-subtle group-hover:text-fg transition-colors">
<span class="i-lucide:arrow-up-right rtl-flip w-4 h-4" aria-hidden="true" />
</div>
<div
class="border-t border-border px-4 py-3 flex items-center gap-2 text-xs text-fg-muted font-mono"
>
<span class="text-fg-subtle">//</span>
<DateTime :datetime="noodle.date" year="numeric" month="2-digit" day="2-digit" />
<template v-if="noodle.dateTo">
<span class="text-fg-subtle">—</span>
<DateTime :datetime="noodle.dateTo" year="numeric" month="2-digit" day="2-digit" />
</template>
</div>
</NuxtLink>
</template>
24 changes: 7 additions & 17 deletions app/components/Noodle/Press/Logo.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
<template>
<TooltipApp interactive position="top">
<template #content>
<p class="text-sm font-medium text-fg mb-1">
A free press makes openness possible for everyone.
<LinkBase to="https://en.wikipedia.org/wiki/World_Press_Freedom_Day"
>Read more about Press Freedom Day</LinkBase
>
</p>
</template>
<ColorSchemeImg
width="320"
class="mb-16 w-80 sm:w-140 max-w-full"
dark-src="/extra/npmx-dark-press.png"
light-src="/extra/npmx-light-press.png"
:alt="$t('alt_logo')"
/>
</TooltipApp>
<ColorSchemeImg
width="320"
class="motion-safe:animate-fade-in motion-safe:animate-scale-in w-80 sm:w-140 max-w-full"
dark-src="/extra/npmx-dark-press.png"
light-src="/extra/npmx-light-press.png"
:alt="$t('alt_logo')"
/>
</template>
58 changes: 32 additions & 26 deletions app/components/Noodle/index.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,44 @@
import type { Component } from 'vue'
import { noodles } from '~/noodles'
import NoodleArtemisLogo from './Artemis/Logo.vue'
import NoodleKawaiiLogo from './Kawaii/Logo.vue'
import NoodleKawaiiPrideLogo from './KawaiiPride/Logo.vue'
import NoodlePressLogo from './Press/Logo.vue'

export type Noodle = {
// Unique identifier for the noodle
key: string
// Timezone for the noodle (default is auto, i.e. user's timezone)
timezone?: string
// Date for the noodle
date?: string
// `Date to` for the noodle
date: string
dateTo?: string
// Logo for the noodle - could be any component. Relative parent - intro section
logo: Component
// Show npmx tagline or not (default is true)
tagline?: boolean
}

// Permanent noodles - always shown on specific query param (e.g. ?kawaii)
export const PERMANENT_NOODLES: Noodle[] = [
{
key: 'kawaii',
logo: NoodleKawaiiLogo,
tagline: false,
},
]
// Keyed by the entry's `key` in app/noodles.ts.
const NOODLE_LOGOS: Record<string, Component> = {
'press': NoodlePressLogo,
'kawaii': NoodleKawaiiLogo,
'kawaii-pride': NoodleKawaiiPrideLogo,
'artemis': NoodleArtemisLogo,
}

export function resolveNoodleLogo(key: string): Component | undefined {
return NOODLE_LOGOS[key]
}

// Active noodles - shown based on date and timezone
export const ACTIVE_NOODLES: Noodle[] = [
{
key: 'press',
logo: NoodlePressLogo,
date: '2026-05-01',
dateTo: '2026-05-04',
timezone: 'auto',
tagline: false,
},
]
export const NOODLES: Noodle[] = noodles.map(entry => {
const logo = NOODLE_LOGOS[entry.key]
if (!logo) {
throw new Error(
`Missing logo registration for noodle key "${entry.key}". Add it to NOODLE_LOGOS in app/components/Noodle/index.ts.`,
)
}
return {
key: entry.key,
logo,
date: entry.date,
dateTo: entry.dateTo,
timezone: entry.timezone,
tagline: entry.tagline,
}
})
Loading
Loading