From 3786ceef918419c974c0fa6a9747120aa2dd4928 Mon Sep 17 00:00:00 2001 From: Adebesin Tolulope Date: Wed, 20 May 2026 16:08:34 +0100 Subject: [PATCH 01/36] feat(noodles): add /noodles archive with per-noodle markdown pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a public archive for every noodle (seasonal/event logo variants) shown on npmx, plus a contributor-friendly layout for adding new ones. What's new - /noodles lists every entry as a card (logo preview + date range or "always available" badge for permanent noodles). - /noodles/[slug] renders a per-noodle markdown page with hero logo, story body, authors, and an image gallery with click-to-lightbox. - modules/noodles.ts scans app/pages/noodles/*.md, validates frontmatter with valibot, resolves Bluesky avatars at build time, and exposes #noodles/entries. - app/components/Noodle/index.ts now derives ACTIVE/PERMANENT noodle arrays from #noodles/entries, so contributors only edit the .md file and the logo registry. - New NoodleGallery + NoodleLightbox (custom dialog-based, no external lib) and NoodleListCard for the archive grid. - Dedicated NoodlePostWrapper registered by the noodles module with a scoped include pattern, so blog and noodle markdown files don't share a wrapper. Blog module's include narrowed to pages/blog/. Other touches - Footer "Other" section now links to /noodles. - canonical-redirects.global.ts allowlist gains /noodles so the server middleware doesn't shortcut it to /package/noodles. - a11y test coverage for the four new components. - i18n keys under noodles.* (English only; CI syncs other locales). Seed Two stub .md files (press, kawaii) marked draft: true ship with this commit so the archive has content immediately. Gallery images should live under public/noodle-gallery/[key]/ — keeping them out of public/noodles/ avoids shadowing the /noodles route in dev. --- app/components/AppFooter.vue | 4 + app/components/Noodle/Gallery.vue | 49 +++++ app/components/Noodle/Lightbox.vue | 96 +++++++++ app/components/Noodle/ListCard.vue | 39 ++++ app/components/Noodle/index.ts | 61 +++--- app/components/global/NoodlePostWrapper.vue | 115 ++++++++++ app/pages/noodles/index.vue | 82 +++++++ app/pages/noodles/kawaii.md | 18 ++ app/pages/noodles/press.md | 24 +++ i18n/locales/en.json | 19 ++ modules/blog.ts | 2 +- modules/noodles.ts | 202 ++++++++++++++++++ .../middleware/canonical-redirects.global.ts | 1 + shared/schemas/noodle.ts | 45 ++++ test/nuxt/a11y.spec.ts | 78 +++++++ 15 files changed, 809 insertions(+), 26 deletions(-) create mode 100644 app/components/Noodle/Gallery.vue create mode 100644 app/components/Noodle/Lightbox.vue create mode 100644 app/components/Noodle/ListCard.vue create mode 100644 app/components/global/NoodlePostWrapper.vue create mode 100644 app/pages/noodles/index.vue create mode 100644 app/pages/noodles/kawaii.md create mode 100644 app/pages/noodles/press.md create mode 100644 modules/noodles.ts create mode 100644 shared/schemas/noodle.ts diff --git a/app/components/AppFooter.vue b/app/components/AppFooter.vue index 179e9b1d64..d493ff2cd2 100644 --- a/app/components/AppFooter.vue +++ b/app/components/AppFooter.vue @@ -87,6 +87,10 @@ const footerSections = computed>(( name: t('pds.title'), href: '/pds', }, + { + name: t('noodles.title'), + href: '/noodles', + }, { name: t('footer.docs'), href: NPMX_DOCS_SITE, diff --git a/app/components/Noodle/Gallery.vue b/app/components/Noodle/Gallery.vue new file mode 100644 index 0000000000..715804fbae --- /dev/null +++ b/app/components/Noodle/Gallery.vue @@ -0,0 +1,49 @@ + + + diff --git a/app/components/Noodle/Lightbox.vue b/app/components/Noodle/Lightbox.vue new file mode 100644 index 0000000000..d2c7f22c16 --- /dev/null +++ b/app/components/Noodle/Lightbox.vue @@ -0,0 +1,96 @@ + + + diff --git a/app/components/Noodle/ListCard.vue b/app/components/Noodle/ListCard.vue new file mode 100644 index 0000000000..eb0511fb8a --- /dev/null +++ b/app/components/Noodle/ListCard.vue @@ -0,0 +1,39 @@ + + + diff --git a/app/components/Noodle/index.ts b/app/components/Noodle/index.ts index abeef9a132..527aab6154 100644 --- a/app/components/Noodle/index.ts +++ b/app/components/Noodle/index.ts @@ -1,38 +1,49 @@ +import type { Component } from 'vue' +import { noodles } from '#noodles/entries' import NoodleKawaiiLogo from './Kawaii/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 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, - }, -] +/** + * Logo component for each noodle, keyed by the `key` field in the matching + * .md file under app/pages/noodles. To add a new noodle: + * 1. Drop a .md file in app/pages/noodles (frontmatter drives dates + copy) + * 2. Add the logo here under the same key + */ +const NOODLE_LOGOS: Record = { + press: NoodlePressLogo, + kawaii: NoodleKawaiiLogo, +} + +export function resolveNoodleLogo(key: string): Component | undefined { + return NOODLE_LOGOS[key] +} + +function entriesToNoodles(filter: (e: (typeof noodles)[number]) => boolean): Noodle[] { + const list: Noodle[] = [] + for (const entry of noodles) { + if (!filter(entry)) continue + const logo = NOODLE_LOGOS[entry.key] + if (!logo) continue + list.push({ + key: entry.key, + logo, + date: entry.date, + dateTo: entry.dateTo, + timezone: entry.timezone, + tagline: entry.tagline, + }) + } + return list +} -// 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 PERMANENT_NOODLES: Noodle[] = entriesToNoodles(e => e.permanent) +export const ACTIVE_NOODLES: Noodle[] = entriesToNoodles(e => !e.permanent) diff --git a/app/components/global/NoodlePostWrapper.vue b/app/components/global/NoodlePostWrapper.vue new file mode 100644 index 0000000000..afe16aaacc --- /dev/null +++ b/app/components/global/NoodlePostWrapper.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/app/pages/noodles/index.vue b/app/pages/noodles/index.vue new file mode 100644 index 0000000000..719e68baea --- /dev/null +++ b/app/pages/noodles/index.vue @@ -0,0 +1,82 @@ + + + diff --git a/app/pages/noodles/kawaii.md b/app/pages/noodles/kawaii.md new file mode 100644 index 0000000000..3b8eba4e3c --- /dev/null +++ b/app/pages/noodles/kawaii.md @@ -0,0 +1,18 @@ +--- +type: noodle +key: kawaii +slug: kawaii +title: Kawaii +excerpt: A permanent noodle hidden behind the ?kawaii query parameter. +tagline: false +gallery: [] +authors: + - name: npmx team +draft: true +--- + +## About this noodle + +Kawaii is a permanent noodle — it's not tied to a date. You can summon it at +any time by visiting [npmx.dev/?kawaii](/?kawaii). Replace this stub with the +real story behind it. diff --git a/app/pages/noodles/press.md b/app/pages/noodles/press.md new file mode 100644 index 0000000000..7db7eef413 --- /dev/null +++ b/app/pages/noodles/press.md @@ -0,0 +1,24 @@ +--- +type: noodle +key: press +slug: press +title: Press +excerpt: A nod to the launch press cycle around npmx. +date: '2026-05-01' +dateTo: '2026-05-04' +timezone: auto +tagline: false +gallery: [] +authors: + - name: npmx team +draft: true +--- + +## About this noodle + +A short story about why we made it and what the iteration looked like. +Replace this stub with the real write-up — quotes from the team, sketches, +links to the launch posts — and drop iteration images under +`public/noodle-gallery/press/` then list them in the `gallery` frontmatter +array as `/noodle-gallery/press/1.png`, etc. (Don't use `public/noodles/` +directly — it would shadow the `/noodles` route in dev.) diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 7e119affa7..df58417353 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -229,6 +229,25 @@ "more_replies": "{count} more reply... | {count} more replies..." } }, + "noodles": { + "title": "noodles", + "meta_description": "Every noodle we've ever shown on npmx — the seasonal logos, easter eggs, and the stories behind them.", + "latest": "Latest noodles", + "what_is": "What is noodles", + "what_is_body": "Noodles are the playful variants of the npmx logo we run on the homepage to mark releases, holidays, events, and other moments worth celebrating. Think of them like Google Doodles, but for npm packages.", + "empty": "No noodles yet.", + "permanent_badge": "always available", + "dates": "Active dates", + "gallery": "Gallery", + "draft_banner": "This noodle write-up is still a draft.", + "back_to_index": "Back to all noodles", + "lightbox": { + "label": "Noodle gallery preview", + "close": "Close preview", + "prev": "Previous image", + "next": "Next image" + } + }, "settings": { "title": "settings", "tagline": "customize your npmx experience", diff --git a/modules/blog.ts b/modules/blog.ts index 6c28c6de1e..a2061a4d8c 100644 --- a/modules/blog.ts +++ b/modules/blog.ts @@ -172,7 +172,7 @@ export default defineNuxtModule({ addVitePlugin(() => Markdown({ - include: [/\.(md|markdown)($|\?)/], + include: [/pages[\\/]blog[\\/].+\.(md|markdown)($|\?)/], wrapperComponent: 'BlogPostWrapper', wrapperClasses: 'text-fg-muted leading-relaxed', async markdownSetup(md) { diff --git a/modules/noodles.ts b/modules/noodles.ts new file mode 100644 index 0000000000..32851c2c4e --- /dev/null +++ b/modules/noodles.ts @@ -0,0 +1,202 @@ +import { join } from 'node:path' +import Markdown from 'unplugin-vue-markdown/vite' +import { addTemplate, addVitePlugin, defineNuxtModule, useNuxt, createResolver } from 'nuxt/kit' +import shiki from '@shikijs/markdown-exit' +import MarkdownItAnchor from 'markdown-it-anchor' +import { read } from 'gray-matter' +import { array, safeParse } from 'valibot' +import { glob } from 'node:fs/promises' +import { existsSync } from 'node:fs' +import crypto from 'node:crypto' +import { mkdir, writeFile } from 'node:fs/promises' +import { RawNoodlePostSchema, type NoodlePostFrontmatter } from '../shared/schemas/noodle' +import { AuthorSchema, type Author, type ResolvedAuthor } from '../shared/schemas/blog' +import { isProduction } from '../config/env' +import { BLUESKY_API } from '../shared/utils/constants' + +/** + * Fetches Bluesky avatars for a set of authors at build time. + * Mirrors the blog module's helper — kept local so the two modules don't share + * mutable state. + */ +async function fetchBlueskyAvatars( + imagesDir: string, + handles: string[], +): Promise> { + const avatarMap = new Map() + if (handles.length === 0) return avatarMap + + try { + const params = new URLSearchParams() + for (const handle of handles) params.append('actors', handle) + + const response = await fetch( + `${BLUESKY_API}/xrpc/app.bsky.actor.getProfiles?${params.toString()}`, + ) + if (!response.ok) { + console.warn(`[noodles] Failed to fetch Bluesky profiles: ${response.status}`) + return avatarMap + } + + const data = (await response.json()) as { + profiles: Array<{ handle: string; avatar?: string }> + } + for (const profile of data.profiles) { + if (!profile.avatar) continue + const hash = crypto.createHash('sha256').update(profile.avatar).digest('hex') + const dest = join(imagesDir, `${hash}.png`) + if (!existsSync(dest)) { + const res = await fetch(`${profile.avatar}@png`) + if (!res.ok || !res.body) continue + await writeFile(dest, res.body) + } + avatarMap.set(profile.handle, `/noodle-avatar/${hash}.png`) + } + } catch (error) { + console.warn(`[noodles] Failed to fetch Bluesky avatars:`, error) + } + + return avatarMap +} + +function resolveAuthors(authors: Author[], avatarMap: Map): ResolvedAuthor[] { + return authors.map(author => ({ + ...author, + avatar: author.blueskyHandle ? (avatarMap.get(author.blueskyHandle) ?? null) : null, + profileUrl: author.blueskyHandle ? `https://bsky.app/profile/${author.blueskyHandle}` : null, + })) +} + +async function loadNoodlePosts( + noodlesDir: string, + options: { imagesDir: string; resolveAvatars: boolean }, +): Promise { + const { imagesDir, resolveAvatars } = options + const files = await Array.fromAsync(glob(join(noodlesDir, '*.md').replace(/\\/g, '/'))) + + const rawEntries: Array> = [] + const allHandles = new Set() + + for (const file of files) { + const { data: frontmatter } = read(file) + if (frontmatter.type !== 'noodle') continue + + if (typeof frontmatter.slug === 'string' && !frontmatter.path) { + frontmatter.path = `/noodles/${frontmatter.slug}` + } + + const authorsResult = safeParse(array(AuthorSchema), frontmatter.authors ?? []) + if (authorsResult.success) { + for (const author of authorsResult.output) { + if (author.blueskyHandle) allHandles.add(author.blueskyHandle) + } + } + rawEntries.push(frontmatter) + } + + const avatarMap = resolveAvatars + ? await fetchBlueskyAvatars(imagesDir, [...allHandles]) + : new Map() + + const entries: NoodlePostFrontmatter[] = [] + for (const frontmatter of rawEntries) { + const result = safeParse(RawNoodlePostSchema, frontmatter) + if (!result.success) { + console.warn( + `[noodles] Skipping ${frontmatter.slug ?? 'unknown'}: invalid frontmatter`, + result.issues, + ) + continue + } + const raw = result.output + entries.push({ + type: 'noodle', + key: raw.key, + title: raw.title, + slug: raw.slug, + path: raw.path ?? `/noodles/${raw.slug}`, + excerpt: raw.excerpt, + authors: resolveAuthors(raw.authors ?? [], avatarMap), + gallery: raw.gallery ?? [], + date: raw.date, + dateTo: raw.dateTo, + timezone: raw.timezone, + tagline: raw.tagline, + draft: raw.draft, + permanent: !raw.date, + }) + } + + // Latest first; permanent noodles sort after dated ones. + entries.sort((a, b) => { + if (a.permanent && !b.permanent) return 1 + if (!a.permanent && b.permanent) return -1 + if (a.permanent && b.permanent) return a.title.localeCompare(b.title) + return Date.parse(b.date!) - Date.parse(a.date!) + }) + return entries +} + +export default defineNuxtModule({ + meta: { name: 'noodles' }, + async setup() { + const nuxt = useNuxt() + const resolver = createResolver(import.meta.url) + const noodlesDir = resolver.resolve('../app/pages/noodles') + const imagesDir = resolver.resolve('../public/noodle-avatar') + const resolveAvatars = !nuxt.options._prepare + + if (!existsSync(noodlesDir)) return + + if (resolveAvatars && !existsSync(imagesDir)) { + await mkdir(imagesDir, { recursive: true }) + } + + addVitePlugin(() => + Markdown({ + include: [/pages[\\/]noodles[\\/].+\.(md|markdown)($|\?)/], + wrapperComponent: 'NoodlePostWrapper', + wrapperClasses: 'text-fg-muted leading-relaxed', + async markdownSetup(md) { + md.use( + await shiki({ + themes: { + dark: 'github-dark', + light: 'github-light', + }, + }), + ) + md.use(MarkdownItAnchor as any) + }, + }), + ) + + const allEntries = await loadNoodlePosts(noodlesDir, { imagesDir, resolveAvatars }) + const showDrafts = nuxt.options.dev || !isProduction + + addTemplate({ + filename: 'noodles/entries.ts', + write: true, + getContents: () => { + const entries = allEntries.filter(e => showDrafts || !e.draft) + return [ + `import type { NoodlePostFrontmatter } from '#shared/schemas/noodle'`, + ``, + `export const noodles: NoodlePostFrontmatter[] = ${JSON.stringify(entries, null, 2)}`, + ].join('\n') + }, + }) + + nuxt.options.alias['#noodles/entries'] = join(nuxt.options.buildDir, 'noodles/entries') + + // Hide draft detail pages from indexers. + for (const entry of allEntries) { + if (entry.draft) { + nuxt.options.routeRules ||= {} + nuxt.options.routeRules[`/noodles/${entry.slug}`] = { + headers: { 'X-Robots-Tag': 'noindex, nofollow' }, + } + } + } + }, +}) diff --git a/server/middleware/canonical-redirects.global.ts b/server/middleware/canonical-redirects.global.ts index e9139a8493..2fbc218d30 100644 --- a/server/middleware/canonical-redirects.global.ts +++ b/server/middleware/canonical-redirects.global.ts @@ -21,6 +21,7 @@ const pages = [ '/blog', '/brand', '/compare', + '/noodles', '/org', '/package', '/package-code', diff --git a/shared/schemas/noodle.ts b/shared/schemas/noodle.ts new file mode 100644 index 0000000000..1644dc4ed4 --- /dev/null +++ b/shared/schemas/noodle.ts @@ -0,0 +1,45 @@ +import { array, boolean, literal, object, optional, string, type InferOutput } from 'valibot' +import { AuthorSchema, ResolvedAuthorSchema } from './blog' + +/** Raw frontmatter as authored in a noodle .md file. */ +export const RawNoodlePostSchema = object({ + type: literal('noodle'), + key: string(), + title: string(), + slug: string(), + excerpt: optional(string()), + authors: optional(array(AuthorSchema)), + gallery: optional(array(string())), + /** ISO date (YYYY-MM-DD). Absence = permanent noodle (query-param triggered). */ + date: optional(string()), + /** ISO date (YYYY-MM-DD). Last day the noodle is active (inclusive). */ + dateTo: optional(string()), + /** IANA timezone name, or "auto" for the visitor's local time. */ + timezone: optional(string()), + /** Whether the npmx tagline is hidden while this noodle is active. */ + tagline: optional(boolean()), + draft: optional(boolean()), + path: optional(string()), +}) + +/** Frontmatter after build-time enrichment (authors resolved, defaults applied). */ +export const NoodlePostSchema = object({ + type: literal('noodle'), + key: string(), + title: string(), + slug: string(), + path: string(), + excerpt: optional(string()), + authors: array(ResolvedAuthorSchema), + gallery: array(string()), + date: optional(string()), + dateTo: optional(string()), + timezone: optional(string()), + tagline: optional(boolean()), + draft: optional(boolean()), + /** Derived: true when no `date` is set (visible via query param only). */ + permanent: boolean(), +}) + +export type RawNoodlePostFrontmatter = InferOutput +export type NoodlePostFrontmatter = InferOutput diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts index 5d0865d77f..b41372f8cf 100644 --- a/test/nuxt/a11y.spec.ts +++ b/test/nuxt/a11y.spec.ts @@ -157,7 +157,11 @@ import { BuildEnvironment, ButtonBase, LandingIntroHeader, + NoodleGallery, NoodleKawaiiLogo, + NoodleLightbox, + NoodleListCard, + NoodlePostWrapper, NoodlePressLogo, LinkBase, CallToAction, @@ -2864,6 +2868,80 @@ describe('component accessibility audits', () => { }) }) + describe('NoodleGallery', () => { + it('should have no accessibility violations', async () => { + const component = await mountSuspended(NoodleGallery, { + props: { + images: ['/noodles/press/1.png', '/noodles/press/2.png'], + alt: 'Press noodle', + }, + }) + const results = await runAxe(component) + expect(results.violations).toEqual([]) + }) + }) + + describe('NoodleLightbox', () => { + it('should have no accessibility violations', async () => { + const component = await mountSuspended(NoodleLightbox, { + props: { + images: ['/noodles/press/1.png'], + open: false, + alt: 'Press noodle', + }, + }) + const results = await runAxe(component) + expect(results.violations).toEqual([]) + }) + }) + + describe('NoodleListCard', () => { + it('should have no accessibility violations', async () => { + const component = await mountSuspended(NoodleListCard, { + props: { + noodle: { + type: 'noodle', + key: 'press', + slug: 'press', + path: '/noodles/press', + title: 'Press', + excerpt: 'A nod to the launch press cycle.', + authors: [], + gallery: [], + date: '2026-05-01', + dateTo: '2026-05-04', + permanent: false, + }, + }, + }) + const results = await runAxe(component) + expect(results.violations).toEqual([]) + }) + }) + + describe('NoodlePostWrapper', () => { + it('should have no accessibility violations', async () => { + const component = await mountSuspended(NoodlePostWrapper, { + props: { + frontmatter: { + type: 'noodle', + key: 'press', + slug: 'press', + title: 'Press', + excerpt: 'A nod to the launch press cycle.', + date: '2026-05-01', + dateTo: '2026-05-04', + authors: [], + gallery: [], + }, + }, + slots: { default: '

Noodle story content.

' }, + }) + const results = await runAxe(component) + expect(results.violations).toEqual([]) + }) + }) + describe('BlueskyComment', () => { it('should have no accessibility violations', async () => { const component = await mountSuspended(BlueskyComment, { From 1eb92ce13139b17cfc2b35961a6f48838094354e Mon Sep 17 00:00:00 2001 From: Adebesin Tolulope Date: Fri, 22 May 2026 01:49:10 +0100 Subject: [PATCH 02/36] feat(noodles): case-study layout for individual noodle pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Landing /noodles hero: solid filled "bowl" (thick theme-aware border, inner + outer shadow), npmx sticker overlapping, thicker dark stripe background with wider gaps. Headings bumped to mono text-xl semibold uppercase per the Figma spec. - Individual noodle pages: restructured into an agency-style case study — full-bleed hero with the bowl, credits row (avatar + name + @handle, N authors), tombstone strip (dates · status), prose body, editorial numbered Figures section, footer back link. - New Noodle/Figures.vue: numbered Fig.NN editorial figure list with lightbox, replaces the 4-tile gallery on individual noodle pages. - New Noodle/BuildLog.vue: scroll-snap drafts → shipped strip, ready to wire once noodles get an authored drafts schema. - ListCard icon: maximize-2 → arrow-up-right (clearer "navigate to"). - Kawaii noodle: gave it active dates (no more "always available"), removed the /?kawaii parenthetical from the body stub. - Press noodle: populated with multi-author credits, real excerpt, 4-section body (brief · decisions · process · cuts), and a 4-image gallery so the case-study layout has something to render. - i18n (en): added noodles.credits, status, status_draft, status_shipped, figures. - Gitignore public/noodle-avatar/ (Bluesky avatars are fetched at build time, mirrors the existing public/blog/avatar pattern). --- .gitignore | 1 + app/components/Noodle/BuildLog.vue | 71 +++++++++++++ app/components/Noodle/Figures.vue | 49 +++++++++ app/components/Noodle/ListCard.vue | 2 +- app/components/global/NoodlePostWrapper.vue | 104 ++++++++++++-------- app/pages/noodles/index.vue | 27 +++-- app/pages/noodles/kawaii.md | 10 +- app/pages/noodles/press.md | 56 ++++++++--- i18n/locales/en.json | 7 +- public/extra/npmx-sticker.png | Bin 0 -> 86678 bytes 10 files changed, 261 insertions(+), 66 deletions(-) create mode 100644 app/components/Noodle/BuildLog.vue create mode 100644 app/components/Noodle/Figures.vue create mode 100644 public/extra/npmx-sticker.png diff --git a/.gitignore b/.gitignore index 9aa2aebd2e..0281bb3ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ test-results/ shared/types/lexicons file-tree-sprite.svg public/blog/avatar +public/noodle-avatar **/__screenshots__/** diff --git a/app/components/Noodle/BuildLog.vue b/app/components/Noodle/BuildLog.vue new file mode 100644 index 0000000000..cfa34b727e --- /dev/null +++ b/app/components/Noodle/BuildLog.vue @@ -0,0 +1,71 @@ + + + diff --git a/app/components/Noodle/Figures.vue b/app/components/Noodle/Figures.vue new file mode 100644 index 0000000000..852d6031e4 --- /dev/null +++ b/app/components/Noodle/Figures.vue @@ -0,0 +1,49 @@ + + + diff --git a/app/components/Noodle/ListCard.vue b/app/components/Noodle/ListCard.vue index eb0511fb8a..418f6cc111 100644 --- a/app/components/Noodle/ListCard.vue +++ b/app/components/Noodle/ListCard.vue @@ -20,7 +20,7 @@ const logo = computed(() => resolveNoodleLogo(props.noodle.key))