splinky dingus

This commit is contained in:
Nicholai Vogel 2026-01-14 14:59:00 -07:00
commit 421f637eba
17 changed files with 1833 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
bun.lock

133
App.tsx Normal file
View File

@ -0,0 +1,133 @@
import React, { useLayoutEffect, useRef } from 'react';
import { Sidebar } from './components/Sidebar';
import { GUIDE_CONTENT } from './constants';
import { ContentRenderer } from './components/ContentRenderer';
import { GitBranch, ArrowDown, Leaf } from 'lucide-react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
const App: React.FC = () => {
const mainRef = useRef<HTMLDivElement>(null);
const heroRef = useRef<HTMLDivElement>(null);
const titleRef = useRef<HTMLHeadingElement>(null);
const subtitleRef = useRef<HTMLParagraphElement>(null);
useLayoutEffect(() => {
const ctx = gsap.context(() => {
// Hero Animation
const tl = gsap.timeline();
tl.fromTo(titleRef.current,
{ y: 50, opacity: 0, rotate: 2 },
{ y: 0, opacity: 1, rotate: 0, duration: 1, ease: "power3.out" }
)
.fromTo(subtitleRef.current,
{ y: 20, opacity: 0 },
{ y: 0, opacity: 1, duration: 0.8, ease: "power2.out" },
"-=0.5"
);
// Section Entry Animations
const sections = gsap.utils.toArray<HTMLElement>('.guide-section');
sections.forEach(section => {
gsap.fromTo(section,
{ opacity: 0, y: 50 },
{
opacity: 1,
y: 0,
duration: 0.8,
ease: "power2.out",
scrollTrigger: {
trigger: section,
start: "top 80%",
toggleActions: "play none none reverse"
}
}
);
});
}, mainRef);
return () => ctx.revert();
}, []);
return (
<div className="flex min-h-screen bg-background text-foreground font-sans selection:bg-primary/20 selection:text-primary">
<Sidebar />
<main ref={mainRef} className="flex-1 lg:ml-72 w-full">
{/* Hero Section */}
<div ref={heroRef} className="min-h-[80vh] flex flex-col justify-center px-8 md:px-16 lg:px-24 bg-card relative overflow-hidden">
{/* Subtle blurred blobs */}
<div className="absolute top-0 right-0 w-96 h-96 bg-primary/10 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2 pointer-events-none"></div>
<div className="absolute bottom-0 left-0 w-64 h-64 bg-secondary/20 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2 pointer-events-none"></div>
<div className="max-w-3xl relative z-10">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary text-sm font-medium mb-6 animate-fade-in">
<GitBranch size={16} />
<span>Interactive Guide</span>
</div>
<h1 ref={titleRef} className="text-5xl md:text-7xl font-bold tracking-tight text-foreground mb-8">
Git Guide <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-primary to-accent">
for Sylvi 🌿
</span>
</h1>
<p ref={subtitleRef} className="text-xl text-muted-foreground max-w-2xl leading-relaxed">
Welcome! This guide will walk you through version control from the ground up.
Think of it as a time machine for your codesave snapshots, experiment freely,
and never lose your work again.
</p>
<div className="mt-12 flex items-center gap-4 text-muted-foreground text-sm animate-bounce">
<ArrowDown size={20} />
<span>Scroll to start learning</span>
</div>
</div>
</div>
{/* Content Sections */}
<div className="px-6 md:px-16 lg:px-24 py-12 max-w-5xl mx-auto pb-32">
{GUIDE_CONTENT.map((section, index) => (
<section
key={section.id}
id={section.id}
className="guide-section mb-24 scroll-mt-24"
>
<div className="flex items-center gap-4 mb-8">
<span className="flex items-center justify-center w-10 h-10 rounded-full bg-foreground text-background font-mono font-bold text-lg shadow-lg">
{index + 1}
</span>
<h2 className="text-3xl font-bold text-foreground">{section.title.split('. ')[1]}</h2>
</div>
<div className="pl-0 md:pl-14">
{section.blocks.map((block, i) => (
<ContentRenderer key={i} block={block} />
))}
</div>
{index < GUIDE_CONTENT.length - 1 && (
<div className="h-px w-full bg-gradient-to-r from-transparent via-border to-transparent mt-16"></div>
)}
</section>
))}
{/* Footer */}
<footer className="mt-20 pt-10 border-t border-border text-center text-muted-foreground">
<p className="flex items-center justify-center gap-2 mb-4">
<Leaf className="text-primary" size={20} />
<span>Made for Sylvi</span>
</p>
<p className="text-sm">Remember, Nicholai is just a message away if you get stuck!</p>
</footer>
</div>
</main>
</div>
);
};
export default App;

547
README.md Normal file
View File

