Skip to content

feat(www): hobby mode with a media log, ASCII stage and avatar mode switch - #3215

Merged
Chia1104 merged 4 commits into
developfrom
feat/www-site-mode
Nov 15, 2026
Merged

Chia1104 merged 4 commits into
developfrom
feat/www-site-mode

Conversation

@Chia1104

Copy link
Copy Markdown
Owner

Summary

www gets a second face. The tech mode (articles, career) stays the default; a hobby mode under /hobby shows films, series, anime and games with its own frame and palette.

  • Modes. A mode is a URL space with its own frame, so the route decides it and nothing stores it. Tech pages move into a (tech) route group with TechFrame; hobby/ has HobbyFrame. The locale layout keeps the document, providers and chat dock. The hobby frame marks itself with data-site-mode; @chia/themes/hobby.css keys off it through :root:has(), and the reader palette steps aside there.
  • Media log. New table chia_media_entry with MediaKind / MediaStatus / MediaSource, a repo in @chia/db, and a media module in @chia/services (list and get public; search, create, update, sync, remove admin). Entries are logged by hand from dash /media: search a catalogue, pick a work, and the service snapshots titles, copies poster and backdrop to storage and takes a cover colour. Writes end in startSiteRevalidation(WwwCacheTag.Media).
  • Catalogues. @chia/integrations/{tmdb,anilist,igdb,steam}. Anime searches both AniList and TMDB. IGDB supplies the Steam app id; Steam supplies playtime and achievements, on create and from a dash button.
  • Hobby pages. No navbar and no column: one full-width line of type per work over a fixed ASCII field (@chia/shaders/ascii-field) that draws the focused work's key art. Every hobby token derives from --media-color, the focused work's cover colour. A work opens at /hobby/[kind]/[slug], intercepted into a cover modal when reached from inside the site; both render the same component, with status, five-star rating, dates and game stats, and the cover in a reflective card (@chia/shaders/reflective-card, adapted from React Bits).
  • Mode switch. The avatar is the switch: the tech home's portrait and a small one beside the hobby wordmark. Hover wipes in the other mode's face; a click switches behind a full-viewport curtain of glyphs (@chia/shaders/ascii-curtain) that spreads from the avatar, draws the destination face while the route changes, and opens from the new page's avatar. The command menu entry runs the same transition. Reduced motion navigates without it.

Before deploy

  • Railway env on service: TMDB_ACCESS_TOKEN, IGDB_CLIENT_ID, IGDB_CLIENT_SECRET, STEAM_API_KEY, STEAM_ID. Not added to .railway/resources.ts, because preserve() needs an existing value.
  • Three migrations: 20261114172203_media_entry, 20261114180924_media_entry_backdrop_color, 20261114184737_media_entry_slug_play_stats (the last backfills slug before setting it NOT NULL).
  • The Steam profile's game details must be public for playtime and achievements.
  • contact/ moved into (tech), which gives its opengraph-image URL a route-group hash suffix; check any Cloudflare cache rule keyed on that path.

Notes

  • Image copies are restricted to the three catalogue CDN hosts, refuse redirects and cap the streamed size. Catalogue colour and titles are validated before storing, because rows are parsed on every public read.
  • The ASCII field needs WebGPU; without it the key art stands in. It pauses three seconds after the last input and draws at most 1.5 canvas pixels per CSS pixel. --media-color is not transitioned: it is inherited, so easing it restyled the whole document every frame.
  • Not exercised against the live APIs: TMDB, IGDB and Steam are covered by mocked tests only. Also unverified: sharp in the Alpine image build (traced through traceDeps), the no-WebGPU fallback, the dash UI behind login, and the production image path (/cdn-cgi/image) for the curtain's portrait.
  • Steam stats refresh on create and on demand; there is no scheduled sync yet.

Verification

  • pnpm turbo run lint type:check test passes for the touched workspaces.
  • Driven in headless Chromium against local www + service: hobby list, work page and sheet, and the mode switch round trip on desktop, phone width and reduced motion. The curtain holds 60fps and takes about 1.9s end to end.
  • Stage cost while sweeping rows (1440×900 @2x): main thread 28% → 17%, style 15% → 6% after the performance pass.

🤖 Generated with Claude Code

Chia1104 and others added 4 commits November 15, 2026 02:00
A mode is a URL space with its own frame. Tech pages move into the
`(tech)` route group; `/hobby` gets its own frame, palette
(`@chia/themes/hobby.css`, keyed off `data-site-mode`) and a mode switch
in the header, the tech footer and the command menu. The reader palette
applies to the tech mode only.

The hobby pages read a new media log: `chia_media_entry`, catalogue
clients for TMDB, AniList and IGDB, a `media` oRPC module that snapshots
a work and copies its cover into storage, and a dash page to manage it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The hobby frame drops its header and column. Every work is one
full-width line; the line in focus hands its key art and cover colour to
a fixed field of glyphs behind the page, and every hobby token derives
from that colour through `--media-color`. The field pauses when the
reader is idle and draws at no more than 1.5 pixels per CSS pixel.

