diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.css | 113 | ||||
| -rw-r--r-- | src/features/workspaces/ui/HomeHeader.svelte | 69 | ||||
| -rw-r--r-- | src/features/workspaces/ui/HomeHeader.test.ts | 5 |
3 files changed, 152 insertions, 35 deletions
diff --git a/src/app.css b/src/app.css index 438dbb5..3f3f8ae 100644 --- a/src/app.css +++ b/src/app.css @@ -66,6 +66,119 @@ } } +/* Navbar buttons — the navbar's (`.app-navbar`) raised button, in the + same depth language as the rest of the UI: at rest they're RAISED (a + gradient a step lighter than the bar, a light highlight inside the top edge, + a thin dark edge + small drop below, light primary-blue text), and when + PRESSED they flip to the inset well (dark inner shadow) — pushed into the + bar. `.navbar-btn-primary` is the main action: the same button, tinted with + the theme's primary colour. `.navbar-btn-icon` is the square icon-only form. + Self-contained (not DaisyUI .btn) so nothing fights the gradients; colours + all come from the theme. */ +@layer components { + .navbar-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + height: 2rem; + padding-inline: 0.75rem; + border-radius: var(--radius-field); + font-size: 0.875rem; + font-weight: 600; + line-height: 1; + white-space: nowrap; + cursor: pointer; + user-select: none; + /* The theme's primary, lifted toward white: crisp on the grey gradient + (plain primary read dim, and an engraved text-shadow muddied it at this + size — the shadow stays on the big titles only). */ + color: color-mix(in srgb, var(--color-primary) 75%, white); + border: 1px solid rgb(0 0 0 / 0.55); + background-image: linear-gradient( + to bottom, + color-mix(in srgb, var(--color-base-100) 72%, var(--color-base-content)), + color-mix(in srgb, var(--color-base-100) 90%, var(--color-base-content)) + ); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.12), + 0 1px 2px rgb(0 0 0 / 0.45); + transition: + filter 120ms ease-out, + box-shadow 120ms ease-out; + } + .navbar-btn:hover { + filter: brightness(1.12); + } + .navbar-btn:active, + .navbar-btn[aria-expanded="true"] { + /* Pressed → the inset well: pushed into the bar. */ + filter: none; + background-image: none; + background-color: color-mix(in srgb, var(--color-base-100) 82%, black); + box-shadow: + inset 0 1px 5px rgb(0 0 0 / 0.7), + 0 1px 0 color-mix(in srgb, var(--color-base-content) 10%, transparent); + } + .navbar-btn:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; + } + .navbar-btn-icon { + width: 2rem; + padding-inline: 0; + } + /* Secondary navbar actions: visually just text — no background, border or + shadow. The light primary-blue colour (same as the buttons' text) and the + spacing mark them as clickable; hover brightens + underlines, press dims. */ + .navbar-link { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + height: 2rem; + min-width: 2rem; /* icon-only (compact) links keep a comfortable tap target */ + padding-inline: 0.375rem; + border-radius: var(--radius-field); + font-size: 0.875rem; + font-weight: 600; + line-height: 1; + white-space: nowrap; + cursor: pointer; + user-select: none; + background: none; + border: 0; + color: color-mix(in srgb, var(--color-primary) 75%, white); + text-underline-offset: 4px; + transition: color 120ms ease-out; + } + .navbar-link:hover { + color: color-mix(in srgb, var(--color-primary) 45%, white); + text-decoration: underline; + } + .navbar-link:active { + color: var(--color-primary); + } + .navbar-link:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; + } + .navbar-btn-primary { + color: var(--color-primary-content); + background-image: linear-gradient( + to bottom, + color-mix(in srgb, var(--color-primary) 82%, white), + color-mix(in srgb, var(--color-primary) 88%, black) + ); + box-shadow: + inset 0 1px 0 rgb(255 255 255 / 0.35), + 0 1px 2px rgb(0 0 0 / 0.45); + } + .navbar-btn-primary:active { + background-color: color-mix(in srgb, var(--color-primary) 80%, black); + } +} + /* Raised — the INVERSE of `.inset-well`, for things that pop OUT of the page (the home page's workspace tiles). Where the well has a dark shadow INSIDE under its top edge and a light catch-line OUTSIDE below it, a raised surface diff --git a/src/features/workspaces/ui/HomeHeader.svelte b/src/features/workspaces/ui/HomeHeader.svelte index 8090489..5329b2d 100644 --- a/src/features/workspaces/ui/HomeHeader.svelte +++ b/src/features/workspaces/ui/HomeHeader.svelte @@ -1,4 +1,8 @@ <script lang="ts"> + import ArchiveIcon from "phosphor-svelte/lib/ArchiveIcon"; + import DotsThreeIcon from "phosphor-svelte/lib/DotsThreeIcon"; + import PlusIcon from "phosphor-svelte/lib/PlusIcon"; + import PuzzlePieceIcon from "phosphor-svelte/lib/PuzzlePieceIcon"; import { isValidSlug } from "../logic/route"; /** The create outcome the page reports back to the popover. */ @@ -78,68 +82,65 @@ </span> </div> - <div class="flex items-center gap-2" class:gap-1={compact}> + <!-- Navbar actions (app.css): "New workspace" is the one raised button + (`.navbar-btn`, primary-tinted); the secondary actions are plain text + links (`.navbar-link`) — the light-blue colour + spacing say clickable + without button chrome. Compact: labels collapse to Phosphor icons. --> + <div class="flex items-center gap-1" class:gap-3={!compact}> <!-- New workspace --> <button type="button" - class="btn btn-primary btn-sm" - class:btn-circle={compact} + class="navbar-btn navbar-btn-primary" + class:navbar-btn-icon={compact} aria-label="New workspace" title="New workspace" onclick={() => (popoverOpen ? closePopover() : openPopover())} > - {#if compact} - <span aria-hidden="true" class="text-lg leading-none">+</span> - {:else} - New workspace - {/if} + <PlusIcon size={16} weight="bold" aria-hidden="true" /> + {#if !compact}<span>New workspace</span>{/if} </button> {#if withPlugins} <button type="button" - class="btn btn-outline btn-sm" - class:btn-circle={compact} + class="navbar-link" aria-label="Plugins" title="Open the plugin manager (load/unload plugins globally)" onclick={onOpenPlugins} > - {#if compact} - <span aria-hidden="true">⚡</span> - {:else} - Plugins - {/if} + {#if compact}<PuzzlePieceIcon size={18} aria-hidden="true" />{:else}<span>Plugins</span>{/if} </button> {/if} + <!-- Archived workspaces --> + <button + type="button" + class="navbar-link" + aria-label="Archived workspaces" + title="View archived workspaces" + onclick={onOpenArchived} + > + {#if compact}<ArchiveIcon size={18} aria-hidden="true" />{:else}<span>Archived</span>{/if} + </button> + {#if withSort} + <!-- Overflow last, like a navbar's trailing menu. --> <div class="dropdown dropdown-end"> - <button type="button" class="btn btn-ghost btn-sm" aria-label="More actions" title="More"> - ⋯ + <button + type="button" + class="navbar-link" + aria-label="More actions" + title="More" + > + <DotsThreeIcon size={20} weight="bold" aria-hidden="true" /> </button> - <ul class="dropdown-content menu z-50 w-52 rounded-box border border-base-content/15 bg-base-100 p-2 shadow-xl"> + <ul class="dropdown-content menu z-50 mt-1 w-52 rounded-box border border-base-content/15 bg-base-100 p-2 shadow-xl"> <li> <button type="button" onclick={onSort}>Reset arrangement by recent activity</button> </li> </ul> </div> {/if} - - <!-- Archived workspaces --> - <button - type="button" - class="btn btn-ghost btn-sm" - class:btn-circle={compact} - aria-label="Archived workspaces" - title="View archived workspaces" - onclick={onOpenArchived} - > - {#if compact} - <span aria-hidden="true">🗄</span> - {:else} - Archived - {/if} - </button> </div> </header> diff --git a/src/features/workspaces/ui/HomeHeader.test.ts b/src/features/workspaces/ui/HomeHeader.test.ts index 40b060f..4db8b04 100644 --- a/src/features/workspaces/ui/HomeHeader.test.ts +++ b/src/features/workspaces/ui/HomeHeader.test.ts @@ -86,6 +86,9 @@ describe("HomeHeader", () => { it("compact mode collapses the primary button to an icon", () => { const p = props(); render(HomeHeader, { props: { ...p, compact: true } }); - expect(screen.getByRole("button", { name: "New workspace" })).toHaveTextContent("+"); + const button = screen.getByRole("button", { name: "New workspace" }); + expect(button).toHaveClass("navbar-btn-icon"); + expect(button).toHaveTextContent(""); // icon only — the label collapses + expect(button.querySelector("svg")).not.toBeNull(); }); }); |
