Skip to content

feat: scatter XP desktop download icons on work pages - #420

Draft
dmnktoe wants to merge 4 commits into
mainfrom
cursor/work-page-desktop-downloads-a902
Draft

dmnktoe wants to merge 4 commits into
mainfrom
cursor/work-page-desktop-downloads-a902

Conversation

@dmnktoe

@dmnktoe dmnktoe commented Sep 4, 2026

Copy link
Copy Markdown
Owner

⇝pull request

*ੈ✩‧₊˚༺☆༻*ੈ✩‧₊˚

what & why

Work pages can now carry a downloads list (name + url). When any files are set, they render as draggable Windows XP–style desktop icons scattered across the viewport. Double-click (or Enter) starts the download.

scope

@httpjpg/ui, @httpjpg/storyblok-ui, @httpjpg/storyblok-sync, @httpjpg/storyblok-utils, @httpjpg/portfolio, @httpjpg/storybook

change type

  • feat — new feature · Added · minor

screenshots / recordings

Storybook: Widgets / DesktopDownloads. Placeholder glyphs until real 32×32 XP icons land in DESKTOP_ICON_SRC.

Scattered XP-style download icons on a desktop-blue stage

Same icons clamped inside a mobile viewport


checklist — every pull request
  • Follows the conventions in CLAUDE.md — I read a neighbouring file before inventing a pattern
  • Scoped: no drive-by refactors bundled with the feature
  • Server Components by default; any new "use client" boundary is as small as possible
  • Styling uses design tokens from @httpjpg/tokens via Panda (no raw hex outside genuinely off-palette decoration)
  • Dependency direction respected (tokens/env stay leaves; ui never imports back from consumers)
  • Tests added or updated next to the source (*.test.ts(x)); E2E updated if a user-facing flow changed
  • No console.log in shipped paths; errors reported through @httpjpg/observability
  • Did not hand-edit the root package.json version, .release-please-manifest.json or CHANGELOG.md