@ -0,0 +1,547 @@
# Git Guide for Sylvi 🌿
Welcome! This guide will walk you through git from the ground up. Git is a version control system that helps you track changes in your projects, collaborate with others, and never lose your work. Think of it as a time machine for your code—you can save snapshots of your project at any point and jump back to them whenever you need.
## Table of Contents
1. What is Git?
2. Initial Setup
3. Understanding File Types in Git
4. Creating Your First Repository
5. The Basic Git Workflow
6. Cloning Repositories
7. Branching & Merging
8. Common Pitfalls & How to Fix Them
9. Using Gitea's Web Interface
10. Quick Reference Cheat Sheet
## 1. What is Git?
Git is a **version control system**. Imagine you're working on a game design document, and you want to try out a new idea for a level, but you're not sure if it'll work. With git, you can save your current version, try the new idea, and if it doesn't work out, you can go back to exactly where you were before. No "final_FINAL_v3_actually_final.txt" nonsense.
Git tracks changes to files over time, creating a history of your project. Every time you "save" (which we call a **commit**), git takes a snapshot of your entire project. You can view any of these snapshots at any time, compare them, or even combine changes from different versions.
## 2. Initial Setup
First, let's configure git with your information. Open your terminal and run these commands:
```bash
# Set your name (this will appear in your commits)
git config --global user.name "Sylvi"
# Set your email (use the email you registered with Gitea)
git config --global user.email "your-email@example.com"
# Set your default text editor (optional, but helpful)
git config --global core.editor "nano" # or vim, code, etc.
# Check your configuration
git config --list
```
**💡 Why This Matters**
Every change you make in git is tagged with your name and email. This helps everyone (including future you) know who made which changes and when.
## 3. Understanding File Types in Git
When working with git, you'll encounter a few special files and formats. Let's break them down:
### .git Directory
This hidden folder is where git stores all its magic. It contains the entire history of your project, all your commits, branches, and configuration. You'll never need to edit files in here directly—git handles it all. If you delete this folder, you lose all your git history (but not your current files).
### .gitignore File
This file tells git which files or folders to ignore. It's super useful for keeping temporary files, build artifacts, or sensitive data out of your repository.
```
# Example .gitignore file
# Ignore all .log files
*.log
# Ignore the node_modules directory
node_modules/
# Ignore OS-specific files
.DS_Store
Thumbs.db
# Ignore your personal config
config/secrets.json
```
### README.md
Not required by git, but it's a convention to include a README file (usually in Markdown format) that explains what your project is, how to use it, and any other important information. This is the first thing people see when they visit your repository.
### LICENSE
If you're sharing your code, it's good practice to include a license file that specifies how others can use your work. Common options include MIT, GPL, or Creative Commons licenses.
### Common File Formats You'll Work With
- **.md** - Markdown files (like README.md) for documentation
- **.html, .css, .js** - Web development files
- **.json** - Configuration and data files
- **.yml/.yaml** - Configuration files (common in DevOps)
- **.txt** - Plain text files
- **No extension** - Shell scripts, config files (like .gitignore)
**Binary Files:** Images (.png, .jpg), compiled code, videos, etc. Git can track these, but it can't show you the differences between versions like it can with text files.
## 4. Creating Your First Repository
There are two ways to start with git: create a new repository from scratch, or clone an existing one. Let's start with creating one locally.
### Method 1: Local Repository (Command Line)
```bash
# Create a new directory for your project
mkdir my-awesome-project
cd my-awesome-project
# Initialize a git repository
git init
# Check the status (you should see "No commits yet")
git status
```
That's it! You now have a git repository. The `git init` command created that `.git` folder we talked about earlier.
### Method 2: Create on Gitea First (Web Interface)
You can also create a repository through Gitea's web interface, then clone it to your computer. This is often easier when you want to set up a README, license, or .gitignore right away.
1. Log into your Gitea account
2. Click the "+" icon or "New Repository" button
3. Give it a name, description, and choose whether it's public or private
4. Optionally add a README, .gitignore template, and license
5. Click "Create Repository"
Gitea will then show you the commands to clone it to your computer (we'll cover cloning in section 6).
### Practice Exercise #1
Create a new directory called `git-practice` and initialize it as a git repository. Then check its status.
**Solution:**
```bash
mkdir git-practice
cd git-practice
git init
git status
```
You should see a message like "On branch main" (or master) and "No commits yet".
## 5. The Basic Git Workflow
The core git workflow has three main stages, and once you understand these, everything else falls into place:
1. **Working Directory** - Your actual files that you're editing
2. **Staging Area** - Files you've marked to be included in the next commit
3. **Repository** - The committed history of your project
Here's the typical flow:
### Step 1: Make Changes
Create or edit files in your project.
```bash
# Create a new file
echo "# My Awesome Project" > README.md
# Check what's changed
git status
```
Git will show you that `README.md` is an "untracked file." It knows the file exists, but it's not tracking changes to it yet.
### Step 2: Stage Changes
Tell git which changes you want to include in the next commit. This is like packing a box before you ship it.
```bash
# Stage a specific file
git add README.md
# Or stage all changes at once
git add .
# Check status again
git status
```
Now the file shows up under "Changes to be committed." It's in the staging area.
### Step 3: Commit Changes
Create a snapshot of your staged changes with a message describing what you did.
```bash
# Commit with a message
git commit -m "Add README file"
# View your commit history
git log
```
**💡 Writing Good Commit Messages**
Your commit messages should be clear and descriptive. Think of them as notes to your future self (or your collaborators). Good examples:
- ✅ "Add player movement controls"
- ✅ "Fix collision detection bug in level 3"
- ✅ "Update homepage layout with new color scheme"
- ❌ "stuff" (too vague)
- ❌ "fixed bug" (which bug?)
### Step 4: Push to Remote
If you're working with Gitea (or any remote repository), you'll want to push your commits to the server so they're backed up and others can see them.
```bash
# Push your commits to the remote repository
git push origin main
# Or if your default branch is named 'master'
git push origin master
```
**Origin** is the name git gives to your remote repository (the one on Gitea). **Main** (or master) is the name of your primary branch.
**⚠️ First Push Setup**
The first time you push to a new repository, you might need to set up the connection:
```bash
# Add your Gitea repository as the remote
git remote add origin https://your-gitea-instance.com/username/repo-name.git
# Set the default upstream branch
git push -u origin main
```
After this initial setup, you can just use `git push`.
### Step 5: Pull Updates
If you're collaborating with others (or working on multiple computers), you'll need to pull changes from the remote repository before you start working.
```bash
# Fetch and merge changes from the remote
git pull origin main
```
This downloads any new commits from Gitea and merges them into your local repository.
### Practice Exercise #2
In your `git-practice` directory:
1. Create a file called `index.html` with some basic HTML
2. Stage the file
3. Commit it with a descriptive message
4. View your commit history
**Solution:**
```bash
# Create the file (you can use your favorite editor instead of echo)
echo "<!DOCTYPE html><html><body><h1>Hello!</h1></body></html>" > index.html
# Stage it
git add index.html
# Commit it
git commit -m "Add basic HTML homepage"
# View history
git log
```
## 6. Cloning Repositories
Cloning is how you download a copy of an existing repository from Gitea (or GitHub, GitLab, etc.) to your computer. It's like getting a complete copy of someone else's project, including all its history.
```bash
# Clone a repository
git clone https://your-gitea-instance.com/username/repo-name.git
# Clone into a specific directory name
git clone https://your-gitea-instance.com/username/repo-name.git my-local-folder
# Clone and automatically set up remote tracking
git clone https://your-gitea-instance.com/username/repo-name.git
cd repo-name
git remote -v # Shows your remote connections
```
When you clone, git automatically sets up the remote connection for you (it calls it "origin"), so you can immediately start pushing and pulling.
**💡 HTTPS vs SSH**
You'll see two types of URLs for cloning: HTTPS (`https://...`) and SSH (`git@...`). HTTPS is simpler to start with—you'll just need to enter your username and password when pushing. SSH is more secure and convenient once set up, but requires generating and adding SSH keys to Gitea.
### Practice Exercise #3
Find a repository on your Gitea instance (maybe one of Nicholai's projects, with permission!) and clone it to your computer. Explore the files and check out the commit history with `git log`.
## 7. Branching & Merging
Branches are one of git's most powerful features. They let you work on new features or experiments without affecting the main codebase. Think of them like parallel universes for your project—you can try things out in one branch, and if they don't work, you can just delete that branch without any consequences to your main code.
### Understanding Branches
When you create a new repository, you start on a default branch (usually called `main` or `master`). This is your primary timeline. When you create a new branch, you're creating a copy of the current state that you can modify independently.
### Creating and Switching Branches
```bash
# Create a new branch
git branch new-feature
# Switch to that branch
git checkout new-feature
# Or create and switch in one command
git checkout -b new-feature
# See all your branches
git branch
# The branch with * is your current branch
```
Now any commits you make will be on the `new-feature` branch, not on `main`.
### Merging Branches
Once you're happy with your changes on a branch, you can merge them back into your main branch.
```bash
# Switch back to main
git checkout main
# Merge the new-feature branch into main
git merge new-feature
# Delete the branch if you're done with it
git branch -d new-feature
```
### A Typical Branching Workflow
1. You're working on `main` and want to add a new feature
2. Create a new branch: `git checkout -b add-menu-system`
3. Make your changes and commit them to this branch
4. Switch back to main: `git checkout main`
5. Merge your feature: `git merge add-menu-system`
6. Delete the feature branch: `git branch -d add-menu-system`
**⚠️ Merge Conflicts**
Sometimes, git can't automatically merge branches because the same lines of code were changed in both branches. This is called a **merge conflict**. Don't panic! Git will mark the conflicting sections in your files, and you'll need to manually choose which changes to keep.
Files with conflicts will look like this:
```
<<<<<<< HEAD
Your changes on the current branch
=======
Changes from the branch being merged
>>>>>>> new-feature
```
Edit the file to keep what you want, remove the markers, then stage and commit the resolved file.
### Practice Exercise #4
In your practice repository:
1. Create a new branch called `experiment`
2. Switch to it and make some changes to your HTML file
3. Commit those changes
4. Switch back to `main`
5. Merge the `experiment` branch into `main`
**Solution:**
```bash
git checkout -b experiment
# Edit index.html with your editor
git add index.html
git commit -m "Try experimental layout"
git checkout main
git merge experiment
git branch -d experiment
```
## 8. Common Pitfalls & How to Fix Them
Everyone makes mistakes with git. Here are some common ones and how to recover from them.
### Mistake #1: Committed to the Wrong Branch
If you haven't pushed yet:
```bash
# Create a new branch with your current changes
git branch correct-branch
# Reset the current branch to the previous commit
git reset --hard HEAD~1
# Switch to the correct branch
git checkout correct-branch
```
### Mistake #2: Want to Undo the Last Commit
```bash
# Undo the last commit but keep the changes
git reset --soft HEAD~1
# Undo the last commit and discard the changes (careful!)
git reset --hard HEAD~1
# Undo the last commit and create a new commit that reverses it
git revert HEAD
```
`revert` is safer if you've already pushed, as it doesn't rewrite history.
### Mistake #3: Accidentally Staged the Wrong Files
```bash
# Unstage a specific file
git reset HEAD file-name.txt
# Unstage all files
git reset HEAD
```
### Mistake #4: Need to See What Changed
```bash
# See unstaged changes
git diff
# See staged changes
git diff --staged
# See changes in a specific file
git diff file-name.txt
# See changes between commits
git diff commit1 commit2
```
### Mistake #5: Merge Conflict Panic
If you're in the middle of a merge and want to abort:
```bash
git merge --abort
```
To resolve conflicts properly:
1. Open the conflicting files
2. Look for the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`)
3. Decide which changes to keep and edit the file
4. Remove the conflict markers
5. Stage the resolved file: `git add file-name.txt`
6. Complete the merge: `git commit`
### Mistake #6: Pushed Sensitive Data (Passwords, Keys, etc.)
**⚠️ Important Security Warning**
If you accidentally commit and push sensitive information (passwords, API keys, etc.), removing it from git history isn't enough—you should consider that data compromised. Change the passwords/keys immediately, then remove them from git history.
This is why .gitignore is so important! Always ignore files with sensitive data.
## 9. Using Gitea's Web Interface
While the command line is powerful, Gitea's web interface can be more convenient for certain tasks.
### Viewing Files and Commits
Browse your repository's files, view the commit history, and see who changed what. Click on any commit to see the exact changes (the "diff").
### Creating and Editing Files
You can create new files or edit existing ones directly in the browser. This is handy for quick fixes or updating documentation.
### Pull Requests
If you're collaborating with Nicholai or others, pull requests (PRs) are how you propose changes. You create a branch, push your changes, then open a PR in Gitea to ask for those changes to be merged into the main branch. Others can review your code and suggest changes before merging.
### Issues
Gitea has a built-in issue tracker for reporting bugs, requesting features, or discussing ideas. Each issue can have labels, assignees, and comments.
### Releases
When your project reaches a milestone, you can create a release—a snapshot of your code at that point, usually with a version number (like v1.0.0).
**💡 Gitea vs GitHub**
Gitea is very similar to GitHub—the interface and workflows are almost identical. If you learn one, you'll be comfortable with the other. The main difference is that Gitea is self-hosted (running on Nicholai's server), while GitHub is a cloud service.
## 10. Quick Reference Cheat Sheet
### Repository Setup
```bash
git init # Create a new repository
git clone <url> # Clone an existing repository
git remote add origin <url> # Connect to a remote repository
```
### Basic Workflow
```bash
git status # Check what's changed
git add <file> # Stage a file
git add . # Stage all changes
git commit -m "message" # Commit staged changes
git push origin main # Push to remote
git pull origin main # Pull from remote
```
### Branching
```bash
git branch # List branches
git branch <name> # Create a new branch
git checkout <branch> # Switch to a branch
git checkout -b <branch> # Create and switch to a new branch
git merge <branch> # Merge a branch into current branch
git branch -d <branch> # Delete a branch
```
### Viewing History
```bash
git log # View commit history
git log --oneline # Compact commit history
git diff # See unstaged changes
git diff --staged # See staged changes
git show <commit> # Show details of a commit
```
### Undoing Changes
```bash
git reset HEAD <file> # Unstage a file
git reset --soft HEAD~1 # Undo last commit, keep changes
git reset --hard HEAD~1 # Undo last commit, discard changes
git revert <commit> # Create new commit that undoes changes
git checkout -- <file> # Discard changes to a file
```
### Helpful Commands
```bash
git config --list # View your configuration
git remote -v # View remote connections
git branch -a # List all branches (including remote)
git fetch # Download remote changes without merging
git clean -fd # Remove untracked files and directories
```
---
## Where to Go From Here
This guide covers the essentials, but git has a lot more to offer. As you get comfortable with these basics, you might want to explore:
- **Git hooks** - Automate tasks when certain git events happen
- **Rebasing** - An alternative to merging that keeps history cleaner
- **Stashing** - Temporarily save changes without committing
- **Submodules** - Include other git repositories within your project
- **Tags** - Mark specific commits as important milestones
The best way to learn git is to use it. Start with small personal projects, make frequent commits with good messages, and don't be afraid to experiment—that's what branches are for! And remember, Nicholai's just a message away if you get stuck.
Happy coding! 🌱

View File

@ -0,0 +1,138 @@
import React from 'react';
import { ContentBlock } from '../types';
import { TerminalBlock } from './TerminalBlock';
import { GitWorkflowDiagram } from './GitWorkflowDiagram';
import { AlertTriangle, Lightbulb, Info, CheckCircle2, ChevronDown } from 'lucide-react';
import gsap from 'gsap';
interface ContentRendererProps {
block: ContentBlock;
}
const Callout: React.FC<{ variant?: string; content: string }> = ({ variant = 'info', content }) => {
const getIcon = () => {
switch(variant) {
case 'warning': return <AlertTriangle className="text-destructive" />;
case 'tip': return <Lightbulb className="text-accent" />;
case 'success': return <CheckCircle2 className="text-primary" />;
default: return <Info className="text-blue-500" />;
}
};
const getStyles = () => {
switch(variant) {
case 'warning': return 'bg-destructive/10 border-destructive/20 text-destructive-foreground';
case 'tip': return 'bg-accent/10 border-accent/20 text-accent-foreground';
case 'success': return 'bg-primary/10 border-primary/20 text-primary-foreground';
default: return 'bg-blue-50 border-blue-200 text-blue-900';
}
};
// Adjusting styles to match new variables more closely for text colors where simple bg/text classes might fail due to variable usage
// The tailwind classes using vars (like text-primary) work if defined in config, which we did.
const getContainerClass = () => {
switch(variant) {
case 'warning': return 'bg-red-50 border-red-200 text-red-900';
case 'tip': return 'bg-amber-50 border-amber-200 text-amber-900';
case 'success': return 'bg-green-50 border-green-200 text-green-900';
default: return 'bg-blue-50 border-blue-200 text-blue-900';
}
}
return (
<div className={`flex gap-4 p-4 my-4 rounded-lg border-l-4 ${getContainerClass()}`}>
<div className="shrink-0 pt-0.5">{getIcon()}</div>
<p className="text-sm md:text-base">{content}</p>
</div>
);
};
const Collapsible: React.FC<{ content: any, variant?: string }> = ({ content, variant }) => {
const [isOpen, setIsOpen] = React.useState(false);
const bodyRef = React.useRef<HTMLDivElement>(null);
const contentRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (!contentRef.current || !bodyRef.current) return;
if (isOpen) {
gsap.to(bodyRef.current, {
height: contentRef.current.scrollHeight,
duration: 0.4,
ease: "power2.out",
opacity: 1
});
} else {
gsap.to(bodyRef.current, {
height: 0,
duration: 0.3,
ease: "power2.in",
opacity: 0
});
}
}, [isOpen]);
const isPractice = variant === 'success';
return (
<div className={`my-4 border rounded-lg overflow-hidden transition-all duration-300 ${isPractice ? 'border-primary/20 shadow-sm' : 'border-border bg-card'}`}>
<button
onClick={() => setIsOpen(!isOpen)}
className={`w-full flex items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors ${isPractice ? 'bg-primary/5' : ''}`}
>
<span className={`font-semibold ${isPractice ? 'text-primary' : 'text-card-foreground'}`}>
{content.title}
</span>
<ChevronDown
className={`transform transition-transform duration-300 text-muted-foreground ${isOpen ? 'rotate-180' : ''}`}
size={20}
/>
</button>
<div ref={bodyRef} className="h-0 opacity-0 overflow-hidden">
<div ref={contentRef} className="p-4 pt-0 border-t border-border">
{Array.isArray(content.body) && content.body.map((subBlock: ContentBlock, idx: number) => (
<ContentRenderer key={idx} block={subBlock} />
))}
</div>
</div>
</div>
);
};
export const ContentRenderer: React.FC<ContentRendererProps> = ({ block }) => {
switch (block.type) {
case 'paragraph':
return <p className="mb-4 text-muted-foreground leading-relaxed text-lg">{block.content as string}</p>;
case 'code':
return <TerminalBlock code={block.content as string} language={block.language} />;
case 'callout':
return <Callout variant={block.variant} content={block.content as string} />;
case 'list':
return (
<ul className="list-disc pl-6 mb-4 space-y-2 text-muted-foreground">
{(block.content as string[]).map((item, i) => (
<li key={i} className="pl-2">{item}</li>
))}
</ul>
);
case 'ordered-list':
return (
<ol className="list-decimal pl-6 mb-4 space-y-2 text-muted-foreground">
{(block.content as string[]).map((item, i) => (
<li key={i} className="pl-2">{item}</li>
))}
</ol>
);
case 'subheading':
return <h3 className="text-xl font-bold text-foreground mt-8 mb-4 flex items-center gap-2">
<span className="w-1.5 h-6 bg-primary rounded-full inline-block"></span>
{block.content as string}
</h3>;
case 'collapsible':
return <Collapsible content={block.content} variant={block.variant} />;
case 'diagram':
return <GitWorkflowDiagram />;
default:
return null;
}
};

View File

@ -0,0 +1,183 @@
import React, { useState, useRef, useEffect } from 'react';
import gsap from 'gsap';
import { FileText, ArrowRight, Database, Layers, HardDrive, RefreshCw } from 'lucide-react';
export const GitWorkflowDiagram: React.FC = () => {
const [state, setState] = useState<'modified' | 'staged' | 'committed'>('modified');
// Refs for animation targets
const fileRef = useRef<HTMLDivElement>(null);
const workingDirRef = useRef<HTMLDivElement>(null);
const stagingRef = useRef<HTMLDivElement>(null);
const repoRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Initial entrance animation
gsap.fromTo(containerRef.current,
{ opacity: 0, scale: 0.95 },
{ opacity: 1, scale: 1, duration: 0.8, ease: "power2.out" }
);
}, []);
useEffect(() => {
const ctx = gsap.context(() => {
// Reset file position when state changes, then animate to new spot
// Note: In a real complex app we might calculate exact coordinates using getBoundingClientRect
// For this demo, we'll rely on the React re-render to place it in the DOM, then animate "from" the previous position if possible,
// or just animate "in" to emphasize the move.
// Simple scale/pop effect on change
if (fileRef.current) {
gsap.fromTo(fileRef.current,
{ scale: 0.5, opacity: 0, y: 20 },
{ scale: 1, opacity: 1, y: 0, duration: 0.5, ease: "back.out(1.7)" }
);
}
}, containerRef);
return () => ctx.revert();
}, [state]);
const handleReset = () => {
setState('modified');
};
return (
<div ref={containerRef} className="my-10 p-4 sm:p-6 bg-card rounded-xl border border-border shadow-sm overflow-hidden">
<div className="flex justify-between items-center mb-6 border-b border-border pb-4">
<h3 className="font-bold text-lg text-primary">Interactive Workflow</h3>
<button
onClick={handleReset}
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-primary transition-colors"
>
<RefreshCw size={14} />
Reset
</button>
</div>
{/* Zones Container */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 relative min-h-[300px]">
{/* Working Directory Zone */}
<div ref={workingDirRef} className={`relative p-4 rounded-lg border-2 transition-colors duration-300 ${state === 'modified' ? 'border-primary bg-primary/5' : 'border-dashed border-muted'}`}>
<div className="flex items-center gap-2 mb-4 text-sm font-semibold text-foreground/80">
<HardDrive size={18} />
<span>Working Directory</span>
</div>
<p className="text-xs text-muted-foreground mb-8">Where you edit files.</p>
{state === 'modified' && (
<div ref={fileRef} className="bg-white p-3 rounded shadow-md border border-gray-200 flex items-center gap-3">
<FileText className="text-orange-500" />
<div>
<div className="text-sm font-bold">script.js</div>
<div className="text-xs text-orange-600">Modified</div>
</div>
</div>
)}
<div className="absolute bottom-4 left-0 w-full flex justify-center">
<button
disabled={state !== 'modified'}
onClick={() => setState('staged')}
className={`
px-4 py-2 rounded-full text-xs font-bold transition-all
${state === 'modified'
? 'bg-primary text-primary-foreground hover:bg-primary/90 hover:scale-105 shadow-md cursor-pointer'
: 'bg-muted text-muted-foreground cursor-not-allowed opacity-50'
}
`}
>
git add
</button>
</div>
</div>
{/* Staging Area Zone */}
<div ref={stagingRef} className={`relative p-4 rounded-lg border-2 transition-colors duration-300 ${state === 'staged' ? 'border-primary bg-primary/5' : 'border-dashed border-muted'}`}>
<div className="flex items-center gap-2 mb-4 text-sm font-semibold text-foreground/80">
<Layers size={18} />
<span>Staging Area</span>
</div>
<p className="text-xs text-muted-foreground mb-8">Files ready to commit.</p>
{state === 'staged' && (
<div ref={fileRef} className="bg-white p-3 rounded shadow-md border border-gray-200 flex items-center gap-3">
<FileText className="text-green-500" />
<div>
<div className="text-sm font-bold">script.js</div>
<div className="text-xs text-green-600">Staged</div>
</div>
</div>
)}
<div className="absolute bottom-4 left-0 w-full flex justify-center">
<button
disabled={state !== 'staged'}
onClick={() => setState('committed')}
className={`
px-4 py-2 rounded-full text-xs font-bold transition-all
${state === 'staged'
? 'bg-primary text-primary-foreground hover:bg-primary/90 hover:scale-105 shadow-md cursor-pointer'
: 'bg-muted text-muted-foreground cursor-not-allowed opacity-50'
}
`}
>
git commit
</button>
</div>
</div>
{/* Repository Zone */}
<div ref={repoRef} className={`relative p-4 rounded-lg border-2 transition-colors duration-300 ${state === 'committed' ? 'border-primary bg-primary/5' : 'border-dashed border-muted'}`}>
<div className="flex items-center gap-2 mb-4 text-sm font-semibold text-foreground/80">
<Database size={18} />
<span>Repository</span>
</div>
<p className="text-xs text-muted-foreground mb-8">Saved history.</p>
{state === 'committed' && (
<div ref={fileRef} className="bg-white p-3 rounded shadow-md border border-gray-200 flex items-center gap-3">
<FileText className="text-blue-500" />
<div>
<div className="text-sm font-bold">script.js</div>
<div className="text-xs text-blue-600">Committed</div>
</div>
</div>
)}
<div className="absolute bottom-4 left-0 w-full flex justify-center">
<button
disabled={state !== 'committed'}
className={`
px-4 py-2 rounded-full text-xs font-bold transition-all
${state === 'committed'
? 'bg-secondary text-secondary-foreground cursor-default'
: 'bg-muted text-muted-foreground cursor-not-allowed opacity-50'
}
`}
>
Saved!
</button>
</div>
</div>
{/* Connecting Arrows (Visual only, hidden on mobile for cleaner look) */}
<div className="hidden md:block absolute top-1/2 left-[33%] -translate-y-1/2 -translate-x-1/2 z-10 text-muted-foreground/30">
<ArrowRight size={24} />
</div>
<div className="hidden md:block absolute top-1/2 left-[66%] -translate-y-1/2 -translate-x-1/2 z-10 text-muted-foreground/30">
<ArrowRight size={24} />
</div>
</div>
<div className="mt-4 p-3 bg-muted/30 rounded text-xs text-muted-foreground text-center">
{state === 'modified' && "You have changed a file. It's in your Working Directory."}
{state === 'staged' && "You've added the file to the Staging Area. It's ready to be part of the next snapshot."}
{state === 'committed' && "Success! The file is safely stored in the Repository history."}
</div>
</div>
);
};

99
components/Sidebar.tsx Normal file
View File

@ -0,0 +1,99 @@
import React, { useState, useEffect } from 'react';
import { GUIDE_CONTENT } from '../constants';
import { Leaf, Menu, X, GitBranch } from 'lucide-react';
export const Sidebar: React.FC = () => {
const [activeId, setActiveId] = useState<string>('');
const [isMobileOpen, setIsMobileOpen] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
},
{ rootMargin: '-20% 0px -60% 0px' }
);
GUIDE_CONTENT.forEach((section) => {
const element = document.getElementById(section.id);
if (element) observer.observe(element);
});
return () => observer.disconnect();
}, []);
const scrollToSection = (id: string) => {
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
setIsMobileOpen(false);
}
};
return (
<>
{/* Mobile Toggle */}
<div className="fixed top-4 right-4 z-50 lg:hidden">
<button
onClick={() => setIsMobileOpen(!isMobileOpen)}
className="p-2 bg-card rounded-full shadow-lg border border-border text-foreground"
>
{isMobileOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
{/* Sidebar Container */}
<nav className={`
fixed top-0 left-0 h-full bg-card/95 backdrop-blur-md border-r border-border
w-72 transform transition-transform duration-300 z-40
${isMobileOpen ? 'translate-x-0' : '-translate-x-full'}
lg:translate-x-0
`}>
<div className="p-6 h-full flex flex-col">
<div className="flex items-center gap-3 mb-10 group cursor-default">
<div className="p-2 bg-primary/10 rounded-lg group-hover:bg-primary/20 transition-colors">
<Leaf className="text-primary" size={24} />
</div>
<div>
<h1 className="font-bold text-foreground text-lg leading-tight">Git for Sylvi</h1>
<span className="text-xs text-muted-foreground font-mono">v1.0.0</span>
</div>
</div>
<div className="flex-1 overflow-y-auto pr-2 custom-scrollbar">
<div className="space-y-1">
{GUIDE_CONTENT.map((section) => (
<button
key={section.id}
onClick={() => scrollToSection(section.id)}
className={`
w-full text-left px-4 py-3 rounded-md text-sm transition-all duration-200
flex items-center gap-3 group
${activeId === section.id
? 'bg-primary/10 text-primary font-semibold shadow-sm'
: 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
}
`}
>
<span className={`w-1.5 h-1.5 rounded-full transition-colors ${activeId === section.id ? 'bg-primary' : 'bg-muted-foreground/30 group-hover:bg-muted-foreground'}`}></span>
<span className="truncate">{section.title.split('. ')[1]}</span>
</button>
))}
</div>
</div>
<div className="mt-6 pt-6 border-t border-border">
<div className="flex items-center gap-3 text-sm text-muted-foreground">
<GitBranch size={16} />
<span>Happy Coding! 🌱</span>
</div>
</div>
</div>
</nav>
</>
);
};

View File

@ -0,0 +1,163 @@
import React, { useRef, useLayoutEffect, useState } from 'react';
import { Copy, Check, Sprout, Leaf, Wind } from 'lucide-react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
interface TerminalBlockProps {
code: string;
language?: string;
}
export const TerminalBlock: React.FC<TerminalBlockProps> = ({ code, language = 'bash' }) => {
const [copied, setCopied] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const copyBtnRef = useRef<HTMLButtonElement>(null);
const handleCopy = () => {
navigator.clipboard.writeText(code);
setCopied(true);
// Organic "rustle" animation on click
if (copyBtnRef.current) {
gsap.to(copyBtnRef.current, {
rotate: 15,
scale: 1.1,
duration: 0.1,
yoyo: true,
repeat: 1,
ease: "sine.inOut"
});
}
setTimeout(() => setCopied(false), 2000);
};
useLayoutEffect(() => {
const ctx = gsap.context(() => {
// Growth animation using a "spring-like" organic entry
gsap.fromTo(containerRef.current,
{
opacity: 0,
y: 40,
scale: 0.95,
skewX: -1
},
{
opacity: 1,
y: 0,
scale: 1,
skewX: 0,
duration: 1,
ease: "elastic.out(1, 0.75)",
scrollTrigger: {
trigger: containerRef.current,
start: "top 92%",
}
}
);
}, containerRef);
return () => ctx.revert();
}, []);
return (
<div
ref={containerRef}
// Using OKLCH based on the primary hue (142.77) for a perfectly matched "dark forest" look
className="my-10 rounded-[2rem] overflow-hidden border border-primary/20 shadow-2xl relative group bg-[oklch(0.18_0.02_142.77)]"
style={{
boxShadow: '0 20px 50px -12px oklch(0.15 0.05 142.77 / 0.5)'
}}
>
{/* Texture Overlay from index.html */}
<div className="absolute inset-0 grain-texture opacity-10 pointer-events-none" />
{/* Nature-inspired Gradient Mask */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-accent/5 pointer-events-none" />
{/* Organic Header - "The Pebble" style */}
<div className="flex items-center justify-between px-8 py-4 bg-[oklch(0.25_0.03_142.77)]/80 backdrop-blur-md border-b border-white/5 relative z-10">
<div className="flex items-center space-x-4">
<div className="flex space-x-2">
{/* Organic leaf-shaded dots */}
<div className="w-2.5 h-2.5 rounded-full bg-primary animate-pulse" style={{ animationDelay: '0s' }}></div>
<div className="w-2.5 h-2.5 rounded-full bg-accent animate-pulse" style={{ animationDelay: '0.2s' }}></div>
<div className="w-2.5 h-2.5 rounded-full bg-secondary animate-pulse" style={{ animationDelay: '0.4s' }}></div>
</div>
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10">
<Sprout size={14} className="text-primary" />
<span className="text-[10px] font-bold uppercase tracking-[0.2em] text-primary-foreground/70">
{language}
</span>
</div>
</div>
<button
ref={copyBtnRef}
onClick={handleCopy}
className={`
relative overflow-hidden px-4 py-2 rounded-full transition-all duration-500 flex items-center gap-2 group/btn
${copied
? 'bg-primary text-primary-foreground'
: 'bg-white/5 hover:bg-white/10 text-white/50 hover:text-white'
}
`}
>
<div className="relative z-10 flex items-center gap-2">
{copied ? (
<>
<Check size={14} className="stroke-[3px]" />
<span className="text-[10px] font-black uppercase">Copied!</span>
</>
) : (
<>
<Copy size={14} className="group-hover/btn:rotate-12 transition-transform" />
<span className="text-[10px] font-black uppercase">Copy</span>
</>
)}
</div>
{/* Hover leaf effect */}
{!copied && (
<Wind
size={16}
className="absolute -right-4 top-1/2 -translate-y-1/2 opacity-0 group-hover/btn:right-2 group-hover/btn:opacity-20 transition-all duration-500 text-primary"
/>
)}
</button>
</div>
{/* Code Area */}
<div className="p-8 overflow-x-auto relative z-10 custom-scrollbar">
<pre className="leading-relaxed font-mono selection:bg-primary/40 selection:text-white">
<code className="text-[oklch(0.95_0.02_142.77)]">
{code.split('\n').map((line, i) => {
const isComment = line.trim().startsWith('#');
const isCommand = line.trim().startsWith('git');
return (
<span key={i} className="block relative group/line">
{/* Subtle line highlight */}
<div className="absolute -left-8 -right-8 top-0 bottom-0 bg-primary/5 opacity-0 group-hover/line:opacity-100 transition-opacity pointer-events-none" />
<span className={isComment ? 'text-primary/50 italic' : isCommand ? 'text-accent font-bold' : ''}>
{line}
</span>
{'\n'}
</span>
);
})}
</code>
</pre>
</div>
{/* Decorative Organic Elements */}
<div className="absolute bottom-4 right-4 opacity-5 group-hover:opacity-20 transition-all duration-1000 transform group-hover:scale-110 group-hover:rotate-6 pointer-events-none">
<Leaf size={120} className="text-primary" />
</div>
{/* "Growing" accent line at bottom */}
<div className="absolute bottom-0 left-0 h-1 bg-gradient-to-r from-transparent via-primary/30 to-transparent w-full scale-x-0 group-hover:scale-x-100 transition-transform duration-1000 origin-center" />
</div>
);
};

244
constants.tsx Normal file
View File

@ -0,0 +1,244 @@
import { SectionData } from './types';
export const GUIDE_CONTENT: SectionData[] = [
{
id: "intro",
title: "1. What is Git?",
blocks: [
{
type: 'paragraph',
content: "Git is a version control system. Imagine you're working on a game design document, and you want to try out a new idea for a level, but you're not sure if it'll work. With git, you can save your current version, try the new idea, and if it doesn't work out, you can go back to exactly where you were before. No \"final_FINAL_v3_actually_final.txt\" nonsense."
},
{
type: 'paragraph',
content: "Git tracks changes to files over time, creating a history of your project. Every time you \"save\" (which we call a commit), git takes a snapshot of your entire project. You can view any of these snapshots at any time, compare them, or even combine changes from different versions."
}
]
},
{
id: "setup",
title: "2. Initial Setup",
blocks: [
{
type: 'paragraph',
content: "First, let's configure git with your information. Open your terminal and run these commands:"
},
{
type: 'code',
language: 'bash',
content: `# Set your name (this will appear in your commits)
git config --global user.name "Sylvi"
# Set your email (use the email you registered with Gitea)
git config --global user.email "your-email@example.com"
# Set your default text editor (optional, but helpful)
git config --global core.editor "nano"
# Check your configuration
git config --list`
},
{
type: 'callout',
variant: 'tip',
content: "Every change you make in git is tagged with your name and email. This helps everyone (including future you) know who made which changes and when."
}
]
},
{
id: "file-types",
title: "3. Understanding Files",
blocks: [
{
type: 'paragraph',
content: "When working with git, you'll encounter a few special files and formats. Let's break them down:"
},
{
type: 'subheading',
content: ".git Directory"
},
{
type: 'paragraph',
content: "This hidden folder is where git stores all its magic. It contains the entire history of your project. You'll never need to edit files in here directly—git handles it all."
},
{
type: 'subheading',
content: ".gitignore File"
},
{
type: 'paragraph',
content: "This file tells git which files or folders to ignore. It's super useful for keeping temporary files, build artifacts, or sensitive data out of your repository."
},
{
type: 'code',
language: 'plaintext',
content: `# Example .gitignore file
*.log
node_modules/
.DS_Store
Thumbs.db
config/secrets.json`
},
{
type: 'collapsible',
content: {
title: "📄 Common File Formats You'll Work With",
body: [
{
type: 'list',
content: [
".md - Markdown files (like README.md) for documentation",
".html, .css, .js - Web development files",
".json - Configuration and data files",
".yml/.yaml - Configuration files",
"Binary Files: Images, compiled code, etc."
]
}
]
}
}
]
},
{
id: "creating-repo",
title: "4. Creating Your First Repo",
blocks: [
{
type: 'paragraph',
content: "There are two ways to start with git: create a new repository from scratch, or clone an existing one."
},
{
type: 'subheading',
content: "Method 1: Local Repository"
},
{
type: 'code',
language: 'bash',
content: `# Create a new directory for your project
mkdir my-awesome-project
cd my-awesome-project
# Initialize a git repository
git init
# Check the status (you should see "No commits yet")
git status`
},
{
type: 'subheading',
content: "Method 2: Create on Gitea First"
},
{
type: 'ordered-list',
content: [
"Log into your Gitea account",
"Click the '+' icon or 'New Repository' button",
"Give it a name and description",
"Click 'Create Repository'"
]
},
{
type: 'collapsible',
variant: 'success',
content: {
title: "✏️ Practice Exercise #1",
body: [
{ type: 'paragraph', content: "Create a new directory called 'git-practice' and initialize it as a git repository. Then check its status." },
{ type: 'subheading', content: "Solution:" },
{ type: 'code', language: 'bash', content: "mkdir git-practice\ncd git-practice\ngit init\ngit status" }
]
}
}
]
},
{
id: "basic-workflow",
title: "5. The Basic Workflow",
blocks: [
{
type: 'paragraph',
content: "The core git workflow has three main stages: Working Directory (your files), Staging Area (files marked for commit), and Repository (saved history)."
},
{
type: 'diagram',
content: 'workflow'
},
{ type: 'subheading', content: "Step 1: Make Changes" },
{ type: 'code', language: 'bash', content: `echo "# My Awesome Project" > README.md\ngit status` },
{ type: 'subheading', content: "Step 2: Stage Changes" },
{ type: 'paragraph', content: "Tell git which changes you want to include in the next commit." },
{ type: 'code', language: 'bash', content: `git add README.md\n# Or stage all\ngit add .` },
{ type: 'subheading', content: "Step 3: Commit Changes" },
{ type: 'paragraph', content: "Create a snapshot of your staged changes." },
{ type: 'code', language: 'bash', content: `git commit -m "Add README file"` },
{ type: 'callout', variant: 'info', content: "Tip: Write clear commit messages! 'Add player movement controls' is better than 'stuff'." },
{ type: 'subheading', content: "Step 4: Push to Remote" },
{ type: 'code', language: 'bash', content: `git push origin main` },
{ type: 'callout', variant: 'warning', content: "First push? You might need: git push -u origin main" }
]
},
{
id: "cloning",
title: "6. Cloning Repositories",
blocks: [
{ type: 'paragraph', content: "Cloning is how you download a copy of an existing repository from Gitea to your computer." },
{ type: 'code', language: 'bash', content: `git clone https://your-gitea-instance.com/username/repo-name.git` },
{ type: 'callout', variant: 'info', content: "HTTPS is simpler (username/password). SSH is more secure but requires key setup." }
]
},
{
id: "branching",
title: "7. Branching & Merging",
blocks: [
{ type: 'paragraph', content: "Branches are like parallel universes for your project. You can try things out without affecting the main code." },
{ type: 'code', language: 'bash', content: `# Create and switch to new branch
git checkout -b new-feature
# Make changes...
# Switch back to main
git checkout main
# Merge the new feature
git merge new-feature
# Delete the old branch
git branch -d new-feature` },
{ type: 'callout', variant: 'warning', content: "Merge Conflicts happen when git can't automatically combine changes. Don't panic! Open the file, look for <<<<<<< HEAD, fix it, and commit." }
]
},
{
id: "common-pitfalls",
title: "8. Common Pitfalls",
blocks: [
{ type: 'collapsible', content: { title: "Mistake #1: Committed to Wrong Branch", body: [
{ type: 'paragraph', content: "If you haven't pushed yet:" },
{ type: 'code', language: 'bash', content: `git branch correct-branch\ngit reset --hard HEAD~1\ngit checkout correct-branch` }
]} },
{ type: 'collapsible', content: { title: "Mistake #2: Undo Last Commit", body: [
{ type: 'paragraph', content: "Undo commit but keep changes:" },
{ type: 'code', language: 'bash', content: `git reset --soft HEAD~1` }
]} },
{ type: 'collapsible', content: { title: "Mistake #3: Pushed Sensitive Data", body: [
{ type: 'callout', variant: 'warning', content: "If you push passwords/keys, consider them compromised. Change the passwords immediately." }
]} }
]
},
{
id: "cheat-sheet",
title: "9. Cheat Sheet",
blocks: [
{ type: 'paragraph', content: "Quick reference for your daily workflow." },
{ type: 'code', language: 'bash', content: `git status # Check status
git add . # Stage all
git commit -m "m" # Commit
git push # Upload
git pull # Download
git log --oneline # History` }
]
}
];

