feat(spaces): move Space-Switcher into the PillNav start slot

Repositions the switcher from its floating spot in the top right of
the workbench into the bottom-fixed PillNav so it sits with the rest
of the nav chrome. Matches how every other persistent nav control
(app switcher, AI tier, sync status) lives in the PillNav.

Mechanics:
- @mana/shared-ui PillNavigation gains a `startSlot?: Snippet` prop
  rendered inside .pill-nav-container, before AppDrawer. Generic slot
  — any host component drops in.
- (app)/+layout.svelte passes the existing <SpaceSwitcher /> as the
  snippet (authenticated only). The old .space-bar wrapper above
  <main> is removed along with its CSS.
- SpaceSwitcher trigger is restyled to match Pill conventions: pill
  radius 999px, 32px height, 0.8125rem text, tighter paddings, shorter
  name cap (7rem). Visually merges with the surrounding Pills.
- Dropdown menu flips upward (bottom: calc(100% + 4px)) because the
  PillNav is position:fixed bottom — opening downward would land
  off-screen.

Type-check: 0 errors across 7200 files.
Scope tests: 10/10 pass.
Go tests + bun tests (mana-auth): all pass.

Plan: docs/plans/spaces-foundation.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-20 20:54:41 +02:00
parent 1d3794f96c
commit fabd45bd87
4 changed files with 38 additions and 20 deletions

View file

@ -174,15 +174,15 @@
.trigger {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.625rem;
border-radius: var(--radius-md, 6px);
background: var(--color-surface-2, transparent);
gap: 0.375rem;
padding: 0.25rem 0.5rem;
border-radius: 999px;
background: transparent;
border: 1px solid var(--color-border, hsl(0 0% 88%));
color: var(--color-text, inherit);
font-size: 0.875rem;
font-size: 0.8125rem;
cursor: pointer;
min-width: 8rem;
height: 32px;
transition: background-color 120ms ease;
}
@ -195,7 +195,7 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 10rem;
max-width: 7rem;
}
.chev {
@ -236,7 +236,9 @@
.dropdown {
position: absolute;
top: calc(100% + 4px);
/* Open upward because the switcher sits inside the bottom-fixed
PillNav — a downward dropdown would land off-screen. */
bottom: calc(100% + 4px);
left: 0;
min-width: 14rem;
background: var(--color-surface-1, white);

View file

@ -981,7 +981,13 @@
{spotlightActions}
{contentSearcher}
positioning="static"
/>
>
{#snippet startSlot()}
{#if authStore.isAuthenticated}
<SpaceSwitcher locale={$locale === 'en' ? 'en' : 'de'} />
{/if}
{/snippet}
</PillNavigation>
</div>
{/if}
@ -1000,11 +1006,6 @@
class="pt-2"
>
<div class="mx-auto max-w-7xl px-3 py-2 sm:px-6 sm:py-3 lg:px-8">
{#if authStore.isAuthenticated}
<div class="space-bar">
<SpaceSwitcher locale={$locale === 'en' ? 'en' : 'de'} />
</div>
{/if}
{#if routeBlocked && routeAppId}
<RouteTierGate
appName={routeAppId.name}
@ -1054,12 +1055,6 @@
<ToastContainer />
<style>
.space-bar {
display: flex;
justify-content: flex-end;
margin-bottom: 0.5rem;
}
.bottom-stack {
position: fixed;
bottom: 0;

View file

@ -269,6 +269,12 @@
prependElements?: PillNavElement[];
/** Additional elements (tab groups, dividers) to show after nav items */
elements?: PillNavElement[];
/**
* Snippet rendered at the very start of the bar, before the app
* switcher. Lets the host drop a custom component (e.g. Space
* switcher) into the nav without adding more dedicated props.
*/
startSlot?: import('svelte').Snippet;
/** Show logout button */
showLogout?: boolean;
/** Theme variant dropdown items */
@ -355,6 +361,7 @@
primaryColor,
prependElements = [],
elements = [],
startSlot,
showLogout = true,
themeVariantItems = [],
currentThemeVariantLabel = 'Theme',
@ -608,6 +615,12 @@
aria-label={ariaLabel}
>
<div class="pill-nav-container">
<!-- Host-provided start slot (e.g. Space switcher). Rendered
before the app drawer so it anchors the left edge of the bar. -->
{#if startSlot}
{@render startSlot()}
{/if}
<!-- App Switcher (optional) -->
{#if showAppSwitcher && appItems.length > 0}
<AppDrawer

View file

@ -202,6 +202,14 @@ export interface PillNavigationProps {
prependElements?: PillNavElement[];
/** Additional elements to show after nav items (tab groups, dividers) */
elements?: PillNavElement[];
/**
* Snippet rendered at the very start of the pill bar, before the app
* switcher and any prepend elements. Lets the host drop an arbitrary
* Svelte component (e.g. a Space switcher) into the bar without
* adding another dedicated prop surface. Keep the rendered content
* short it's first-in-line real estate.
*/
startSlot?: Snippet;
/** Bottom offset from viewport bottom (default: '0px'). Use to position above other fixed bars. */
bottomOffset?: string;
}