Skip to content

Commit 52acb4b

Browse files
committed
Polish + performance updates
1 parent 3eb486a commit 52acb4b

9 files changed

Lines changed: 193 additions & 40 deletions

File tree

manifest.config.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,25 @@ export default defineManifest({
1212
'48': 'icons/icon48.png',
1313
'128': 'icons/icon128.png',
1414
},
15-
permissions: ['storage'],
15+
// WithHostAccess (not declarativeNetRequest) so install only shows Plato
16+
// host access — no "Block content on any page you visit" warning.
17+
permissions: ['storage', 'declarativeNetRequestWithHostAccess'],
1618
host_permissions: [
1719
'http://plato.stanford.edu/*',
1820
'https://plato.stanford.edu/*',
1921
],
22+
// Drop unused Font Awesome CSS (native chrome that used it is hidden).
23+
// Network-layer block beats the preload scanner; same-origin only so we
24+
// don't need Google Fonts host permissions or the broader DNR warning.
25+
declarative_net_request: {
26+
rule_resources: [
27+
{
28+
id: 'block-unused-fonts',
29+
enabled: true,
30+
path: 'src/rules/block-unused-fonts.json',
31+
},
32+
],
33+
},
2034
content_scripts: [
2135
{
2236
matches: [

src/content/palette/Palette.tsx

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type PaletteProps = {
2121
open: boolean;
2222
onClose: () => void;
2323
anchorRef: RefObject<HTMLElement | null>;
24+
/** Collapsed sidebar: center the palette instead of morphing from the slot. */
25+
collapsed: boolean;
2426
dark: boolean;
2527
};
2628

@@ -118,10 +120,36 @@ const EXIT_MS = 150;
118120
const PROTRUDE_PX = 60;
119121
/** Sidebar content padding-right; included so width clears the sidebar edge. */
120122
const SIDEBAR_PAD_RIGHT = 20;
123+
/** Centered (collapsed-sidebar) palette width and viewport insets. */
124+
const CENTERED_WIDTH = 560;
125+
const CENTERED_MARGIN = 16;
126+
/** Vertical placement as a fraction of viewport height (upper-center). */
127+
const CENTERED_TOP_RATIO = 0.22;
121128
const CLOSED_HEIGHT = 40;
122129
/** Wait out rapid ↑/↓ before kicking off a prefetch. */
123130
const PREFETCH_DEBOUNCE_MS = 75;
124131

132+
type PaletteLayout = {
133+
top: number;
134+
left: number;
135+
width: number;
136+
};
137+
138+
function getCenteredLayout(
139+
viewportWidth = window.innerWidth,
140+
viewportHeight = window.innerHeight
141+
): PaletteLayout {
142+
const width = Math.min(
143+
CENTERED_WIDTH,
144+
Math.max(0, viewportWidth - CENTERED_MARGIN * 2)
145+
);
146+
return {
147+
width,
148+
left: Math.max(CENTERED_MARGIN, Math.round((viewportWidth - width) / 2)),
149+
top: Math.round(viewportHeight * CENTERED_TOP_RATIO),
150+
};
151+
}
152+
125153
function getPaletteMount(): HTMLElement | null {
126154
const layer = document.getElementById('sep-plus-palette-layer');
127155
return layer?.shadowRoot?.getElementById('sep-plus-palette-mount') ?? null;
@@ -143,7 +171,13 @@ function measureAnchor(
143171
};
144172
}
145173

146-
export function Palette({ open, onClose, anchorRef, dark }: PaletteProps) {
174+
export function Palette({
175+
open,
176+
onClose,
177+
anchorRef,
178+
collapsed,
179+
dark,
180+
}: PaletteProps) {
147181
const inputRef = useRef<HTMLInputElement>(null);
148182
const listRef = useRef<HTMLUListElement>(null);
149183
const [query, setQuery] = useState('');
@@ -157,14 +191,25 @@ export function Palette({ open, onClose, anchorRef, dark }: PaletteProps) {
157191
const [mounted, setMounted] = useState(open);
158192
const [expanded, setExpanded] = useState(false);
159193
const [anchor, setAnchor] = useState<AnchorRect | null>(null);
194+
const [viewport, setViewport] = useState(() => ({
195+
width: window.innerWidth,
196+
height: window.innerHeight,
197+
}));
160198

161199
const closing = mounted && !open;
200+
// Layout mode is captured when opening so a sidebar toggle mid-session
201+
// (or during close) doesn't jump between centered and anchored geometry.
202+
const collapsedRef = useRef(collapsed);
203+
collapsedRef.current = collapsed;
204+
const [centered, setCentered] = useState(collapsed);
162205

163206
useEffect(() => {
164207
if (!open) {
165208
return;
166209
}
210+
setCentered(collapsedRef.current);
167211
setRelated(collectRelatedEntries());
212+
setViewport({ width: window.innerWidth, height: window.innerHeight });
168213
const next = measureAnchor(anchorRef);
169214
setAnchor(next);
170215
setMounted(true);
@@ -208,6 +253,7 @@ export function Palette({ open, onClose, anchorRef, dark }: PaletteProps) {
208253
}
209254

210255
function onResize(): void {
256+
setViewport({ width: window.innerWidth, height: window.innerHeight });
211257
const next = measureAnchor(anchorRef);
212258
if (next) {
213259
setAnchor(next);
@@ -393,9 +439,15 @@ export function Palette({ open, onClose, anchorRef, dark }: PaletteProps) {
393439
return null;
394440
}
395441

396-
const width = expanded
397-
? anchor.width + SIDEBAR_PAD_RIGHT + PROTRUDE_PX
398-
: anchor.width;
442+
const layout: PaletteLayout = centered
443+
? getCenteredLayout(viewport.width, viewport.height)
444+
: {
445+
top: anchor.top,
446+
left: anchor.left,
447+
width: expanded
448+
? anchor.width + SIDEBAR_PAD_RIGHT + PROTRUDE_PX
449+
: anchor.width,
450+
};
399451

400452
return createPortal(
401453
<div
@@ -423,6 +475,7 @@ export function Palette({ open, onClose, anchorRef, dark }: PaletteProps) {
423475
<div
424476
className={[
425477
'sep-palette-inline',
478+
centered ? 'is-centered' : '',
426479
expanded ? 'is-open' : '',
427480
closing ? 'is-closing' : '',
428481
]
@@ -432,9 +485,9 @@ export function Palette({ open, onClose, anchorRef, dark }: PaletteProps) {
432485
aria-modal={expanded ? true : undefined}
433486
aria-label="Search"
434487
style={{
435-
top: anchor.top,
436-
left: anchor.left,
437-
width,
488+
top: layout.top,
489+
left: layout.left,
490+
width: layout.width,
438491
}}
439492
>
440493
<div className="sep-palette-input-row">

src/content/sidebar/ContentsTab.tsx

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { useEffect, useRef } from 'react';
21
import type { SiteNavSection, TocItem } from '../../lib/types';
32
import { smoothScrollToHref } from '../../host/toc';
43

@@ -26,36 +25,9 @@ export function ContentsTab({
2625
activeIndex,
2726
siteNav,
2827
}: ContentsTabProps) {
29-
const listRef = useRef<HTMLDivElement>(null);
30-
31-
useEffect(() => {
32-
const list = listRef.current;
33-
if (!list || activeIndex < 0 || siteNav?.length) {
34-
return;
35-
}
36-
// First section: pin to the top so the "Contents" label stays visible.
37-
if (activeIndex === 0) {
38-
list.scrollTop = 0;
39-
return;
40-
}
41-
const active = list.querySelector<HTMLElement>(
42-
`[data-index="${activeIndex}"]`
43-
);
44-
if (!active) {
45-
return;
46-
}
47-
const top = active.offsetTop;
48-
const bottom = top + active.offsetHeight;
49-
if (top < list.scrollTop) {
50-
list.scrollTop = top;
51-
} else if (bottom > list.scrollTop + list.clientHeight) {
52-
list.scrollTop = bottom - list.clientHeight;
53-
}
54-
}, [activeIndex, siteNav]);
55-
5628
if (siteNav?.length) {
5729
return (
58-
<div className="sep-toc-scroll" ref={listRef}>
30+
<div className="sep-toc-scroll">
5931
{siteNav.map((section) => (
6032
<div key={section.title} className="sep-site-nav-section">
6133
<div className="sep-contents-label">{section.title}</div>
@@ -84,13 +56,12 @@ export function ContentsTab({
8456
}
8557

8658
return (
87-
<div className="sep-toc-scroll" ref={listRef}>
59+
<div className="sep-toc-scroll">
8860
<div className="sep-contents-label">Contents</div>
8961
{items.map((item, index) => (
9062
<button
9163
key={`${item.href}-${index}`}
9264
type="button"
93-
data-index={index}
9465
className={[
9566
'sep-toc-link',
9667
`sep-toc-link--level-${item.level}`,

src/content/sidebar/Sidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ export function Sidebar({
231231
open={paletteOpen}
232232
onClose={onClosePalette}
233233
anchorRef={searchSlotRef}
234+
collapsed={collapsed}
234235
dark={dark}
235236
/>
236237
</div>

src/content/styles.css

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,52 @@
526526
pointer-events: none;
527527
}
528528

529+
/*
530+
* Collapsed sidebar: full-size shell immediately; only scale + opacity.
531+
* (Sidebar-open mode keeps the width/height morph above.)
532+
*/
533+
.sep-palette-inline.is-centered,
534+
.sep-palette-inline.is-centered.is-open,
535+
.sep-palette-inline.is-centered.is-closing {
536+
/* Same open height as .is-open — no expand/collapse morph */
537+
max-height: min(298px, calc(100vh - 96px));
538+
background: var(--sep-surface);
539+
box-shadow:
540+
0 0 0 1px var(--sep-border-subtle),
541+
0 8px 28px color-mix(in srgb, var(--sand-12) 12%, transparent);
542+
transform-origin: center center;
543+
transition:
544+
opacity var(--sep-duration-ui) var(--sep-ease-out),
545+
transform var(--sep-duration-ui) var(--sep-ease-out);
546+
}
547+
548+
.sep-palette-inline.is-centered {
549+
opacity: 0;
550+
transform: scale(0.95);
551+
}
552+
553+
.sep-palette-inline.is-centered.is-open {
554+
opacity: 1;
555+
transform: scale(1);
556+
}
557+
558+
.sep-palette-inline.is-centered.is-closing {
559+
opacity: 0;
560+
transform: scale(0.95);
561+
}
562+
563+
/* Parent opacity handles the fade; skip nested reveal fades. */
564+
.sep-palette-inline.is-centered .sep-palette-esc,
565+
.sep-palette-inline.is-centered .sep-palette-results-wrap {
566+
opacity: 1;
567+
transition: none;
568+
}
569+
570+
.sep-palette-inline.is-centered .sep-palette-input-row::after {
571+
background: var(--sep-border-subtle);
572+
transition: none;
573+
}
574+
529575
.sep-palette-input-row {
530576
position: relative;
531577
z-index: 1;
@@ -665,6 +711,13 @@
665711
box-shadow var(--sep-duration-fast) var(--sep-ease);
666712
}
667713

714+
.sep-palette-inline.is-centered,
715+
.sep-palette-inline.is-centered.is-open,
716+
.sep-palette-inline.is-centered.is-closing {
717+
transform: none;
718+
transition: opacity var(--sep-duration-fast) var(--sep-ease-out);
719+
}
720+
668721
.sep-palette-input-row::after {
669722
transition: background-color var(--sep-duration-fast) var(--sep-ease);
670723
}
@@ -792,7 +845,8 @@
792845
}
793846
}
794847

795-
.sep-plus-app.is-dark .sep-palette-inline.is-open {
848+
.sep-plus-app.is-dark .sep-palette-inline.is-open,
849+
.sep-plus-app.is-dark .sep-palette-inline.is-centered {
796850
background: var(--sep-surface);
797851
box-shadow:
798852
0 0 0 1px var(--sep-border-subtle),
@@ -802,3 +856,9 @@
802856
.sep-plus-app.is-dark .sep-palette-input {
803857
color: var(--sep-text);
804858
}
859+
860+
/* Surface and elevated share sand-4 in dark; lift the row highlight. */
861+
.sep-plus-app.is-dark button.sep-palette-result:hover,
862+
.sep-plus-app.is-dark button.sep-palette-result.is-active {
863+
background: var(--sep-panel-bg);
864+
}

src/host/footnotes.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@
2929
pointer-events: none;
3030
}
3131

32+
/* sand-12 is light in dark mode — use sand-1 like the article sheet shadow. */
33+
:host-context(body.dark) .sep-footnote-preview {
34+
box-shadow: 0 10px 30px
35+
color-mix(in srgb, var(--sand-1) 45%, transparent);
36+
}
37+
3238
.sep-footnote-preview[hidden] {
3339
display: none;
3440
}

src/host/styles.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,13 @@ body.sep-plus .sep-plus-related-entries a:hover {
917917
.sep-plus-footnotes .sep-plus-footnote > :last-child {
918918
margin-bottom: 0;
919919
}
920+
921+
/* Leading "1." backrefs — match body text, not brand link pink/red. */
922+
body.sep-plus .sep-plus-footnotes .sep-plus-footnote > :first-child a:first-of-type:link,
923+
body.sep-plus .sep-plus-footnotes .sep-plus-footnote > :first-child a:first-of-type:visited,
924+
body.sep-plus .sep-plus-footnotes .sep-plus-footnote > :first-child a:first-of-type:hover {
925+
color: var(--sep-text) !important;
926+
}
920927
/* Dark Mode — sand scale flips on body.dark; only overrides that need
921928
different semantic roles or non-sand brand colors live here. */
922929

@@ -1004,6 +1011,16 @@ body.dark a:hover {
10041011
color: #ffcbcb !important;
10051012
}
10061013

1014+
/* Related-entry pills stay sand (beat the pink link accent above). */
1015+
body.dark.sep-plus .sep-plus-related-entries a:link,
1016+
body.dark.sep-plus .sep-plus-related-entries a:visited {
1017+
color: var(--sep-muted) !important;
1018+
}
1019+
1020+
body.dark.sep-plus .sep-plus-related-entries a:hover {
1021+
color: var(--sep-text) !important;
1022+
}
1023+
10071024
body.dark .btn:hover i {
10081025
color: #ffcbcb !important;
10091026
}

src/rules/block-unused-fonts.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"id": 1,
4+
"action": { "type": "block" },
5+
"condition": {
6+
"initiatorDomains": ["plato.stanford.edu"],
7+
"requestDomains": ["plato.stanford.edu"],
8+
"urlFilter": "font-awesome",
9+
"resourceTypes": ["stylesheet"]
10+
}
11+
}
12+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { describe, expect, it } from 'vitest';
2+
import rules from './block-unused-fonts.json';
3+
4+
describe('block-unused-fonts ruleset', () => {
5+
it('only blocks same-origin Font Awesome stylesheets on SEP', () => {
6+
expect(rules).toHaveLength(1);
7+
const [rule] = rules;
8+
expect(rule?.action.type).toBe('block');
9+
expect(rule?.condition.initiatorDomains).toEqual(['plato.stanford.edu']);
10+
expect(rule?.condition.requestDomains).toEqual(['plato.stanford.edu']);
11+
expect(rule?.condition.urlFilter).toBe('font-awesome');
12+
expect(rule?.condition.resourceTypes).toEqual(['stylesheet']);
13+
});
14+
15+
it('gives every rule a unique id', () => {
16+
const ids = rules.map((rule) => rule.id);
17+
expect(new Set(ids).size).toBe(ids.length);
18+
});
19+
});

0 commit comments

Comments
 (0)