Media entries gain a backdrop and a colour, copied and extracted when a
work is logged. The mode switch is removed until the mode is finished.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A logged work has its own URL, `/hobby/[kind]/[slug]`. From the list it
opens as a sheet over it through the `@modal` slot; opened directly it
is a page. Both show the key art across the whole surface under a
translucent wash of the work's colour, the cover as a card lit by a
WebGL overlay, the rating as five stars and, for a game, playtime and
achievements.

Entries gain a slug and play stats. A game logged from IGDB takes its
Steam app id from the store link and reads its stats from Steam;
`media.sync` refreshes them. Image copies are limited to the catalogue
CDNs with a streamed size cap, and catalogue titles and colours are
validated before they are stored.

The stage stops behind a sheet and is not mounted under a page whose
art covers the viewport. The mode switch is back in both frames.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The portrait is now the mode switch: the tech home's avatar and a small one
beside the hobby wordmark. Hovering wipes in the other mode's face and a click
switches behind a full-viewport curtain of glyphs that spreads from the
avatar, draws the destination face while the route changes, and opens from
the new page's avatar. The segmented switch is gone from the header, the
footer and the hobby cluster; the command menu entry runs the same transition.

- @chia/shaders/ascii-curtain: canvas 2D wipe that redraws only changed cells
  and takes its colours from the theme at paint time
- ModeTransitionProvider in the shared layout owns the cover, the navigation
  and the reveal; reduced motion navigates without the curtain
- readableImageSourceOf: script reads of storage images use their own URL
  outside production, where a cached <img> response lacks CORS headers
- meta.secondaryAvatar is the hobby face

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
chia1104 Error Error Nov 14, 2026 9:41pm UTC

@coderabbitai

coderabbitai Bot commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 131 files, which is 31 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d8f132fe-7a17-4ad8-a12b-b18dd8d3e97e
Commits

Reviewing files that changed from the base of the PR and between 06c37f0 and 568b297.

Files ignored due to path filters: 5

  • apps/dash/.next/dev/types/link.d.ts is excluded by !**/.next/**
  • apps/dash/.next/dev/types/routes.d.ts is excluded by !**/.next/**
  • apps/www/.next/dev/types/link.d.ts is excluded by !**/.next/**
  • apps/www/.next/dev/types/routes.d.ts is excluded by !**/.next/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
