New updates to the page

This commit is contained in:
2025-10-16 15:38:09 -05:00
parent c6f6ad293f
commit 1acd8fc152
4 changed files with 44 additions and 52 deletions

93
archive/design-doc.OLD.md Normal file
View File

@@ -0,0 +1,93 @@
# SessionZero Design Doc
***© Bellsworne LLC 2025***
***Last modified by Christopher Bell on August 14th 2025***
## About SessionZero
SessionZero is a free and open-source TTRPG "companion" application for managing characters, templates, data, and game sessions. It allows you to utilize any TTRPG system by being **completely data-driven** with user-generated content.
## Features
SessionZero at its core is all about managing and creating data. Everything from sets of items or NPCs to characters themselves are all user-generated pieces of content that can be used throughout the application.
### Data Types
The client allows you to Create, View, Manage, Import & Export, and even Download various data types:
#### Datasets
Datasets are flexible collections of user-defined data, such as items, NPCs, or monsters.
- The type and content are completely arbitrary and user-defined.
- Datasets contain "entries" with custom fields and values.
- Entries can be linked throughout SessionZero, including in Character Templates.
#### Characters
Characters are made up of two parts: The **Template** and the **Character instance**.
##### Template
The template is the blueprint for a character sheet and is completely user-defined. It allows you to:
- Create sections with custom fields (e.g., text, numbers, booleans).
- Set default values for fields.
- Define calculated values using formulas.
- Reference entries from one or more Datasets.
- Create special list-based sections for things like inventories or spellbooks.
##### Character (Instance)
This is an instance of a character created from a template. You choose a template, fill in the values, and all calculated fields populate automatically.
### Game Sessions
SessionZero offers a simple yet robust system for managing game sessions. You can manage player characters, handle resources via datasets, use a turn tracker, and more.
The application supports several modes:
#### Default (Offline) Mode
By default, SessionZero works completely offline.
- You have total ownership over your locally stored session data.
- Manually manage characters for your players or use it for your own session notes.
#### Online Player
Connect to a SessionZero server to join sessions hosted by others.
- Access your character sheet, in-game chat, and session notes.
- All data updates in real-time.
- Keep private notes in your personal journal for each session.
#### Online Game Master
Host an online session and invite others to join.
- Set a character template for the session.
- Manage all session data (NPCs, items, etc.).
- Make real-time updates to character sheets.
- Use tools like in-game chat, commands, and a turn tracker.
### SessionZeroDB
SessionZeroDB is an online repository of user-created SZF data. When enabled, the application can automatically find and download required datasets and templates.
### SZF (SessionZero Format)
SessionZero uses a custom, human-readable data format called SZF for all data types. For more details, read the [SZF Docs](https://sessionzero.app/szf-docs.html).
## Philosophy
### Free
SessionZero is **FREE**. No account or subscription fees are required for the core application.
### Open Source
SessionZero is first and foremost **open-source software**.
### Offline-First
By default, SessionZero works **completely offline**. Your data is stored locally on your machine. No account required. No phoning home.
### No BS
No AI chatbots, no ads, no bloat. We promise.
## Online Accounts & Server Connections
All SessionZero accounts are created and managed on the official SessionZero servers, which is always free.
For hosting and playing in online game sessions, the application uses Godot's built in peer-to-peer networking capability:
## Technical Details
- **Client:** Godot C#
- **Backend (for accounts and SessionZeroDB):** ASP.NET, PostgreSQL

736
archive/szf-docs.OLD.html Normal file
View File

@@ -0,0 +1,736 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Session Zero Format (.szf) Documentation</title>
<link href="https://fonts.googleapis.com/css2?family=PT+Serif:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<style>
:root {
--background-color: #1a202e;
--primary-color: #3a4f6f;
--secondary-color: #2d3f5c;
--accent-color: #5b89b3;
--text-color: #ffffff;
--heading-color: #c5d5e6;
--form-background: #222837;
--text-on-primary: #ffffff;
--button-text: #ffffff;
--neutral-light: #d8e1ea;
--neutral-medium: #8494a7;
--neutral-dark: #2d364a;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: "PT Serif", serif;
font-weight: 400;
font-style: normal;
}
body {
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
.page-container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background-color: var(--background-color);
padding: 1rem 2rem;
box-shadow: 0 6px 8px rgba(0, 0, 0, 0.5);
position: sticky;
top: 0;
z-index: 100;
}
.nav-content {
max-width: 1200px;
margin: 0;
width: 100%;
}
.logo-container {
display: flex;
align-items: center;
gap: 1rem;
text-decoration: none;
color: inherit;
}
.logo-placeholder {
width: 40px;
height: 40px;
object-fit: contain;
}
.logo-text {
font-size: 1.5rem;
font-weight: bold;
color: var(--heading-color);
}
.container {
display: flex;
flex: 1;
max-width: 1200px;
margin: 0 auto;
width: 100%;
}
.sidebar {
width: 250px;
background-color: var(--form-background);
padding: 2rem 1rem;
border-right: 1px solid var(--neutral-dark);
position: sticky;
top: 80px;
height: calc(100vh - 80px);
overflow-y: auto;
}
.sidebar h3 {
color: var(--heading-color);
margin-bottom: 1rem;
font-size: 1.1rem;
}
.sidebar ul {
list-style: none;
margin-bottom: 2rem;
}
.sidebar li {
margin-bottom: 0.5rem;
}
.sidebar a {
color: var(--neutral-medium);
text-decoration: none;
padding: 0.25rem 0;
display: block;
transition: color 0.2s ease;
font-size: 0.9rem;
}
.sidebar a:hover {
color: var(--accent-color);
}
.sidebar .subsection {
margin-left: 1rem;
font-size: 0.85rem;
}
main {
flex: 1;
padding: 2rem;
overflow-y: auto;
}
h1 {
font-size: 2.5rem;
color: var(--heading-color);
margin-bottom: 1rem;
border-bottom: 2px solid var(--accent-color);
padding-bottom: 0.5rem;
}
h2 {
font-size: 2rem;
color: var(--heading-color);
margin: 3rem 0 1rem 0;
border-bottom: 1px solid var(--neutral-dark);
padding-bottom: 0.5rem;
}
h3 {
font-size: 1.5rem;
color: var(--heading-color);
margin: 2rem 0 1rem 0;
}
h4 {
font-size: 1.2rem;
color: var(--heading-color);
margin: 1.5rem 0 0.5rem 0;
}
p {
margin-bottom: 1rem;
color: var(--text-color);
}
ul, ol {
margin-left: 2rem;
margin-bottom: 1rem;
}
li {
margin-bottom: 0.5rem;
color: var(--text-color);
}
code {
background-color: var(--neutral-dark);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-family: 'Courier New', monospace;
color: var(--neutral-light);
}
pre {
background-color: var(--form-background);
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
margin: 1rem 0;
border-left: 4px solid var(--accent-color);
}
pre code {
background: none;
padding: 0;
color: var(--neutral-light);
}
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
background-color: var(--form-background);
border-radius: 6px;
overflow: hidden;
}
th, td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--neutral-dark);
}
th {
background-color: var(--secondary-color);
color: var(--heading-color);
font-weight: bold;
}
td {
color: var(--text-color);
}
.highlight {
background-color: var(--accent-color);
color: var(--button-text);
padding: 0.2rem 0.4rem;
border-radius: 3px;
font-weight: bold;
}
.section {
margin-bottom: 3rem;
}
.back-to-top {
position: fixed;
bottom: 2rem;
right: 2rem;
background-color: var(--primary-color);
color: var(--button-text);
padding: 0.5rem 1rem;
border-radius: 50px;
text-decoration: none;
opacity: 0.8;
transition: opacity 0.2s ease;
}
.back-to-top:hover {
opacity: 1;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.sidebar {
width: 100%;
height: auto;
position: static;
border-right: none;
border-bottom: 1px solid var(--neutral-dark);
}
main {
padding: 1rem;
}
h1 {
font-size: 2rem;
}
h2 {
font-size: 1.5rem;
}
}
</style>
</head>
<body>
<div class="page-container">
<header>
<nav>
<div class="nav-content">
<a href="index.html" class="logo-container">
<img src="d20_no-bg_medium.png" alt="SessionZero Logo" class="logo-placeholder">
<div class="logo-text">SessionZero</div>
</a>
</div>
</nav>
</header>
<div class="container">
<aside class="sidebar">
<h3>Contents</h3>
<ul>
<li><a href="#overview">Overview</a></li>
<li><a href="#file-structure">File Structure</a>
<ul class="subsection">
<li><a href="#header">Header</a></li>
<li><a href="#sections">Sections</a></li>
<li><a href="#nested-sections">Nested Sections</a></li>
<li><a href="#fields">Fields</a></li>
<li><a href="#comments">Comments</a></li>
</ul>
</li>
<li><a href="#field-types">Field Types</a></li>
<li><a href="#file-types">File Type Specifications</a>
<ul class="subsection">
<li><a href="#dataset">Dataset</a></li>
<li><a href="#character-template">Character Template</a></li>
<li><a href="#character">Character</a></li>
</ul>
</li>
<li><a href="#advanced-features">Advanced Features</a></li>
<li><a href="#best-practices">Best Practices</a></li>
</ul>
</aside>
<main>
<h1 id="overview">Session Zero Format (.szf) Specification</h1>
<div class="section">
<h2>Overview</h2>
<p>The Session Zero Format (.szf) is a structured, human-readable data format for defining and storing tabletop RPG data, including characters, templates, and game assets. It is designed to be simple to write by hand, easy to parse, and flexible enough to support a wide variety of game systems.</p>
<h3>Core Principles</h3>
<ul>
<li><strong>Human-Readable:</strong> The format uses plain text and a clear, indented structure that is easy to read and edit.</li>
<li><strong>Structured & Typed:</strong> Data is organized into logical sections with explicitly declared field types defined in templates.</li>
<li><strong>Extensible:</strong> The format can be extended with new fields and sections without breaking existing parsers.</li>
<li><strong>Portable:</strong> .szf files are self-contained and can be easily shared and used across different platforms.</li>
</ul>
</div>
<div class="section">
<h2 id="file-structure">File Structure</h2>
<p>Every .szf file consists of three main parts: a <strong>Header</strong>, a <strong>Metadata Section</strong>, and one or more <strong>Content Sections</strong>.</p>
<h3 id="header">Header</h3>
<p>The header is mandatory and must be the first two lines of the file. It declares the file type and the schema version.</p>
<pre><code>!type: [file_type]
!schema: [schema_version]</code></pre>
<ul>
<li><strong>!type</strong>: Defines the purpose of the file. Valid types are:
<ul>
<li><span class="highlight">dataset</span>: A collection of related game elements (e.g., items, spells).</li>
<li><span class="highlight">character_template</span>: A blueprint defining the structure of a character sheet.</li>
<li><span class="highlight">character</span>: An instance of a character created from a template.</li>
<li><span class="highlight">session</span>: Data related to a specific game session (future use).</li>
</ul>
</li>
<li><strong>!schema</strong>: The version of the .szf specification the file adheres to (e.g., 1.1.0).</li>
</ul>
<h3 id="sections">Sections</h3>
<p>Data is organized into named sections. Sections create a hierarchical structure for organizing related fields.</p>
<p><strong>Syntax:</strong> <code>[SectionName]</code></p>
<p>SectionName must be a single word, is case-sensitive, and must use <strong>PascalCase</strong> (e.g., AbilityScores, CharacterInfo).</p>
<h3 id="nested-sections">Nested Sections</h3>
<p>Sections can be nested to create a logical hierarchy. The dot (.) notation is used to define a parent-child relationship.</p>
<p><strong>Syntax:</strong> <code>[ParentSection.ChildSection]</code></p>
<pre><code>[AbilityScores]
Strength (number) = 10
[AbilityScores.Modifiers]
StrengthMod (calculated) = (AbilityScores.Strength - 10) / 2</code></pre>
<h3 id="fields">Fields</h3>
<p>Fields represent individual pieces of data within a section. Each field has a name, a type, and a value.</p>
<p><strong>Syntax:</strong> <code>FieldName (type) = value</code></p>
<ul>
<li><strong>FieldName</strong>: The name of the field. Must be a single word, is case-sensitive, and must use <strong>PascalCase</strong>.</li>
<li><strong>(type)</strong>: The data type of the field, enclosed in parentheses.</li>
<li><strong>=</strong>: The assignment operator.</li>
<li><strong>value</strong>: The data assigned to the field.</li>
</ul>
<h3 id="comments">Comments</h3>
<p>Comments are denoted by a hash symbol (#) at the beginning of a line. Parsers should ignore comments.</p>
<pre><code># This is a full-line comment.
FieldName (text) = Some Value</code></pre>
</div>
<div class="section">
<h2 id="field-types">Field Types</h2>
<p>The .szf format supports a variety of field types to handle different kinds of data:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Description</th>
<th>Example Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>text</td>
<td>A single line of text.</td>
<td>Elara the Brave</td>
</tr>
<tr>
<td>text-field</td>
<td>A multi-line block of text.</td>
<td>A long sword forged by...\nIt glows faintly.</td>
</tr>
<tr>
<td>number</td>
<td>An integer or floating-point number.</td>
<td>16 or 3.5</td>
</tr>
<tr>
<td>bool</td>
<td>A boolean value.</td>
<td>true or false</td>
</tr>
<tr>
<td>calculated</td>
<td>A value derived from a formula.</td>
<td>(Strength - 10) / 2</td>
</tr>
<tr>
<td>system</td>
<td>A special field that controls application behavior.</td>
<td></td>
</tr>
<tr>
<td>entry-reference</td>
<td>A reference to a single entry from a dataset.</td>
<td>ClassData or ItemData.Longsword</td>
</tr>
<tr>
<td>entry-reference-list</td>
<td>A comma-separated list of references to entries from a dataset.</td>
<td>CoreFeats.PowerAttack, CoreFeats.Cleave</td>
</tr>
</tbody>
</table>
</div>
<div class="section">
<h2 id="file-types">File Type Specifications</h2>
<h3 id="dataset">Dataset (!type: dataset)</h3>
<p>A dataset file is a collection of structured entries.</p>
<h4>Structure:</h4>
<ul>
<li><strong>[Metadata] Section</strong>: Contains information about the dataset.
<ul>
<li><strong>Name (text)</strong>: The human-readable name of the dataset.</li>
<li><strong>Type (text)</strong>: The category of the dataset (e.g., items, spells, classes, etc.). This is a completely arbitrary value, and user-definable, but should match the intended use of the dataset.</li>
<li><strong>Guid (text)</strong>: A globally unique identifier. <strong>Optional</strong>: If left blank, the system can generate one.</li>
<li><strong>Version (text)</strong>: The semantic version of the dataset.</li>
</ul>
</li>
<li><strong>[EntryName] Section</strong>: In datasets, sections are referred to as entries. Each entry represents a specific item, spell, or other game element.
<ul>
<li>EntryName is the unique key for the entry within the dataset.</li>
<li>Every entry must have a non-empty <code>Name (text)</code> field. This is the display name for the entry.</li>
</ul>
</li>
</ul>
<h4>Example: CoreItems.szf</h4>
<pre><code>!type: dataset
!schema: 1.1.0
# Metadata for the entire item collection
[Metadata]
# Required metadata for datasets
Name (text) = Core Fantasy Items
Type (text) = items
Guid (text) = c3d4e5f6-g7h8-9012-cdef-123456789012
Version (text) = 1.0.0
# Optional metadata
Author (text) = Fantasy Creator
Description (text-field) = A collection of basic items for any fantasy campaign.
# Definition for a Longsword
[Longsword]
Name (text) = Longsword
Description (text-field) = A standard sword with a long blade and crossguard.
Category (text) = Weapon
[Longsword.Stats]
Damage (text) = 1d8 slashing
Weight (number) = 3
Cost (number) = 15
IsMartial (bool) = true</code></pre>
<h3 id="character-template">Character Template (!type: character_template)</h3>
<p>A character_template defines the structure, fields, and rules for a character sheet.</p>
<h4>Structure:</h4>
<ul>
<li><strong>[Metadata] Section</strong>: Contains information about the template.</li>
<li><strong>[RequiredDatasets] Section</strong>: Lists all datasets required by this template.
<ul>
<li>The field name is a local alias for the dataset (e.g., CoreItems).</li>
<li>The value is a pipe-separated string: DatasetName|GUID|Version.</li>
</ul>
</li>
<li><strong>[SectionName]</strong>: Defines sections on the character sheet with fields, types, and default values.
<ul>
<li>Subsections can be defined using dot notation (e.g., <code>[AbilityScores.Modifiers]</code>).</li>
</ul>
</li>
<li><strong>System Fields</strong>: Special fields that modify section behavior:
<ul>
<li><code>DatasetType (system)</code>: Restricts a section to hold references from datasets of a specific type (e.g., <code>items</code>).</li>
<li><code>DatasetReference (system)</code>: Restricts a section to hold references from a specific dataset, identified by its alias from <code>[RequiredDatasets]</code>.</li>
<li><code>AllowQuantity (system)</code>: When set, allows entry-reference fields within that section to have an associated quantity field.</li>
</ul>
</li>
</ul>
<h4>Example: FantasyTemplate.szf</h4>
<pre><code>!type: character_template
!schema: 1.1.0
[Metadata]
# Required metadata for character templates
Name (text) = Standard Fantasy Character
Guid (text) = f9e8d7c6-b5a4-3210-9876-543210fedcba
Version (text) = 2.1.0
# Optional metadata
Author (text) = Template Master
# Datasets needed for this character sheet
[RequiredDatasets]
ClassData (text) = Core Classes|aaa-bbb-ccc|1.5.0
ItemData (text) = Core Fantasy Items|c3d4e5f6-g7h8-9012-cdef-123456789012|1.0.0
WeaponData (text) = Core Weapons|12345678-90ab-cdef-1234567890ab|1.0.0
# Character information section
[Info]
CharacterName (text) =
# This field expects a single entry from the 'ClassData' dataset.
Class (entry-reference) = ClassData
# Ability scores with default values
[AbilityScores]
Strength (number) = 10
Dexterity (number) = 10
[AbilityScores.Modifiers]
StrengthMod (calculated) = (AbilityScores.Strength - 10) / 2
DexterityMod (calculated) = (AbilityScores.Dexterity - 10) / 2
# An equipment section linked to a specific dataset `WeaponData`
# This section can only contain references to entries in the `WeaponData` dataset.
[Equipment]
DatasetReference (system) = WeaponData
# An inventory section that can hold multiple items from the `ItemData` dataset.
[Inventory]
DatasetType (system) = items
AllowQuantity (system) = true</code></pre>
<h3 id="character">Character (!type: character)</h3>
<h4>Structure:</h4>
<ul>
<li><strong>[Metadata] Section</strong>: Contains information about the character.
<ul>
<li><strong>TemplateRef (text)</strong>: A mandatory reference to the source template: TemplateName|GUID|Version.</li>
</ul>
</li>
<li><strong>Content Sections</strong>: The sections defined in the template are populated with the character's specific values. Field types are not re-declared.</li>
</ul>
<p>A character file is an instance of a character_template filled with specific data. It <strong>only contains values</strong>, as the structure and types are defined by the template.</p>
<h4>Structure:</h4>
<ul>
<li><strong>[Metadata] Section</strong>: Contains information about the character.
<ul>
<li><strong>TemplateRef (text)</strong>: A mandatory reference to the source template: TemplateName|GUID|Version.</li>
</ul>
</li>
<li><strong>Content Sections</strong>: The sections defined in the template are populated with the character's specific values. Field types are not re-declared.</li>
</ul>
<h4>Example: Aela.szf</h4>
<pre><code>!type: character
!schema: 1.1.0
[Metadata]
# Required metadata for character files
Name = Aela the Huntress
Guid = a1b2c3d4-e5f6-7890-abcd-ef1234567890
Version = 1.0.0
TemplateRef = Standard Fantasy Character|f9e8d7c6-b5a4-3210-9876-543210fedcba|2.1.0
[Info]
CharacterName = Aela the Huntress
# The value 'Ranger' is an entry from the 'ClassData' dataset,
# as specified by the 'Class' field in the template.
Class = Ranger
[AbilityScores]
Strength = 16
Dexterity = 14
# Calculated values are not stored in the character file.
[Equipment]
# This section becomes an addable list of items with quantities restricted to the `WeaponData` dataset, as defined in the template.
# 'PrimaryWeapon' is a user-defined field name for the item.
# The value 'Longsword' refers to the 'Longsword' entry in the 'WeaponData' dataset.
PrimaryWeapon = ItemData.Longsword
PrimaryWeapon.Quantity = 1
[Inventory]
# This section becomes an addable list of items from any dataset of type 'items', and allows specifying quantities.
# 'HealingPotion' and 'HempRope' are user-defined field names for the items,
# collected from the 'ItemData' notated by the prefix.
HealingPotion = ItemData.HealingPotion
HealingPotion.Quantity = 5
HempRope = ItemData.HempRope
HempRope.Quantity = 2</code></pre>
</div>
<div class="section">
<h2 id="advanced-features">Advanced Features</h2>
<h3>System Properties</h3>
<p>System properties are special fields within a character_template that modify the behavior of a section:</p>
<ul>
<li><code>DatasetType (system) = type</code>: Restricts a section to holding references from datasets of a specific type (e.g., items).</li>
<li><code>DatasetReference (system) = alias</code>: Restricts a section to holding references from a specific dataset, identified by its alias from <code>[RequiredDatasets]</code>.</li>
<li><strong>Note:</strong> A section can have DatasetType or DatasetReference, but not both.</li>
<li><code>AllowQuantity (system) = true</code>: When set, allows entry-reference fields within that section to have an associated quantity field.</li>
</ul>
<h3>Quantity Fields</h3>
<p>If a template section has <code>AllowQuantity (system) = true</code>, a character file can specify quantities using:</p>
<p><strong>Syntax:</strong> <code>ReferenceFieldName.Quantity = value</code></p>
<h3>Calculated Fields</h3>
<p>Fields of type <code>calculated</code> contain formulas that are evaluated by the application:</p>
<ul>
<li>Formulas can reference other fields using dot notation to access other sections (e.g., AbilityScores.Strength).</li>
<li>Supported operations should include basic arithmetic (+, -, *, /), and parentheses for grouping.</li>
</ul>
<h3>Wildcard Calculations</h3>
<p>For sections where AllowQuantity is enabled, special wildcard syntax can be used in calculated fields to perform aggregate calculations:</p>
<ul>
<li><code>SectionName.*.FieldName</code>: Refers to the FieldName property of <em>all</em> items referenced in SectionName.</li>
</ul>
<h4>Example (in a template):</h4>
<pre><code>[Inventory]
DatasetType (system) = items
AllowQuantity (system) = true
TotalWeight (calculated) = Inventory.*.Weight * Inventory.*.Quantity</code></pre>
</div>
<div class="section">
<h2 id="best-practices">Best Practices</h2>
<ol>
<li><strong>GUIDs:</strong> Leave this blank, and the system will generate a GUID for you. This ensures uniqueness across datasets and templates. If you need to specify one, use a valid GUID format.</li>
<li><strong>Versioning:</strong> Use semantic versioning for your files to manage updates and breaking changes.</li>
<li><strong>Naming Convention:</strong> Strictly use <strong>PascalCase</strong> for all SectionNames and FieldNames.</li>
<li><strong>Nested sections/Sub-sections:</strong> Use dot notation to create logical hierarchies but avoid excessive nesting to maintain readability.</li>
<li><strong>Dependencies:</strong> Explicitly list all required datasets in templates.</li>
<li><strong>Validation:</strong> Before use, validate that a file's structure is correct and all references are valid.</li>
</ol>
</div>
</main>
</div>
<a href="#overview" class="back-to-top">↑ Top</a>
</div>
<script>
// Smooth scrolling for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
}
});
});
// Highlight current section in sidebar
const observerOptions = {
rootMargin: '-20% 0px -70% 0px',
threshold: 0
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.id;
document.querySelectorAll('.sidebar a').forEach(link => {
link.classList.remove('active');
});
const activeLink = document.querySelector(`.sidebar a[href="#${id}"]`);
if (activeLink) {
activeLink.style.color = 'var(--accent-color)';
}
}
});
}, observerOptions);
document.querySelectorAll('h2[id], h3[id]').forEach(heading => {
observer.observe(heading);
});
</script>
</body>
</html>