1
0
Fork 0
Code Issues Pull requests Projects Releases Packages Wiki Activity Actions Pages
Docs/manual.html

1071 lines
32 KiB
HTML
Raw Permalink Normal View History

2026-06-29 12:59:15 -04:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Worldshaper UI System Manual</title>
<style>
:root {
--bg: #12161d;
--bg-soft: #1a212b;
--panel: rgba(17, 23, 31, 0.88);
--panel-strong: rgba(25, 32, 42, 0.96);
--line: rgba(143, 179, 215, 0.2);
--text: #e8edf4;
--muted: #9fb0c3;
--accent: #d7a04b;
--accent-soft: rgba(215, 160, 75, 0.16);
--accent-cool: #7dc0c9;
--good: #8ad0a2;
--shadow: 0 22px 60px rgba(0, 0, 0, 0.35);
--radius: 20px;
--content-width: 1240px;
--nav-width: 280px;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande",
sans-serif;
color: var(--text);
background:
radial-gradient(circle at top left, rgba(125, 192, 201, 0.17), transparent 30%),
radial-gradient(circle at top right, rgba(215, 160, 75, 0.12), transparent 24%),
linear-gradient(180deg, #0b0f14 0%, #12161d 42%, #10151d 100%);
min-height: 100vh;
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px);
background-size: 28px 28px;
mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.65), transparent 92%);
}
a {
color: inherit;
}
code,
pre {
font-family: Consolas, "Liberation Mono", Menlo, monospace;
}
.shell {
width: min(100%, var(--content-width));
margin: 0 auto;
padding: 24px;
}
.topbar {
position: sticky;
top: 0;
z-index: 30;
backdrop-filter: blur(16px);
background: rgba(11, 15, 20, 0.78);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.topbar-inner {
width: min(100%, var(--content-width));
margin: 0 auto;
padding: 14px 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.brand {
display: flex;
align-items: center;
gap: 14px;
}
.brand-badge {
width: 44px;
height: 44px;
border-radius: 14px;
background:
linear-gradient(135deg, rgba(215, 160, 75, 0.22), rgba(125, 192, 201, 0.3)),
rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.1);
display: grid;
place-items: center;
box-shadow: var(--shadow);
font-size: 0.92rem;
letter-spacing: 0.12em;
}
.brand-copy h1 {
margin: 0;
font-family: Georgia, "Times New Roman", serif;
font-size: 1.2rem;
font-weight: 700;
letter-spacing: 0.03em;
}
.brand-copy p {
margin: 2px 0 0;
color: var(--muted);
font-size: 0.92rem;
}
.menu-button {
display: none;
border: 1px solid var(--line);
background: var(--panel);
color: var(--text);
border-radius: 12px;
padding: 10px 14px;
font: inherit;
}
.layout {
display: grid;
grid-template-columns: var(--nav-width) minmax(0, 1fr);
gap: 24px;
padding-top: 22px;
}
.sidebar {
position: sticky;
top: 88px;
align-self: start;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.sidebar-header {
padding: 18px 18px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
background: linear-gradient(180deg, rgba(215, 160, 75, 0.08), transparent);
}
.sidebar-header h2 {
margin: 0;
font-size: 0.96rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
}
.sidebar-header p {
margin: 8px 0 0;
color: var(--muted);
line-height: 1.45;
font-size: 0.92rem;
}
.nav-group {
padding: 14px 14px 8px;
}
.nav-group + .nav-group {
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.nav-label {
margin: 0 0 10px;
color: var(--muted);
font-size: 0.76rem;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.nav-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 8px;
}
.nav-list a {
display: block;
text-decoration: none;
padding: 10px 12px;
border-radius: 12px;
color: var(--muted);
border: 1px solid transparent;
transition: 160ms ease;
}
.nav-list a:hover,
.nav-list a:focus-visible {
color: var(--text);
border-color: rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.04);
outline: none;
}
.nav-list a.active {
color: var(--text);
border-color: rgba(215, 160, 75, 0.26);
background: linear-gradient(135deg, var(--accent-soft), rgba(125, 192, 201, 0.08));
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.03);
}
.content {
display: grid;
gap: 20px;
}
.hero,
.section-card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: hidden;
}
.hero {
position: relative;
}
.hero::after {
content: "";
position: absolute;
inset: auto -10% -40% auto;
width: 360px;
height: 360px;
border-radius: 50%;
background: radial-gradient(circle, rgba(125, 192, 201, 0.18), transparent 70%);
pointer-events: none;
}
.hero-inner {
padding: 34px;
display: grid;
gap: 20px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 10px;
width: fit-content;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
color: var(--accent-cool);
font-size: 0.82rem;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero h2 {
margin: 0;
max-width: 12ch;
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(2.2rem, 4vw, 4rem);
line-height: 0.98;
}
.hero p {
margin: 0;
max-width: 68ch;
color: var(--muted);
font-size: 1.05rem;
line-height: 1.75;
}
.hero-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.mini-panel {
padding: 16px;
border-radius: 16px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.mini-panel strong {
display: block;
margin-bottom: 8px;
color: var(--text);
}
.mini-panel span {
color: var(--muted);
line-height: 1.55;
}
.section-card {
padding: 28px;
}
.section-card h2 {
margin: 0 0 6px;
font-family: Georgia, "Times New Roman", serif;
font-size: 2rem;
}
.section-intro {
margin: 0 0 22px;
color: var(--muted);
line-height: 1.7;
max-width: 70ch;
}
.callout {
margin: 0 0 24px;
padding: 18px 20px;
border-radius: 16px;
background: linear-gradient(135deg, rgba(125, 192, 201, 0.12), rgba(215, 160, 75, 0.08));
border: 1px solid rgba(125, 192, 201, 0.2);
}
.callout strong {
display: block;
margin-bottom: 6px;
color: var(--text);
}
.callout p {
margin: 0;
color: var(--muted);
line-height: 1.65;
}
.grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.panel {
padding: 18px;
border-radius: 16px;
background: var(--panel-strong);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.panel h3 {
margin: 0 0 10px;
font-size: 1rem;
color: var(--accent);
letter-spacing: 0.04em;
}
.panel p {
margin: 0;
color: var(--muted);
line-height: 1.65;
}
.panel ul,
.section-card ul,
.section-card ol {
margin: 14px 0 0;
padding-left: 20px;
color: var(--text);
}
.section-card li + li,
.panel li + li {
margin-top: 8px;
}
.section-card li::marker,
.panel li::marker {
color: var(--accent-cool);
}
.subsection {
padding-top: 8px;
}
.subsection + .subsection {
margin-top: 22px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
padding-top: 22px;
}
.subsection h3 {
margin: 0 0 8px;
font-size: 1.15rem;
}
.subsection p {
margin: 0;
color: var(--muted);
line-height: 1.7;
}
pre {
margin: 16px 0 0;
padding: 18px;
border-radius: 16px;
overflow-x: auto;
background: #0d1218;
color: #d6e2ef;
border: 1px solid rgba(125, 192, 201, 0.14);
}
.footer-note {
color: var(--muted);
text-align: center;
padding: 8px 0 28px;
font-size: 0.92rem;
}
@media (max-width: 980px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
display: none;
}
.sidebar.open {
display: block;
}
.menu-button {
display: inline-flex;
}
.hero-grid,
.grid-2 {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.shell,
.topbar-inner {
padding-left: 16px;
padding-right: 16px;
}
.hero-inner,
.section-card {
padding: 22px;
}
.hero h2 {
max-width: none;
}
}
</style>
</head>
<body>
<header class="topbar">
<div class="topbar-inner">
<div class="brand">
<div class="brand-badge">UI</div>
<div class="brand-copy">
<h1>Worldshaper UI System Manual</h1>
<p>Shared architecture for the engine, runtime, and editor</p>
</div>
</div>
<button class="menu-button" type="button" aria-expanded="false" aria-controls="manual-nav">
Browse Sections
</button>
</div>
</header>
<div class="shell">
<div class="layout">
<aside class="sidebar" id="manual-nav">
<div class="sidebar-header">
<h2>Manual Index</h2>
<p>Navigate by responsibility, then drill down into systems, workflow, and the shared asset format.</p>
</div>
<div class="nav-group">
<p class="nav-label">Foundation</p>
<ul class="nav-list">
<li><a href="#overview">Overview</a></li>
<li><a href="#shared-model">Shared Asset Model</a></li>
<li><a href="#package-layout">Package Layout</a></li>
</ul>
</div>
<div class="nav-group">
<p class="nav-label">Systems</p>
<ul class="nav-list">
<li><a href="#engine">Engine</a></li>
<li><a href="#engine-api">Runtime API</a></li>
<li><a href="#editor">Editor</a></li>
<li><a href="#game-runtime">Game Runtime</a></li>
</ul>
</div>
<div class="nav-group">
<p class="nav-label">Data</p>
<ul class="nav-list">
<li><a href="#ui-assets">UI Assets</a></li>
<li><a href="#asset-example">Asset Example</a></li>
<li><a href="#versioning">Versioning</a></li>
</ul>
</div>
</aside>
<main class="content">
<section class="hero">
<div class="hero-inner">
<div class="eyebrow">Design Manual</div>
<h2>UI as a shared language between tools and play.</h2>
<p>
The editor should author UI definitions, the engine should render and manage them,
and the game should supply live data plus behavior bindings. That keeps creation,
execution, and meaning cleanly separated while still letting the same assets move
directly from tooling into runtime.
</p>
<div class="hero-grid">
<div class="mini-panel">
<strong>Engine</strong>
<span>Owns runtime behavior: rendering, layout, focus, input, and theme systems.</span>
</div>
<div class="mini-panel">
<strong>Editor</strong>
<span>Owns authoring: visual layout, hierarchy editing, inspectors, and previews.</span>
</div>
<div class="mini-panel">
<strong>Game</strong>
<span>Owns meaning: opening windows, injecting data, and resolving action ids.</span>
</div>
</div>
</div>
</section>
<section class="section-card" id="overview">
<h2>Overview</h2>
<p class="section-intro">
The system should support a workflow where the editor creates and modifies UI
visually, saves UI definitions as assets, the game loads those assets at runtime,
and the engine provides the shared UI runtime both sides depend on.
</p>
<div class="callout">
<strong>Core Rule</strong>
<p>
UI should be stored as data assets, not embedded directly in the editor and not
hardcoded only in game code. The editor authors structure, the runtime executes it,
and the game binds it to real gameplay state.
</p>
</div>
<div class="grid-2">
<div class="panel">
<h3>Recommended Split</h3>
<ul>
<li>Engine owns how UI works.</li>
<li>Editor owns how UI is authored.</li>
<li>Game runtime owns what the UI means.</li>
</ul>
</div>
<div class="panel">
<h3>Design Direction</h3>
<ul>
<li>Use a retained-mode UI system.</li>
<li>Store screens and windows as declarative assets.</li>
<li>Render them through shared HTML, CSS, and JavaScript runtime code.</li>
</ul>
</div>
</div>
</section>
<section class="section-card" id="shared-model">
<h2>Shared Asset Model</h2>
<p class="section-intro">
The editor should write structured UI data, and the game should read that same data
through the engine runtime. This avoids arbitrary code generation and keeps assets
portable, testable, and safe to evolve.
</p>
<div class="grid-2">
<div class="panel">
<h3>The Editor Should Save</h3>
<ul>
<li>layout</li>
<li>widget hierarchy</li>
<li>theme and style references</li>
<li>data-binding keys</li>
<li>symbolic action identifiers</li>
</ul>
</div>
<div class="panel">
<h3>The Editor Should Not Save</h3>
<ul>
<li>raw executable game logic</li>
<li>hardcoded runtime object instances</li>
<li>engine-internal transient state</li>
<li>function bodies embedded in assets</li>
</ul>
</div>
</div>
</section>
<section class="section-card" id="package-layout">
<h2>Package Layout</h2>
<p class="section-intro">
A package split like this keeps schema, runtime, editor, and game-facing code
independent while still sharing a common vocabulary.
</p>
<pre><code>/packages
/ui-schema
/ui-runtime
/ui-editor
/game-runtime</code></pre>
<div class="grid-2" style="margin-top: 18px;">
<div class="panel">
<h3><code>ui-schema</code></h3>
<ul>
<li>UI node definitions</li>
<li>validation</li>
<li>schema versioning</li>
<li>migrations</li>
</ul>
</div>
<div class="panel">
<h3><code>ui-runtime</code></h3>
<ul>
<li>renderer</li>
<li>layout engine</li>
<li>window manager</li>
<li>widget registry</li>
<li>theme system</li>
</ul>
</div>
<div class="panel">
<h3><code>ui-editor</code></h3>
<ul>
<li>scene or workspace</li>
<li>selection tools</li>
<li>property inspector</li>
<li>asset browser</li>
<li>save and load flow</li>
</ul>
</div>
<div class="panel">
<h3><code>game-runtime</code></h3>
<ul>
<li>game-specific data providers</li>
<li>action handlers</li>
<li>UI asset loading</li>
<li>window orchestration</li>
</ul>
</div>
</div>
</section>
<section class="section-card" id="engine">
<h2>Engine</h2>
<p class="section-intro">
The engine should be the single owner of the UI runtime so the editor and game do
not drift apart in behavior over time.
</p>
<div class="grid-2">
<div class="panel">
<h3>Responsibilities</h3>
<ul>
<li>window lifecycle</li>
<li>widget rendering</li>
<li>layout calculation</li>
<li>input routing</li>
<li>focus management</li>
<li>dragging and resizing behavior</li>
<li>modal behavior</li>
<li>z-order and activation</li>
<li>theming and skinning</li>
<li>serialization and asset loading hooks</li>
</ul>
</div>
<div class="panel">
<h3>Why It Belongs Here</h3>
<ul>
<li>accurate editor preview</li>
<li>fewer runtime and editor mismatches</li>
<li>one place to fix layout and interaction bugs</li>
<li>reusable widgets and themes</li>
</ul>
</div>
</div>
<div class="subsection">
<h3>Core Runtime Systems</h3>
<div class="grid-2">
<div class="panel">
<h3>UI Manager</h3>
<ul>
<li>open windows</li>
<li>focus order</li>
<li>active modal</li>
<li>hovered and pressed widgets</li>
<li>drag and resize operations</li>
</ul>
</div>
<div class="panel">
<h3>Window Manager</h3>
<ul>
<li>create</li>
<li>close</li>
<li>minimize</li>
<li>maximize</li>
<li>bring to front</li>
<li>move</li>
<li>resize</li>
</ul>
</div>
<div class="panel">
<h3>Widget Registry</h3>
<ul>
<li><code>window</code></li>
<li><code>panel</code></li>
<li><code>label</code></li>
<li><code>button</code></li>
<li><code>input</code></li>
<li><code>list</code></li>
<li><code>tabs</code></li>
</ul>
</div>
<div class="panel">
<h3>Theme System</h3>
<ul>
<li>fonts</li>
<li>spacing</li>
<li>borders</li>
<li>colors</li>
<li>states</li>
<li>animation references</li>
</ul>
</div>
</div>
</div>
</section>
<section class="section-card" id="engine-api">
<h2>Runtime API</h2>
<p class="section-intro">
The engine should expose stable, high-level APIs that both the editor and the game
can consume without caring about rendering internals.
</p>
<pre><code>ui.loadAsset("quest-log");
ui.createWindow(windowDefinition, bindings);
ui.closeWindow("quest-log");
ui.updateBindings("quest-log", bindings);</code></pre>
<pre><code>ui.createWindow({
definition: windowDefinition,
bindings: {
data: gameViewModel,
actions: actionRegistry
}
});</code></pre>
</section>
<section class="section-card" id="editor">
<h2>Editor</h2>
<p class="section-intro">
The editor should be an authoring tool for UI assets, not a place where gameplay code
gets embedded.
</p>
<div class="grid-2">
<div class="panel">
<h3>Responsibilities</h3>
<ul>
<li>create windows and widgets visually</li>
<li>arrange hierarchy</li>
<li>move and resize elements</li>
<li>edit properties</li>
<li>assign themes or style references</li>
<li>set data-binding keys</li>
<li>assign symbolic action identifiers</li>
<li>preview final layout using the shared runtime</li>
</ul>
</div>
<div class="panel">
<h3>Recommended Early Features</h3>
<ul>
<li>hierarchy tree</li>
<li>canvas or workspace</li>
<li>property inspector</li>
<li>theme picker</li>
<li>save and load</li>
<li>live preview</li>
</ul>
</div>
</div>
<div class="subsection">
<h3>Editor Workflow</h3>
<ol>
<li>Create or open a UI asset.</li>
<li>Add a root node such as a <code>window</code> or <code>screen</code>.</li>
<li>Add child widgets like <code>panel</code>, <code>label</code>, <code>list</code>, or <code>button</code>.</li>
<li>Configure properties in an inspector.</li>
<li>Save the asset as JSON.</li>
<li>Preview the result using the same UI runtime the game uses.</li>
</ol>
</div>
<div class="subsection">
<h3>Symbolic Actions</h3>
<p>
Instead of embedding code, a button should store a stable action id and let the
game resolve it later.
</p>
<pre><code>{
"action": "inventory.useSelectedItem"
}</code></pre>
</div>
</section>
<section class="section-card" id="game-runtime">
<h2>Game Runtime</h2>
<p class="section-intro">
The game runtime consumes UI assets and supplies them with live data and behavior.
It decides when a screen opens, what it displays, and how action ids map into actual
game systems.
</p>
<div class="grid-2">
<div class="panel">
<h3>Responsibilities</h3>
<ul>
<li>deciding when UI opens or closes</li>
<li>selecting which asset to load</li>
<li>supplying live data</li>
<li>resolving symbolic actions into real functions</li>
<li>updating bindings as game state changes</li>
</ul>
</div>
<div class="panel">
<h3>Runtime Mutability</h3>
<ul>
<li>visibility</li>
<li>enabled state</li>
<li>bound text</li>
<li>bound lists</li>
<li>dynamic styling flags</li>
<li>open and closed state of windows</li>
</ul>
</div>
</div>
<div class="subsection">
<h3>Consumption Flow</h3>
<ol>
<li>Load a UI asset.</li>
<li>Validate it through the shared schema layer.</li>
<li>Create a runtime instance through the engine UI manager.</li>
<li>Inject data bindings and action handlers.</li>
<li>Let the engine handle rendering and interaction.</li>
</ol>
</div>
<div class="subsection">
<h3>Example Usage</h3>
<pre><code>const definition = await ui.loadAsset("quest-log");
ui.createWindow({
definition,
bindings: {
data: gameViewModel,
actions: {
"ui.closeWindow": ({ windowId }) =&gt; ui.closeWindow(windowId),
"inventory.useSelectedItem": () =&gt; inventory.useSelectedItem()
}
}
});</code></pre>
</div>
</section>
<section class="section-card" id="ui-assets">
<h2>UI Assets</h2>
<p class="section-intro">
UI assets should start as serializable JSON files. The format should be stable,
versioned, and expressive enough to define hierarchy, layout, binding, and symbolic
actions without containing gameplay logic.
</p>
<div class="grid-2">
<div class="panel">
<h3>Schema Goals</h3>
<ul>
<li>schema versioning</li>
<li>stable ids</li>
<li>nested hierarchy</li>
<li>typed widgets</li>
<li>layout properties</li>
<li>style references</li>
<li>data bindings</li>
<li>symbolic actions</li>
</ul>
</div>
<div class="panel">
<h3>Recommended Fields</h3>
<ul>
<li><code>schemaVersion</code></li>
<li><code>id</code></li>
<li><code>type</code></li>
<li><code>children</code></li>
<li><code>title</code>, <code>x</code>, <code>y</code>, <code>width</code>, <code>height</code></li>
<li><code>bind</code>, <code>visibleWhen</code>, <code>enabledWhen</code>, <code>action</code></li>
<li><code>theme</code>, <code>variant</code>, <code>className</code></li>
</ul>
</div>
</div>
</section>
<section class="section-card" id="asset-example">
<h2>Asset Example</h2>
<p class="section-intro">
A typical authored window should look like structured data the engine can interpret
consistently in both the editor and the game.
</p>
<pre><code>{
"schemaVersion": 1,
"id": "quest-log",
"type": "window",
"title": "Quest Log",
"x": 120,
"y": 80,
"width": 420,
"height": 300,
"flags": {
"draggable": true,
"resizable": true,
"closable": true
},
"children": [
{
"id": "quest-list",
"type": "list",
"bind": "quests.active"
},
{
"id": "close-button",
"type": "button",
"text": "Close",
"action": "ui.closeWindow"
}
]
}</code></pre>
</section>
<section class="section-card" id="versioning">
<h2>Versioning</h2>
<p class="section-intro">
Every asset should declare a schema version so the format can evolve without breaking
older editor output.
</p>
<div class="callout">
<strong>Migration Policy</strong>
<p>
When the asset format changes, add migration functions in the shared schema layer.
The editor can write the newest version, while the game and engine can continue to
load older assets by upgrading them during validation.
</p>
</div>
<pre><code>{
"schemaVersion": 1
}</code></pre>
<div class="subsection">
<h3>Long-Term Extensions</h3>
<ul>
<li>prefabs and templates</li>
<li>inherited style variants</li>
<li>animation descriptors</li>
<li>localization keys</li>
<li>responsive layout rules</li>
<li>asset references to icons and images</li>
</ul>
</div>
</section>
<p class="footer-note">
Manual generated from the UI system design notes for Worldshaper.
</p>
</main>
</div>
</div>
<script>
const menuButton = document.querySelector(".menu-button");
const sidebar = document.querySelector(".sidebar");
const navLinks = [...document.querySelectorAll(".nav-list a")];
const sections = navLinks
.map((link) => document.querySelector(link.getAttribute("href")))
.filter(Boolean);
if (menuButton && sidebar) {
menuButton.addEventListener("click", () => {
const open = sidebar.classList.toggle("open");
menuButton.setAttribute("aria-expanded", String(open));
});
}
const activateLink = (id) => {
navLinks.forEach((link) => {
const active = link.getAttribute("href") === `#${id}`;
link.classList.toggle("active", active);
});
};
const observer = new IntersectionObserver(
(entries) => {
const visible = entries
.filter((entry) => entry.isIntersecting)
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
if (visible) {
activateLink(visible.target.id);
}
},
{
rootMargin: "-20% 0px -60% 0px",
threshold: [0.15, 0.35, 0.6]
}
);
sections.forEach((section) => observer.observe(section));
if (sections[0]) {
activateLink(sections[0].id);
}
navLinks.forEach((link) => {
link.addEventListener("click", () => {
if (window.innerWidth <= 980 && sidebar) {
sidebar.classList.remove("open");
menuButton?.setAttribute("aria-expanded", "false");
}
});
});
</script>
</body>
</html>