Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ npm run screenshots:smoke # gating interaction tests (search, Orbit, nav)
| `src/ui/publisher/components/asset-uploader.ts` | Asset uploader component (Phase 3pd image-sequence pipeline) |
| `src/ui/publisher/components/chip-input.ts` | Chip-input control — entries become removable chips as the user types |
| `src/ui/publisher/components/markdown-toolbar.ts` | GitHub-issue-style markdown toolbar over a `<textarea>` |
| `src/ui/publisher/components/topbar.ts` | Glass-surface top bar with section tabs |
| `src/ui/publisher/components/sidebar.ts` | Left-rail sidebar nav — grouped section links (Catalog / Current events / Insights / Settings) + account footer; the publisher app-shell nav (replaced the horizontal topbar in the UI refresh) |
| `src/ui/publisher/components/error-card.ts` | Shared error-card renderer used by every portal page |
| `src/ui/publisher/components/events/match-badge.ts` | Events-tab **Match Badge** primitive — Topic/Time/Geo facet tags + composite %, threshold-toned (`docs/events-tab-handoff/EVENTS_TAB_IMPLEMENTATION_BRIEF.md` §5) |
| `src/ui/publisher/components/events/events-model.ts` | Events-tab wire types + pure helpers (`AUTO_PAIR_THRESHOLD`, `autoPairTargets`, `compositePercent`, `locatorPoint`, `primaryCategory`) shared by the queue/detail components |
Expand Down
4 changes: 4 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,10 @@
"publisher.nav.featuredHero": "Right now",
"publisher.nav.feedback": "Feedback",
"publisher.nav.feeds": "Feeds",
"publisher.nav.group.catalog": "Catalog",
"publisher.nav.group.events": "Current events",
"publisher.nav.group.insights": "Insights",
"publisher.nav.group.settings": "Settings",
"publisher.nav.import": "Import",
"publisher.nav.nodeProfile": "Node profile",
"publisher.nav.profile": "Profile",
Expand Down
2 changes: 1 addition & 1 deletion scripts/screenshots/scenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ async function openCatalog(page: Page): Promise<void> {
*/
async function openPublish(page: Page, path: string): Promise<void> {
await gotoApp(page, path)
await page.locator('#publisher-root .publisher-topbar').waitFor({ state: 'visible' })
await page.locator('#publisher-root .publisher-sidebar').waitFor({ state: 'visible' })
}

/**
Expand Down
14 changes: 7 additions & 7 deletions scripts/screenshots/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ const checks: Check[] = [
fixtures: publisherFixtures({ admin: true }),
async run(page) {
await gotoApp(page, '/publish/datasets')
await page.locator('#publisher-root .publisher-topbar').waitFor({ state: 'visible' })
await page.locator('#publisher-root .publisher-sidebar').waitFor({ state: 'visible' })
await page
.locator('#publisher-root', { hasText: 'Global Sea Surface Temperature' })
.waitFor({ timeout: 15_000 })
Expand All @@ -208,7 +208,7 @@ const checks: Check[] = [
fixtures: publisherFixtures({ admin: true }),
async run(page) {
await gotoApp(page, '/publish/events')
await page.locator('#publisher-root .publisher-topbar').waitFor({ state: 'visible' })
await page.locator('#publisher-root .publisher-sidebar').waitFor({ state: 'visible' })
// Direction A master–detail: the queue (left) lists events; the
// first auto-selects into the detail (right).
await page.locator('.publisher-events-queue-list').first().waitFor({ timeout: 15_000 })
Expand Down Expand Up @@ -250,7 +250,7 @@ const checks: Check[] = [
fixtures: publisherFixtures({ admin: true }),
async run(page) {
await gotoApp(page, '/publish/node-profile')
await page.locator('#publisher-root .publisher-topbar').waitFor({ state: 'visible' })
await page.locator('#publisher-root .publisher-sidebar').waitFor({ state: 'visible' })
const org = page.locator('#nodeprofile-org')
await org.waitFor({ timeout: 15_000 })
assert(
Expand All @@ -270,7 +270,7 @@ const checks: Check[] = [
// Authoring: the editor's three sections mount, with the picker
// enabled once the catalog fixture loads.
await gotoApp(page, '/publish/blog/new')
await page.locator('#publisher-root .publisher-topbar').waitFor({ state: 'visible' })
await page.locator('#publisher-root .publisher-sidebar').waitFor({ state: 'visible' })
await page.locator('#blog-title').waitFor({ timeout: 15_000 })
await page.locator('.publisher-blog-generate-btn').waitFor()
await page.locator('#publisher-root input[type="search"]:not([disabled])').first().waitFor({ timeout: 10_000 })
Expand All @@ -292,7 +292,7 @@ const checks: Check[] = [
fixtures: publisherFixtures({ admin: true }),
async run(page) {
await gotoApp(page, '/publish/feeds')
await page.locator('#publisher-root .publisher-topbar').waitFor({ state: 'visible' })
await page.locator('#publisher-root .publisher-sidebar').waitFor({ state: 'visible' })
// The registered connectors render with state dots + bookkeeping.
await page
.locator('.publisher-feeds-row', { hasText: 'NASA EONET' })
Expand Down Expand Up @@ -331,7 +331,7 @@ const checks: Check[] = [
fixtures: publisherFixtures(),
async run(page) {
await gotoApp(page, '/publish/workflows/new')
await page.locator('#publisher-root .publisher-topbar').waitFor({ state: 'visible' })
await page.locator('#publisher-root .publisher-sidebar').waitFor({ state: 'visible' })

// Pick the curated drought template via the template <select>,
// identified by its option value (the visible label is i18n).
Expand Down Expand Up @@ -369,7 +369,7 @@ const checks: Check[] = [
fixtures: publisherFixtures(),
async run(page) {
await gotoApp(page, '/publish/datasets/01HEXAMPLEDATASET00000001/edit')
await page.locator('#publisher-root .publisher-topbar').waitFor({ state: 'visible' })
await page.locator('#publisher-root .publisher-sidebar').waitFor({ state: 'visible' })
// The thumbnail uploader's globe-thumbnail generator block.
await page
.locator('.publisher-asset-uploader-generate')
Expand Down
197 changes: 153 additions & 44 deletions src/styles/publisher.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@

.publisher-portal {
min-height: 100vh;
/* Two-column app shell: the fixed-width sidebar rail down the
inline-start edge + the scrollable content column. */
display: flex;
align-items: stretch;
background: var(--color-bg, #0a0d14);
color: var(--color-text, #e6e8ec);
font-family: var(--font-family-sans, system-ui, sans-serif);
font-family: var(--font-sans, system-ui, sans-serif);
box-sizing: border-box;
/* Declare the whole portal as a dark color scheme so the browser
renders every native control — <select> dropdown lists, the
Expand All @@ -25,96 +29,199 @@
}

.publisher-content {
flex: 1 1 auto;
min-width: 0;
padding: calc(2rem * var(--ui-scale));
}

.publisher-shell {
max-width: calc(960px * var(--ui-scale));
max-width: calc(1080px * var(--ui-scale));
margin: 0 auto;
}

/* --- Topbar (glass-surface, sticky) --- */
/* --- Sidebar rail (flat, sticky, grouped nav) --- */

.publisher-topbar {
.publisher-sidebar {
position: sticky;
top: 0;
z-index: 100;
align-self: flex-start;
flex-shrink: 0;
box-sizing: border-box;
width: calc(212px * var(--ui-scale));
height: 100vh;
display: flex;
align-items: center;
gap: calc(2rem * var(--ui-scale));
padding: calc(0.75rem * var(--ui-scale)) calc(2rem * var(--ui-scale));
border-radius: 0;
border-block-start: 0;
border-inline-start: 0;
border-inline-end: 0;
flex-direction: column;
gap: var(--space-lg, 0.75rem);
padding: var(--space-lg, 0.75rem) var(--space-md, 0.5rem);
background: var(--color-surface-alt, rgba(255, 255, 255, 0.04));
border-inline-end: 1px solid var(--color-surface-border-subtle, rgba(255, 255, 255, 0.08));
overflow-y: auto;
}

.publisher-topbar-title-block {
.publisher-sidebar-brand {
display: flex;
align-items: center;
gap: calc(0.75rem * var(--ui-scale));
gap: var(--space-sm, 0.4rem);
padding: var(--space-xs, 0.25rem) var(--space-sm, 0.4rem);
}

.publisher-topbar-back {
.publisher-sidebar-back {
color: var(--color-text-muted, #999999);
text-decoration: none;
font-size: calc(1.1rem * var(--ui-scale));
padding: calc(0.25rem * var(--ui-scale)) calc(0.5rem * var(--ui-scale));
border-radius: calc(6px * var(--ui-scale));
font-size: var(--text-xl, 1rem);
line-height: 1;
padding: var(--space-2xs, 0.2rem) var(--space-sm, 0.4rem);
border-radius: var(--radius-md, 6px);
}

.publisher-topbar-back:hover {
background: var(--white-o08, rgba(255, 255, 255, 0.08));
.publisher-sidebar-back:hover {
background: var(--color-surface-hover, rgba(255, 255, 255, 0.12));
color: var(--color-text, #e8eaf0);
}

.publisher-topbar-back:focus-visible {
outline: 2px solid var(--white-o20, rgba(255, 255, 255, 0.2));
.publisher-sidebar-back:focus-visible {
outline: 2px solid var(--color-accent, #4da6ff);
outline-offset: 2px;
}

.publisher-topbar-title {
font-size: calc(0.95rem * var(--ui-scale));
.publisher-sidebar-title {
font-size: var(--text-md, 0.8rem);
font-weight: 600;
letter-spacing: 0.02em;
color: var(--color-text, #e8eaf0);
}

.publisher-nav {
display: flex;
gap: calc(0.25rem * var(--ui-scale));
flex-direction: column;
gap: var(--space-lg, 0.75rem);
flex: 1 1 auto;
}

.publisher-nav-group {
display: flex;
flex-direction: column;
gap: 1px;
}

.publisher-nav-group-label {
font-family: var(--font-mono, ui-monospace, monospace);
font-size: var(--text-2xs, 0.6rem);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--color-text-faint, #666666);
padding: var(--space-xs, 0.25rem) var(--space-sm, 0.4rem);
}

.publisher-nav-link {
color: var(--color-text-muted, #999999);
color: var(--color-text-secondary, #bbbbbb);
text-decoration: none;
font-size: calc(0.875rem * var(--ui-scale));
padding: calc(0.5rem * var(--ui-scale)) calc(0.875rem * var(--ui-scale));
border-radius: calc(6px * var(--ui-scale));
border: 1px solid transparent;
font-size: var(--text-md, 0.8rem);
padding: var(--space-sm, 0.4rem) var(--space-sm, 0.4rem);
border-radius: var(--radius-md, 6px);
border-inline-start: 2px solid transparent;
transition: color 120ms ease, background 120ms ease, border-color 120ms ease;
}

.publisher-nav-link:hover {
color: var(--color-text, #e8eaf0);
background: var(--white-o05, rgba(255, 255, 255, 0.05));
background: var(--color-surface, rgba(255, 255, 255, 0.06));
}

.publisher-nav-link:focus-visible {
outline: 2px solid var(--white-o20, rgba(255, 255, 255, 0.2));
outline: 2px solid var(--color-accent, #4da6ff);
outline-offset: 2px;
}

.publisher-nav-link-active {
color: var(--color-text, #e8eaf0);
background: var(--white-o10, rgba(255, 255, 255, 0.1));
border-color: var(--white-o12, rgba(255, 255, 255, 0.12));
background: var(--accent-o08, rgba(77, 166, 255, 0.08));
border-inline-start-color: var(--color-accent, #4da6ff);
}

/* Account footer — the account link + sign-out, pinned to the rail's
* block-end edge, divided from the grouped nav. */
.publisher-sidebar-footer {
display: flex;
flex-direction: column;
gap: 1px;
padding-block-start: var(--space-md, 0.5rem);
border-block-start: 1px solid var(--color-surface-border-subtle, rgba(255, 255, 255, 0.08));
}

/* Sign out lives at the far right, visually separated from the
* section tabs. The `margin-inline-start: auto` shoves it across
* the flex row, leaving the section nav on the left. */
.publisher-nav-link-signout {
margin-inline-start: auto;
color: var(--color-text-muted, #999999);
}

/* Mobile: the rail can't hold a 212px column beside the content on a
* phone, so stack — the portal becomes a column and the sidebar a
* full-width top strip whose groups lay out in a horizontally
* scrolling row. */
@media (max-width: 768px) {
.publisher-portal {
flex-direction: column;
}

.publisher-sidebar {
position: static;
width: 100%;
height: auto;
flex-direction: row;
align-items: center;
gap: var(--space-md, 0.5rem);
overflow-x: auto;
border-inline-end: 0;
border-block-end: 1px solid var(--color-surface-border-subtle, rgba(255, 255, 255, 0.08));
}

.publisher-sidebar-brand {
flex-shrink: 0;
}

.publisher-nav {
flex-direction: row;
align-items: stretch;
gap: var(--space-md, 0.5rem);
}

/* Group labels are noise in the horizontal strip — the links carry
* the meaning; hide the eyebrows but keep them for screen readers. */
.publisher-nav-group {
flex-direction: row;
align-items: center;
gap: 1px;
}

.publisher-nav-group-label {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}

.publisher-nav-link {
white-space: nowrap;
border-inline-start: 0;
border-block-end: 2px solid transparent;
}

.publisher-nav-link-active {
border-inline-start: 0;
border-block-end-color: var(--color-accent, #4da6ff);
}

.publisher-sidebar-footer {
flex-direction: row;
flex-shrink: 0;
padding-block-start: 0;
padding-inline-start: var(--space-md, 0.5rem);
border-block-start: 0;
border-inline-start: 1px solid var(--color-surface-border-subtle, rgba(255, 255, 255, 0.08));
}
}

/* --- Lifecycle tabs (Drafts / Published / Retracted) --- */
Expand Down Expand Up @@ -1083,14 +1190,16 @@
margin: 0;
}

/* --- Glass-surface card (SPA-matched) --- */
/* --- Surface card (flat app-shell — UI refresh) --- */

/* Formerly a frosted-glass surface (backdrop-filter blur). The
* redesign drops glass everywhere in favour of flat white-alpha
* surfaces. The `publisher-glass` class name is kept as the shared
* hook so the ~30 call sites need no churn; only its look changed. */
.publisher-glass {
background: var(--glass-bg, rgba(13, 13, 18, 0.92));
backdrop-filter: var(--glass-blur, blur(12px));
-webkit-backdrop-filter: var(--glass-blur, blur(12px));
border: 1px solid var(--white-o08, rgba(255, 255, 255, 0.08));
border-radius: calc(12px * var(--ui-scale));
background: var(--color-surface-alt, rgba(255, 255, 255, 0.04));
border: 1px solid var(--color-surface-border-subtle, rgba(255, 255, 255, 0.08));
border-radius: var(--radius-2xl, 12px);
}

.publisher-card {
Expand Down
Loading
Loading