Mockups Library / Design v3
26 AUG 2026 · SUPERSEDES V2 (21 AUG) · NOTHING IMPLEMENTED

Where your
mockups live.

Likes, your own photos, and folders you make yourself — one place on the dashboard, built out of components that already exist. This deck picks the layout and the data model so the build has nothing left to invent.

Recommendation
Option A — header tabs, folders as cards

The surface

Browse and Library become two route-driven tabs in the header, exactly the AI Studio pattern. Library is a grid of folder cards, exactly the Projects pattern.

The data

A new MockupFolder with membership entries, because catalog mockups are not yours to move. Uploaded mockups already sit in UploadFolder.

The gesture

Add to folder rides on the multi-select toolbar that already ships. No drag and drop in v1.

What broke

Prototype, 21 Aug

Not a taste dispute. The prototype wrote 577 lines of MockupsLibraryView.tsx and imported four things from the design system: Button, Dialog, Typography, Menu. It never touched Tabs, never touched EmptyState, never touched LayoutHeaderTabs. Every problem below follows from that one fact.

01

A pill switch where tabs belong

Browse / Library was hand-rolled as a full-width segmented control. The dashboard already has a route-driven tab system, centred in the header, and AI Studio uses it for exactly this job.

Fix → register a mockups tab set in LayoutHeaderTabs/configs.ts
02

"Only you" under every row

Visibility was rendered as a grey caption below each rail item, so the rail read as four labels and two links. Visibility is a property of a folder, not a paragraph about it.

Fix → lock / people icon on the card, tooltip carries the words
03

One sentence where a screen belongs

The zero state was a centred line of bold text in an otherwise blank column. The kit ships an EmptyState module with illustration, description and an action slot.

Fix → never be empty — see the three variants below
Also stale: the prototype branched off main on 21 Aug, three days before Vasili's #14389 moved search into the taxonomy row. Anything rebuilt on top of it starts by rebasing past that, or the search argument gets re-litigated for no reason.

The component contract

Two kits exist. They are not interchangeable.

Dashboard & website — @repo/ui-kit

