- Add transfer dialog with workspace selector and email input
- Support moving projects between workspaces (instant) and
transferring to another user via notification (async)
- Add project transfer notification type with accept/decline flow
- Verify sender ownership and auto-decline stale transfers
- Add audit logging for ownership changes
- Add canTransfer permission for owners and admins
- Use camelCase notification types (workspaceInvite, projectTransfer)
- Use .select("id").single() on all notification status updates
- Debounce email search with stale response rejection
- Use Set-based loading state for concurrent notification actions
- Filter unknown notification types before rendering
- Use theme tokens for all spacing values
When a workspace owner's plan no longer supports workspaces (maxWorkspaces <= 1),
member access is revoked while preserving workspace data and memberships:
- Add getOwnerPlanFeatures callback to AppContext for cross-user plan lookups
- Check workspace owner plan in hasProjectPermit to deny non-owner access
- Mark member workspaces as isDowngraded in findMany based on owner plan
- Gate updateWorkspaceRelation behind maxWorkspaces check
- Block downgraded workspace members in builder route loader
- Show ProBadge "Suspended" and disable selection for downgraded workspaces
- Replace workspaces feature flag with maxWorkspaces-based gating
- Default maxWorkspaces to 0 (controlled via MAX_WORKSPACES env var)
- Skip workspace queries in dashboard loader when maxWorkspaces is 0
- Extract pure functions (resolveCurrentWorkspace, isWorkspaceRelationPermitted, getView) with tests
- Remove workspaces feature flag from all files
- Show user name next to profile avatar in dashboard sidebar
- Separate welcome view from templates into its own component
- Welcome subtitle with create blank project button (permission-gated)
- Allow PROJECT_TEMPLATES to skip marketplace approval check
- Templates view shows clean "Starter templates" title
## Description
1. What is this PR about (link the issue and add a short description)
## Steps for reproduction
1. click button
2. expect xyz
## Code Review
- [ ] hi @kof, I need you to do
- conceptual review (architecture, feature-correctness)
- detailed review (read every line)
- test it on preview
## Before requesting a review
- [ ] made a self-review
- [ ] added inline comments where things may be not obvious (the "why",
not "what")
## Before merging
- [ ] tested locally and on preview environment (preview dev login:
0000)
- [ ] updated [test
cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md)
document
- [ ] added tests
- [ ] if any new env variables are added, added them to `.env` file
- Add new story variants across ~100 files covering previously untested props
- Fix all story titles to use sentence case per UI label guidelines
- Consolidate duplicate card, thumbnail, and value-text stories into parent files
- Add interactive/controlled variants for checkbox, radio, switch, select, etc.
- Add style source, disabled, icon, placeholder, unit options variants to CssValueInput
- Add sub-menu, radio group, destructive, and disabled variants to dropdown/context menus
- Add dialog resize, maximize, actions, and non-draggable variants
- Add popover title, actions, and side placement variants
- Add tooltip side placements and content variant stories
- Fix shouldHideDot axis mapping: x always maps to justify-content
(main-axis), y always maps to align-items (cross-axis). The visual
transposition for column direction is handled by the CSS grid swap
in the render code, not in the dot-hiding logic.
- Export shouldHideDot and cover with 21 unit tests.
- Refactor stories into Flex_ and Grid_ combination grids showing
all alignItems x justifyContent combinations.
- Delete old flex-alignment.stories.tsx (used hooks, could not work).
- Rewrite shouldHideDot as cross-axis × main-axis intersection check
- Use visibility:hidden instead of conditional rendering to preserve layout
- Consolidate stories into FlexRow/FlexColumn combination grids
- Include normal/default values in story combinations
- Use real style panel dimensions (64×62)
- Skip loader/optimization for data: src in getImageAttributes
- Replace external picsum.photos URL with inline SVG data URI in social-preview story
- Remove unnecessary ThumbnailWrapper from thumbnail stories
- Rename dialog title and dropdown item from "Manage members" to "Members"
- Remove icons from workspace selector dropdown menu items
- Remove separator before Delete item
- Set withIndicator={false} to fix indentation without icons
- Rename userRelation → workspaceRelation across schema, DB, router, and UI
- Rename MemberRelation type → WorkspaceRelation everywhere
- Create canonical UserPlanFeatures/UserPurchase module in trpc-interface
- Make userPlanFeatures non-optional in AppContext
- Separate purchases from UserPlanFeatures
- Compute workspaceRelation in route loaders instead of consumer sites
- Add $workspaceRelation atom, use real relation in $permissions computed
- Remove permissions prop-drilling from all dashboard components
- Leaf components use useStore($permissions) directly
- Create setSharedStores() for unified store initialization
- Combine workspace + membership into single PostgREST query in builder route
- Replace purchases.length checks with canDownloadAssets/canRestoreBackups
- getUserPlanFeatures() → getUserPlanInfo() returning { userPlanFeatures, purchases }
- Add unified getPermissions() combining role + plan permissions
- Gate project actions (create, delete, publish, share, etc.) by workspace role
- Introduce MemberRelation schema (owners/admins/builders/viewers)
- Compute $permissions from $userPlanFeatures + $authPermit in nano-states
- Set $userPlanFeatures globally in DashboardSetup (matching builder pattern)
- Add workspace dialogs for managing members and invitations
- Enforce permissions across dashboard, builder, and settings UI
- Extract softDeleteProject helper so unique domain invariant lives in one place
- Projects created in a workspace are owned by workspace owner, not creator
- Remove unnecessary useEffect around $data.set
- Remove unused parseTrpcError helper
- Remove unsafe `as TokenAuthPermit` cast
- Add sortWorkspaces tests
- Document idempotent retry safety in workspace remove()
- Backend: accept deleteProjects flag to soft-delete all projects before removing workspace
- Frontend: warning text mentions all projects will be deleted, with 'All projects' in red
- Dashboard: detect stale workspaceId in URL and redirect to default workspace
- Pass userId to WorkspaceSelector for ownership checks
- Disable Rename/Delete menu items for non-owners
- Hide invite input and remove buttons for non-owners in ManageMembersDialog
- Add empty state for member list
- Add Avatar with small size variant (data-size attribute) to workspace selector
- Use userId prop instead of isOwner boolean for future role extensibility
- Add workspace dialogs: create, rename, manage members, delete
- Rewrite workspace selector as dropdown menu with action items
- Support comma-separated multi-email member invites
- Create placeholder users for non-existing email invites
- Add ScrollAreaNative component with native thin scrollbar
- Show newest members first in member list
- Preserve workspaceId query param in navigation links
- Fix DashboardSetup to use useEffect for data sync
Add/remove/list workspace members by email.
- addMember: owner-only, looks up user by email, inserts as
administrator, rejects duplicates and self-add
- removeMember: owner-only, prevents removing the owner
- listMembers: visible to owner and members, returns user email/username
tRPC procedures: workspace.addMember, workspace.removeMember,
workspace.listMembers.