storyblok / cms — only if a blok or schema moved
  • Schema added or updated in packages/storyblok-sync/scripts/blocks/* (download_item nestable + work.downloads)
  • sync:components still needs to run against Storyblok (Management API) so editors get the field
  • No extra SbDownloadItem page component — same pattern as badge_item: the parent (SbPageWork) maps the nestable into UI. Not registered in apps/portfolio/lib/storyblok.ts
  • Cache tags / revalidation unchanged (downloads ride along on the work story)

notes for reviewers

  • Positions are seeded from each item _uid, not Math.random(), so SSR/Storybook/Argos stay stable.
  • The overlay is position: fixed with pointer-events: none on the layer and auto on each icon, so the page still scrolls and clicks through.
  • File-kind placeholders (pdf / zip / image / video / audio / document / file) live in DesktopDownloadPlaceholder. Swap real assets later via DESKTOP_ICON_SRC (pixelated rendering is already on).
  • Download names are added to the search-index allowlist (name) so “Press kit.pdf” is findable.
  • After merge: pnpm --filter @httpjpg/storyblok-sync sync:components so the CMS field exists for editors.

Verified in Storybook: scatter + extension glyphs, single-click select, click-outside deselect, pointer-drag to pixel coords, double-click opens the file URL, icons stay inside a 390px viewport.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Work stories can list downloads by name and URL. When a work page has
files, they render as draggable Windows XP-style desktop icons; double-click
or Enter starts the download. Placeholder glyphs follow the file extension
until real icons are wired through DESKTOP_ICON_SRC.
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 31.39kB (0.18%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
portfolio-server-cjs 11.06MB 10.2kB (0.09%) ⬆️
portfolio-client-array-push 3.42MB 21.19kB (0.62%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: portfolio-client-array-push

Assets Changed:

Asset Name Size Change Total Size Change (%)
static/chunks/7223-*.js (New) 190.42kB 190.42kB 100.0% 🚀
static/css/*.css 408 bytes 123.75kB 0.33%
server/app/_global-*.js 812 bytes 97.36kB 0.84%
server/app/_not-*.js 812 bytes 97.36kB 0.84%
server/app/(portfolio)/[...slug]/page_client-*.js 812 bytes 97.3kB 0.84%
server/app/page_client-*.js 812 bytes 97.27kB 0.84%
server/app/(portfolio)/cookie-*.js 668 bytes 88.66kB 0.76%
server/app/api/psn-*.js 804 bytes 63.94kB 1.27%
server/app/api/letterboxd/route_client-*.js 802 bytes 63.77kB 1.27%
server/app/api/cloudflare/route_client-*.js 802 bytes 63.77kB 1.27%
server/app/work/feed.xml/route_client-*.js 801 bytes 63.69kB 1.27%
server/app/api/discord/route_client-*.js 799 bytes 63.52kB 1.27%
server/app/api/discogs/route_client-*.js 799 bytes 63.52kB 1.27%
server/app/api/weather/route_client-*.js 799 bytes 63.52kB 1.27%
server/app/api/status/route_client-*.js 797 bytes 63.36kB 1.27%
server/app/api/x/route_client-*.js 793 bytes 63.03kB 1.27%
static/chunks/app/layout-*.js 226 bytes 47.45kB 0.48%
server/app/api/og/[...slug]/route_client-*.js 616 bytes 43.74kB 1.43%
static/chunks/app/(portfolio)/[...slug]/page-*.js 40 bytes 11.42kB 0.35%
static/chunks/app/api/cloudflare/route-*.js 40 bytes 3.48kB 1.16%
static/chunks/app/api/discogs/route-*.js 40 bytes 3.48kB 1.16%
static/chunks/app/api/discord/route-*.js 40 bytes 3.48kB 1.16%
static/chunks/app/api/letterboxd/route-*.js 40 bytes 3.48kB 1.16%
static/chunks/app/api/status/route-*.js 40 bytes 3.48kB 1.16%
static/chunks/app/api/weather/route-*.js 40 bytes 3.48kB 1.16%
static/chunks/app/api/x/route-*.js 40 bytes 3.48kB 1.16%
static/chunks/app/work/feed.xml/route-*.js 40 bytes 3.48kB 1.16%
static/chunks/app/api/psn-*.js 40 bytes 3.48kB 1.16%
static/chunks/app/(portfolio)/cookie-*.js 42 bytes 3.3kB 1.29%
static/chunks/app/(portfolio)/[...slug]/not-*.js 42 bytes 3.12kB 1.36%
static/chunks/app/not-*.js 42 bytes 3.12kB 1.36%
static/chunks/app/(portfolio)/loading-*.js 42 bytes 3.12kB 1.36%
static/chunks/app/loading-*.js 42 bytes 3.12kB 1.36%
static/chunks/app/api/og/[...slug]/route-*.js 42 bytes 3.12kB 1.36%
static/T3DlONFn-*.js (New) 1.5kB 1.5kB 100.0% 🚀
static/T3DlONFn-*.js (New) 77 bytes 77 bytes 100.0% 🚀
server/middleware-*.js 347 bytes 842 bytes 70.1% ⚠️
server/middleware-*.js -347 bytes 495 bytes -41.21%
static/chunks/1343.*.js (New) 681 bytes 681 bytes 100.0% 🚀
static/chunks/6807.*.js (New) 681 bytes 681 bytes 100.0% 🚀
static/chunks/9378-*.js (Deleted) -182.24kB 0 bytes -100.0% 🗑️
static/sFSdLuN4TFQ8DKJwNDPqs/_buildManifest.js (Deleted) -1.5kB 0 bytes -100.0% 🗑️
static/chunks/3218.*.js (Deleted) -681 bytes 0 bytes -100.0% 🗑️
static/chunks/8586.*.js (Deleted) -681 bytes 0 bytes -100.0% 🗑️
static/sFSdLuN4TFQ8DKJwNDPqs/_ssgManifest.js (Deleted) -77 bytes 0 bytes -100.0% 🗑️

App Routes Affected:

App Route Size Change Total Size Change (%)
/ 226 bytes 78.36kB 0.29%
view changes for bundle: portfolio-server-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
7321.js (New) 429.17kB 429.17kB 100.0% 🚀
9229.js (New) 241.26kB 241.26kB 100.0% 🚀
../app/(portfolio)/[...slug]/page.js 89 bytes 160.42kB 0.06%
3772.js (New) 74.51kB 74.51kB 100.0% 🚀
6040.js 186 bytes 66.84kB 0.28%
2728.js 2 bytes 52.37kB 0.0%
../app/_global-error/page.js 1 bytes 22.61kB 0.0%
../app/(portfolio)/cookie-policy/page.js 81 bytes 22.03kB 0.37%
../app/api/ask/route.js 7 bytes 20.56kB 0.03%
../app/api/og/[...slug]/route.js 1 bytes 17.89kB 0.01%
../app/api/search/route.js 7 bytes 16.41kB 0.04%
../app/work/feed.xml/route.js 1 bytes 12.46kB 0.01%
../app/api/cloudflare/route.js 1 bytes 11.61kB 0.01%
../app/api/discord/route.js 1 bytes 11.03kB 0.01%
../app/api/weather/route.js 1 bytes 10.54kB 0.01%
../app/api/psn-trophies/route.js 1 bytes 9.75kB 0.01%
../app/api/x/route.js 1 bytes 9.55kB 0.01%
../app/api/letterboxd/route.js 1 bytes 9.54kB 0.01%
../app/api/discogs/route.js 1 bytes 9.51kB 0.01%
../app/api/status/route.js 1 bytes 9.24kB 0.01%
../app/page.js 1 bytes 7.33kB 0.01%
6451.js (New) 6.64kB 6.64kB 100.0% 🚀
9275.js (New) 6.28kB 6.28kB 100.0% 🚀
../app/_not-found/page.js 1 bytes 4.07kB 0.02%
../app/(portfolio)/[...slug]/page.js.nft.json 3 bytes 1.26kB 0.24%
../app/api/discogs/route.js.nft.json 3 bytes 1.24kB 0.24%
../app/api/discord/route.js.nft.json 3 bytes 1.24kB 0.24%
../app/api/letterboxd/route.js.nft.json 3 bytes 1.24kB 0.24%
../app/api/psn-trophies/route.js.nft.json 3 bytes 1.24kB 0.24%
../app/api/status/route.js.nft.json 3 bytes 1.24kB 0.24%
../app/api/weather/route.js.nft.json 3 bytes 1.24kB 0.24%
../app/api/x/route.js.nft.json 3 bytes 1.24kB 0.24%
../app/api/cloudflare/route.js.nft.json 3 bytes 1.24kB 0.24%
../app/work/feed.xml/route.js.nft.json 3 bytes 1.16kB 0.26%
../app/api/og/[...slug]/route.js.nft.json 1 bytes 977 bytes 0.1%
../app/page.js.nft.json 3 bytes 854 bytes 0.35%
../app/(portfolio)/cookie-policy/page.js.nft.json 3 bytes 776 bytes 0.39%
5771.js (New) 616 bytes 616 bytes 100.0% 🚀
2392.js (New) 616 bytes 616 bytes 100.0% 🚀
../app/_not-found/page.js.nft.json 3 bytes 546 bytes 0.55%
../app/_global-error/page.js.nft.json 1 bytes 201 bytes 0.5%
463.js (Deleted) -429.17kB 0 bytes -100.0% 🗑️
6524.js (Deleted) -231.92kB 0 bytes -100.0% 🗑️
4493.js (Deleted) -74.24kB 0 bytes -100.0% 🗑️
5696.js (Deleted) -6.56kB 0 bytes -100.0% 🗑️
1775.js (Deleted) -6.2kB 0 bytes -100.0% 🗑️
718.js (Deleted) -614 bytes 0 bytes -100.0% 🗑️
853.js (Deleted) -614 bytes 0 bytes -100.0% 🗑️

App Routes Affected:

App Route Size Change Total Size Change (%)
/api/search 7 bytes 16.41kB 0.04%
/api/weather 5 bytes 262.27kB 0.0%
/api/discogs 5 bytes 261.24kB 0.0%
/api/og/[...slug] 1 bytes 17.89kB 0.01%
/work/feed.xml 5 bytes 264.19kB 0.0%
/api/cloudflare 5 bytes 263.34kB 0.0%
/api/psn-trophies 5 bytes 261.48kB 0.0%
/(portfolio)/[...slug] 93 bytes 412.15kB 0.02%
/api/x 5 bytes 261.28kB 0.0%
/api/ask 7 bytes 20.56kB 0.03%
/(portfolio) 174 bytes 434.19kB 0.04%
/api/letterboxd 5 bytes 261.27kB 0.0%
/ 272 bytes 356.18kB 0.08%
/api/status 5 bytes 260.97kB 0.0%
/(portfolio)/cookie-policy 85 bytes 273.76kB 0.03%
/api/discord 5 bytes 262.76kB 0.0%

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.35%. Comparing base (5fad6f3) to head (5d3253a).
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #420      +/-   ##
==========================================
+ Coverage   97.29%   97.35%   +0.05%     
==========================================
  Files         356      360       +4     
  Lines        9109     9312     +203     
  Branches     3091     3142      +51     
==========================================
+ Hits         8863     9066     +203     
  Misses        238      238              
  Partials        8        8              
Components Coverage Δ
ui 97.60% <100.00%> (+0.15%) ⬆️
storyblok 96.83% <100.00%> (+0.02%) ⬆️
integrations 93.73% <ø> (ø)
foundations 92.10% <ø> (ø)
portfolio 98.15% <ø> (ø)
Files with missing lines Coverage Δ
apps/portfolio/lib/search/story-text.ts 100.00% <ø> (ø)
packages/storyblok-sync/scripts/blocks/pages.ts 100.00% <ø> (ø)
...oryblok-ui/src/components/page-work/SbPageWork.tsx 100.00% <100.00%> (ø)
...onents/desktop-downloads/desktop-download-icon.tsx 100.00% <100.00%> (ø)
...desktop-downloads/desktop-download-placeholder.tsx 100.00% <100.00%> (ø)
...components/desktop-downloads/desktop-downloads.tsx 100.00% <100.00%> (ø)
...ackages/ui/src/components/desktop-downloads/lib.ts 100.00% <100.00%> (ø)

A fieldset legend leaked a Work downloads chip onto the overlay. The
layer is now a labelled section so the icons stay the only chrome.
Drop the unreachable null-ref guard in favour of currentTarget, and add
cases for a foreign pointer id, pointer cancel, kind overrides, and a
scheme that URL cannot parse.
Icons already set data-draggable; mount CustomCursor in the XP stage so
hover gets the same 👋 animation as the Spotify widget.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants