mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 10:03:29 +00:00
feat(layout): in-app Resources menu in user dropdown (TASK-905) (#316)
* feat(layout): in-app Resources menu in user dropdown (TASK-905) New UserMenuResources component adds a Resources block to the user-menu dropdown in TopBar, closing the product → marketing handoff seam. Logged-in users now have a clear path back out to Docs / Changelog / GitHub / Status / Support without having to remember getpad.dev URLs or visit the marketing site separately. Cloud-mode (cloudMode=true) shows: Docs / Changelog / GitHub / Status / Support. Replaces the prior inline Support/Status pair — that block became a special case of this unified Resources component. Self-hosted (cloudMode=false) shows the trimmed Docs / GitHub set. Changelog / Status are Cloud-specific surfaces; getpad.dev's support@getpad.dev mailbox isn't the operator's to direct people to. The Docs link still points at getpad.dev because that's the canonical project documentation regardless of deployment shape. Component is wired into BOTH the desktop and mobile branches of TopBar (the existing dropdown duplication). All links open in a new tab so a user mid-task doesn't lose state. Each entry has a small external-link icon so the off-property nature is visible without the user having to hover-and-read the title. The `:global(.user-dropdown)` selectors keep the new styles scoped to the existing dropdown surface in TopBar without forcing a CSS refactor of that component. Visual contract: docs/brand.md §6/§7. Companion to AuthHeader, AuthFooter, and +error.svelte from PLAN-900. Test plan: - web/npm run check — 0 errors (694 files, +1 new component) - web/npm run build — clean - Svelte autofixer — clean * fix(layout): UserMenuResources mirrors dropdown-item styles per Codex (round 2) Codex caught that .dropdown-item and .dropdown-divider rules in TopBar.svelte's <style> are scoped to that component — Svelte's scoped CSS attaches a per-component hash so the rules don't apply to DOM rendered by UserMenuResources.svelte (a separate component). The new resource links lost the dropdown padding/color/text-decoration/ hover styling, and the divider rendered as an unstyled empty 1px row. Mirror the base .dropdown-item / .dropdown-divider / .dropdown-item:hover rules inside UserMenuResources using :global(.user-dropdown) qualifiers so the dropdown surface remains the styling boundary — the rules apply to anything dropped into the menu but never leak outside it. Same scoping pattern that already worked for .resources-label and .external-icon in this component, just extended to the base classes. * fix(layout): respect canonical link order from brand spec per Codex (round 3) Codex caught that UserMenuResources rendered links in the order Docs / Changelog / GitHub / Status / Support, but docs/brand.md §7 defines a canonical relative order with GitHub before Docs and Changelog. The whole point of the brand spec is one canonical order across surfaces; violating it in the user menu undermines that. Reorder Cloud to GitHub / Docs / Changelog / Status / Support, and self-hosted to GitHub / Docs. Status and Support are user-menu-specific additions that don't appear in the marketing footer; they land at the end so the brand-spec subset stays in canonical position at the front.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import PadLogo from '$lib/components/layout/PadLogo.svelte';
|
||||
import WorkspaceSwitcher from '$lib/components/layout/WorkspaceSwitcher.svelte';
|
||||
import UserMenuResources from '$lib/components/layout/UserMenuResources.svelte';
|
||||
import ConnectWorkspaceModal from '$lib/components/ConnectWorkspaceModal.svelte';
|
||||
import { workspaceRestoreTarget } from '$lib/utils/workspace-route';
|
||||
|
||||
@@ -855,32 +856,22 @@
|
||||
<button class="dropdown-item" onclick={toggleTheme}>
|
||||
{currentTheme === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||
</button>
|
||||
{#if authStore.cloudMode}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a
|
||||
href="mailto:support@getpad.dev"
|
||||
class="dropdown-item"
|
||||
onclick={closeUserMenu}
|
||||
>
|
||||
Support
|
||||
</a>
|
||||
<a
|
||||
href="https://status.getpad.dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="dropdown-item"
|
||||
onclick={closeUserMenu}
|
||||
>
|
||||
Status
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
"Connect a project…" sits after the cloud-mode Support/Status
|
||||
block (when present) and just above the Sign-out divider —
|
||||
it's a CLI-onboarding action, semantically closer to
|
||||
Settings/Support than to account actions, but visually we
|
||||
want it adjacent to the divider so it reads as a discrete
|
||||
action rather than another link.
|
||||
Resources block (TASK-905). Replaces the prior inline Cloud
|
||||
Support/Status pair — that block became a special case of
|
||||
the unified Resources component, which adds Docs, Changelog,
|
||||
and GitHub on Cloud and a trimmed Docs/GitHub list on
|
||||
self-hosted. Closes the product → marketing handoff seam.
|
||||
-->
|
||||
<UserMenuResources cloudMode={authStore.cloudMode} onclose={closeUserMenu} />
|
||||
|
||||
<!--
|
||||
"Connect a project…" sits after the Resources block and just
|
||||
above the Sign-out divider — it's a CLI-onboarding action,
|
||||
semantically closer to Settings/Resources than to account
|
||||
actions, but visually we want it adjacent to the divider so
|
||||
it reads as a discrete action rather than another link.
|
||||
-->
|
||||
{#if workspaceStore.current?.slug}
|
||||
<button
|
||||
@@ -976,25 +967,10 @@
|
||||
<button class="dropdown-item" onclick={toggleTheme}>
|
||||
{currentTheme === 'dark' ? 'Light mode' : 'Dark mode'}
|
||||
</button>
|
||||
{#if authStore.cloudMode}
|
||||
<div class="dropdown-divider"></div>
|
||||
<a
|
||||
href="mailto:support@getpad.dev"
|
||||
class="dropdown-item"
|
||||
onclick={closeUserMenu}
|
||||
>
|
||||
Support
|
||||
</a>
|
||||
<a
|
||||
href="https://status.getpad.dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="dropdown-item"
|
||||
onclick={closeUserMenu}
|
||||
>
|
||||
Status
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<!-- Resources — see desktop branch for placement rationale. -->
|
||||
<UserMenuResources cloudMode={authStore.cloudMode} onclose={closeUserMenu} />
|
||||
|
||||
<!-- Connect a project — see desktop branch for placement rationale. -->
|
||||
{#if workspaceStore.current?.slug}
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<script lang="ts">
|
||||
// Resources block rendered inside the user-menu dropdown in TopBar.svelte
|
||||
// (desktop and mobile branches both consume this).
|
||||
//
|
||||
// Closes the product → marketing handoff seam: a logged-in user looking
|
||||
// for Docs / Changelog / GitHub / Status / Support has no obvious path
|
||||
// from inside the app today. This block sits at the bottom of the user
|
||||
// menu (just above "Connect a project…" and "Sign out") and gives them
|
||||
// a quiet escape hatch back out to the surrounding ecosystem.
|
||||
//
|
||||
// Cloud (cloudMode=true): Docs / Changelog / GitHub / Status / Support
|
||||
// Self-hosted (cloudMode=false): Docs / GitHub only — Changelog and
|
||||
// Status are Cloud-specific surfaces, and getpad.dev's support@getpad.dev
|
||||
// mailbox is not the operator's to direct people to. The Docs link still
|
||||
// points at getpad.dev because that's the canonical project documentation
|
||||
// even for self-hosted deployments.
|
||||
//
|
||||
// All links open in a new tab so a user mid-task doesn't lose state.
|
||||
// Replaces the prior inline Cloud-only Support/Status block in
|
||||
// TopBar.svelte; that block became a special case of this one.
|
||||
//
|
||||
// Visual contract: docs/brand.md §6 (link-list canonical order) and §7
|
||||
// (external-link convention). Companion to AuthHeader/AuthFooter/+error
|
||||
// from PLAN-900.
|
||||
|
||||
let {
|
||||
cloudMode = false,
|
||||
onclose
|
||||
}: {
|
||||
cloudMode?: boolean;
|
||||
// Caller hands in the close-the-dropdown handler (typically
|
||||
// `closeUserMenu` from TopBar). Triggered on every link click so
|
||||
// the menu collapses without waiting for the link to navigate.
|
||||
onclose?: () => void;
|
||||
} = $props();
|
||||
|
||||
type ResourceLink = {
|
||||
label: string;
|
||||
href: string;
|
||||
// Most links open in a new tab. The Support entry on Cloud uses a
|
||||
// mailto:, which doesn't navigate the browser but still benefits
|
||||
// from `noopener` semantics; we keep target="_blank" for parity
|
||||
// and to surface the action as "off-property".
|
||||
};
|
||||
|
||||
// Order follows the canonical link order from docs/brand.md §7: GitHub,
|
||||
// Docs, Changelog, then anything else. Status and Support are user-menu-
|
||||
// specific additions (the brand-spec footer doesn't carry them) so they
|
||||
// land at the end. Keeping the same relative order as the marketing
|
||||
// footer means a user visiting both surfaces sees the same linear pattern
|
||||
// — small cohesion win that Codex flagged on first review.
|
||||
const cloudLinks: ResourceLink[] = [
|
||||
{ label: 'GitHub', href: 'https://github.com/PerpetualSoftware/pad' },
|
||||
{ label: 'Docs', href: 'https://getpad.dev/docs' },
|
||||
{ label: 'Changelog', href: 'https://getpad.dev/changelog' },
|
||||
{ label: 'Status', href: 'https://status.getpad.dev' },
|
||||
{ label: 'Support', href: 'mailto:support@getpad.dev' }
|
||||
];
|
||||
|
||||
// Self-hosted is a subset that preserves the relative canonical order —
|
||||
// GitHub before Docs. Changelog/Status/Support are Cloud-only surfaces
|
||||
// (operators have their own changelog/status if any; getpad.dev's
|
||||
// support@ mailbox is not theirs to direct people to).
|
||||
const selfHostedLinks: ResourceLink[] = [
|
||||
{ label: 'GitHub', href: 'https://github.com/PerpetualSoftware/pad' },
|
||||
{ label: 'Docs', href: 'https://getpad.dev/docs' }
|
||||
];
|
||||
|
||||
const links = $derived(cloudMode ? cloudLinks : selfHostedLinks);
|
||||
</script>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<div class="resources-label">Resources</div>
|
||||
{#each links as link (link.label)}
|
||||
<a
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="dropdown-item resources-item"
|
||||
onclick={() => onclose?.()}
|
||||
>
|
||||
<span>{link.label}</span>
|
||||
<svg
|
||||
class="external-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||
<polyline points="15 3 21 3 21 9" />
|
||||
<line x1="10" y1="14" x2="21" y2="3" />
|
||||
</svg>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
/* Svelte's scoped CSS hashes class selectors so styles only apply to
|
||||
DOM produced by the same component. The base .dropdown-divider and
|
||||
.dropdown-item rules live in TopBar.svelte and would NOT apply to
|
||||
the elements rendered here without explicit :global() opt-in. We
|
||||
mirror the parent's rules under `:global(.user-dropdown)` so the
|
||||
dropdown surface stays the styling boundary — the rules apply to
|
||||
anything dropped into the menu, but never leak outside it.
|
||||
Codex review (TASK-905, round 1) caught the missing styling. */
|
||||
:global(.user-dropdown) :global(.dropdown-divider) {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
:global(.user-dropdown) :global(.dropdown-item) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: 0.85em;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: background 0.1s, color 0.1s;
|
||||
}
|
||||
|
||||
:global(.user-dropdown) :global(.dropdown-item):hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
:global(.user-dropdown) .resources-label {
|
||||
padding: var(--space-2) var(--space-4) var(--space-1);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
:global(.user-dropdown) .resources-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
:global(.user-dropdown) .external-icon {
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
transition: opacity 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
:global(.user-dropdown) .resources-item:hover .external-icon,
|
||||
:global(.user-dropdown) .resources-item:focus-visible .external-icon {
|
||||
opacity: 1;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user