154
index.html Normal file
View File

@ -0,0 +1,154 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Git Guide for Sylvi 🌿</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Sora:wght@400;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)', 'Sora', 'Inter', 'sans-serif'],
mono: ['var(--font-mono)', 'JetBrains Mono', 'monospace'],
},
colors: {
background: 'var(--background)',
foreground: 'var(--foreground)',
card: 'var(--card)',
'card-foreground': 'var(--card-foreground)',
popover: 'var(--popover)',
'popover-foreground': 'var(--popover-foreground)',
primary: {
DEFAULT: 'var(--primary)',
foreground: 'var(--primary-foreground)',
50: '#f0fdf4',
100: '#dcfce7',
500: 'var(--primary)',
600: 'var(--primary)',
700: '#15803d',
},
secondary: {
DEFAULT: 'var(--secondary)',
foreground: 'var(--secondary-foreground)',
},
muted: {
DEFAULT: 'var(--muted)',
foreground: 'var(--muted-foreground)',
},
accent: {
DEFAULT: 'var(--accent)',
foreground: 'var(--accent-foreground)',
},
destructive: {
DEFAULT: 'var(--destructive)',
foreground: 'var(--destructive-foreground)',
},
border: 'var(--border)',
input: 'var(--input)',
ring: 'var(--ring)',
}
}
}
}
</script>
<style>
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: oklch(0.9818 0.0054 95.0986);
}
::-webkit-scrollbar-thumb {
background: oklch(0.8847 0.0069 97.3627);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--primary);
}
body {
background-color: var(--background);
color: var(--foreground);
overflow-x: hidden;
font-family: var(--font-sans);
}
:root {
--background: oklch(0.9818 0.0054 95.0986);
--foreground: oklch(0.3438 0.0269 95.7226);
--card: oklch(0.9818 0.0054 95.0986);
--card-foreground: oklch(0.2174 0.0019 106.5582);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.2671 0.0196 98.9390);
--primary: oklch(0.6551 0.0198 142.7714);
--primary-foreground: oklch(0.9688 0.0066 106.5223);
--secondary: oklch(0.9245 0.0138 92.9892);
--secondary-foreground: oklch(0.3821 0.0407 166.3004);
--muted: oklch(0.9369 0.0124 91.5218);
--muted-foreground: oklch(0.4866 0.0160 145.3235);
--accent: oklch(0.8097 0.0177 145.4098);
--accent-foreground: oklch(0.2891 0 0);
--destructive: oklch(0.6507 0.1836 39.0189);
--destructive-foreground: oklch(0.8429 0.1511 88.3005);
--border: oklch(0.8847 0.0069 97.3627);
--input: oklch(0.7621 0.0156 98.3528);
--ring: oklch(0.6507 0.1836 39.0189);
--font-sans: 'Sora', 'Inter', sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--radius: 0.5rem;
}
.grain-texture {
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
opacity: 0.05;
pointer-events: none;
}
.dark {
--background: oklch(0.2178 0 0);
--foreground: oklch(0.8074 0.0142 93.0137);
--card: oklch(0.2679 0.0036 106.6427);
--card-foreground: oklch(0.9818 0.0054 95.0986);
--popover: oklch(0.3085 0.0035 106.6039);
--popover-foreground: oklch(0.9211 0.0040 106.4781);
--primary: oklch(0.6551 0.0198 142.7714);
--primary-foreground: oklch(0.9688 0.0066 106.5223);
--secondary: oklch(0.9245 0.0138 92.9892);
--secondary-foreground: oklch(0.3821 0.0407 166.3004);
--muted: oklch(0.2561 0.0071 145.3653);
--muted-foreground: oklch(0.7713 0.0169 99.0657);
--accent: oklch(0.4890 0.0180 145.2933);
--accent-foreground: oklch(0.9663 0.0080 98.8792);
--destructive: oklch(0.6507 0.1836 39.0189);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0.2809 0 0);
--input: oklch(0.6551 0.0198 142.7714);
--ring: oklch(0.6507 0.1836 39.0189);
}
</style>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@^19.2.3",
"react/": "https://esm.sh/react@^19.2.3/",
"lucide-react": "https://esm.sh/lucide-react@^0.562.0",
"react-dom/": "https://esm.sh/react-dom@^19.2.3/",
"gsap": "https://esm.sh/gsap@^3.14.2",
"gsap/": "https://esm.sh/gsap@^3.14.2/"
}
}
</script>
<link rel="stylesheet" href="/index.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>