Everything in apps/website and packages/features/*. Composed through @repo/feature-dashboard for layout-level pieces, @repo/ui-icons for icons, @repo/resources for query hooks.

This work is entirely here.

Extensions — @kittl/kit

Only for extensions/ in the sandboxed-extensions repo. Different package, different design language, different release train.

Nothing in this document touches it. Custom Mockups the extension is a separate product from custom mockups on the dashboard — same words, different thing.

JobUsePrecedentDo not
Top-level tabstabSet on LoggedInUserLayout → LayoutHeaderTabs → Tabs.Root size="m" underlineai-studio/layout.tsx:41hand-rolled segmented switch
Zero stateEmptyState — @repo/ui-kit/components/modules/EmptyStateAiArtFeed.tsx:280a centred sentence
Create affordancedashed create tile, always in the gridNewFolderCard.tsx, NewProjectCard.tsxa button hidden in a rail
Two-state toggleSegmentedSwitch — @repo/ui-kit/components/core/SwitchPlanTiersIntervalSelection.tsx:31for route navigation
Filter chipsexisting MockupsCategoryNavigation rowMockupsToolbar.tsx:85a second parallel filter bar
Bulk actionsMockupsSelectionToolbar (already ships)app/mockups/components/per-card overflow menus only
Query hooks@repo/resources/src/queries/…queries/folders/useCreateFolderMutation.ts@repo/api-client (rules ban it)
Correction to v2: the v2 spec said query hooks go in @repo/api-react. Folders, the closest precedent in the repo, put them in @repo/resources, and .rules/coding-standards.md:18 only bans packages/api-client. Follow folders.

Four ways the surface could work

Click the tabs and folders — the sketches are live.

A

Header tabs, folders as cards

Library is a place you navigate to, shaped like Projects.

Recommended
Library
Your likes, your photos, and folders you make
Likes
24 mockups
Your mockups
3 photos
Summer tees
12 mockups
+New folder
try Browse · Summer tees

For

  • Same mental model as Projects — folder cards, dashed create tile.
  • The zero state solves itself: Likes and Your mockups are always there.
  • Folders are linkable places with covers and counts.
  • Header tabs are a solved, route-driven pattern.

Against

  • Two clicks to a mockup inside a folder.
  • Folder covers need the batch resolver or they are N+1.
  • Header tabs suppress the page title on desktop.
Build: tabSet entry · /mockups/library + [folderId] routes · folder card · reuse MockupsGrid, selection toolbar, EmptyState
B

Header tabs, sidebar rail

What the prototype meant to be, done properly.

Alternative
Likes
Only you can see these
visibility as one subline, not a caption per row

For

  • One click to any folder, context always visible.
  • Scales past a dozen folders without paging.
  • Counts sit naturally beside names.

Against

  • A nav level the dashboard uses nowhere else.
  • Eats 130 px the mockup grid wants.
  • Empty rail on day one is the exact screen that failed review.
  • Needs a shell extraction before the routes can exist.
Build: A, plus a persistent rail component and the shared-shell refactor of MockupsDashboardContent:544
C

No second tab — filter in place

Library becomes a filter on the browse feed.

Cheapest
Mockups
AllLikedYoursFolders ▾Search
Liked
one surface, one control row, no new routes

For

  • Smallest possible change; no shell work, no new tab set.
  • Search, taxonomy and library share one row.
  • Nothing to onboard into — the filter explains itself.

Against

  • Folders become a dropdown, not a place — no cover, no count, no link.
  • No room for an onboarding moment.
  • Collides with the taxonomy chips already in that row.
  • Weakest version of the organise story.
Build: one filter control + feed switch. No routes, no layout change.
D

Library as a drawer

Organise where you discover, without leaving the grid.

Highest risk
Mockups
Library
AllT-ShirtHoodieSearch
T-Shirt
Library
Likes
Summer tees
+New
drop 2 selected into a folder without navigating

For

  • Collecting happens at the moment of discovery.
  • Matches the editor panel model, so the two surfaces rhyme.
  • Multi-select into a folder becomes obvious.

Against

  • Drag and drop is a large build with real a11y cost.
  • Drawer and grid fight for width on 1280.
  • Not linkable — you cannot send someone a folder.
  • Hardest of the four to demo convincingly.
Build: drawer shell + dnd layer + keyboard equivalents. The only option that needs new interaction primitives.
The synthesis worth arguing for: take A for the place and steal D's gesture without its cost. Multi-select already exists in MockupsSelectionToolbar; adding one "Add to folder" action there gives the collect-while-browsing moment with no drawer and no drag and drop.

The screen that has to teach

Three treatments, in the order they should be reached for.

Projects has no designed empty state at all — it renders a dashed New tile into the grid and lets that carry the meaning. That is the precedent worth copying, and it is why option A's library root can never be blank.

1 · Never empty

Library root. Likes, Your mockups and the dashed New folder tile always render, so the model is legible before anything exists. No empty state needed.

Likes
Nothing liked yet
Your mockups
Add a photo
+New folder

2 · EmptyState module

Inside a folder that really is empty, and inside Likes before anything is liked. Illustration, one instruction, one action — the kit component, not a sentence.

Library / Summer tees
Nothing in this folder yet
Pick mockups while browsing and add them here. Select several at once to add them together.
Browse mockups

3 · First-run strip

Shown above the grid on a first visit only, dismissible, gone forever after. Three steps because collecting genuinely is a sequence — like, group, apply. This is the one place numbering earns itself.

Like what you'd reuseTap the heart on any mockup while browsing.
Group them into foldersSelect several, then add them to a folder.
Apply a folder to a designPut one design on every mockup in the folder at once.
Likes
24 mockups
Your mockups
3 photos
Summer tees
12 mockups
+New folder
Cut from the prototype: the full-bleed upload banner and the "Use your own photo" tile at the head of the first lane. The tile competed with the mockups it sat beside and promised something that opens a dialog and stops. Your own photos get a home in the Library, and nothing above the fold until the core works.

Folders, for real

The part that has to exist before any of the above is more than a sketch.

Two populations, one folder

A mockup on the dashboard is one of two completely different things, and this is the fact the whole model turns on.

Catalog mockupsYour own photos
IdentityMockupTemplateId = number, CMS-backedUpload cuid, UploadType.MOCKUP
Database rownone — there is no Mockup tableUpload
Owned by the usernoyes
Already folderednoyes — a reserved "Mockups" UploadFolder
Can be "moved"no — you cannot move what you don't ownyes
So folders cannot be containment. Folder and UploadFolder both work by a single-valued FK on the child — a design is in one folder. A catalog mockup has no row to carry that FK, and belongs in as many folders as you like. The model has to be membership, with a source-qualified reference. Call it folders in the UI; build it as membership underneath.

Schema

Conventions copied verbatim from UploadFolder: cuid id, workspace FK authoritative, creator recorded, soft delete, denormalized children timestamp, unique name per workspace.

// apps/api/prisma/schema/editor/mockup.prisma  (new file)

model MockupFolder {
  /// @zod.cuid()
  id               String              @id @default(cuid())
  name             String
  user             User                @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId           String
  workspaceId      String
  workspace        Workspace           @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
  entries          MockupFolderEntry[]
  createdAt        DateTime            @default(now())
  deletedAt        DateTime?
  entriesUpdatedAt DateTime            @default(now())
  sortOrder        Int                 @default(0)

  @@unique([workspaceId, name], name: "workspaceIdMockupFolderName")
  @@index([workspaceId])
}

model MockupFolderEntry {
  /// @zod.cuid()
  id          String       @id @default(cuid())
  folder      MockupFolder @relation(fields: [folderId], references: [id], onDelete: Cascade)
  folderId    String
  source      MockupSource                // the load-bearing column
  referenceId String                      // CMS numeric id as string, or Upload cuid
  sortOrder   Int          @default(0)
  placement   Json?                       // normalized intent, nullable, P5
  createdAt   DateTime     @default(now())

  @@unique([folderId, source, referenceId], name: "folderSourceReference")
  @@index([folderId])
}

enum MockupSource { CATALOG  UPLOAD }

Endpoints

Express router → controller → service, mounted beside /folders, following folders.router.ts exactly. Typed client regenerated with pnpm generate:api-client; hooks land in @repo/resources/src/queries/mockupFolders/.

RouteDoes
GET /mockups/foldersList for the workspace, excluding soft-deleted. Returns counts and up to four cover references per folder.
POST /mockups/folders/createCreate. Unique name per workspace, same 409 handling as folders.
PATCH /mockups/folders/:idRename, reorder.
DELETE /mockups/folders/:idSoft delete via deletedAt.
POST /mockups/folders/:id/restoreUndo, because soft delete without restore is just a bug.
POST /mockups/folders/:id/entriesBatch add [{source, referenceId}]. Idempotent on the unique key.
DELETE /mockups/folders/:id/entriesBatch remove.
POST /mockups/resolveThe one genuinely new thing. Hydrate many mixed-source mockups in one call.
Why /mockups/resolve is not optional. There is no batch endpoint today: useMockupById is one GET /api/mockups/{id} per card, and the Likes page already pays that — listBookmarks runs Promise.all and calls ElementsService.getMockup once per bookmark. A 15-entry folder plus four covers on each of six folder cards is 39 CMS round trips for one screen. Measured usage says a realistic folder is about 15 entries, so this is a real page, not a hypothetical.

Likes stay where they are

Liking is Bookmark with BookmarkType.MOCKUP, per user, already shipped server-side. It does not move into folders and it is not a folder — it is the flat list you collect from. One catch: createBookmark validates the id against the CMS and throws 404 otherwise, with an integration test asserting exactly that. Liking your own uploaded photo therefore needs a server change.

Recommendation: don't. In v1 likes are catalog-only. Your own photos already have a home — the Your mockups tile — and they do not need a second one. This reverses v2 and matches what the editor spec already does.

Order of work

P0
Server: folders, entries, batch resolveInert without callers, so it can land early and alone.
new
P1
Heart on the dashboard cardPR 13913 already does this. Rebase it behind 14322 and add guest suppression.
exists — do not rebuild
P2
Library tab, folder grid, folder routetabSet entry, two routes, folder card, EmptyState. Option A.
reuses MockupsGrid
P3
Add to folder in the selection toolbarThe collect gesture. One action on a toolbar that already ships.
small
P4
Your mockups tileReads existing Upload rows of type MOCKUP. Convert-to-mockup feeds it.
depends on editor track
P5
Placement per entryNormalized intent on the entry row. Needs a live placement callback that does not exist yet.
blocked

What the room has to settle

Five questions. Everything else follows from them.

1

Personal or shared?

Workspace FK is the house convention for both Folder and UploadFolder — which means teammates see your folders by default. Likes are per-user either way.

RecommendWorkspace-scoped. It matches every other folder in the product, and a per-user variant later is a filter, not a migration.
2

Folders or Sets?

v2 chose "Sets" because POD sellers buy mockup sets on Etsy. The dashboard already says Folders everywhere else.

RecommendFolders on the dashboard. One vocabulary per surface beats one vocabulary per market. Sets can be what we call a bundle we sell.
3

Header tabs, or tabs under the header?

Header-centre tabs suppress the desktop page title, exactly as AI Studio does. Below-header-left keeps the title.

RecommendHeader centre. It is the pattern you pointed at, and Mockups is a destination, not a settings page.
4

Can you like your own photo?

Needs a server change to createBookmark and a source-qualified referenceId, plus a test that currently asserts the opposite.

RecommendNot in v1. Your mockups is already the place they live.
5

Does a folder remember placement?

The idea that makes folders more than a bookmark list — apply a folder to a design and every mockup keeps its framing. It is also the only part with no working mechanism today.

RecommendColumn now, feature later. Ship placement Json? nullable in P0 so P5 is not a migration, and demo it as the direction.