Files selected for processing: 131

  • apps/AGENTS.md
  • apps/dash/__tests__/media-form.test.ts
  • apps/dash/src/app/(workspace)/(operator)/media/layout.tsx
  • apps/dash/src/app/(workspace)/(operator)/media/page.tsx
  • apps/dash/src/components/assets/file-explorer.tsx
  • apps/dash/src/components/assets/utils.ts
  • apps/dash/src/components/media/add-drawer.tsx
  • apps/dash/src/components/media/entry-form.tsx
  • apps/dash/src/components/media/form.ts
  • apps/dash/src/components/media/media-manager.tsx
  • apps/dash/src/shared/routes.tsx
  • apps/service/.env.example
  • apps/service/nitro.config.ts
  • apps/service/package.json
  • apps/www/DESIGN.md
  • apps/www/__tests__/libs/palette.test.ts
  • apps/www/__tests__/shared/routes.test.ts
  • apps/www/__tests__/shared/site-mode.test.ts
  • apps/www/src/app/[locale]/(tech)/(blog)/[type]/[slug]/error.ts
  • apps/www/src/app/[locale]/(tech)/(blog)/[type]/[slug]/llm.md/route.ts
  • apps/www/src/app/[locale]/(tech)/(blog)/[type]/[slug]/not-found.ts
  • apps/www/src/app/[locale]/(tech)/(blog)/[type]/[slug]/opengraph-image.tsx
  • apps/www/src/app/[locale]/(tech)/(blog)/[type]/[slug]/page.tsx
  • apps/www/src/app/[locale]/(tech)/(blog)/[type]/error.ts
  • apps/www/src/app/[locale]/(tech)/(blog)/[type]/not-found.ts
  • apps/www/src/app/[locale]/(tech)/(blog)/[type]/opengraph-image.tsx
  • apps/www/src/app/[locale]/(tech)/(blog)/[type]/page.tsx
  • apps/www/src/app/[locale]/(tech)/(blog)/layout.tsx
  • apps/www/src/app/[locale]/(tech)/(blog)/tags/[slug]/page.tsx
  • apps/www/src/app/[locale]/(tech)/(blog)/tags/page.tsx
  • apps/www/src/app/[locale]/(tech)/contact/opengraph-image.tsx
  • apps/www/src/app/[locale]/(tech)/contact/page.tsx
  • apps/www/src/app/[locale]/(tech)/error.ts
  • apps/www/src/app/[locale]/(tech)/layout.tsx
  • apps/www/src/app/[locale]/(tech)/not-found.ts
  • apps/www/src/app/[locale]/(tech)/page.tsx
  • apps/www/src/app/[locale]/(tech)/projects/error.ts
  • apps/www/src/app/[locale]/(tech)/projects/layout.tsx
  • apps/www/src/app/[locale]/(tech)/projects/loading-fallbacks.tsx
  • apps/www/src/app/[locale]/(tech)/projects/loading.tsx
  • apps/www/src/app/[locale]/(tech)/projects/page.tsx
  • apps/www/src/app/[locale]/@modal/(.)hobby/[kind]/[slug]/page.tsx
  • apps/www/src/app/[locale]/@modal/[...catchAll]/page.ts
  • apps/www/src/app/[locale]/hobby/[kind]/[slug]/page.tsx
  • apps/www/src/app/[locale]/hobby/[kind]/page.tsx
  • apps/www/src/app/[locale]/hobby/layout.tsx
  • apps/www/src/app/[locale]/hobby/page.tsx
  • apps/www/src/app/[locale]/layout.tsx
  • apps/www/src/app/global-not-found.tsx
  • apps/www/src/app/sitemap.ts
  • apps/www/src/components/about/about-me.tsx
  • apps/www/src/components/commons/app-layout.tsx
  • apps/www/src/components/commons/command-menu.tsx
  • apps/www/src/components/commons/copyright.tsx
  • apps/www/src/components/commons/footer.tsx
  • apps/www/src/components/commons/mode-switch.tsx
  • apps/www/src/components/commons/mode-transition.tsx
  • apps/www/src/components/commons/nav-menu.tsx
  • apps/www/src/components/commons/settings.tsx
  • apps/www/src/components/commons/site-shell.tsx
  • apps/www/src/components/commons/tech-frame.tsx
  • apps/www/src/components/hobby/hobby-footer.tsx
  • apps/www/src/components/hobby/hobby-frame.tsx
  • apps/www/src/components/hobby/hobby-index.tsx
  • apps/www/src/components/hobby/hobby-stage.tsx
  • apps/www/src/components/hobby/media-detail.tsx
  • apps/www/src/components/hobby/media-list.tsx
  • apps/www/src/components/hobby/media-modal.tsx
  • apps/www/src/components/hobby/media-pin.tsx
  • apps/www/src/components/hobby/star-rating.tsx
  • apps/www/src/libs/image-source.ts
  • apps/www/src/libs/media.rsc.ts
  • apps/www/src/libs/orpc/client.rsc.ts
  • apps/www/src/libs/palette.ts
  • apps/www/src/shared/media.ts
  • apps/www/src/shared/routes/index.tsx
  • apps/www/src/shared/site-mode.ts
  • apps/www/src/stores/hobby/store.ts
  • apps/www/src/styles/globals.css
  • packages/db/.drizzle/migrations/20261114172203_media_entry/migration.sql
  • packages/db/.drizzle/migrations/20261114172203_media_entry/snapshot.json
  • packages/db/.drizzle/migrations/20261114180924_media_entry_backdrop_color/migration.sql
  • packages/db/.drizzle/migrations/20261114180924_media_entry_backdrop_color/snapshot.json
  • packages/db/.drizzle/migrations/20261114184737_media_entry_slug_play_stats/migration.sql
  • packages/db/.drizzle/migrations/20261114184737_media_entry_slug_play_stats/snapshot.json
  • packages/db/package.json
  • packages/db/src/libs/media/index.ts
  • packages/db/src/libs/validator/media.ts
  • packages/db/src/schemas/media.schema.ts
  • packages/db/src/schemas/relations.ts
  • packages/db/src/schemas/schema.ts
  • packages/db/src/types.ts
  • packages/i18n/www/en-US.json
  • packages/i18n/www/zh-TW.json
  • packages/integrations/AGENTS.md
  • packages/integrations/anilist/index.spec.ts
  • packages/integrations/anilist/index.ts
  • packages/integrations/igdb/env.ts
  • packages/integrations/igdb/index.spec.ts
  • packages/integrations/igdb/index.ts
  • packages/integrations/package.json
  • packages/integrations/s3/s3.service.ts
  • packages/integrations/steam/env.ts
  • packages/integrations/steam/index.spec.ts
  • packages/integrations/steam/index.ts
  • packages/integrations/tmdb/env.ts
  • packages/integrations/tmdb/index.spec.ts
  • packages/integrations/tmdb/index.ts
  • packages/integrations/turbo.json
  • packages/meta/index.ts
  • packages/services/AGENTS.md
  • packages/services/media/__tests__/catalogue.service.test.ts
  • packages/services/media/__tests__/image.service.test.ts
  • packages/services/media/__tests__/media.route.test.ts
  • packages/services/media/catalogue.service.ts
  • packages/services/media/image.service.ts
  • packages/services/media/media.contract.ts
  • packages/services/media/media.route.ts
  • packages/services/media/play-stats.service.ts
  • packages/services/media/slug.ts
  • packages/services/package.json
  • packages/services/router.contract.ts
  • packages/services/router.ts
  • packages/shaders/src/ascii-curtain.tsx
  • packages/shaders/src/ascii-field-layer.tsx
  • packages/shaders/src/ascii-field.tsx
  • packages/shaders/src/reflective-card.tsx
  • packages/themes/AGENTS.md
  • packages/themes/hobby.css
  • packages/themes/package.json
  • packages/utils/src/config/index.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

This branch had an error being deployed

1 failed deployment
Preview — 568b297b Deployed Nov 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant