+
+
{
+ if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
+ const tabs = Array.from(
+ (e.currentTarget as HTMLElement).querySelectorAll('[role="tab"]')
+ );
+ const idx = tabs.indexOf(document.activeElement as HTMLElement);
+ if (idx === -1) return;
+ e.preventDefault();
+ const next = e.key === 'ArrowRight'
+ ? (idx + 1) % tabs.length
+ : (idx - 1 + tabs.length) % tabs.length;
+ // Automatic activation: panels are display-toggles (always
+ // mounted), so activating on arrow-focus is free and keeps the
+ // roving tabindex consistent (tabindex derives from activeTab).
+ activeTab = PANE_TABS[next].id;
+ tabs[next].focus();
+ }}
>
- {starredStore.isStarred(item.id) ? '★' : '☆'}
-
-
- {#if collection && (quickActions.length > 0 || isOwner)}
-
- {#key itemSlug}
- {
- editCollectionSection = 'actions';
- editCollectionOpen = true;
+ {#each PANE_TABS as t (t.id)}
+
+
-
-
{
- if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
- const tabs = Array.from(
- (e.currentTarget as HTMLElement).querySelectorAll
('[role="tab"]')
- );
- const idx = tabs.indexOf(document.activeElement as HTMLElement);
- if (idx === -1) return;
- e.preventDefault();
- const next = e.key === 'ArrowRight'
- ? (idx + 1) % tabs.length
- : (idx - 1 + tabs.length) % tabs.length;
- // Automatic activation: panels are display-toggles (always
- // mounted), so activating on arrow-focus is free and keeps the
- // roving tabindex consistent (tabindex derives from activeTab).
- activeTab = PANE_TABS[next].id;
- tabs[next].focus();
- }}
- >
- {#each PANE_TABS as t (t.id)}
-
+
+
+
{
- // Mouse only: touch pointerdown fires on scroll-start, and a
- // drag beginning on a tab must not switch panels (Codex).
- // Mobile has no peeking master, so the click-swallow race
- // this guards against doesn't exist there.
- if (e.pointerType === 'mouse') activeTab = t.id;
- }}
- onclick={() => (activeTab = t.id)}
+ class="action-btn star-btn"
+ class:starred={starredStore.isStarred(item.id)}
+ onclick={() => { if (!item) return; starredStore.toggle(wsSlug, item.slug, item.id); }}
+ title={starredStore.isStarred(item.id) ? 'Unstar' : 'Star'}
>
- {t.label}
+ {starredStore.isStarred(item.id) ? '★' : '☆'}
- {/each}
+
+ {#if collection && (quickActions.length > 0 || isOwner)}
+
+ {#key itemSlug}
+ {
+ editCollectionSection = 'actions';
+ editCollectionOpen = true;
+ }}
+ oncollectionupdated={(updated) => {
+ // Switch-safety note (BUG-2280 — investigated, NOT a live
+ // bug; do NOT add a template-side {@const keyedSlug =
+ // itemSlug} "snapshot" fence here). A quick-action save can
+ // resolve AFTER an A->B switch and fire this callback from a
+ // destroyed {#key} instance, but it's already safe by two
+ // independent layers, so no parent fence is needed:
+ // 1. QuickActionsMenu's OWN child-side guard
+ // (`collection?.id !== baseCollection.id`, captured
+ // pre-await) drops the callback on a CROSS-collection
+ // switch: a destroyed instance's `collection` prop reads
+ // the LIVE parent value (B's collection), not a frozen
+ // A — so the guard fails and oncollectionupdated is
+ // never invoked. (A {@const} snapshot would NOT freeze
+ // in Svelte 5 — it's a lazily-pulled derived that reads
+ // the current itemSlug — so a keyedSlug fence here is a
+ // no-op: the literal BUG-2129 trap. Verified empirically.)
+ // 2. On a SAME-collection switch the callback DOES fire,
+ // but `updated` is that same collection, so assigning it
+ // is correct; and loadData's collection write
+ // (`collGen === collectionGen || collection?.id !==
+ // collData.id`) forces the right collection regardless
+ // of the collectionGen bump below.
+ // Bump the unified fence so an in-flight stale load/refresh
+ // can't revert this fresh write (Codex).
+ collectionGen++;
+ collection = updated;
+ }}
+ />
+ {/key}
+ {/if}
+ {#if childTotal > 0}
+
+ jumpToSection('relationships', 'item-children')}
+ >
+ 🌳 {childDone}/{childTotal}
+
+ {/if}
+ {#if backlinksCount > 0}
+
+ jumpToSection('relationships', 'item-backlinks')}
+ >
+ 📎 {backlinksCount}
+
+ {/if}
+
+
+
@@ -5515,12 +5544,80 @@
color: var(--text-muted);
}
+ /* ── Tab strip (PLAN-2326 / TASK-2328) ────────────────────────────────
+ One band: the `.pane-tabs` tablist plus the right-aligned
+ `.strip-actions` group, which before TASK-2328 was its own
+ `.meta-actions` row above. The divider that used to belong to
+ `.pane-tabs` lives here now so it runs the full width of the strip
+ rather than stopping where the tabs do.
+
+ `container-type: inline-size` makes this the query container for
+ TASK-2329's tier rule. It has to be here rather than on an ancestor:
+ the docked pane's width is user-dragged and persisted, decoupled from
+ the viewport, so a media query cannot see it (DR-2).
+
+ Deliberately NO `overflow`, `contain`, `clip-path`, `transform`,
+ `filter` or `will-change` on this element — it is an ancestor of both
+ `.menu-anchor` and QuickActionsMenu's anchor, and any of those would
+ clip the anchored panels they position. DR-9's scroll rule goes on
+ `.pane-tabs`, which is their SIBLING. (`container-type` itself is
+ safe: it applies layout/style/inline-size containment but not PAINT
+ containment, so the panels still escape — DR-3, re-verified in
+ Chromium against this markup, including the mobile BottomSheet's
+ `position: fixed` overlay, which still resolves against the
+ viewport.) */
+ .tab-strip {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ container-type: inline-size;
+ container-name: item-tab-strip;
+ border-bottom: 1px solid var(--border-subtle);
+ margin: var(--space-4) 0;
+ }
+
/* ── Pane tabs (PLAN-2290 Phase 4) ────────────────────────────────── */
.pane-tabs {
display: flex;
gap: 2px;
- border-bottom: 1px solid var(--border-subtle);
- margin: var(--space-4) 0 var(--space-4);
+ /* DR-9 width allocation: `.strip-actions` never shrinks, so the tab
+ list scrolls horizontally instead of wrapping to a second row or
+ crushing the actions. `min-width: 0` is what lets a flex item
+ shrink below its content width at all. */
+ min-width: 0;
+ overflow-x: auto;
+ scrollbar-width: none;
+ /* `overflow-x: auto` computes `overflow-y` to `auto` too, which would
+ clip `.pane-tab`'s `margin-bottom: -1px` overlap and leave the
+ active tab with a 1px accent sitting on 1px of divider instead of a
+ solid 2px underline (measured). Extending the padding box by the
+ same 1px keeps the overlap inside the scrollport; the negative
+ margin pulls the outer box back so the strip's height and the
+ divider's position are unchanged. */
+ padding-bottom: 1px;
+ margin-bottom: -1px;
+ }
+
+ .pane-tabs::-webkit-scrollbar {
+ display: none;
+ }
+
+ /* `margin-left: auto` right-aligns the group however few tabs there are;
+ `flex: 0 0 auto` is DR-9's other half — the actions hold their size and
+ the tab list gives way. */
+ .strip-actions {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ margin-left: auto;
+ flex: 0 0 auto;
+ }
+
+ /* `.action-btn`'s 70px floor exists for the standalone graph-drawer
+ controls, which are text buttons. In the strip these are icon-sized, so
+ the floor is overridden HERE rather than removed from the base rule. */
+ .strip-actions .action-btn {
+ min-width: 0;
}
.pane-tab {
@@ -5557,7 +5654,14 @@
.tab-hidden {
display: block !important;
}
- .pane-tabs {
+ /* All three named explicitly. Before TASK-2328 this rule hid
+ `.pane-tabs` and the print block further down hid `.meta-actions`;
+ the new `.tab-strip` wrapper inherits NEITHER, so hiding it is not
+ free (DR-5). Its children are listed alongside it so the strip
+ stays hidden even if the wrapper is later restyled. */
+ .tab-strip,
+ .pane-tabs,
+ .strip-actions {
display: none !important;
}
}
@@ -6421,14 +6525,9 @@
border-top: 1px solid var(--border);
}
- /* History */
- .meta-actions {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- margin-bottom: var(--space-6);
- flex-wrap: wrap;
- }
+ /* History. `.action-btn` is generic — the graph-drawer controls below use
+ it too, which is why `.strip-actions` overrides `min-width` rather than
+ this rule dropping it. */
.action-btn {
padding: var(--space-1) var(--space-3);
min-width: 70px;
@@ -6563,10 +6662,12 @@
padding: 0;
}
- /* Hide interactive / screen-only chrome inside the item page. */
+ /* Hide interactive / screen-only chrome inside the item page.
+ `.meta-actions` is gone (TASK-2328) — its contents now print-hide
+ with `.tab-strip` / `.strip-actions` in the tab-strip print block
+ above. */
.sticky-header,
.pane-header,
- .meta-actions,
.editor-mode-toggle,
.add-relationship-section,
.save-status,