website-xp-phone 2.2.1 → 2.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +8 -0
- package/Makefile +5 -1
- package/README.md +9 -0
- package/build/routes.ts +27 -10
- package/package.json +2 -1
- package/public/Click.ogg +0 -0
- package/public/addons/rage-volley.txt +1 -1
- package/public/addons/rueg-wa-druid.txt +1 -1
- package/public/addons/rueg-wa-mage.txt +1 -1
- package/public/addons/rueg-wa-paladin.txt +1 -0
- package/public/addons/rueg-wa-priest.txt +1 -1
- package/public/addons/rueg-wa-rueg.txt +1 -1
- package/public/addons/rueg-wa-shaman.txt +1 -1
- package/public/blog/random/branch.md +29 -0
- package/public/blog/random/google.md +25 -0
- package/public/checksums.txt +70 -47
- package/public/documents/drift-report.json +46 -148
- package/public/loading.css +7 -0
- package/scripts/asset-reference.mjs +37 -0
- package/scripts/mutable-assets.mjs +46 -39
- package/scripts/refresh-soundcloud.mjs +152 -414
- package/scripts/verify-policy.mjs +206 -0
- package/scripts/write-dist-checksums.mjs +251 -135
- package/src/content/addons.json +13 -17
- package/src/content/blog.json +5 -26
- package/src/content/characters.json +6 -6
- package/src/content/configs.json +6 -2
- package/src/content/index.ts +105 -12
- package/src/content/pages.json +10 -1
- package/src/content/projects/development.json +9 -1
- package/src/content/projects/doftiles.json +9 -1
- package/src/content/projects/frontend.json +9 -1
- package/src/content/projects/graphbrowser.json +9 -1
- package/src/content/projects/herbstutil.json +93 -4
- package/src/content/projects/lightdni-jssas-toggle.json +9 -1
- package/src/content/projects/pagerts.json +13 -2
- package/src/content/projects/rarebert.json +12 -2
- package/src/content/projects/webuser.json +9 -1
- package/src/content/schema.ts +10 -1
- package/src/content/sections.json +2 -3
- package/src/content/soundcloud.json +204 -72
- package/src/lib/audio.ts +14 -0
- package/src/lib/clippy.ts +1 -7
- package/src/lib/embeds.ts +1 -1
- package/src/lib/musicSchema.ts +1 -0
- package/src/main.tsx +8 -0
- package/src/pages/MusicPage.tsx +0 -1
- package/src/pages/SitemapPage.tsx +1 -1
- package/src/pages/WowPage.tsx +1 -1
- package/src/shell/App.tsx +4 -1
- package/src/styles/extras.css +85 -30
- package/src/styles/layout.css +27 -18
- package/src/styles/window.css +53 -50
- package/src/themes/pageSlide/index.ts +115 -13
- package/src/themes/pageSlide/pageSlide.css +99 -6
- package/src/window/Embed.tsx +3 -1
- package/src/window/MaximizedWindow.tsx +250 -0
- package/src/window/OkButton.tsx +6 -1
- package/src/window/PageWindows.tsx +3 -9
- package/src/window/Window.tsx +129 -53
- package/src/window/model.ts +22 -0
- package/src/window/state.tsx +54 -30
- package/test/asset-reference.test.js +53 -0
- package/test/content.test.js +60 -6
- package/test/mutable-assets.test.js +27 -0
- package/test/verify-policy.test.js +92 -0
- package/tsconfig.node.tsbuildinfo +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/xp.css/README.md +10 -0
|
@@ -35,11 +35,25 @@ html.theme-page-slide .page-slide-ghost > .page {
|
|
|
35
35
|
min-height: 0;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/* Fixed footer bars of the old page
|
|
38
|
+
/* Fixed footer bars of the old page exit on their own beat: the slide
|
|
39
|
+
starts immediately (no fold-delay) and fades as it goes, so the bar
|
|
40
|
+
visibly rides out instead of blinking away at the route commit. */
|
|
39
41
|
html.theme-page-slide .page-slide-bar {
|
|
40
42
|
pointer-events: none;
|
|
41
|
-
animation: page-slide-
|
|
42
|
-
|
|
43
|
+
animation: page-slide-bar-exit 280ms ease-in forwards;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@keyframes page-slide-bar-exit {
|
|
47
|
+
to {
|
|
48
|
+
translate: 0 100vh;
|
|
49
|
+
opacity: 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* The live bar yields to its ride-out clone for the exit (visibility —
|
|
54
|
+
display would collapse the reserved body padding row). */
|
|
55
|
+
html.theme-page-slide .page-slide-bar-hide {
|
|
56
|
+
visibility: hidden;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/* The new page eases from the old page's bottom padding to its own, so its
|
|
@@ -55,16 +69,22 @@ html.theme-page-slide[data-page-slide] main:not(.page-slide-ghost) > .page {
|
|
|
55
69
|
edge falls to meet it (the outline sucks downward, as in XP), while the
|
|
56
70
|
whole window is drawn the last stretch toward the measured fold point.
|
|
57
71
|
Contents ride the squash, as in XP itself. */
|
|
58
|
-
html.theme-page-slide .page-slide-ghost .page > .window
|
|
72
|
+
html.theme-page-slide .page-slide-ghost .page > .window,
|
|
73
|
+
html.theme-page-slide .page-slide-ghost .page > .maximized-shell > .window {
|
|
59
74
|
transform-origin: bottom center;
|
|
60
75
|
/* Linear, per preference: the top edge falls towards the taskbar line
|
|
61
76
|
at a constant rate for the entire fold. */
|
|
62
77
|
animation: page-slide-fold var(--ps-minimize, 98ms) linear forwards;
|
|
63
78
|
}
|
|
64
79
|
|
|
65
|
-
/*
|
|
80
|
+
/* Fallback look for the swapped-out iframes (music embeds etc.) — the
|
|
81
|
+
PAGE's background colour: the placeholder is what shows through while
|
|
82
|
+
the page fades/slides, and a white flash reads as visual pollution
|
|
83
|
+
against the themed background. Document frames that carry their own
|
|
84
|
+
opaque background (the white resume preview) adopt it in JS instead —
|
|
85
|
+
a dark placeholder there reads as the PDF unloading mid-exit. */
|
|
66
86
|
html.theme-page-slide .page-slide-ghost .page-slide-placeholder-frame {
|
|
67
|
-
background:
|
|
87
|
+
background: var(--background-color);
|
|
68
88
|
}
|
|
69
89
|
|
|
70
90
|
@keyframes page-slide-fold {
|
|
@@ -79,6 +99,79 @@ html.theme-page-slide .page-slide-ghost .page-slide-placeholder-frame {
|
|
|
79
99
|
}
|
|
80
100
|
}
|
|
81
101
|
|
|
102
|
+
/* Departing dimmed overlays: the dark level fades in step with the
|
|
103
|
+
departing windows (slide-off on dismissal, page fold on navigation);
|
|
104
|
+
ride clones keep the fade going even when the portals unmount
|
|
105
|
+
mid-animation. The ride carries its OWN full backdrop look — it must
|
|
106
|
+
drop the .maximized-backdrop class, or body:has() would keep the page
|
|
107
|
+
raised over the menu bar for the ride's whole lifetime. */
|
|
108
|
+
.maximized-backdrop.page-slide-backdrop-close,
|
|
109
|
+
.page-slide-backdrop-ride {
|
|
110
|
+
position: fixed;
|
|
111
|
+
inset: 0;
|
|
112
|
+
z-index: 4000;
|
|
113
|
+
pointer-events: none;
|
|
114
|
+
background: rgba(0, 0, 0, 0.6);
|
|
115
|
+
/* The ride carries its OWN flex alignment that used to come from
|
|
116
|
+
.maximized-backdrop (dropped from the clone): without it, the
|
|
117
|
+
window copy block-docks left of the page. */
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: safe center;
|
|
120
|
+
justify-content: safe center;
|
|
121
|
+
animation: page-slide-backdrop-fade 180ms ease-out forwards;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@keyframes page-slide-backdrop-fade {
|
|
125
|
+
to {
|
|
126
|
+
opacity: 0;
|
|
127
|
+
background: rgba(0, 0, 0, 0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Departing exit on a closing maximized window: MaximizedWindow adds
|
|
132
|
+
.window-minimize to the ride-out clone and measures --page-fold-dy to
|
|
133
|
+
the taskbar line exactly like the ghost clone above, and the squish
|
|
134
|
+
plays the same fold: the bottom edge slides to the taskbar line while
|
|
135
|
+
the top edge falls to meet it. The !important outranks theme
|
|
136
|
+
animations (theme-experimental-flash on maximized experimental windows
|
|
137
|
+
would otherwise replace the fold); the clone paints above the raised
|
|
138
|
+
page, above its fading dimmed layer. Since the ride clone is inert and
|
|
139
|
+
the real window returns to the flow the moment the fold starts, the
|
|
140
|
+
collapsing content never glitches beneath the squash. */
|
|
141
|
+
html.theme-page-slide .window.window-minimize {
|
|
142
|
+
z-index: 4001;
|
|
143
|
+
transform-origin: bottom center;
|
|
144
|
+
animation: window-minimize-exit var(--ps-minimize, 98ms) linear forwards
|
|
145
|
+
!important;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@keyframes window-minimize-exit {
|
|
149
|
+
to {
|
|
150
|
+
/* Fold onto the taskbar line, then clear it: without the extra
|
|
151
|
+
100% offset the squashed sliver would rest exactly AT the bottom
|
|
152
|
+
edge (the page drop that normally carries it away doesn't exist
|
|
153
|
+
for a standalone dismissal). */
|
|
154
|
+
transform: translateY(calc(var(--page-fold-dy, 80vh) + 100%))
|
|
155
|
+
scaleY(0.06);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Parked (a hyperlink/maximized seed load): play NOTHING. Aiming the
|
|
160
|
+
entrance at zero distance would still emit translate(0,0) — a live
|
|
161
|
+
transform on <main> makes it the containing block for the in-tree
|
|
162
|
+
position:fixed maximized backdrop, which then centers against main's
|
|
163
|
+
box instead of the viewport. No animation = no transform = the
|
|
164
|
+
backdrop stays viewport-anchored. First navigation applies a real
|
|
165
|
+
direction (update of data-page-slide) and restarts the animations. */
|
|
166
|
+
html.theme-page-slide[data-page-slide="parked"] main:not(.page-slide-ghost) {
|
|
167
|
+
animation: none;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
html.theme-page-slide[data-page-slide="parked"] main:not(.page-slide-ghost)
|
|
171
|
+
> .page {
|
|
172
|
+
animation: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
82
175
|
@keyframes page-slide-pad {
|
|
83
176
|
from {
|
|
84
177
|
padding-bottom: var(--ps-pad-from, 0px);
|
package/src/window/Embed.tsx
CHANGED
|
@@ -34,7 +34,9 @@ export function EmbedView({ embed }: { embed: EmbedItem }) {
|
|
|
34
34
|
height={spec.height}
|
|
35
35
|
allowFullScreen
|
|
36
36
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
|
37
|
-
loading="lazy"
|
|
37
|
+
// NO loading="lazy": these frames live inside fixed-size window
|
|
38
|
+
// cards, so lazy loads never trigger on mobile and leave tall
|
|
39
|
+
// blank areas behind.
|
|
38
40
|
referrerPolicy="strict-origin-when-cross-origin"
|
|
39
41
|
/>
|
|
40
42
|
);
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { useRef, useState, type ReactNode, type RefObject } from "react";
|
|
2
|
+
import type { WindowFlags } from "./state";
|
|
3
|
+
import { descendantIds } from "./model";
|
|
4
|
+
import { permalinkFor, useWindowStore } from "./state";
|
|
5
|
+
|
|
6
|
+
/** Machinery for maximized-foreground windows: the dimmed sibling layer,
|
|
7
|
+
* the reserved flow slot beneath the fixed overlay, the dismissal paths
|
|
8
|
+
* (silent restore / title-bar-minimize slide) and the URL-hash hand-off.
|
|
9
|
+
*
|
|
10
|
+
* The component ALWAYS wraps the window element so React keeps its DOM
|
|
11
|
+
* identity across maximize/dismiss — the window lives in the last child
|
|
12
|
+
* slot in both states and is never remounted. */
|
|
13
|
+
|
|
14
|
+
export type MaximizedController = {
|
|
15
|
+
/** Enters the maximized view: anchors the section hash, reserves the
|
|
16
|
+
* flow slot (measured pre-lift) and lifts the window. Silent. */
|
|
17
|
+
maximize: () => void;
|
|
18
|
+
/** Silent, instant restore: ✕ on a maximized window, Escape and the
|
|
19
|
+
* out-of-bounds backdrop click all restore without sound (sounds
|
|
20
|
+
* stay with the callers) and without the slide. */
|
|
21
|
+
dismiss: () => void;
|
|
22
|
+
/** OK entry: restore AND reveal the window's content on return. */
|
|
23
|
+
dismissRevealed: () => void;
|
|
24
|
+
/** Title-bar minimize only: restores with the slide-off ride. */
|
|
25
|
+
dismissWithSlide: () => void;
|
|
26
|
+
backdropRef: RefObject<HTMLDivElement | null>;
|
|
27
|
+
parkedSize: { width: number; height: number } | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function useMaximizedController(
|
|
31
|
+
nodeId: string,
|
|
32
|
+
patch: (id: string, flags: WindowFlags) => void,
|
|
33
|
+
flagsOf: (id: string) => WindowFlags,
|
|
34
|
+
windowRef: RefObject<HTMLDivElement | null>,
|
|
35
|
+
): MaximizedController {
|
|
36
|
+
const backdropRef = useRef<HTMLDivElement | null>(null);
|
|
37
|
+
const { windows, permalinkMode } = useWindowStore();
|
|
38
|
+
/** The window's own flags at maximize time: dismissal restores them
|
|
39
|
+
* instead of expanding anything ("only scroll to" — and a window that
|
|
40
|
+
* was minimized before maximizing gets minimized back). */
|
|
41
|
+
const preFlags = useRef<WindowFlags | null>(null);
|
|
42
|
+
/** The maximized window keeps its flow slot reserved underneath: the
|
|
43
|
+
* surrounding windows never reflow when it lifts or returns. */
|
|
44
|
+
const [parkedSize, setParkedSize] = useState<{
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
} | null>(null);
|
|
48
|
+
|
|
49
|
+
/** Hands the URL to the topmost remaining maximized window's permalink
|
|
50
|
+
* after a dismissal (the page's own address form — ?post=slug on the
|
|
51
|
+
* blog — so a refresh lands back there), or clears it entirely when
|
|
52
|
+
* none remain — pathname WITHOUT the query, so a stale ?post can't
|
|
53
|
+
* resurrect a maximized window after the last one was dismissed. The
|
|
54
|
+
* dismissed window's own backdrop is excluded via backdropRef, and
|
|
55
|
+
* ride-out clones carry no id to ever qualify. */
|
|
56
|
+
const settleHashAfterDismiss = () => {
|
|
57
|
+
const remaining = [
|
|
58
|
+
...document.querySelectorAll<HTMLElement>(
|
|
59
|
+
".maximized-backdrop[data-window-id]",
|
|
60
|
+
),
|
|
61
|
+
].filter((candidate) => candidate !== backdropRef.current);
|
|
62
|
+
const topId = remaining[remaining.length - 1]?.dataset.windowId;
|
|
63
|
+
const topNode = topId ? windows.find((w) => w.id === topId) : undefined;
|
|
64
|
+
history.replaceState(
|
|
65
|
+
history.state,
|
|
66
|
+
"",
|
|
67
|
+
topNode ? permalinkFor(topNode, permalinkMode) : `${location.pathname}`,
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const dismiss = (slide: boolean, revealed = false) => {
|
|
72
|
+
const riders: HTMLElement[] = [];
|
|
73
|
+
const shell = backdropRef.current;
|
|
74
|
+
if (shell) {
|
|
75
|
+
// The whole foreground (dimmed layer + the window inside it) leaves
|
|
76
|
+
// with a FADE, not a blink — even on non-slide dismissals. Clone
|
|
77
|
+
// first, then hide the real one at once so the two never stack on
|
|
78
|
+
// screen: two rgba(0,0,0,.6) layers read as complete darkness
|
|
79
|
+
// before the commit removes the real one. The clone mounts INSIDE
|
|
80
|
+
// the page tree (the raised page paints above body-level content
|
|
81
|
+
// while a backdrop exists) — see the z-index ladder in extras.css.
|
|
82
|
+
const riding = shell.cloneNode(true) as HTMLElement;
|
|
83
|
+
riding.classList.add("page-slide-backdrop-ride");
|
|
84
|
+
// Drop the backdrop class: body:has(.maximized-backdrop) raises the
|
|
85
|
+
// whole page over the menu bar, and it must unlock the instant the
|
|
86
|
+
// real backdrop unmounts — the ride's darkening carries its own
|
|
87
|
+
// fixed/full-size look via .page-slide-backdrop-ride instead.
|
|
88
|
+
riding.classList.remove("maximized-backdrop");
|
|
89
|
+
riding.removeAttribute("data-window-id");
|
|
90
|
+
riding.setAttribute("inert", "");
|
|
91
|
+
if (!slide) {
|
|
92
|
+
// Non-slide dismissals restore the real window at once; the ride
|
|
93
|
+
// rides only the darkening, so strip the frozen window copy.
|
|
94
|
+
riding.querySelector(".window")?.remove();
|
|
95
|
+
} else {
|
|
96
|
+
const popped = windowRef.current;
|
|
97
|
+
if (popped) {
|
|
98
|
+
const rect = popped.getBoundingClientRect();
|
|
99
|
+
riding
|
|
100
|
+
.querySelector<HTMLElement>(".window")
|
|
101
|
+
?.style.setProperty(
|
|
102
|
+
"--page-fold-dy",
|
|
103
|
+
`${Math.max(0, Math.round(window.innerHeight - rect.bottom))}px`,
|
|
104
|
+
);
|
|
105
|
+
riding
|
|
106
|
+
.querySelector<HTMLElement>(".window")
|
|
107
|
+
?.classList.add("window-minimize");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const page = shell.closest(".page") ?? document.body;
|
|
111
|
+
page.appendChild(riding);
|
|
112
|
+
riders.push(riding);
|
|
113
|
+
shell.style.opacity = "0";
|
|
114
|
+
}
|
|
115
|
+
settleHashAfterDismiss();
|
|
116
|
+
// The title-bar minimize parks the restored window MINIMIZED (the
|
|
117
|
+
// mascot's un-hide list / summon brings it back). Every other exit
|
|
118
|
+
// restores the window's OWN flags as they stood at maximize time —
|
|
119
|
+
// never maximizing content, only scrolling back to it; hash-navigated
|
|
120
|
+
// windows hold no snapshot and simply persist their inline state.
|
|
121
|
+
if (slide) {
|
|
122
|
+
patch(nodeId, { maximized: false, minimized: true });
|
|
123
|
+
} else {
|
|
124
|
+
patch(nodeId, { maximized: false, ...(preFlags.current ?? {}) });
|
|
125
|
+
if (revealed) {
|
|
126
|
+
const root = windows.find((w) => w.id === nodeId);
|
|
127
|
+
const direct =
|
|
128
|
+
root?.children.flatMap((c) =>
|
|
129
|
+
c.kind === "window" ? [c.node.id] : [],
|
|
130
|
+
) ?? [];
|
|
131
|
+
for (const id of [nodeId, ...direct]) {
|
|
132
|
+
patch(id, { expanded: true, minimized: false });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (riders.length) {
|
|
137
|
+
window.setTimeout(() => riders.forEach((rider) => rider.remove()), 400);
|
|
138
|
+
}
|
|
139
|
+
// Parked windows land nowhere: a minimized window has no content to
|
|
140
|
+
// settle, so only the non-minimize exits bring the page back to
|
|
141
|
+
// their hash position.
|
|
142
|
+
if (!slide) {
|
|
143
|
+
window.setTimeout(
|
|
144
|
+
() =>
|
|
145
|
+
windowRef.current?.scrollIntoView({
|
|
146
|
+
behavior: "smooth",
|
|
147
|
+
block: "start",
|
|
148
|
+
}),
|
|
149
|
+
riders.length ? 240 : 60,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
maximize: () => {
|
|
156
|
+
// Snapshot the window's own flags first: the dismissal restores
|
|
157
|
+
// exactly these (never maximizes content, re-minimizes if it was
|
|
158
|
+
// minimized before the lift).
|
|
159
|
+
preFlags.current = { ...flagsOf(nodeId) };
|
|
160
|
+
// The hash anchors the maximized window so a refresh lands back on
|
|
161
|
+
// the revealed section. replaceState is untouched by the pageSlide
|
|
162
|
+
// pushState patch — no transition, no sound.
|
|
163
|
+
const el = windowRef.current;
|
|
164
|
+
if (el) {
|
|
165
|
+
const rect = el.getBoundingClientRect();
|
|
166
|
+
setParkedSize({
|
|
167
|
+
width: Math.round(rect.width),
|
|
168
|
+
height: Math.round(rect.height),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// Anchor the URL so a refresh lands back on the revealed section —
|
|
172
|
+
// in the page's own permalink form (?post=slug on the blog, hash
|
|
173
|
+
// elsewhere). Mobile still PUSHes a #tree-index hash: the OS back
|
|
174
|
+
// button's dismissal keys off the hash leaving THIS window; a
|
|
175
|
+
// query-only change never fires hashchange, so the permalink form
|
|
176
|
+
// can't drive that choreography.
|
|
177
|
+
if (matchMedia("(max-width: 768px)").matches) {
|
|
178
|
+
history.pushState(null, "", `#${nodeId}`);
|
|
179
|
+
} else {
|
|
180
|
+
const node = windows.find((w) => w.id === nodeId);
|
|
181
|
+
const url = node ? permalinkFor(node, permalinkMode) : `#${nodeId}`;
|
|
182
|
+
history.replaceState(null, "", url);
|
|
183
|
+
}
|
|
184
|
+
// A maximized window always shows its body — a stale minimized
|
|
185
|
+
// flag would leave it title-bar-only in the foreground — and the
|
|
186
|
+
// WHOLE subtree expands: every child window reveals beneath it.
|
|
187
|
+
patch(nodeId, { maximized: true, minimized: false });
|
|
188
|
+
for (const id of descendantIds(windows, nodeId)) {
|
|
189
|
+
patch(id, { expanded: true, minimized: false });
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
dismiss: () => dismiss(false),
|
|
193
|
+
dismissRevealed: () => dismiss(false, true),
|
|
194
|
+
dismissWithSlide: () => dismiss(true),
|
|
195
|
+
backdropRef,
|
|
196
|
+
parkedSize,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function MaximizedWindow({
|
|
201
|
+
maximized,
|
|
202
|
+
controller,
|
|
203
|
+
nodeId,
|
|
204
|
+
onOutsideClick,
|
|
205
|
+
windowEl,
|
|
206
|
+
}: {
|
|
207
|
+
/** Whether the window is currently lifted over the backdrop. */
|
|
208
|
+
maximized: boolean;
|
|
209
|
+
controller: MaximizedController;
|
|
210
|
+
nodeId: string;
|
|
211
|
+
onOutsideClick: () => void;
|
|
212
|
+
/** The live window element — always rendered INSIDE the shell (the
|
|
213
|
+
* background element while maximized), in the same child slot, so its
|
|
214
|
+
* DOM identity survives maximize/dismiss round-trips. */
|
|
215
|
+
windowEl: ReactNode;
|
|
216
|
+
}) {
|
|
217
|
+
return (
|
|
218
|
+
<>
|
|
219
|
+
{maximized && controller.parkedSize && (
|
|
220
|
+
// The flow slot the lifted window left behind: measured at lift
|
|
221
|
+
// time, keeps the surrounding windows from reflowing (no jitter
|
|
222
|
+
// on maximize/unmaximize).
|
|
223
|
+
<div
|
|
224
|
+
className="window-parked"
|
|
225
|
+
style={{
|
|
226
|
+
width: controller.parkedSize.width,
|
|
227
|
+
height: controller.parkedSize.height,
|
|
228
|
+
}}
|
|
229
|
+
aria-hidden
|
|
230
|
+
/>
|
|
231
|
+
)}
|
|
232
|
+
<div
|
|
233
|
+
ref={controller.backdropRef}
|
|
234
|
+
className={maximized ? "maximized-backdrop" : "maximized-shell"}
|
|
235
|
+
data-window-id={nodeId}
|
|
236
|
+
onClick={
|
|
237
|
+
maximized
|
|
238
|
+
? (event) => {
|
|
239
|
+
// Clicks inside the window bubble up to the backdrop
|
|
240
|
+
// (containment!) — only dimmed-area clicks dismiss.
|
|
241
|
+
if (event.target === event.currentTarget) onOutsideClick();
|
|
242
|
+
}
|
|
243
|
+
: undefined
|
|
244
|
+
}
|
|
245
|
+
>
|
|
246
|
+
{windowEl}
|
|
247
|
+
</div>
|
|
248
|
+
</>
|
|
249
|
+
);
|
|
250
|
+
}
|
package/src/window/OkButton.tsx
CHANGED
|
@@ -23,13 +23,18 @@ export function OkButton({
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
const { link } = node;
|
|
26
|
+
// OK revealing a section no longer rewrites the hash — the hash follows
|
|
27
|
+
// the maximized window (Window.tsx) and off-site departures only.
|
|
26
28
|
if (!link) {
|
|
27
29
|
expand(node.id);
|
|
28
|
-
history.replaceState(null, "", `#${node.id}`);
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
if (/^https?:/.test(link) || node.externalLink || /\.\w+$/.test(link)) {
|
|
33
|
+
// Leaving the site: bookmark this window into the hash first, so a
|
|
34
|
+
// return visit (via back or the external page) lands back on the
|
|
35
|
+
// last revealed maximized section.
|
|
32
36
|
remember(node.id);
|
|
37
|
+
history.replaceState(null, "", `#${node.id}`);
|
|
33
38
|
window.location.assign(link);
|
|
34
39
|
} else {
|
|
35
40
|
navigate(link);
|
|
@@ -10,15 +10,9 @@ import {
|
|
|
10
10
|
} from "./state";
|
|
11
11
|
import { Window } from "./Window";
|
|
12
12
|
|
|
13
|
-
function Roots({
|
|
14
|
-
mode,
|
|
15
|
-
children,
|
|
16
|
-
}: {
|
|
17
|
-
mode: PermalinkMode;
|
|
18
|
-
children?: ReactNode;
|
|
19
|
-
}) {
|
|
13
|
+
function Roots({ children }: { children?: ReactNode }) {
|
|
20
14
|
const { windows } = useWindowStore();
|
|
21
|
-
usePermalinkTarget(
|
|
15
|
+
usePermalinkTarget();
|
|
22
16
|
useRememberLeave();
|
|
23
17
|
return (
|
|
24
18
|
<>
|
|
@@ -55,7 +49,7 @@ export function PageWindows({
|
|
|
55
49
|
permalinkMode={permalinkMode}
|
|
56
50
|
onLeave={onLeave}
|
|
57
51
|
>
|
|
58
|
-
<Roots
|
|
52
|
+
<Roots>{children}</Roots>
|
|
59
53
|
</WindowProvider>
|
|
60
54
|
);
|
|
61
55
|
}
|