15
index.tsx Normal file
View File

@ -0,0 +1,15 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
}
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

5
metadata.json Normal file
View File

@ -0,0 +1,5 @@
{
"name": "Sylvi's Git Guide",
"description": "An interactive, animated guide to Git version control, tailored for Sylvi.",
"requestFramePermissions": []
}

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "sylvi's-git-guide",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"deploy": "bun run build && wrangler pages deploy dist",
"pages:dev": "wrangler pages dev dist --port 8788"
},
"dependencies": {
"react": "^19.2.3",
"lucide-react": "^0.562.0",
"react-dom": "^19.2.3",
"gsap": "^3.14.2"
},
"devDependencies": {
"@types/node": "^22.14.0",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.8.2",
"vite": "^6.2.0",
"wrangler": "^4.59.1"
}
}

14
public/_headers Normal file
View File

@ -0,0 +1,14 @@
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()
/*.js
Cache-Control: public, max-age=31536000, immutable
/*.css
Cache-Control: public, max-age=31536000, immutable
/index.html
Cache-Control: public, max-age=0, must-revalidate

29
tsconfig.json Normal file
View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"skipLibCheck": true,
"types": [
"node"
],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": [
"./*"
]
},
"allowImportingTsExtensions": true,
"noEmit": true
}
}

29
types.ts Normal file
View File

@ -0,0 +1,29 @@
import { ReactNode } from "react";
export type ContentType =
| 'paragraph'
| 'code'
| 'callout'
| 'list'
| 'ordered-list'
| 'subheading'
| 'collapsible'
| 'diagram';
export interface ContentBlock {
type: ContentType;
content: string | string[] | { title: string; body: ContentBlock[] };
variant?: 'warning' | 'info' | 'success' | 'tip';
language?: string;
}
export interface SectionData {
id: string;
title: string;
blocks: ContentBlock[];
}
export interface NavItem {
id: string;
label: string;
}

23
vite.config.ts Normal file
View File

@ -0,0 +1,23 @@
import path from 'path';
import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, '.', '');
return {
server: {
port: 3000,
host: '0.0.0.0',
},
plugins: [react()],
define: {
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
},
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
}
}
};
});

6
wrangler.toml Normal file
View File

@ -0,0 +1,6 @@
name = "sylvi-git-guide"
compatibility_date = "2025-01-14"
main = "workers-site/index.js"
[site]
bucket = "./dist"