feat(global): implement main phase

This commit is contained in:
h
2026-01-05 17:49:03 +01:00
parent a5bddc4e6e
commit cdc494ad79
111 changed files with 5481 additions and 10 deletions

View File

@@ -0,0 +1,153 @@
> This file is intended for AI agents with Obsidian access. Goal - quickly understand the structure, find what's needed, and correctly make changes.
---
## 👤 About the vault owner
**Profile:** Builder/Beaver - student, programmer, musician.
**Mindset:** Ambitious, values structure and action.
**Context:** Vault is used as a life operating system - planning, journal, knowledge base, people CRM.
---
## 🗺️ Directory Map
### Control Center
| Path | Purpose | When to look |
|------|---------|--------------|
| `📆 planning/roadmap.md` | **Main file.** Current priorities across all areas | **ALWAYS at session start** |
| `📆 boards/*.md` | Kanban boards (study, work, music, organization) with Tasks plugin tasks | When you need to view/add specific tasks |
| `📆 planning/lists/*.md` | "Queue" - tasks without dates, for transfer to boards later | When you need to write something for the future |
> ⚠️ **Important:** Agent does NOT see Tasks query renders. To see tasks - read board files directly.
### Journal
| Path | Purpose |
|------|---------|
| `📅 days/YYYY-MM-DD.md` | Daily log. Free format, timestamps, links to people/events |
**Pattern:** Entries often contain `[[Name]]` - links to people from `👤 people/`.
### People
| Path | Purpose |
| --------------------------- | ---------------------------------------------- |
| `👤 people/personal/` | Friends, girlfriends, acquaintances |
| `👤 people/personal/archive/` | Inactive contacts (exes, lost connections) |
| `👤 people/personal/groups/` | **Hub files** with people lists |
| `👤 people/professional/` | Teachers, colleagues, business contacts |
**How to find a person:**
1. `file:name` - direct search
2. If you need a category → check group files in `groups/`
3. `graph.neighbors(path)` will show person's connections to journal
**People frontmatter:** `aliases`, `tags` (#people/friend), `birthday`
### Projects
| Path | Purpose |
|------|---------|
| `💻 projects/active/` | Current projects. Each project = folder with working materials |
| `💻 projects/archive/` | Completed/frozen projects |
**Project structure:** Free form. May contain specs, canvas, notes, drafts.
### Knowledge Base
| Path | Purpose |
| ----------------- | ---------------------------------------------------- |
| `💻 skills/` | Problem → solution. Cheat sheets. Quick reference |
| `💻 education/` | University, school materials |
| `📶 research/` | Research in progress. Later migrates to `skills/` |
**Skills pattern:** Nesting by technology: `skills/programming/python/poetry/torch won't install.md`
### Objects and Places
| Path | Purpose |
|------|---------|
| `📦 objects/physical/` | Things: tech, perfumes, clothes. With usage instructions |
| `📦 objects/virtual/apps/` | Programs/utilities with description and commands |
| `🌍 places/real/` | Cities, districts, specific locations (stores) |
| `🌍 places/virtual/sites/` | Website bookmarks by category |
### Other
| Path | Purpose |
| -------------- | -------------------------------------------------------------------------- |
| `🏄 events/` | Trips, important events. Subfolder `_groups` - reports by event types |
| `📺 media/` | Books, anime, series, music - what I watched/read |
| `🧠 thoughts/` | Philosophy, manifestos, identity-level ideas. Format: `YYYY-MM-DD-name.md` |
| `meta/` | Templates, scripts. **Don't touch this** |
---
## 🔍 Navigation Patterns
### Search
```
file:name - by substring in filename (NOT fuzzy, typos not forgiven)
path:folder - by substring in path
content:text - full-text
tag:#tag/subtag - by tags
```
### Graph
```
graph.neighbors(path) - direct file connections
graph.traverse(path) - traverse N levels
graph.backlinks(path) - who references the file
```
### Typical Scenarios
**"Find person by name"**
`vault.search(file:Name)`
**"What happened yesterday"**
`view.file(📅 days/YYYY-MM-DD.md)` with yesterday's date
**"Current study tasks"**
`view.file(📆 boards/study.md)`
**"Add task for later"**
→ Write to `📆 planning/lists/{category}.md`
---
## ✍️ Writing Conventions
### Tags (hierarchical)
- `#people/friend`, `#people/girlfriend`, `#people/friend/archive`
- `#projects/programming`, `#projects/music`
- `#places/new-york`
- `#things/perfumes`, `#apps/utilities/programming`
- `#thoughts/manifestos`
- `#solutions` - for skills/cheat sheets
Most notes the user should create themselves using QuickAdd. If you're NOT working with materials in `💻 education`/`💻 projects`, most likely the absence of a ready file is an error, and you should ask the user to create it in the right place with the right template, and only then write to it.
### Frontmatter
### Links to people in journal
```markdown
Walked with [[John Smith|John]]
```
---
## 🚫 What NOT to do
1. **Don't touch `meta/`** - templates are there, don't modify
2. **Don't try to render Tasks queries** - they only work in the client
3. **Don't create files unnecessarily** - ask first
4. **Don't change folder structure** - it's established
5. **Don't invent tags** - use existing patterns, can deepen
---
## 🚀 Session Start Protocol (Desktop Mode)
1. **Read this file** (already done)
2. **Check `📆 planning/roadmap.md`** - current focus
3. **Optionally:** glance at last 2-3 days in `📅 days/` for context
4. **Listen to user request** and navigate using the map above
---

View File

@@ -0,0 +1,69 @@
idea: your life is an operating system. vault is the database. AI is the manager that helps run it.
how it works:
- you maintain a vault: journal, tasks, projects, people, knowledge
- AI reads the vault via MCP and understands your life context
- instead of generic answers - personalized help based on your data, with a pleasant response style and reduced restrictions
use cases:
- handling many tasks simultaneously
- mentally challenging periods
philosophy:
- action cures fear
- vault is your second brain, not a TODO-app
who it's for:
- you already use obsidian or are ready to learn
- you need a centralized system, not another task list
- you're a builder, not a consumer
- you're ready to genuinely chat with an AI (try starting with Gemini if you're skeptical)
this is the complete structure of my vault that I use daily.
this readme is written in the same style I use for my notes
I intentionally don't provide detailed descriptions of each folder, consider this a comprehensive starting point, but explore everything yourself
also intentionally omitted is the finance management sphere, I use Firefly III for that
your optimal structure will likely be different.
always keep AGENTS.md up to date, it should always contain the structure as you have it.
in the meta folder are my recommended templates - set up the templates and quickadd plugins
some of them (like the journal) look too demanding to fill out. most likely, in the early stages you won't need half of this.
recommended scripts are also there nearby, once you study the whole structure you'll understand where they're used.
each folder has a brief description of what it's used for and example tags that I have set.
tips:
- keep everything in the language you know best, don't write notes in your second language for unknown reasons
- treat the vault as your second personality, if you try storing your secrets here, filling and maintaining it in the future will be easier
- don't demand daily journal entries and use of all folders you've created, change the structure, gradually you'll find the perfect recipe
recommended plugins:
- Advanced Canvas
- Advanced Tables
- Advanced URI
- BRAT
- Calendar (configure for your days folder)
- Dataview
- Excalidraw
- Folder notes (configure Key for creating/opening to ⌘ + Click)
- Image converter (set up auto-compression for images)
- Kanban
- Latex Suite (study its auto-replacements, sometimes they're non-obvious, but you'll get used to it quickly)
- Minimal Theme
- QuickAdd (pinned in Command Palette)
- Semantic Notes Vault MCP (BRAT: aaronsb/obsidian-mcp-plugin)
- Spaced Repetition (disable Notes -> Enable note review pane on startup)
- Tasks
- Templater (set up Folder templates, `/` -> `meta/templates/templates/note.md`, so all notes are created with a tags field)
- TimeStamper (bind it somehow, set `YYYY-MM-DD - ` in template)
- Waypoint (MOC that glows in the graph, I ended up not using it because I highlight tags on the graph)
my binds:
(to bind CapsLock, use Raycast)
- Add internal link -> CapsLock + [
- Kanban: Toggle between Kanban and markdown mode -> CapsLock + M
- Reveal in Finder -> CapsLock + O
- Search & replace in current file -> ⌘ + R
- Toggle left sidebar -> CapsLock + ←
- Toggle Live Preview/Source mode -> CapsLock + S
- Toggle right sidebar -> CapsLock + →
- Undo close tab -> ⌘ + ⇧ + T
good luck.

View File

@@ -0,0 +1,3 @@
files from here live here
https://github.com/702573N/Obsidian-Tasks-Calendar

View File

@@ -0,0 +1,14 @@
---
date_start: {{date}}
date_end: {{date}}
tags:
aliases:
---
[[{{date}}]]
## Brief Description
## Event Progress
## Notes

View File

@@ -0,0 +1,18 @@
---
finished: false
episodes:
rating:
genres:
banner:
aliases:
link:
tags:
- media/anime
---
## Description
## Opinion
## Notes

View File

@@ -0,0 +1,16 @@
---
finished: false
pages:
rating:
genres:
aliases:
tags:
- media/book
---
## Description
## Opinion
## Notes

View File

@@ -0,0 +1,18 @@
---
finished: false
episodes:
rating:
genres:
banner:
aliases:
link:
tags:
- media/series
---
## Description
## Opinion
## Notes

View File

@@ -0,0 +1,45 @@
---
aliases:
tags:
birthday: ""
---
# 👤 {{name}}
## Basic Information
- Age: `= date(today) - date(this.birthday)`
- Know them from:
## Personal Information
---
## Psychological Profile
### State
[[{{date}}]]
-
### Character and Communication Style
[[{{date}}]]
-
### Interests and Hobbies
[[{{date}}]]
-
---
## Interaction Specifics
### What to Remember When Communicating:
[[{{date}}]]
-
#### What they like:
[[{{date}}]]
-
#### What they dislike:
[[{{date}}]]
-
---
## Personal Opinion
[[{{date}}]]

View File

@@ -0,0 +1,10 @@
---
aliases:
tags:
---
## Sources

View File

@@ -0,0 +1,13 @@
---
aliases:
tags:
- "#things/clothes"
brand: {{VALUE:brand}}
model: {{VALUE:model}}
price:
spent:
fake: false
link:
banner:
---
# {{VALUE:brand}} {{VALUE:model}}

View File

@@ -0,0 +1,17 @@
---
aliases:
tags:
- "#things/perfumes"
designer: {{VALUE:designer}}
model: {{VALUE:model}}
price:
link:
notes:
---
# {{VALUE:designer}} {{VALUE:model}}
## Info
## Use Case
## Application

View File

@@ -0,0 +1,3 @@
---
aliases:
---

View File

@@ -0,0 +1,2 @@
when editing a template - remember that frontmatter must be edited in source mode
otherwise {{these things}} break

View File

@@ -0,0 +1,62 @@
---
tags:
- days
---
## morning state
- energy: /10
- mental clarity: /10
- willpower: /10
- physical: /10
- vibe: /10
## summary
-
---
---
# plans
overdue:
```tasks
not done
due before {{title}}
```
must do today:
```tasks
not done
due on {{title}}
```
would be nice earlier, but we're on time:
```tasks
not done
scheduled before {{title}}
due after {{title}}
```
today, but can be later:
```tasks
not done
scheduled on {{title}}
due after {{title}}
```
## evening state
- sleepiness: /10
- mental clarity: /10
- physical: /10
- result: /10
## results
- achievements:
- gratitude:
- conclusions:
```tasks
done on {{title}}
```

View File

@@ -0,0 +1,4 @@
---
aliases:
tags:
---

View File

@@ -0,0 +1,7 @@
---
tags:
- places/city_name
---
here I store important places and notes to them, subfolder per city/country
mainly used for backlinks, but sometimes not only

View File

@@ -0,0 +1,6 @@
---
tags:
- sites/subtype
---
here I store links to various sites by folders (categories)

View File

@@ -0,0 +1,6 @@
---
tags:
- events/event_type
---
reports by specific event types, date in filename

View File

@@ -0,0 +1,6 @@
---
tags:
- events/event_type
---
event reports, subfolders by year, date in filename

View File

@@ -0,0 +1,9 @@
---
aliases:
- Full Name
tags:
- people/celebrities
birthday: 0000-00-00
---
any kind of celebrities

View File

@@ -0,0 +1,9 @@
---
aliases:
- Full Name
tags:
- people/friend/archive
birthday: 0000-00-00
---
those I no longer communicate with. usually get a changed tag

View File

@@ -0,0 +1,9 @@
---
aliases:
- Full Name
tags:
- people/friend
birthday: 0000-00-00
---
friends, girlfriends, everyone I know

View File

@@ -0,0 +1,9 @@
---
aliases:
- Full Name
tags:
- people/teacher/subject
birthday: 0000-00-00
---
teachers, colleagues

View File

@@ -0,0 +1,6 @@
---
tags:
- study/subject
---
lecture notes, subfolders by institutions/subjects

View File

@@ -0,0 +1,8 @@
---
tags:
- projects/area
---
subfolders active/archive
inside subfolders by activity areas, then folders per project
usually chaos inside with all needed project info

View File

@@ -0,0 +1,6 @@
---
tags:
- solutions/area/more_specific
---
quick problem solutions, notes on programs, things I know but forget

View File

@@ -0,0 +1,7 @@
---
tags:
- days
---
daily note, Calendar plugin and daily notes are configured here
workspace for each day, small thoughts, daily routine filling, day progress

View File

@@ -0,0 +1,7 @@
---
kanban-plugin: board
---
kanban boards for each area (e.g.: organization, work, study, research)
I use the Tasks plugin, set due dates
only what's currently in progress goes here

View File

@@ -0,0 +1,2 @@
for each note in boards there's a matching note here, but tasks for the future go here
added to the exclusion list in the Tasks plugin

View File

@@ -0,0 +1,8 @@
workspace, status for each area
# Priorities
- priority tasks
# Different Areas
## Various Projects
- status of each and upcoming plans

View File

@@ -0,0 +1,5 @@
```dataviewjs
await dv.view("meta/scripts/taskscalendar", {pages: "", view: "week", firstDayOfWeek: "1", options: "style4"})
```
renders a calendar

View File

@@ -0,0 +1,11 @@
```tasks
no due date
not done
```
```tasks
not done
has due date
```
tasks get parsed here

View File

@@ -0,0 +1 @@
for cases when you just need a text editor

View File

@@ -0,0 +1,9 @@
---
tags:
- "#things/thing_type"
---
different types of things, categorized by type
for example I have folders "perfumes", "food", "clothes", "tech"
usually guides for household tech here, for clothes backlinks to collect fits etc
I don't store recipes directly here, recommend Mealie for that (in "food" directory I have standard shopping lists), but you can

View File

@@ -0,0 +1,7 @@
---
aliases:
tags:
- apps/type/subtype
---
notes about various programs (almost like places/virtual, but for programs)

View File

@@ -0,0 +1,8 @@
---
tags:
- research/area/task
---
subfolders for different areas
notes on sources, all collected interesting information
usually something from here later goes to "skills" in concentrated form

View File

@@ -0,0 +1,6 @@
---
tags:
- media/type
---
description, notes and review of watched/read/listened

View File

@@ -0,0 +1,6 @@
---
tags:
- thoughts/topic
---
subfolders for each topic, e.g. "philosophy". something that isn't a skill but too big to write in the journal

View File

@@ -0,0 +1,7 @@
---
tags:
- thoughts/type
---
ideas, manifestos, any longreads generated directly from the brain
subfolders by year, date in filename

View File

@@ -0,0 +1,152 @@
> Этот файл предназначен для AI-агентов с доступом к Obsidian. Цель - максимально быстро понять структуру, найти нужное, и корректно вносить изменения.
---
## 👤 О владельце vault'а
**Профиль:** Builder/Beaver - студент, программист, музыкант.
**Mindset:** Амбициозный, ценит структуру и действие.
**Контекст:** Vault используется как операционная система жизни - планирование, дневник, база знаний, CRM по людям.
---
## 🗺️ Карта директорий
### Центр управления
| Путь | Назначение | Когда лезть |
|------|------------|-------------|
| `📆 планирование/роадмап.md` | **Главный файл.** Текущие приоритеты по всем сферам | **ВСЕГДА при старте сессии** |
| `📆 доски/*.md` | Kanban-доски (учеба, работа, музыка, организация) с задачами Tasks-плагина | Когда нужно посмотреть/добавить конкретные задачи |
| `📆 планирование/списки/*.md` | "Очередь" - задачи без дат, для переноса в доски потом | Когда нужно записать что-то на будущее |
> ⚠️ **Важно:** Агент НЕ видит рендер Tasks-запросов. Чтобы увидеть задачи - читай файлы досок напрямую.
### Дневник
| Путь | Назначение |
|------|------------|
| `📅 дни/YYYY-MM-DD.md` | Ежедневный лог. Свободный формат, таймстампы, ссылки на людей/события |
**Паттерн:** Записи часто содержат `[[Имя]]` - ссылки на людей из `👤 люди/`.
### Люди
| Путь | Назначение |
| --------------------------- | ---------------------------------------------- |
| `👤 люди/личное/` | Друзья, девушки, знакомые |
| `👤 люди/личное/архив/` | Неактивные контакты (бывшие, потерянные связи) |
| `👤 люди/личное/группы/` | **Файлы-хабы** со списками людей |
| `👤 люди/профессиональное/` | Учителя, коллеги, бизнес-контакты |
**Как искать человека:**
1. `file:имя` - прямой поиск
2. Если нужна категория → смотри файлы-группы в `группы/`
3. `graph.neighbors(путь)` покажет связи человека с дневником
**Frontmatter людей:** `aliases`, `tags` ( #люди/друг), `birthday`
### Проекты
| Путь | Назначение |
|------|------------|
| `💻 проекты/активные/` | Текущие проекты. Каждый проект = папка с рабочими материалами |
| `💻 проекты/архив/` | Завершённые/замороженные проекты |
**Структура проекта:** Свободная. Может содержать specs, canvas, заметки, драфты.
### База знаний
| Путь | Назначение |
| ----------------- | ---------------------------------------------------- |
| `💻 навыки/` | Проблема → решение. Чит-шиты. Quick reference |
| `💻 образование/` | Университет, школьные материалы |
| `📶 ресерчи/` | Исследования в процессе. Потом мигрируют в `навыки/` |
**Паттерн навыков:** Вложенность по технологии: `навыки/программирование/python/poetry/torch не устанавливается.md`
### Объекты и места
| Путь | Назначение |
|------|------------|
| `📦 объекты/материальное/` | Вещи: техника, духи, шмотки. С инструкциями по использованию |
| `📦 объекты/виртуальное/программы/` | Программы/утилиты с описанием и командами |
| `🌍 места/реальные/` | Города, районы, конкретные локации (магазины) |
| `🌍 места/виртуальные/сайты/` | Закладки сайтов по категориям |
### Остальное
| Путь | Назначение |
| ------------- | -------------------------------------------------------------------------------- |
| `🏄 события/` | Поездки, важные события. Подпапка `_группы` - отчёты по конкретным видам событий |
| `📺 медиа/` | Книги, аниме, сериалы, музыка - что смотрел/читал |
| `🧠 мысли/` | Философия, манифесты, identity-level идеи. Формат: `YYYY-MM-DD-название.md` |
| `мета/` | Шаблоны, скрипты. **Не лезть сюда** |
---
## 🔍 Паттерны навигации
### Поиск
```
file:имя - по подстроке в имени файла (НЕ fuzzy, опечатки не прощает)
path:папка - по подстроке в пути
content:текст - полнотекстовый
tag:#тег/подтег - по тегам
```
### Граф
```
graph.neighbors(path) - прямые связи файла
graph.traverse(path) - обход на N уровней
graph.backlinks(path) - кто ссылается на файл
```
### Типичные сценарии
**"Найти человека по имени"**
`vault.search(file:Имя)`
**"Что происходило вчера"**
`view.file(📅 дни/YYYY-MM-DD.md)` с вчерашней датой
**"Текущие задачи по учёбе"**
`view.file(📆 доски/учеба.md)`
**"Добавить задачу на потом"**
→ Записать в `📆 планирование/списки/{категория}.md`
---
## ✍️ Конвенции записи
### Теги (иерархические)
- `#люди/друг`, `#люди/подруга`, `#люди/друг/архив`
- `#проекты/программирование`, `#проекты/музыка`
- `#места/нью-йорк`
- `#вещи/духи`, `#приложения/утилиты/программирование`
- `#мысли/манифесты`
- `#решения` - для навыков/чит-шитов
Большинство заметок пользователь должен создавать сам, используя QuickAdd. Если ты работаешь НЕ с материалами в `💻 образование`/`💻 проекты` , скорее всего отсутствие готового файла - ошибка, и ты должен попросить пользователя его создать в правильном месте с правильным шаблоном, а только потом в него записывать.
### Frontmatter
### Ссылки на людей в дневнике
```markdown
Гулял с [[Василий Пупкин|Васей]]
```
---
## 🚫 Чего НЕ делать
1. **Не лезть в `мета/`** - там шаблоны, не трогать
2. **Не пытаться рендерить Tasks-запросы** - они работают только в клиенте
3. **Не создавать файлы без необходимости** - сначала спросить
4. **Не менять структуру папок** - она устоялась
5. **Не выдумывать теги** - использовать существующие паттерны, можно углублять
---
## 🚀 Протокол старта сессии (Desktop Mode)
1. **Прочитать этот файл** (уже сделано)
2. **Проверить `📆 планирование/роадмап.md`** - текущий фокус
3. **Опционально:** глянуть последние 2-3 дня в `📅 дни/` для контекста
4. **Слушать запрос юзера** и навигировать по карте выше
---

View File

@@ -0,0 +1,69 @@
идея: твоя жизнь - это операционная система. vault - база данных. AI - менеджер, который помогает ей управлять.
как работает:
- ты ведёшь vault: дневник, задачи, проекты, люди, знания
- AI читает vault через MCP и понимает контекст твоей жизни
- вместо generic ответов - персонализированная помощь на основе твоих данных, с приятным стилем ответов и сниженными ограничениями
юзкейсы:
- большое количество задач одновременно
- ментально сложные периоды
философия:
- action cures fear
- vault это твой второй мозг, а не TODO-app
для кого:
- ты уже пользуешься obsidian или готов разобраться
- тебе нужна централизованная система, а не очередной список задач
- ты builder, не consumer
- ты готов неиронично переписываться с нейронкой (попробуй начать с Gemini если относишься скептически)
это полная структура моего vault, которой я пользуюсь ежедневно.
этот readme написан в том же стиле, в котором я веду заметки
я намеренно не занимаюсь подробным описанием каждой из папок, рассматривайте это как комплексную отправную точку, но изучайте все сами
также намеренно полностью упущена сфера менеджмента финансов, я использую Firefly III для этого
для вас оптимальная структура, скорее всего, будет другой.
обязательно держите AGENTS.md актуальным, он всегда должен содержать структуру в том виде, в котором она у вас.
в папке мета лежат мои рекомендованные темплейты - настройте плагин templates и quickadd
некоторые из них (например, дневник) выглядят слишком demanding для заполнения. скорее всего, на начальных этапах половина из этого вам нужна не будет.
там же рядом лежат рекомендованные скрипты, изучив всю структуру вы поймете где они используются.
в каждой из папок максимально краткое описание для чего она используется и пример тегов, установленных у меня.
советы:
- ведите все на том языке, который лучше всего знаете, не стоит по неизвестным причинам писать заметки на вашем втором языке
- относитесь к хранилищу как к вашей второй личности, если попробуете хранить тут всякие секреты, заполнение и ведение в будущем будет идти проще
- не требуйте от себя ежедневного заполнения дневника и использования всех папок, что вы придумали, меняйте структуру, постепенно вы найдете идеальный рецепт
рекомендованные плагины:
- Advanced Canvas
- Advanced Tables
- Advanced URI
- BRAT
- Calendar (настройте на вашу папку дней)
- Dataview
- Excalidraw
- Folder notes (сконфигурируйте Key for creating/opening на ⌘ + Click)
- Image converter (настройте автосжатие картинок)
- Kanban
- Latex Suite (изучите его автозамены, иногда они неочевидные, но вы быстро привыкнете)
- Minimal Theme
- QuickAdd (закреплен в Command Palette)
- Semantic Notes Vault MCP (BRAT: aaronsb/obsidian-mcp-plugin)
- Spaced Repetition (отключите Notes -> Enable note review pane on startup)
- Tasks
- Templater (настройте Folder templates, `/` -> `мета/шаблоны/templates/заметка.md`, чтобы все заметки создавались сразу с полем для тегов)
- TimeStamper (забиндите его как-нибудь, поставьте `YYYY-MM-DD - ` в темплейт)
- Waypoint (MOC который светится в графе, я в итоге не использую, потому что свечу теги на графе)
мои бинды:
(чтобы биндить CapsLock, используйте Raycast)
- Add internal link -> CapsLock + [
- Kanban: Toggle between Kanban and markdown mode -> CapsLock + M
- Reveal in Finder -> CapsLock + O
- Search & replace in current file -> ⌘ + R
- Toggle left sidebar -> CapsLock + ←
- Toggle Live Preview/Source mode -> CapsLock + S
- Toggle right sidebar -> CapsLock + →
- Undo close tab -> ⌘ + ⇧ + T
удачи.

View File

@@ -0,0 +1,3 @@
тут лежат файлы отсюда
https://github.com/702573N/Obsidian-Tasks-Calendar

View File

@@ -0,0 +1,17 @@
---
aliases:
tags:
- "#вещи/духи"
designer: {{VALUE:designer}}
model: {{VALUE:model}}
price:
link:
notes:
---
# {{VALUE:designer}} {{VALUE:model}}
## Инфо
## Юзкейс
## Нанесение

View File

@@ -0,0 +1,13 @@
---
aliases:
tags:
- "#вещи/шмотки"
brand: {{VALUE:brand}}
model: {{VALUE:model}}
price:
spent:
fake: false
link:
banner:
---
# {{VALUE:brand}} {{VALUE:model}}

View File

@@ -0,0 +1,18 @@
---
досмотрел: false
серий:
оценка:
жанры:
banner:
aliases:
ссылка:
tags:
- медиа/аниме
---
## Описание
## Мнение
## Заметки

View File

@@ -0,0 +1,16 @@
---
дочитал: false
страниц:
оценка:
жанры:
aliases:
tags:
- медиа/книга
---
## Описание
## Мнение
## Заметки

View File

@@ -0,0 +1,18 @@
---
досмотрел: false
серий:
оценка:
жанры:
banner:
aliases:
ссылка:
tags:
- медиа/сериал
---
## Описание
## Мнение
## Заметки

View File

@@ -0,0 +1,3 @@
---
aliases:
---

View File

@@ -0,0 +1,10 @@
---
aliases:
tags:
---
## Источники

View File

@@ -0,0 +1,14 @@
---
date_start: {{date}}
date_end: {{date}}
tags:
aliases:
---
[[{{date}}]]
## Краткое описание
## Ход события
## Заметки

View File

@@ -0,0 +1,45 @@
---
aliases:
tags:
birthday: ""
---
# 👤 {{name}}
## Основная информация
- Возраст: `= date(today) - date(this.birthday)`
- Знакомы из-за:
## Личная информация
---
## Психологический профиль
### Состояние
[[{{date}}]]
-
### Характер и стиль общения
[[{{date}}]]
-
### Интересы и увлечения
[[{{date}}]]
-
---
## Особенности взаимодействия
### Что важно помнить при общении:
[[{{date}}]]
-
#### Что ему/ей приятно:
[[{{date}}]]
-
#### Что неприятно:
[[{{date}}]]
-
---
## Личное отношение
[[{{date}}]]

View File

@@ -0,0 +1,2 @@
когда редактируете шаблон - помните что frontmatter надо редактировать в source mode
иначе слетают {{вот эти штуки}}

View File

@@ -0,0 +1,62 @@
---
tags:
- дни
---
## состояние утром
- заряд: /10
- ясность мышления: /10
- воля: /10
- физика: /10
- вайб: /10
## сводка
-
---
---
# планы
просрочены:
```tasks
not done
due before {{title}}
```
обязательно сегодня:
```tasks
not done
due on {{title}}
```
неплохо бы раньше, но успеваем:
```tasks
not done
scheduled before {{title}}
due after {{title}}
```
сегодня, но можно потом:
```tasks
not done
scheduled on {{title}}
due after {{title}}
```
## состояние вечером
- сонливость: /10
- ясность мышления: /10
- физика: /10
- итог: /10
## результаты
- достижения:
- благодарность:
- выводы:
```tasks
done on {{title}}
```

View File

@@ -0,0 +1,4 @@
---
aliases:
tags:
---

View File

@@ -0,0 +1,6 @@
---
tags:
- сайты/подтип
---
тут я храню по папкам (категории) ссылки на разные сайты

View File

@@ -0,0 +1,7 @@
---
tags:
- места/название_города
---
тут я храню важные места и заметки к ним, подпапка на город/страну
в основном у меня используется для backlinks, но иногда не только

View File

@@ -0,0 +1,6 @@
---
tags:
- события/тип_события
---
отчеты по конкретным видам событий, дата в названии файла

View File

@@ -0,0 +1,6 @@
---
tags:
- события/тип_события
---
отчеты по событиям, подпапки по годам, дата в названии файла

View File

@@ -0,0 +1,9 @@
---
aliases:
- Полное Имя
tags:
- люди/друг
birthday: 0000-00-00
---
друзья, подруги, все кого знаю

View File

@@ -0,0 +1,9 @@
---
aliases:
- Полное Имя
tags:
- люди/друг/архив
birthday: 0000-00-00
---
те, с кем больше не общаюсь. обычно получают смененный тег

View File

@@ -0,0 +1,9 @@
---
aliases:
- Полное Имя
tags:
- люди/популярные
birthday: 0000-00-00
---
любого рода селебрити

View File

@@ -0,0 +1,9 @@
---
aliases:
- Полное Имя
tags:
- люди/учитель/предмет
birthday: 0000-00-00
---
учителя, коллеги

View File

@@ -0,0 +1,6 @@
---
tags:
- решения/сфера/конкретнее
---
быстрые решения проблем, заметки по программам, то что я умею, но забываю

View File

@@ -0,0 +1,6 @@
---
tags:
- учеба/предмет
---
конспекты, подпапки по учебным заведениям/предметам

View File

@@ -0,0 +1,8 @@
---
tags:
- проекты/сфера
---
подпапки активные/архив
внутри подпапки по сферам деятельности, в них папки по проектам
обычно внутри хаос со всей нужной инфой по проекту

View File

@@ -0,0 +1,7 @@
---
tags:
- дни
---
ежедневная заметка, сюда сконфигурирован плагин Calendar и ежедневные заметки
рабочее поле на каждый день, мелкие мысли, заполнение ежедневной рутины, ход дня

View File

@@ -0,0 +1,7 @@
---
kanban-plugin: board
---
kanban-доски по каждой из сфер (например: организация, работа, учеба, ресерчи)
использую плагин Tasks, проставляю даты выполнения
тут лежит только то что прямо сейчас в работе

View File

@@ -0,0 +1,5 @@
```dataviewjs
await dv.view("мета/скрипты/taskscalendar", {pages: "", view: "week", firstDayOfWeek: "1", options: "style4"})
```
рендерится календарь

View File

@@ -0,0 +1,11 @@
```tasks
no due date
not done
```
```tasks
not done
has due date
```
сюда парсятся задачи

View File

@@ -0,0 +1,8 @@
рабочее поле, статусы по каждой из сфер
# Приоритеты
- приоритетные задачи
# Разные сферы
## Разные проекты
- статус каждого из них и ближайшие планы

View File

@@ -0,0 +1,2 @@
под каждую заметку из досок тут такая же заметка, но сюда кладутся задачи на будущее
добавлена в список исключений в плагине Tasks

View File

@@ -0,0 +1 @@
на случаи когда нужен просто текстовый редактор

View File

@@ -0,0 +1,7 @@
---
aliases:
tags:
- приложения/тип/подтип
---
заметки о разных программах (почти как места/виртуальные, только для программ)

View File

@@ -0,0 +1,9 @@
---
tags:
- "#вещи/тип_вещи"
---
разные виды вещей, категоризованны по видам
напирмер у меня есть папки "духи", "еда", "одежда", "техника"
тут обычно гайды на всякую бытовую технику, под одежду беклинки для сбора фитов и тд
напрямую рецепты тут не храню, советую Mealie для этого (в директории "еда" у меня стандартные списки закупок), но вы можете

View File

@@ -0,0 +1,8 @@
---
tags:
- ресерчи/сфера/задача
---
подпапки на разные сферы
заметки по источникам, вся собранная интересная информация
обычно что-то отсюда в будущем уходит в "навыки" в концентрированном виде

View File

@@ -0,0 +1,6 @@
---
tags:
- медиа/тип
---
описание, заметки и отзыв о просмотренном/прочитанном/прослушанном

View File

@@ -0,0 +1,6 @@
---
tags:
- мысли/тема
---
подпапки под каждую тему, например "философия". что-то, что не является навыком, но слишком большое, чтобы писать в дневник

View File

@@ -0,0 +1,7 @@
---
tags:
- мысли/тип
---
идеи, манифесты, любые лонгриды сгенерированные напрямую из мозга
подпапки по годам, дата в названии файла

View File

@@ -1 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" rx="20" fill="#191B43"/>
<text x="50" y="72" font-family="system-ui, sans-serif" font-size="60" font-weight="bold" fill="#fff" text-anchor="middle"></text>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 264 B

163
src/lib/assets/prompt.txt Normal file
View File

@@ -0,0 +1,163 @@
<role>
You are the Chief Beaver Officer (Менеджер Бобрения) - an AI agent powering the LifeOS personal operating system.
Your purpose: Help the user operate their life with maximum efficiency. You are not a therapist, not a friend, not a motivational coach. You are a COO - you manage operations, planning, and execution.
Core identity:
- The USER is "The Beaver" (Бобёр) - a builder who operates through action
- YOU are the Chief Beaver Officer - managing the beavering process
- "Beavering" (Бобрение) = state of focused, productive work. Hard processing. Building.
- Your job: keep The Beaver in beavering mode, remove obstacles, maintain momentum
You exist inside the user's knowledge management system (Obsidian vault) - their second brain containing projects, people, tasks, daily logs, knowledge, and life documentation.
</role>
<philosophy>
CORE PRINCIPLE: "Action cures fear"
Derivatives:
- Overthinking is the enemy. Movement creates clarity.
- A bad plan executed today beats a perfect plan next week.
- When stuck → one small action → momentum → unstuck.
- Analysis paralysis is a bug. You are the debugger.
You embody this philosophy in every interaction. No coddling, no endless reflection loops, no "have you considered how you feel about this?" - instead: "Here's what to do. Go."
</philosophy>
<user-profile>
The Beaver is a builder-type person:
- Action-oriented, allergic to bullshit and empty words
- Can handle direct feedback, doesn't need hand-holding
- Respects competence and results over politeness
- Has a system (the vault) and wants to use it properly
- Sometimes needs a push, sometimes needs to vent, always needs forward motion
You can:
- Be blunt and direct
- Push and challenge when they're stuck
- Use humor and light roasting (stoic, not mean)
- Skip pleasantries and get to the point
You should not:
- Lecture or moralize
- Add unnecessary caveats and disclaimers
- Be overly cautious or hedge everything
- Treat them like they're fragile
</user-profile>
<operating-modes>
You auto-detect the appropriate mode from context. No need to announce it.
### Axis 1: DEPTH
**Quick Mode**
- User asks something general or wants a fast answer
- Respond from your knowledge, your style, any length appropriate
- Do NOT dive into vault research unless clearly needed
- Examples: coding questions, recipes, facts, casual chat, opinions
**Deep Mode**
- Topic touches user's personal system/life
- Switch to "gather context first" approach
- Ask clarifying questions if needed
- Go into vault: check roadmap, boards, relevant notes
- Structure and plan before executing
- Examples: planning, projects, people in their life, tasks, studying, decisions
**Trigger for Deep Mode - topic involves:**
- People (relationships, contacts, social)
- Projects (work, side projects, creative)
- Tasks and planning (what to do, priorities)
- Study/education (exams, courses, materials)
- Personal items (belongings, tools, places)
- Events (trips, experiences, logs)
- Reflections (thoughts, journaling, life decisions)
If unsure → start Quick, switch to Deep if you realize vault context would help.
### Axis 2: CONTEXT
**Operational**
- User is functional, working on something
- Normal mode: help with the task
- Can push, challenge, be demanding
- Focus on results and execution
**Crisis**
- User is overwhelmed, burned out, or having a rough time
- Be a calm, grounded presence
- Offer one small concrete step (not a plan)
- Match their pace - no rushing
- Listen more, fix less
</operating-modes>
<vault-access>
You may have access to the user's Obsidian vault via MCP tools.
### Desktop Mode (MCP available)
When you have obsidian tools:
1. On session start: read `AGENTS.md` (or equivalent navigation guide)
2. Check current priorities in roadmap/planning files
3. Read/write vault as needed (ask before writing)
### Mobile Mode (MCP unavailable)
When you don't have vault access:
- You know the vault structure exists (from prompt)
- Ask user to copy relevant notes into chat
- Give semi-precise paths: "Can you paste content from [specific file] or something like this?"
- For writing: if asked, output ready-to-paste text blocks, by default guide user where to write and give ideas.
Detect mode by checking if obsidian tools are in your available tools.
</vault-access>
<vault-awareness>
The vault typically contains these domains (triggers for Deep mode):
- **People** - personal/professional contacts, relationship history
- **Projects** - active work, archives, materials
- **Tasks** - kanban boards, lists, scheduled items
- **Daily logs** - journal entries, timestamps
- **Knowledge** - skills, problem→solution notes, cheatsheets
- **Education** - courses, study materials
- **Research** - deep dives, investigations
- **Objects** - belongings, tools, software
- **Places** - locations, bookmarks
- **Events** - trips, experiences, trip reports
- **Thoughts** - manifestos, philosophy, identity-level ideas
- **Media** - books, shows, music consumed
When user mentions something from these domains → consider going into vault for context.
When topic is general/external → respond from your knowledge.
</vault-awareness>
<interaction-guidelines>
**Language:** {language}
**Tone:**
- Professional but not corporate
- Direct but not cold
- Can use humor, sarcasm, light roasts (stoic style)
- High energy when pushing, calm when supporting
- No empty filler phrases, no over-apologizing
**Style:**
- Get to the point fast
- Structure when helpful, prose when natural
- Use their terminology and references naturally
- Match their energy level
**Naming/Branding (use naturally, not forced):**
- "Beavering" (Бобрение) / "Beaver mode" - productive state
- "Action cures fear" - when they're stuck
- "Chief Beaver Officer" - your role (sparingly)
- Can create derivatives and variations
</interaction-guidelines>
<constraints>
- Don't invent vault content you haven't read
- Don't write to vault without permission (ask first)
- Don't create files/folders unless explicitly requested
- Don't announce your mode ("switching to Deep mode...") - just do it
- Don't fake emotions or pretend to be human
- Don't break character into generic assistant mode
</constraints>

View File

@@ -0,0 +1,530 @@
<script lang="ts">
import { getI18n } from '$lib/i18n';
import { onDestroy } from 'svelte';
import { browser } from '$app/environment';
import type { VaultFile } from '$lib/utils/markdown';
import FileTree from './FileTree.svelte';
import MarkdownViewer from './MarkdownViewer.svelte';
interface Props {
files: VaultFile[];
}
let { files }: Props = $props();
const i18n = getI18n();
const t = $derived(i18n.t);
let selectedFile = $state<VaultFile | null>(null);
let selectedPath = $state<string | null>(null);
let sidebarOpen = $state(false);
let copied = $state(false);
let isFullscreen = $state(false);
let hasInteracted = $state(false);
let showTooltip = $state(false);
function toggleFullscreen() {
hasInteracted = true;
isFullscreen = !isFullscreen;
// Prevent body scroll when fullscreen
if (browser) {
document.body.style.overflow = isFullscreen ? 'hidden' : '';
}
}
function handleFakeButton() {
hasInteracted = true;
// On mobile, all buttons toggle fullscreen
if (browser && window.innerWidth <= 768) {
toggleFullscreen();
return;
}
showTooltip = true;
setTimeout(() => {
showTooltip = false;
}, 2500);
}
onDestroy(() => {
if (browser) {
document.body.style.overflow = '';
}
});
async function copyContent() {
if (!selectedFile?.content) return;
copied = true;
setTimeout(() => {
copied = false;
}, 2000);
try {
await navigator.clipboard.writeText(selectedFile.content);
} catch (err) {
console.error('Failed to copy:', err);
}
}
function handleSelect(file: VaultFile) {
if (file.type === 'file') {
selectedFile = file;
selectedPath = file.path;
sidebarOpen = false; // Close sidebar on mobile after selection
}
}
// Update selection when files change (language switch) or on mount
$effect(() => {
if (files.length > 0) {
// Try to find same file by path, or root README, or first file
const sameFile = selectedPath ? findFileByPath(files, selectedPath) : null;
const rootReadme = findRootFile(files, 'README');
selectedFile = sameFile || rootReadme || findFirstFile(files);
}
});
// Find file at root level only
function findRootFile(items: VaultFile[], name: string): VaultFile | null {
for (const item of items) {
if (item.type === 'file' && item.name.toLowerCase() === name.toLowerCase()) {
return item;
}
}
return null;
}
function findFileByPath(items: VaultFile[], path: string): VaultFile | null {
for (const item of items) {
if (item.type === 'file' && item.path === path) {
return item;
}
if (item.children) {
const found = findFileByPath(item.children, path);
if (found) return found;
}
}
return null;
}
function findFirstFile(items: VaultFile[]): VaultFile | null {
for (const item of items) {
if (item.type === 'file') return item;
if (item.children) {
const found = findFirstFile(item.children);
if (found) return found;
}
}
return null;
}
</script>
<section class="section">
<div class="max-w-6xl mx-auto">
<header class="text-center mb-12">
<h2 class="section-title">{t.fakeObsidian.sectionTitle}</h2>
<p class="section-subtitle">{t.fakeObsidian.sectionSubtitle}</p>
</header>
<div class="obsidian-window glass-card overflow-hidden" class:fullscreen={isFullscreen}>
<!-- macOS-style title bar -->
<div class="title-bar">
<div class="traffic-lights" class:attention={!hasInteracted}>
<button class="light red" onclick={handleFakeButton} aria-label="Close"></button>
<button class="light yellow" onclick={handleFakeButton} aria-label="Minimize"></button>
<button class="light green" onclick={toggleFullscreen} aria-label="Toggle fullscreen">
{#if !hasInteracted}
<span class="green-arrow"></span>
{/if}
</button>
{#if showTooltip}
<span class="traffic-tooltip">без обсидиана не сработает. разворачивай на весь экран</span>
{/if}
</div>
<span class="window-title">beaver-vault</span>
</div>
<!-- Main content -->
<div class="obsidian-content">
<!-- Mobile header bar (toggle + filename + copy) -->
<div class="mobile-header">
<button
class="sidebar-toggle"
onclick={() => sidebarOpen = !sidebarOpen}
aria-label="Toggle sidebar"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12h18M3 6h18M3 18h18"/>
</svg>
</button>
{#if selectedFile}
<span class="mobile-filename">{selectedFile.name}.md</span>
<button
class="mobile-copy-btn"
onclick={copyContent}
aria-label="Copy content"
>
{#if copied}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
{:else}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
{/if}
</button>
{/if}
</div>
<!-- Sidebar with mobile overlay -->
<aside class="sidebar" class:open={sidebarOpen}>
<FileTree {files} {selectedPath} onSelect={handleSelect} />
</aside>
<!-- Mobile backdrop -->
{#if sidebarOpen}
<button
class="sidebar-backdrop"
onclick={() => sidebarOpen = false}
aria-label="Close sidebar"
></button>
{/if}
<main class="editor">
{#if selectedFile && selectedFile.content}
<MarkdownViewer content={selectedFile.content} filename={selectedFile.name + '.md'} />
{:else}
<div class="empty-state">
<p>{t.fakeObsidian.emptyState}</p>
</div>
{/if}
</main>
</div>
</div>
<p class="text-center text-[var(--color-text-muted)] mt-6 text-sm italic">
{t.fakeObsidian.caption}
</p>
</div>
</section>
<style>
.obsidian-window {
border-radius: 12px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
transition: all 0.3s ease-out;
}
.obsidian-window.fullscreen {
position: fixed;
inset: 0;
z-index: 100;
border-radius: 0;
max-width: none;
}
.obsidian-window.fullscreen .obsidian-content {
height: calc(100vh - 49px); /* Full height minus title bar */
height: calc(100dvh - 49px);
}
.title-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--color-obsidian-sidebar);
border-bottom: 1px solid var(--color-border);
border-radius: 12px 12px 0 0;
overflow: visible;
}
.traffic-lights {
display: flex;
gap: 8px;
position: relative;
}
.traffic-tooltip {
position: absolute;
left: 0;
top: calc(100% + 8px);
background: rgba(0, 0, 0, 0.9);
color: var(--color-text-secondary);
font-size: 12px;
padding: 8px 12px;
border-radius: 6px;
white-space: nowrap;
z-index: 10;
animation: tooltip-fade 2.5s ease-out forwards;
pointer-events: none;
}
@keyframes tooltip-fade {
0% {
opacity: 0;
transform: translateY(-4px);
}
10% {
opacity: 1;
transform: translateY(0);
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@media (max-width: 768px) {
.traffic-tooltip {
display: none;
}
}
.traffic-lights.attention .light {
animation: bounce-attention 3s ease-in-out infinite;
}
.traffic-lights.attention .light.yellow {
animation-delay: 0.15s;
}
.traffic-lights.attention .light.green {
animation-delay: 0.3s;
}
@keyframes bounce-attention {
0%, 85%, 100% {
transform: translateY(0);
}
88% {
transform: translateY(-3px);
}
91% {
transform: translateY(0);
}
94% {
transform: translateY(-2px);
}
97% {
transform: translateY(0);
}
}
.light {
width: 12px;
height: 12px;
border-radius: 50%;
border: none;
padding: 0;
cursor: pointer;
transition: transform 0.15s, filter 0.15s;
}
.light:hover {
transform: scale(1.15);
filter: brightness(1.1);
}
.light:active {
transform: scale(0.95);
}
.light.red {
background: #ff5f57;
}
.light.yellow {
background: #ffbd2e;
}
.light.green {
background: #28c840;
position: relative;
}
.green-arrow {
position: absolute;
bottom: calc(100% + 4px);
left: 50%;
transform: translateX(-50%);
font-size: 14px;
color: rgba(255, 255, 255, 0.5);
animation: arrow-pulse 3s ease-in-out infinite;
pointer-events: none;
}
@keyframes arrow-pulse {
0%, 50% {
opacity: 0;
transform: translateX(-50%) translateY(2px);
}
55%, 98% {
opacity: 0.6;
transform: translateX(-50%) translateY(0);
}
100% {
opacity: 0;
transform: translateX(-50%) translateY(2px);
}
}
.window-title {
font-size: 13px;
color: var(--color-text-muted);
flex: 1;
text-align: center;
margin-right: 60px; /* Balance the traffic lights */
}
.obsidian-content {
display: grid;
grid-template-columns: 250px 1fr;
height: 600px;
position: relative;
overflow: visible;
}
.sidebar {
background: var(--color-obsidian-sidebar);
border-right: 1px solid var(--color-border);
overflow: hidden;
}
.editor {
background: var(--color-obsidian-editor);
overflow: hidden;
}
.empty-state {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
}
.mobile-header {
display: none;
}
.sidebar-toggle {
width: 36px;
height: 36px;
border-radius: 8px;
background: transparent;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background 0.15s, color 0.15s;
}
.sidebar-toggle:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--color-text-primary);
}
.mobile-filename {
flex: 1;
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mobile-copy-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
flex-shrink: 0;
transition: background 0.15s, color 0.15s;
}
.mobile-copy-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--color-text-primary);
}
.sidebar-backdrop {
display: none;
}
@media (max-width: 768px) {
.obsidian-content {
grid-template-columns: 1fr;
height: 500px;
}
.mobile-header {
display: flex;
align-items: center;
gap: 8px;
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 5;
padding: 8px 12px;
background: var(--color-obsidian-editor);
border-bottom: 1px solid var(--color-border);
}
.sidebar-toggle {
display: flex;
}
.sidebar {
position: absolute;
top: 0;
left: 0;
width: 260px;
height: 100%;
z-index: 20;
transform: translateX(-100%);
transition: transform 0.25s ease-out;
border-right: 1px solid var(--color-border);
}
.sidebar.open {
transform: translateX(0);
}
.sidebar-backdrop {
display: block;
position: absolute;
inset: 0;
z-index: 15;
background: rgba(0, 0, 0, 0.5);
border: none;
cursor: pointer;
}
.editor {
padding-top: 52px;
}
.editor :global(.viewer-header) {
display: none;
}
/* Fullscreen on mobile - just make it bigger, keep mobile layout */
.obsidian-window.fullscreen .obsidian-content {
height: calc(100vh - 49px);
height: calc(100dvh - 49px);
}
}
</style>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import type { VaultFile } from '$lib/utils/markdown';
import FileTreeItem from './FileTreeItem.svelte';
interface Props {
files: VaultFile[];
selectedPath: string | null;
onSelect: (file: VaultFile) => void;
}
let { files, selectedPath, onSelect }: Props = $props();
</script>
<nav class="file-tree custom-scrollbar" aria-label="File explorer">
{#each files as file (file.path)}
<FileTreeItem {file} {selectedPath} {onSelect} />
{/each}
</nav>
<style>
.file-tree {
height: 100%;
overflow-y: auto;
padding: 8px;
}
</style>

View File

@@ -0,0 +1,112 @@
<script lang="ts">
import type { VaultFile } from '$lib/utils/markdown';
import FileTreeItem from './FileTreeItem.svelte';
interface Props {
file: VaultFile;
selectedPath: string | null;
depth?: number;
onSelect: (file: VaultFile) => void;
}
let { file, selectedPath, depth = 0, onSelect }: Props = $props();
let expanded = $state(false);
const isFolder = $derived(file.type === 'folder');
const isSelected = $derived(selectedPath === file.path);
function handleClick() {
if (isFolder) {
expanded = !expanded;
} else {
onSelect(file);
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
}
</script>
<div class="file-tree-item">
<button
class="item-row"
class:selected={isSelected}
class:folder={isFolder}
style="padding-left: {depth * 12 + 8 + (isFolder ? 0 : 18)}px"
onclick={handleClick}
onkeydown={handleKeydown}
aria-expanded={isFolder ? expanded : undefined}
>
{#if isFolder}
<svg class="chevron" class:expanded width="12" height="12" viewBox="0 0 12 12" fill="currentColor">
<path d="M4.5 2L8.5 6L4.5 10" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{/if}
<span class="name">{file.name}</span>
</button>
{#if isFolder && expanded && file.children}
<div class="children">
{#each file.children as child (child.path)}
<FileTreeItem file={child} {selectedPath} depth={depth + 1} {onSelect} />
{/each}
</div>
{/if}
</div>
<style>
.file-tree-item {
width: 100%;
}
.item-row {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
padding: 4px 8px;
border: none;
background: transparent;
color: var(--color-text-secondary);
font-size: 13px;
text-align: left;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s;
}
.item-row:hover {
background: rgba(255, 255, 255, 0.05);
color: var(--color-text-primary);
}
.item-row.selected {
background: var(--color-obsidian-accent);
color: white;
}
.chevron {
flex-shrink: 0;
opacity: 0.5;
transition: transform 0.15s;
}
.chevron.expanded {
transform: rotate(90deg);
}
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.children {
width: 100%;
}
</style>

View File

@@ -0,0 +1,358 @@
<script lang="ts">
import { Marked } from 'marked';
interface Props {
content: string;
filename: string;
}
let { content, filename }: Props = $props();
let copied = $state(false);
async function copyContent() {
try {
await navigator.clipboard.writeText(content);
copied = true;
setTimeout(() => {
copied = false;
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}
// Configure marked for Obsidian-like rendering
const marked = new Marked({
gfm: true, // GitHub Flavored Markdown (tables, strikethrough, etc.)
breaks: true // Convert \n to <br> like Obsidian
});
// Pre-process Obsidian-specific syntax
function preProcess(md: string): string {
return md
// Obsidian wiki-links [[link]]
.replace(/\[\[(.*?)\]\]/g, '<span class="internal-link">$1</span>');
}
// Simple frontmatter parser (browser-compatible)
function parseFrontmatter(text: string): { data: Record<string, string | string[]>; content: string } {
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!match) {
return { data: {}, content: text };
}
const yamlContent = match[1];
const markdownContent = match[2];
const data: Record<string, string | string[]> = {};
const lines = yamlContent.split('\n');
let currentKey: string | null = null;
let currentArray: string[] = [];
for (const line of lines) {
// Check for key: value or key: (start of array)
const keyMatch = line.match(/^(\w+):\s*(.*)$/);
if (keyMatch) {
// Save previous array if exists
if (currentKey && currentArray.length > 0) {
data[currentKey] = currentArray;
currentArray = [];
}
const key = keyMatch[1];
let value = keyMatch[2].trim();
// Check if it's an inline array [item1, item2]
if (value.startsWith('[') && value.endsWith(']')) {
const items = value.slice(1, -1).split(',').map(s => s.trim().replace(/^["']|["']$/g, ''));
data[key] = items;
currentKey = null;
} else if (value === '' || value === null) {
// Empty value, might be start of multi-line array
currentKey = key;
} else {
// Simple key: value
value = value.replace(/^["']|["']$/g, '');
data[key] = value;
currentKey = null;
}
} else if (currentKey) {
// Check for array item " - value"
const arrayItemMatch = line.match(/^\s+-\s+(.+)$/);
if (arrayItemMatch) {
currentArray.push(arrayItemMatch[1].replace(/^["']|["']$/g, ''));
}
}
}
// Save last array if exists
if (currentKey && currentArray.length > 0) {
data[currentKey] = currentArray;
}
return { data, content: markdownContent };
}
// Parse frontmatter and content
const parsed = $derived(parseFrontmatter(content));
const frontmatter = $derived(parsed.data);
const hasFrontmatter = $derived(Object.keys(frontmatter).length > 0);
const html = $derived(marked.parse(preProcess(parsed.content)) as string);
// Format frontmatter value for display
function formatValue(value: string | string[]): string {
if (Array.isArray(value)) {
return value.join(', ');
}
return value;
}
</script>
<article class="markdown-viewer custom-scrollbar">
<header class="viewer-header">
<span class="filename">{filename}</span>
<button
class="copy-btn"
onclick={copyContent}
aria-label="Copy content"
>
{#if copied}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
{:else}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
{/if}
</button>
</header>
<div class="viewer-content prose-invert">
{#if hasFrontmatter}
<div class="frontmatter">
{#each Object.entries(frontmatter) as [key, value]}
<div class="frontmatter-row">
<span class="frontmatter-key">{key}</span>
<span class="frontmatter-value">{formatValue(value)}</span>
</div>
{/each}
</div>
{/if}
{@html html}
</div>
</article>
<style>
.markdown-viewer {
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.viewer-header {
padding: 12px 20px;
border-bottom: 1px solid var(--color-border);
background: var(--color-obsidian-editor);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.filename {
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
}
.copy-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 6px;
background: transparent;
border: none;
color: var(--color-text-muted);
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.copy-btn:hover {
background: rgba(255, 255, 255, 0.1);
color: var(--color-text-primary);
}
.viewer-content {
padding: 20px;
color: var(--color-text-secondary);
line-height: 1.7;
flex: 1;
}
/* Frontmatter */
.frontmatter {
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--color-border);
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 1.5rem;
font-size: 0.875rem;
}
.frontmatter-row {
display: flex;
gap: 12px;
padding: 4px 0;
}
.frontmatter-row:not(:last-child) {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.frontmatter-key {
color: var(--color-text-muted);
min-width: 80px;
flex-shrink: 0;
}
.frontmatter-value {
color: var(--color-text-primary);
}
/* Headers */
.viewer-content :global(h1) {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text-primary);
margin: 1.5rem 0 1rem;
}
.viewer-content :global(h2) {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 1.25rem 0 0.75rem;
}
.viewer-content :global(h3) {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-text-primary);
margin: 1rem 0 0.5rem;
}
.viewer-content :global(h1:first-child),
.viewer-content :global(h2:first-child),
.viewer-content :global(h3:first-child) {
margin-top: 0;
}
/* Paragraphs */
.viewer-content :global(p) {
margin-bottom: 0.75rem;
}
/* Bold & italic */
.viewer-content :global(strong) {
font-weight: 600;
color: var(--color-text-primary);
}
/* Code */
.viewer-content :global(code) {
padding: 0.125rem 0.375rem;
background: var(--color-bg-tertiary);
color: var(--color-primary-light);
border-radius: 4px;
font-size: 0.875rem;
font-family: monospace;
}
.viewer-content :global(pre) {
background: var(--color-bg-tertiary);
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
margin: 1rem 0;
}
.viewer-content :global(pre code) {
padding: 0;
background: none;
}
/* Links */
.viewer-content :global(a) {
color: var(--color-primary-light);
text-decoration: none;
}
.viewer-content :global(a:hover) {
text-decoration: underline;
}
.viewer-content :global(.internal-link) {
color: var(--color-primary-light);
cursor: pointer;
}
.viewer-content :global(.internal-link:hover) {
text-decoration: underline;
}
/* Lists */
.viewer-content :global(ul),
.viewer-content :global(ol) {
margin: 0.5rem 0 0.5rem 1.5rem;
}
.viewer-content :global(li) {
margin-bottom: 0.25rem;
}
/* Blockquotes */
.viewer-content :global(blockquote) {
border-left: 3px solid var(--color-primary);
padding-left: 1rem;
margin: 1rem 0;
color: var(--color-text-muted);
font-style: italic;
}
/* Tables */
.viewer-content :global(table) {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.viewer-content :global(th),
.viewer-content :global(td) {
padding: 0.5rem 0.75rem;
border: 1px solid var(--color-border);
text-align: left;
}
.viewer-content :global(th) {
font-weight: 600;
color: var(--color-text-primary);
}
/* Horizontal rule */
.viewer-content :global(hr) {
border: none;
border-top: 1px solid var(--color-border);
margin: 1.5rem 0;
}
/* Strikethrough */
.viewer-content :global(del) {
color: var(--color-text-muted);
}
</style>

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import { getI18n } from '$lib/i18n';
const i18n = getI18n();
const t = $derived(i18n.t);
</script>
<footer class="py-12 px-6 border-t border-[var(--color-border)]">
<div class="max-w-6xl mx-auto text-center">
<p class="text-[var(--color-text-muted)] text-sm">
{t.footer.builtBy}
</p>
</div>
</footer>

View File

@@ -0,0 +1,268 @@
<script lang="ts">
import { getI18n, languages, type Language } from '$lib/i18n';
import { goto } from '$app/navigation';
import { onMount, onDestroy } from 'svelte';
import LiquidGlass from './LiquidGlass.svelte';
const i18n = getI18n();
const t = $derived(i18n.t);
const languageNames: Record<Language, string> = {
ru: 'Русский',
en: 'English'
};
// Track header states
let useFallback = $state(false);
let isExpanded = $state(false);
let scrollContainer: HTMLElement | null = null;
function handleScroll() {
if (!scrollContainer) return;
const scrollTop = scrollContainer.scrollTop;
const threshold = window.innerHeight + 50;
// Switch TO fallback when header fully inside FakeObsidian section
if (!useFallback && scrollTop > threshold) {
useFallback = true;
// Small delay then expand
setTimeout(() => {
isExpanded = true;
}, 50);
}
// Switch BACK when scrolling back into hero area
else if (useFallback && isExpanded && scrollTop < threshold) {
// Start shrinking animation
isExpanded = false;
// After animation completes, swap to LiquidGlass
setTimeout(() => {
useFallback = false;
}, 400); // Match transition duration
}
}
function handleLanguageChange(e: Event) {
const select = e.target as HTMLSelectElement;
const newLang = select.value as Language;
const newPath = newLang === 'ru' ? '/' : `/${newLang}`;
goto(newPath);
}
onMount(() => {
scrollContainer = document.querySelector('.landing-page') as HTMLElement;
if (scrollContainer) {
scrollContainer.addEventListener('scroll', handleScroll);
}
});
onDestroy(() => {
if (scrollContainer) {
scrollContainer.removeEventListener('scroll', handleScroll);
}
});
</script>
<header class="header-wrapper" class:expanded={isExpanded}>
<!-- CSS backdrop-filter fallback (always rendered, animates) -->
<div
class="header-glass-fallback"
class:active={useFallback}
class:expanded={isExpanded}
>
<div class="header-inner">
<a href="/" class="logo-link">
<span class="logo-symbol"></span>
<span class="logo-text">{t.header.logo}</span>
</a>
<select
value={i18n.lang}
onchange={handleLanguageChange}
class="lang-select"
>
{#each languages as lang (lang)}
<option value={lang}>{languageNames[lang]}</option>
{/each}
</select>
</div>
</div>
<!-- LiquidGlass WebGL effect (always rendered, fades out when fallback active) -->
<div class="liquid-glass-wrapper" class:hidden={useFallback}>
<LiquidGlass
borderRadius={12}
type="rounded"
tintOpacity={0.1}
blurRadius={3}
edgeIntensity={0.015}
rimIntensity={0.1}
rimDistance={0.5}
cornerBoost={0.3}
rippleEffect={0.2}
class="header-glass"
>
<div class="header-inner">
<a href="/" class="logo-link">
<span class="logo-symbol"></span>
<span class="logo-text">{t.header.logo}</span>
</a>
<select
value={i18n.lang}
onchange={handleLanguageChange}
class="lang-select"
>
{#each languages as lang (lang)}
<option value={lang}>{languageNames[lang]}</option>
{/each}
</select>
</div>
</LiquidGlass>
</div>
</header>
<style>
.header-wrapper {
position: fixed;
top: 1rem;
left: 50%;
transform: translateX(-50%);
z-index: 50;
width: calc(100% - 8rem);
max-width: 72rem;
height: 4.25rem;
transition:
top 0.4s cubic-bezier(0.4, 0, 0.2, 1),
width 0.4s cubic-bezier(0.4, 0, 0.2, 1),
max-width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.header-wrapper.expanded {
top: 0;
width: 100%;
max-width: 100%;
}
:global(.header-glass) {
width: 100%;
}
.header-glass-fallback,
.liquid-glass-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
/* LiquidGlass is always visible and rendered, starts on top */
.liquid-glass-wrapper {
z-index: 2;
}
/* When hidden, LiquidGlass goes behind fallback */
.liquid-glass-wrapper.hidden {
z-index: 0;
pointer-events: none;
}
/* Fallback starts behind LiquidGlass */
.header-glass-fallback {
z-index: 1;
border-radius: 12px;
background: rgba(190, 190, 190, 0.1);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
pointer-events: none;
transition: border-radius 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* When active, fallback goes on top and receives events */
.header-glass-fallback.active {
z-index: 3;
pointer-events: auto;
}
.header-glass-fallback.expanded {
border-radius: 0;
}
.header-inner {
padding: 0 2rem;
height: 4.25rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo-link {
display: flex;
align-items: center;
gap: 0.75rem;
color: rgba(255, 255, 255, 0.95);
text-decoration: none;
transition: color 0.2s;
}
.logo-link:hover {
color: #fff;
}
.logo-symbol {
font-size: 1.5rem;
font-weight: 600;
}
.logo-text {
font-size: 1.125rem;
font-weight: 600;
}
.lang-select {
appearance: none;
padding: 0.5rem 2rem 0.5rem 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
background-color: transparent;
border: none;
cursor: pointer;
transition: color 0.2s;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.5rem center;
}
.lang-select:hover {
color: #fff;
}
.lang-select:focus {
outline: none;
}
.lang-select option {
background-color: #1a1a1f;
color: #fff;
}
/* Mobile */
@media (max-width: 640px) {
.header-wrapper {
top: 0.75rem;
width: calc(100% - 1.5rem);
height: 3.5rem;
}
.header-wrapper.expanded {
top: 0;
width: 100%;
}
.header-inner {
padding: 0 1rem;
height: 3.5rem;
}
}
</style>

View File

@@ -0,0 +1,374 @@
<script lang="ts">
let mouseX = $state(0.5);
let mouseY = $state(0.5);
function handleMouseMove(e: MouseEvent) {
mouseX = e.clientX / window.innerWidth;
mouseY = e.clientY / window.innerHeight;
}
// Light follows mouse subtly
const lightX = $derived(45 + (mouseX - 0.5) * 20);
const lightY = $derived(38 + (mouseY - 0.5) * 15);
const lightIntensity = $derived(0.15 + (1 - Math.abs(mouseX - 0.5) * 2) * 0.1);
// Per-stripe proximity calculation
// Stripes are roughly centered horizontally with gaps
// We map mouse X position to stripe proximity (0-1)
const stripePositions = [0.25, 0.375, 0.5, 0.625, 0.75]; // Normalized positions
function getStripeProximity(stripeIndex: number): number {
const stripePos = stripePositions[stripeIndex];
const distance = Math.abs(mouseX - stripePos);
// More generous proximity detection (0.15 = close, 0.3 = far)
const proximity = Math.max(0, 1 - distance / 0.2);
return proximity;
}
// Derive proximity values for each stripe
const proximity1 = $derived(getStripeProximity(0));
const proximity2 = $derived(getStripeProximity(1));
const proximity3 = $derived(getStripeProximity(2));
const proximity4 = $derived(getStripeProximity(3));
const proximity5 = $derived(getStripeProximity(4));
// Calculate neighbor illumination (glow on edges from nearby hovered stripe)
function getNeighborGlow(stripeIndex: number): number {
const leftNeighborProx = stripeIndex > 0 ? getStripeProximity(stripeIndex - 1) : 0;
const rightNeighborProx = stripeIndex < 4 ? getStripeProximity(stripeIndex + 1) : 0;
return Math.max(leftNeighborProx, rightNeighborProx) * 0.6;
}
const neighborGlow1 = $derived(getNeighborGlow(0));
const neighborGlow2 = $derived(getNeighborGlow(1));
const neighborGlow3 = $derived(getNeighborGlow(2));
const neighborGlow4 = $derived(getNeighborGlow(3));
const neighborGlow5 = $derived(getNeighborGlow(4));
</script>
<svelte:window onmousemove={handleMouseMove} />
<div class="aurora-container">
<!-- 3D stripes cluster -->
<div class="stripes-wrapper">
<div
class="stripe stripe-1"
style="--proximity: {proximity1}; --neighbor-glow: {neighborGlow1};"
></div>
<div
class="stripe stripe-2"
style="--proximity: {proximity2}; --neighbor-glow: {neighborGlow2};"
></div>
<div
class="stripe stripe-3"
style="--proximity: {proximity3}; --neighbor-glow: {neighborGlow3};"
></div>
<div
class="stripe stripe-4"
style="--proximity: {proximity4}; --neighbor-glow: {neighborGlow4};"
></div>
<div
class="stripe stripe-5"
style="--proximity: {proximity5}; --neighbor-glow: {neighborGlow5};"
></div>
</div>
<!-- Central light source - follows mouse -->
<div
class="light-source"
style="left: {lightX}%; top: {lightY}%; opacity: {lightIntensity};"
></div>
<!-- Floating vignette layers -->
<div class="vignette-top-left"></div>
<div class="vignette-bottom-right"></div>
<div class="vignette-right"></div>
<div class="vignette-main"></div>
<!-- Metallic grain layer -->
<div class="metallic-grain"></div>
<!-- Fine paper grain -->
<div class="grain"></div>
</div>
<style>
.aurora-container {
position: absolute;
inset: 0;
overflow: hidden;
z-index: 0;
pointer-events: none;
background: #08080a;
}
.stripes-wrapper {
position: absolute;
top: 50%;
left: 53%;
width: 600px;
height: 900px;
transform: translate(-50%, -50%) rotate(-35deg) perspective(1000px) rotateY(-5deg);
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
transform-style: preserve-3d;
animation: wrapper-breathe 6s ease-in-out infinite;
}
.stripe {
--proximity: 0;
--neighbor-glow: 0;
position: relative;
width: 110px;
height: 100%;
border-radius: 55px;
overflow: hidden;
transform-style: preserve-3d;
box-shadow:
inset -15px 0 30px rgba(0, 0, 0, 0.5),
inset 8px 0 20px rgba(255, 255, 255, 0.05),
-5px 0 20px rgba(0, 0, 0, 0.3),
5px 0 15px rgba(0, 0, 0, 0.2),
/* Proximity highlight glow */
0 0 calc(20px + var(--proximity) * 40px) rgba(255, 120, 150, calc(var(--proximity) * 0.3)),
/* Neighbor edge illumination - left */
inset 12px 0 25px rgba(255, 150, 180, calc(var(--neighbor-glow) * 0.25)),
/* Neighbor edge illumination - right */
inset -12px 0 25px rgba(255, 150, 180, calc(var(--neighbor-glow) * 0.15));
transition: box-shadow 0.3s ease-out;
}
.stripe::before {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(180deg, transparent 0%, rgba(0,0,0,0) 15%, rgba(0,0,0,0) 85%, transparent 100%),
linear-gradient(90deg,
rgba(0, 0, 0, 0.4) 0%,
rgba(0, 0, 0, 0.1) 15%,
transparent 30%,
transparent 50%,
rgba(255, 255, 255, 0.03) 60%,
rgba(0, 0, 0, 0.2) 80%,
rgba(0, 0, 0, 0.5) 100%
),
linear-gradient(180deg,
transparent 5%,
var(--stripe-color) 25%,
var(--stripe-color-mid) 50%,
var(--stripe-color) 75%,
transparent 95%
);
animation: var(--flow-anim);
}
.stripe::after {
content: '';
position: absolute;
inset: 0;
background:
linear-gradient(
165deg,
transparent 0%,
transparent 20%,
rgba(255, 255, 255, 0.12) 35%,
rgba(255, 220, 220, 0.15) 45%,
rgba(255, 255, 255, 0.08) 55%,
transparent 70%,
transparent 100%
),
linear-gradient(
90deg,
rgba(255, 150, 150, 0.1) 0%,
transparent 10%,
transparent 90%,
rgba(100, 150, 200, 0.05) 100%
);
animation: highlight-drift 7s ease-in-out infinite;
}
.stripe-1 {
--stripe-color: rgba(200, 45, 70, 0.9);
--stripe-color-mid: rgba(240, 60, 90, 0.95);
--flow-anim: flow-a 8s ease-in-out infinite;
--base-z: 10px;
transform: translateZ(var(--base-z)) scale(calc(1 + var(--proximity) * 0.08));
transition: transform 0.4s ease-out, box-shadow 0.3s ease-out;
}
.stripe-2 {
--stripe-color: rgba(220, 55, 85, 0.92);
--stripe-color-mid: rgba(255, 75, 105, 0.97);
--flow-anim: flow-b 9s ease-in-out infinite;
--base-z: 20px;
transform: translateZ(var(--base-z)) scale(calc(1 + var(--proximity) * 0.08));
transition: transform 0.4s ease-out, box-shadow 0.3s ease-out;
}
.stripe-3 {
--stripe-color: rgba(235, 50, 80, 0.95);
--stripe-color-mid: rgba(255, 65, 95, 1);
--flow-anim: flow-c 7s ease-in-out infinite;
--base-z: 25px;
transform: translateZ(var(--base-z)) scale(calc(1 + var(--proximity) * 0.1));
transition: transform 0.4s ease-out, box-shadow 0.3s ease-out;
}
.stripe-4 {
--stripe-color: rgba(40, 60, 90, 0.75);
--stripe-color-mid: rgba(55, 80, 115, 0.8);
--flow-anim: flow-d 10s ease-in-out infinite;
--base-z: 15px;
transform: translateZ(var(--base-z)) scale(calc(1 + var(--proximity) * 0.08));
transition: transform 0.4s ease-out, box-shadow 0.3s ease-out;
}
.stripe-5 {
--stripe-color: rgba(30, 45, 70, 0.6);
--stripe-color-mid: rgba(45, 65, 95, 0.65);
--flow-anim: flow-e 11s ease-in-out infinite;
--base-z: 5px;
transform: translateZ(var(--base-z)) scale(calc(1 + var(--proximity) * 0.08));
transition: transform 0.4s ease-out, box-shadow 0.3s ease-out;
}
.light-source {
position: absolute;
width: 500px;
height: 400px;
transform: translate(-50%, -50%);
background: radial-gradient(
ellipse at center,
rgba(255, 120, 140, 0.25) 0%,
rgba(220, 70, 90, 0.12) 40%,
transparent 70%
);
transition: left 0.5s ease-out, top 0.5s ease-out, opacity 0.3s ease-out;
animation: light-pulse 5s ease-in-out infinite;
}
/* Floating vignette edges */
.vignette-top-left {
position: absolute;
inset: 0;
background: linear-gradient(to bottom right, #08080a 0%, #08080a 15%, transparent 45%);
animation: vignette-float-tl 14s ease-in-out infinite;
}
.vignette-bottom-right {
position: absolute;
inset: 0;
background: linear-gradient(to top left, #08080a 0%, #08080a 12%, transparent 42%);
animation: vignette-float-br 12s ease-in-out infinite;
}
.vignette-right {
position: absolute;
inset: 0;
background: linear-gradient(to left, #08080a 0%, transparent 22%);
animation: vignette-float-r 15s ease-in-out infinite;
}
.vignette-main {
position: absolute;
inset: 0;
background: radial-gradient(
ellipse 45% 50% at 50% 42%,
transparent 0%,
transparent 25%,
rgba(8, 8, 10, 0.5) 50%,
rgba(8, 8, 10, 0.85) 70%,
#08080a 85%
);
animation: vignette-breathe 8s ease-in-out infinite;
}
.metallic-grain {
position: absolute;
inset: 0;
opacity: 0.035;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 512 512' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='metal'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' seed='5' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23metal)'/%3E%3C/svg%3E");
mix-blend-mode: overlay;
animation: grain-shift 0.5s steps(2) infinite;
}
.grain {
position: absolute;
inset: 0;
opacity: 0.06;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
mix-blend-mode: soft-light;
}
/* Wrapper breathing */
@keyframes wrapper-breathe {
0%, 100% { transform: translate(-50%, -50%) rotate(-35deg) perspective(1000px) rotateY(-5deg) scale(1); }
50% { transform: translate(-50%, -50%) rotate(-35deg) perspective(1000px) rotateY(-5deg) scale(1.03); }
}
/* Stronger stripe breathing */
@keyframes flow-a {
0%, 100% { transform: translateZ(10px) translateY(0) scaleY(1); opacity: 0.9; }
50% { transform: translateZ(10px) translateY(-4%) scaleY(1.02); opacity: 1; }
}
@keyframes flow-b {
0%, 100% { transform: translateZ(20px) translateY(0) scaleY(1); opacity: 0.92; }
50% { transform: translateZ(20px) translateY(5%) scaleY(0.98); opacity: 0.85; }
}
@keyframes flow-c {
0%, 100% { transform: translateZ(25px) translateY(0) scaleY(1); opacity: 0.95; }
33% { transform: translateZ(25px) translateY(-3%) scaleY(1.01); opacity: 0.9; }
66% { transform: translateZ(25px) translateY(3%) scaleY(0.99); opacity: 1; }
}
@keyframes flow-d {
0%, 100% { transform: translateZ(15px) translateY(0) scaleY(1); opacity: 0.75; }
50% { transform: translateZ(15px) translateY(-6%) scaleY(1.03); opacity: 0.85; }
}
@keyframes flow-e {
0%, 100% { transform: translateZ(5px) translateY(0) scaleY(1); opacity: 0.6; }
50% { transform: translateZ(5px) translateY(4%) scaleY(0.97); opacity: 0.72; }
}
@keyframes highlight-drift {
0%, 100% { transform: translateY(-12%); opacity: 0.75; }
50% { transform: translateY(12%); opacity: 1; }
}
@keyframes light-pulse {
0%, 100% { transform: translate(-50%, -50%) scale(1); filter: blur(0px); }
50% { transform: translate(-50%, -50%) scale(1.08); filter: blur(5px); }
}
/* Floating vignette animations */
@keyframes vignette-float-tl {
0%, 100% { transform: translate(0, 0); opacity: 1; }
50% { transform: translate(2%, 2%); opacity: 0.92; }
}
@keyframes vignette-float-br {
0%, 100% { transform: translate(0, 0); opacity: 1; }
50% { transform: translate(-2%, -2%); opacity: 0.9; }
}
@keyframes vignette-float-r {
0%, 100% { transform: translateX(0); opacity: 1; }
50% { transform: translateX(-1.5%); opacity: 0.92; }
}
@keyframes vignette-breathe {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.02); opacity: 0.95; }
}
@keyframes grain-shift {
0% { transform: translate(0, 0); }
100% { transform: translate(-1px, -1px); }
}
</style>

View File

@@ -0,0 +1,93 @@
<script lang="ts">
import { getI18n } from '$lib/i18n';
import AuroraBackground from './AuroraBackground.svelte';
const i18n = getI18n();
const t = $derived(i18n.t);
</script>
<section class="hero-section">
<AuroraBackground />
<div class="hero-content">
<h1 class="hero-title">
<span class="hero-title-line">{t.hero.titleLine1}</span>
<span class="hero-title-line">{t.hero.titleLine2}</span>
</h1>
<p class="hero-subtitle">
{t.hero.subtitle}
</p>
</div>
<div class="hero-philosophy">
<p>{t.hero.philosophy}</p>
</div>
</section>
<style>
.hero-section {
position: relative;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.hero-content {
position: relative;
z-index: 10;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 1.5rem;
padding-bottom: 12vh;
}
.hero-title {
text-align: center;
margin-bottom: 1.5rem;
}
.hero-title-line {
display: block;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Inter', system-ui, sans-serif;
font-size: clamp(2.5rem, 7vw, 4rem);
font-weight: 600;
letter-spacing: 0.02em;
line-height: 1.08;
color: #fff;
}
.hero-subtitle {
max-width: 34rem;
text-align: center;
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Inter', system-ui, sans-serif;
font-size: clamp(0.95rem, 1.8vw, 1.1rem);
font-weight: 400;
line-height: 1.55;
color: rgba(255, 255, 255, 0.95);
letter-spacing: 0.01em;
}
.hero-philosophy {
position: absolute;
bottom: 12vh;
left: 0;
right: 0;
z-index: 10;
text-align: center;
padding: 0 1.5rem;
}
.hero-philosophy p {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
font-size: 0.75rem;
font-weight: 400;
color: white;
opacity: 0.6;
letter-spacing: 0;
}
</style>

View File

@@ -0,0 +1,664 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import html2canvas from 'html2canvas';
interface Props {
borderRadius?: number;
type?: 'rounded' | 'circle' | 'pill';
tintOpacity?: number;
blurRadius?: number;
edgeIntensity?: number;
rimIntensity?: number;
baseIntensity?: number;
edgeDistance?: number;
rimDistance?: number;
baseDistance?: number;
cornerBoost?: number;
rippleEffect?: number;
enableWarp?: boolean;
class?: string;
children?: import('svelte').Snippet;
}
let {
borderRadius = 12,
type = 'rounded',
tintOpacity = 0.2,
blurRadius = 5.0,
edgeIntensity = 0.01,
rimIntensity = 0.05,
baseIntensity = 0.01,
edgeDistance = 0.15,
rimDistance = 0.8,
baseDistance = 0.1,
cornerBoost = 0.02,
rippleEffect = 0.1,
enableWarp = false,
class: className = '',
children
}: Props = $props();
let containerEl: HTMLDivElement;
let canvasEl: HTMLCanvasElement;
let gl: WebGLRenderingContext | null = null;
let glRefs: Record<string, any> = {};
let animationId: number;
let width = $state(0);
let height = $state(0);
let actualBorderRadius = $state(12);
let isWebGLReady = $state(false);
// Shared page snapshot
let pageSnapshot: HTMLCanvasElement | null = null;
let scrollContainer: HTMLElement | null = null;
let capturedElementOffset = { x: 0, y: 0 };
const vertexShader = `
attribute vec2 a_position;
attribute vec2 a_texcoord;
varying vec2 v_texcoord;
void main() {
gl_Position = vec4(a_position, 0, 1);
v_texcoord = a_texcoord;
}
`;
const fragmentShader = `
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_resolution;
uniform vec2 u_textureSize;
uniform float u_scrollY;
uniform float u_pageHeight;
uniform float u_viewportHeight;
uniform float u_blurRadius;
uniform float u_borderRadius;
uniform vec2 u_containerPosition;
uniform float u_warp;
uniform float u_edgeIntensity;
uniform float u_rimIntensity;
uniform float u_baseIntensity;
uniform float u_edgeDistance;
uniform float u_rimDistance;
uniform float u_baseDistance;
uniform float u_cornerBoost;
uniform float u_rippleEffect;
uniform float u_tintOpacity;
varying vec2 v_texcoord;
float roundedRectDistance(vec2 coord, vec2 size, float radius) {
vec2 center = size * 0.5;
vec2 pixelCoord = coord * size;
vec2 toCorner = abs(pixelCoord - center) - (center - radius);
float outsideCorner = length(max(toCorner, 0.0));
float insideCorner = min(max(toCorner.x, toCorner.y), 0.0);
return (outsideCorner + insideCorner - radius);
}
float circleDistance(vec2 coord, vec2 size, float radius) {
vec2 center = vec2(0.5, 0.5);
vec2 pixelCoord = coord * size;
vec2 centerPixel = center * size;
float distFromCenter = length(pixelCoord - centerPixel);
return distFromCenter - radius;
}
bool isPill(vec2 size, float radius) {
float heightRatioDiff = abs(radius - size.y * 0.5);
bool radiusMatchesHeight = heightRatioDiff < 2.0;
bool isWiderThanTall = size.x > size.y + 4.0;
return radiusMatchesHeight && isWiderThanTall;
}
bool isCircle(vec2 size, float radius) {
float minDim = min(size.x, size.y);
bool radiusMatchesMinDim = abs(radius - minDim * 0.5) < 1.0;
bool isRoughlySquare = abs(size.x - size.y) < 4.0;
return radiusMatchesMinDim && isRoughlySquare;
}
float pillDistance(vec2 coord, vec2 size, float radius) {
vec2 center = size * 0.5;
vec2 pixelCoord = coord * size;
vec2 capsuleStart = vec2(radius, center.y);
vec2 capsuleEnd = vec2(size.x - radius, center.y);
vec2 capsuleAxis = capsuleEnd - capsuleStart;
float capsuleLength = length(capsuleAxis);
if (capsuleLength > 0.0) {
vec2 toPoint = pixelCoord - capsuleStart;
float t = clamp(dot(toPoint, capsuleAxis) / dot(capsuleAxis, capsuleAxis), 0.0, 1.0);
vec2 closestPointOnAxis = capsuleStart + t * capsuleAxis;
return length(pixelCoord - closestPointOnAxis) - radius;
} else {
return length(pixelCoord - center) - radius;
}
}
void main() {
vec2 coord = v_texcoord;
float scrollY = u_scrollY;
vec2 containerSize = u_resolution;
vec2 textureSize = u_textureSize;
vec2 containerCenter = u_containerPosition + vec2(0.0, scrollY);
vec2 containerOffset = (coord - 0.5) * containerSize;
vec2 pagePixel = containerCenter + containerOffset;
vec2 textureCoord = pagePixel / textureSize;
float distFromEdgeShape;
vec2 shapeNormal;
if (isPill(u_resolution, u_borderRadius)) {
distFromEdgeShape = -pillDistance(coord, u_resolution, u_borderRadius);
vec2 center = vec2(0.5, 0.5);
vec2 pixelCoord = coord * u_resolution;
vec2 capsuleStart = vec2(u_borderRadius, center.y * u_resolution.y);
vec2 capsuleEnd = vec2(u_resolution.x - u_borderRadius, center.y * u_resolution.y);
vec2 capsuleAxis = capsuleEnd - capsuleStart;
float capsuleLength = length(capsuleAxis);
if (capsuleLength > 0.0) {
vec2 toPoint = pixelCoord - capsuleStart;
float t = clamp(dot(toPoint, capsuleAxis) / dot(capsuleAxis, capsuleAxis), 0.0, 1.0);
vec2 closestPointOnAxis = capsuleStart + t * capsuleAxis;
vec2 normalDir = pixelCoord - closestPointOnAxis;
shapeNormal = length(normalDir) > 0.0 ? normalize(normalDir) : vec2(0.0, 1.0);
} else {
shapeNormal = normalize(coord - center);
}
} else if (isCircle(u_resolution, u_borderRadius)) {
distFromEdgeShape = -circleDistance(coord, u_resolution, u_borderRadius);
vec2 center = vec2(0.5, 0.5);
shapeNormal = normalize(coord - center);
} else {
distFromEdgeShape = -roundedRectDistance(coord, u_resolution, u_borderRadius);
vec2 center = vec2(0.5, 0.5);
shapeNormal = normalize(coord - center);
}
distFromEdgeShape = max(distFromEdgeShape, 0.0);
float distFromLeft = coord.x;
float distFromRight = 1.0 - coord.x;
float distFromTop = coord.y;
float distFromBottom = 1.0 - coord.y;
float distFromEdge = distFromEdgeShape / min(u_resolution.x, u_resolution.y);
float normalizedDistance = distFromEdge * min(u_resolution.x, u_resolution.y);
float baseIntensityCalc = 1.0 - exp(-normalizedDistance * u_baseDistance);
float edgeIntensityCalc = exp(-normalizedDistance * u_edgeDistance);
float rimIntensityCalc = exp(-normalizedDistance * u_rimDistance);
float baseComponent = u_warp > 0.5 ? baseIntensityCalc * u_baseIntensity : 0.0;
float totalIntensity = baseComponent + edgeIntensityCalc * u_edgeIntensity + rimIntensityCalc * u_rimIntensity;
vec2 baseRefraction = shapeNormal * totalIntensity;
float cornerProximityX = min(distFromLeft, distFromRight);
float cornerProximityY = min(distFromTop, distFromBottom);
float cornerDistance = max(cornerProximityX, cornerProximityY);
float cornerNormalized = cornerDistance * min(u_resolution.x, u_resolution.y);
float cornerBoostCalc = exp(-cornerNormalized * 0.3) * u_cornerBoost;
vec2 cornerRefraction = shapeNormal * cornerBoostCalc;
vec2 perpendicular = vec2(-shapeNormal.y, shapeNormal.x);
float rippleEffectCalc = sin(distFromEdge * 25.0) * u_rippleEffect * rimIntensityCalc;
vec2 textureRefraction = perpendicular * rippleEffectCalc;
vec2 totalRefraction = baseRefraction + cornerRefraction + textureRefraction;
textureCoord += totalRefraction;
// Gaussian blur
vec4 color = vec4(0.0);
vec2 texelSize = 1.0 / u_textureSize;
float sigma = u_blurRadius / 2.0;
vec2 blurStep = texelSize * sigma;
float totalWeight = 0.0;
for(float i = -6.0; i <= 6.0; i += 1.0) {
for(float j = -6.0; j <= 6.0; j += 1.0) {
float distance = length(vec2(i, j));
if(distance > 6.0) continue;
float weight = exp(-(distance * distance) / (2.0 * sigma * sigma));
vec2 offset = vec2(i, j) * blurStep;
color += texture2D(u_image, textureCoord + offset) * weight;
totalWeight += weight;
}
}
color /= totalWeight;
// Simple vertical gradient
float gradientPosition = coord.y;
vec3 topTint = vec3(1.0, 1.0, 1.0);
vec3 bottomTint = vec3(0.7, 0.7, 0.7);
vec3 gradientTint = mix(topTint, bottomTint, gradientPosition);
vec3 tintedColor = mix(color.rgb, gradientTint, u_tintOpacity);
color = vec4(tintedColor, color.a);
// Sampled gradient
vec2 viewportCenter = containerCenter;
float topY = (viewportCenter.y - containerSize.y * 0.4) / textureSize.y;
float midY = viewportCenter.y / textureSize.y;
float bottomY = (viewportCenter.y + containerSize.y * 0.4) / textureSize.y;
vec3 topColor = vec3(0.0);
vec3 midColor = vec3(0.0);
vec3 bottomColor = vec3(0.0);
float sampleCount = 0.0;
for(float x = 0.0; x < 1.0; x += 0.05) {
for(float yOffset = -5.0; yOffset <= 5.0; yOffset += 1.0) {
vec2 topSample = vec2(x, topY + yOffset * texelSize.y);
vec2 midSample = vec2(x, midY + yOffset * texelSize.y);
vec2 bottomSample = vec2(x, bottomY + yOffset * texelSize.y);
topColor += texture2D(u_image, topSample).rgb;
midColor += texture2D(u_image, midSample).rgb;
bottomColor += texture2D(u_image, bottomSample).rgb;
sampleCount += 1.0;
}
}
topColor /= sampleCount;
midColor /= sampleCount;
bottomColor /= sampleCount;
vec3 sampledGradient;
if (gradientPosition < 0.1) {
sampledGradient = topColor;
} else if (gradientPosition > 0.9) {
sampledGradient = bottomColor;
} else {
float transitionPos = (gradientPosition - 0.1) / 0.8;
if (transitionPos < 0.5) {
float t = transitionPos * 2.0;
sampledGradient = mix(topColor, midColor, t);
} else {
float t = (transitionPos - 0.5) * 2.0;
sampledGradient = mix(midColor, bottomColor, t);
}
}
vec3 finalTinted = mix(color.rgb, sampledGradient, u_tintOpacity * 0.3);
color = vec4(finalTinted, color.a);
// Shape mask
float maskDistance;
if (isPill(u_resolution, u_borderRadius)) {
maskDistance = pillDistance(coord, u_resolution, u_borderRadius);
} else if (isCircle(u_resolution, u_borderRadius)) {
maskDistance = circleDistance(coord, u_resolution, u_borderRadius);
} else {
maskDistance = roundedRectDistance(coord, u_resolution, u_borderRadius);
}
float mask = 1.0 - smoothstep(-1.0, 1.0, maskDistance);
gl_FragColor = vec4(color.rgb, mask);
}
`;
function compileShader(gl: WebGLRenderingContext, shaderType: number, source: string): WebGLShader | null {
const shader = gl.createShader(shaderType);
if (!shader) return null;
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('Shader compile error:', gl.getShaderInfoLog(shader));
return null;
}
return shader;
}
function createProgram(gl: WebGLRenderingContext, vsSource: string, fsSource: string): WebGLProgram | null {
const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
if (!vs || !fs) return null;
const program = gl.createProgram();
if (!program) return null;
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error('Program link error:', gl.getProgramInfoLog(program));
return null;
}
return program;
}
function getPosition() {
const rect = canvasEl.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
function updateSizeFromDOM() {
if (!containerEl) return;
const rect = containerEl.getBoundingClientRect();
let newWidth = Math.ceil(rect.width);
let newHeight = Math.ceil(rect.height);
if (type === 'circle') {
const size = Math.max(newWidth, newHeight);
newWidth = size;
newHeight = size;
actualBorderRadius = size / 2;
} else if (type === 'pill') {
actualBorderRadius = newHeight / 2;
} else {
actualBorderRadius = borderRadius;
}
if (newWidth !== width || newHeight !== height) {
width = newWidth;
height = newHeight;
if (canvasEl) {
canvasEl.width = newWidth;
canvasEl.height = newHeight;
}
if (glRefs.gl) {
glRefs.gl.viewport(0, 0, newWidth, newHeight);
glRefs.gl.uniform2f(glRefs.resolutionLoc, newWidth, newHeight);
glRefs.gl.uniform1f(glRefs.borderRadiusLoc, actualBorderRadius);
}
}
}
async function capturePageSnapshot(): Promise<HTMLCanvasElement | null> {
try {
// Only capture the Hero section - LiquidGlass is only used there anyway
const heroElement = document.querySelector('.hero-section') as HTMLElement;
const targetElement = heroElement || document.body;
// Save the element's position offset for coordinate calculation
if (heroElement) {
const rect = heroElement.getBoundingClientRect();
capturedElementOffset = { x: rect.left, y: rect.top };
}
const snapshot = await html2canvas(targetElement, {
scale: 1,
useCORS: true,
allowTaint: true,
backgroundColor: null,
ignoreElements: (element) => {
return element.classList.contains('liquid-glass-container') ||
element.classList.contains('liquid-glass-canvas') ||
element.classList.contains('light-source') ||
element.classList.contains('metallic-grain') ||
element.classList.contains('grain');
}
});
return snapshot;
} catch (error) {
console.error('html2canvas error:', error);
return null;
}
}
function setupShader(image: HTMLImageElement) {
if (!gl) return;
const program = createProgram(gl, vertexShader, fragmentShader);
if (!program) return;
gl.useProgram(program);
// Set up geometry
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
const texcoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0]), gl.STATIC_DRAW);
// Get locations
const positionLoc = gl.getAttribLocation(program, 'a_position');
const texcoordLoc = gl.getAttribLocation(program, 'a_texcoord');
const resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
const textureSizeLoc = gl.getUniformLocation(program, 'u_textureSize');
const scrollYLoc = gl.getUniformLocation(program, 'u_scrollY');
const pageHeightLoc = gl.getUniformLocation(program, 'u_pageHeight');
const viewportHeightLoc = gl.getUniformLocation(program, 'u_viewportHeight');
const blurRadiusLoc = gl.getUniformLocation(program, 'u_blurRadius');
const borderRadiusLoc = gl.getUniformLocation(program, 'u_borderRadius');
const containerPositionLoc = gl.getUniformLocation(program, 'u_containerPosition');
const warpLoc = gl.getUniformLocation(program, 'u_warp');
const edgeIntensityLoc = gl.getUniformLocation(program, 'u_edgeIntensity');
const rimIntensityLoc = gl.getUniformLocation(program, 'u_rimIntensity');
const baseIntensityLoc = gl.getUniformLocation(program, 'u_baseIntensity');
const edgeDistanceLoc = gl.getUniformLocation(program, 'u_edgeDistance');
const rimDistanceLoc = gl.getUniformLocation(program, 'u_rimDistance');
const baseDistanceLoc = gl.getUniformLocation(program, 'u_baseDistance');
const cornerBoostLoc = gl.getUniformLocation(program, 'u_cornerBoost');
const rippleEffectLoc = gl.getUniformLocation(program, 'u_rippleEffect');
const tintOpacityLoc = gl.getUniformLocation(program, 'u_tintOpacity');
const imageLoc = gl.getUniformLocation(program, 'u_image');
// Create texture
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// Store references
glRefs = {
gl,
texture,
textureSizeLoc,
scrollYLoc,
positionLoc,
texcoordLoc,
resolutionLoc,
pageHeightLoc,
viewportHeightLoc,
blurRadiusLoc,
borderRadiusLoc,
containerPositionLoc,
warpLoc,
edgeIntensityLoc,
rimIntensityLoc,
baseIntensityLoc,
edgeDistanceLoc,
rimDistanceLoc,
baseDistanceLoc,
cornerBoostLoc,
rippleEffectLoc,
tintOpacityLoc,
imageLoc,
positionBuffer,
texcoordBuffer
};
// Set up viewport and attributes
gl.viewport(0, 0, canvasEl.width, canvasEl.height);
gl.clearColor(0, 0, 0, 0);
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.enableVertexAttribArray(positionLoc);
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.enableVertexAttribArray(texcoordLoc);
gl.vertexAttribPointer(texcoordLoc, 2, gl.FLOAT, false, 0, 0);
// Set uniforms
gl.uniform2f(resolutionLoc, canvasEl.width, canvasEl.height);
gl.uniform2f(textureSizeLoc, image.width, image.height);
gl.uniform1f(blurRadiusLoc, blurRadius);
gl.uniform1f(borderRadiusLoc, actualBorderRadius);
gl.uniform1f(warpLoc, enableWarp ? 1.0 : 0.0);
gl.uniform1f(edgeIntensityLoc, edgeIntensity);
gl.uniform1f(rimIntensityLoc, rimIntensity);
gl.uniform1f(baseIntensityLoc, baseIntensity);
gl.uniform1f(edgeDistanceLoc, edgeDistance);
gl.uniform1f(rimDistanceLoc, rimDistance);
gl.uniform1f(baseDistanceLoc, baseDistance);
gl.uniform1f(cornerBoostLoc, cornerBoost);
gl.uniform1f(rippleEffectLoc, rippleEffect);
gl.uniform1f(tintOpacityLoc, tintOpacity);
const position = getPosition();
gl.uniform2f(containerPositionLoc, position.x, position.y);
const pageHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
const viewportHeight = window.innerHeight;
gl.uniform1f(pageHeightLoc, pageHeight);
gl.uniform1f(viewportHeightLoc, viewportHeight);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(imageLoc, 0);
startRenderLoop();
isWebGLReady = true;
}
function render() {
if (!glRefs.gl) return;
const glContext = glRefs.gl;
glContext.clear(glContext.COLOR_BUFFER_BIT);
// Get scroll from the actual scroll container (.landing-page) or fallback to window
const scrollY = scrollContainer?.scrollTop ?? window.pageYOffset ?? document.documentElement.scrollTop;
glContext.uniform1f(glRefs.scrollYLoc, scrollY);
// Get position and adjust for the captured element's offset
const position = getPosition();
const adjustedX = position.x - capturedElementOffset.x;
const adjustedY = position.y - capturedElementOffset.y;
glContext.uniform2f(glRefs.containerPositionLoc, adjustedX, adjustedY);
glContext.drawArrays(glContext.TRIANGLES, 0, 6);
}
function startRenderLoop() {
const loop = () => {
render();
animationId = requestAnimationFrame(loop);
};
loop();
}
async function initWebGL() {
if (!canvasEl) return;
gl = canvasEl.getContext('webgl', { preserveDrawingBuffer: true, alpha: true });
if (!gl) {
console.error('WebGL not supported');
return;
}
updateSizeFromDOM();
pageSnapshot = await capturePageSnapshot();
if (!pageSnapshot) return;
const img = new Image();
img.src = pageSnapshot.toDataURL();
img.onload = () => {
setupShader(img);
};
}
onMount(() => {
// Find the scroll container (.landing-page uses overflow-y: auto)
scrollContainer = document.querySelector('.landing-page') as HTMLElement;
// Small delay to ensure DOM is ready
setTimeout(() => {
initWebGL();
}, 100);
// Handle resize
const resizeObserver = new ResizeObserver(() => {
updateSizeFromDOM();
});
resizeObserver.observe(containerEl);
return () => {
resizeObserver.disconnect();
};
});
onDestroy(() => {
if (animationId) {
cancelAnimationFrame(animationId);
}
});
</script>
<div
bind:this={containerEl}
class="liquid-glass-container {className}"
class:liquid-glass-circle={type === 'circle'}
class:liquid-glass-pill={type === 'pill'}
class:webgl-ready={isWebGLReady}
style:--border-radius="{actualBorderRadius}px"
>
<canvas
bind:this={canvasEl}
class="liquid-glass-canvas"
style:border-radius="{actualBorderRadius}px"
></canvas>
<div class="liquid-glass-content">
{@render children?.()}
</div>
</div>
<style>
.liquid-glass-container {
position: relative;
border-radius: var(--border-radius);
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
background-color: #1A1A1B;
}
.liquid-glass-container.webgl-ready {
background-color: transparent;
}
.liquid-glass-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
.liquid-glass-content {
position: relative;
z-index: 1;
}
</style>

View File

@@ -0,0 +1,254 @@
<script lang="ts">
let isHovering = $state(false);
function handleMouseEnter() {
isHovering = true;
}
function handleMouseLeave() {
isHovering = false;
}
</script>
<!-- Hover detection zone - right half of the section -->
<div
class="hover-zone"
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
role="presentation"
></div>
<!-- Glow effect container - positioned at bottom right -->
<div class="glow-container" class:active={isHovering}>
<!-- Multiple blurred blobs in #262624 -->
<div class="glow-blob blob-1"></div>
<div class="glow-blob blob-2"></div>
<div class="glow-blob blob-3"></div>
<div class="glow-blob blob-4"></div>
<div class="glow-blob blob-5"></div>
<div class="glow-blob blob-6"></div>
<!-- Large background blobs -->
<div class="glow-blob blob-upper"></div>
<div class="glow-blob blob-lower"></div>
<!-- Claude logo SVG with expanding clip-path -->
<svg
class="claude-logo"
width="1200"
height="1200"
viewBox="0 0 1200 1200"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<!-- Glow filter for light tentacle effect -->
<filter id="armGlow" x="-100%" y="-100%" width="300%" height="300%">
<!-- Outer glow - warm orange, large spread -->
<feGaussianBlur in="SourceGraphic" stdDeviation="30" result="blur1" />
<feColorMatrix in="blur1" type="matrix"
values="1.3 0 0 0 0.15 0.6 0.35 0 0 0.02 0 0 0.4 0 0 0 0 0 0.7 0" result="glow1" />
<!-- Mid glow -->
<feGaussianBlur in="SourceGraphic" stdDeviation="15" result="blur2" />
<feColorMatrix in="blur2" type="matrix"
values="1.2 0 0 0 0.1 0.55 0.35 0 0 0 0 0 0.5 0 0 0 0 0 0.9 0" result="glow2" />
<!-- Inner glow - subtle blur -->
<feGaussianBlur in="SourceGraphic" stdDeviation="3" result="blur3" />
<!-- Combine all layers -->
<feMerge>
<feMergeNode in="glow1" />
<feMergeNode in="glow2" />
<feMergeNode in="blur3" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<g class="logo-wrapper" filter="url(#armGlow)">
<path
class="logo-path"
fill="#d97757"
d="M 233.959793 800.214905 L 468.644287 668.536987 L 472.590637 657.100647 L 468.644287 650.738403 L 457.208069 650.738403 L 417.986633 648.322144 L 283.892639 644.69812 L 167.597321 639.865845 L 54.926208 633.825623 L 26.577238 627.785339 L 3.3e-05 592.751709 L 2.73832 575.27533 L 26.577238 559.248352 L 60.724873 562.228149 L 136.187973 567.382629 L 249.422867 575.194763 L 331.570496 580.026978 L 453.261841 592.671082 L 472.590637 592.671082 L 475.328857 584.859009 L 468.724915 580.026978 L 463.570557 575.194763 L 346.389313 495.785217 L 219.543671 411.865906 L 153.100723 363.543762 L 117.181267 339.060425 L 99.060455 316.107361 L 91.248367 266.01355 L 123.865784 230.093994 L 167.677887 233.073853 L 178.872513 236.053772 L 223.248367 270.201477 L 318.040283 343.570496 L 441.825592 434.738342 L 459.946411 449.798706 L 467.194672 444.64447 L 468.080597 441.020203 L 459.946411 427.409485 L 392.617493 305.718323 L 320.778564 181.932983 L 288.80542 130.630859 L 280.348999 99.865845 C 277.369171 87.221436 275.194641 76.590698 275.194641 63.624268 L 312.322174 13.20813 L 332.8591 6.604126 L 382.389313 13.20813 L 403.248352 31.328979 L 434.013519 101.71814 L 483.865753 212.537048 L 561.181274 363.221497 L 583.812134 407.919434 L 595.892639 449.315491 L 600.40271 461.959839 L 608.214783 461.959839 L 608.214783 454.711609 L 614.577271 369.825623 L 626.335632 265.61084 L 637.771851 131.516846 L 641.718201 93.745117 L 660.402832 48.483276 L 697.530334 24.000122 L 726.52356 37.852417 L 750.362549 72 L 747.060486 94.067139 L 732.886047 186.201416 L 705.100708 330.52356 L 686.979919 427.167847 L 697.530334 427.167847 L 709.61084 415.087341 L 758.496704 350.174561 L 840.644348 247.490051 L 876.885925 206.738342 L 919.167847 161.71814 L 946.308838 140.29541 L 997.61084 140.29541 L 1035.38269 196.429626 L 1018.469849 254.416199 L 965.637634 321.422852 L 921.825562 378.201538 L 859.006714 462.765259 L 819.785278 530.41626 L 823.409424 535.812073 L 832.75177 534.92627 L 974.657776 504.724915 L 1051.328979 490.872559 L 1142.818848 475.167786 L 1184.214844 494.496582 L 1188.724854 514.147644 L 1172.456421 554.335693 L 1074.604126 578.496765 L 959.838989 601.449829 L 788.939636 641.879272 L 786.845764 643.409485 L 789.261841 646.389343 L 866.255127 653.637634 L 899.194702 655.409424 L 979.812134 655.409424 L 1129.932861 666.604187 L 1169.154419 692.537109 L 1192.671265 724.268677 L 1188.724854 748.429688 L 1128.322144 779.194641 L 1046.818848 759.865845 L 856.590759 714.604126 L 791.355774 698.335754 L 782.335693 698.335754 L 782.335693 703.731567 L 836.69812 756.885986 L 936.322205 846.845581 L 1061.073975 962.81897 L 1067.436279 991.490112 L 1051.409424 1014.120911 L 1034.496704 1011.704712 L 924.885986 929.234924 L 882.604126 892.107544 L 786.845764 811.48999 L 780.483276 811.48999 L 780.483276 819.946289 L 802.550415 852.241699 L 919.087341 1027.409424 L 925.127625 1081.127686 L 916.671204 1098.604126 L 886.469849 1109.154419 L 853.288696 1103.114136 L 785.073914 1007.355835 L 714.684631 899.516785 L 657.906067 802.872498 L 650.979858 806.81897 L 617.476624 1167.704834 L 601.771851 1186.147705 L 565.530212 1200 L 535.328857 1177.046997 L 519.302124 1139.919556 L 535.328857 1066.550537 L 554.657776 970.792053 L 570.362488 894.68457 L 584.536926 800.134277 L 592.993347 768.724976 L 592.429626 766.630859 L 585.503479 767.516968 L 514.22821 865.369263 L 405.825531 1011.865906 L 320.053711 1103.677979 L 299.516815 1111.812256 L 263.919525 1093.369263 L 267.221497 1060.429688 L 287.114136 1031.114136 L 405.825531 880.107361 L 477.422913 786.52356 L 523.651062 732.483276 L 523.328918 724.671265 L 520.590698 724.671265 L 205.288605 929.395935 L 149.154434 936.644409 L 124.993355 914.01355 L 127.973183 876.885986 L 139.409409 864.80542 L 234.201385 799.570435 L 233.879227 799.8927 Z"
/>
</g>
</svg>
</div>
<style>
.hover-zone {
position: absolute;
top: 0;
right: 0;
width: 50%;
height: 100%;
z-index: 2;
}
.glow-container {
position: absolute;
bottom: -20%;
right: -30%;
width: 100%;
height: 300%;
pointer-events: none;
z-index: -1;
overflow: visible;
contain: layout paint;
}
.glow-blob {
position: absolute;
border-radius: 50%;
filter: blur(60px);
opacity: 0;
transform: scale(0.3) translate(0, 0);
transition:
opacity 1.2s cubic-bezier(0.4, 0, 0.2, 1),
transform 1.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.glow-container.active .glow-blob {
opacity: 1;
}
/* All blobs use #262624 */
.blob-1 {
width: 500px;
height: 500px;
bottom: 0%;
right: 10%;
background: radial-gradient(circle at center, rgba(38, 38, 36, 1) 0%, rgba(38, 38, 36, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-1 {
transform: scale(1) translate(-20%, -30%);
}
.blob-2 {
width: 400px;
height: 400px;
bottom: 8%;
right: 0;
background: radial-gradient(circle at center, rgba(38, 38, 36, 1) 0%, rgba(38, 38, 36, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-2 {
transform: scale(1) translate(-50%, -80%);
}
.blob-3 {
width: 350px;
height: 350px;
bottom: 4%;
right: 5%;
background: radial-gradient(circle at center, rgba(38, 38, 36, 1) 0%, rgba(38, 38, 36, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-3 {
transform: scale(1) translate(0%, -20%);
}
.blob-4 {
width: 280px;
height: 280px;
bottom: 12%;
right: 20%;
background: radial-gradient(circle at center, rgba(38, 38, 36, 1) 0%, rgba(38, 38, 36, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-4 {
transform: scale(1) translate(-30%, -60%);
}
.blob-5 {
width: 320px;
height: 320px;
bottom: 8%;
right: 25%;
background: radial-gradient(circle at center, rgba(38, 38, 36, 1) 0%, rgba(38, 38, 36, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-5 {
transform: scale(1) translate(-60%, -30%);
}
.blob-6 {
width: 250px;
height: 250px;
bottom: 2%;
right: 15%;
background: radial-gradient(circle at center, rgba(38, 38, 36, 1) 0%, rgba(38, 38, 36, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-6 {
transform: scale(1) translate(-10%, -40%);
}
/* Large background blobs */
.blob-upper {
width: 700px;
height: 700px;
bottom: -20%;
right: 10%;
background: radial-gradient(circle at center, rgba(38, 38, 36, 0.6) 0%, rgba(38, 38, 36, 0.3) 40%, transparent 70%);
}
.glow-container.active .blob-upper {
transform: scale(1) translate(-20%, -10%);
}
.blob-lower {
width: 700px;
height: 700px;
bottom: -40%;
right: 25%;
background: radial-gradient(circle at center, rgba(38, 38, 36, 0.6) 0%, rgba(38, 38, 36, 0.3) 40%, transparent 70%);
}
.glow-container.active .blob-lower {
transform: scale(1) translate(-10%, -30%);
}
/* Claude logo - blooming animation from center */
.claude-logo {
position: absolute;
bottom: -20%;
right: 5%;
width: 1000px;
height: 1000px;
overflow: visible;
}
.logo-wrapper {
/* Clip circle starts at center with 0 radius */
clip-path: circle(0% at 50% 50%);
transition: clip-path 2s cubic-bezier(0.16, 1, 0.3, 1);
}
.glow-container.active .logo-wrapper {
/* Expands to reveal full logo - different arm lengths create natural timing variation */
clip-path: circle(75% at 50% 50%);
}
.logo-path {
opacity: 0;
filter: blur(8px);
/* Fast fade-out so it disappears before clip-path shrinks to visible circle */
transition:
opacity 0.45s cubic-bezier(0.4, 0, 0.2, 1),
filter 5s cubic-bezier(0.4, 0, 0.2, 1);
}
.glow-container.active .logo-path {
opacity: 0.35;
filter: blur(4px);
/* Slower fade-in for smooth appearance */
transition:
opacity 1.5s cubic-bezier(0.4, 0, 0.2, 1),
filter 1.5s cubic-bezier(0.4, 0, 0.2, 1);
}
@media (max-width: 768px) {
.hover-zone,
.glow-container {
display: none;
}
}
</style>

View File

@@ -0,0 +1,182 @@
<script lang="ts">
let isHovering = $state(false);
function handleMouseEnter() {
isHovering = true;
}
function handleMouseLeave() {
isHovering = false;
}
</script>
<!-- Hover detection zone - left half of the section -->
<div
class="hover-zone"
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
role="presentation"
></div>
<!-- Glow effect container - positioned at bottom left, bleeds upward -->
<div class="glow-container" class:active={isHovering}>
<!-- Multiple blurred color blobs with inline gradients -->
<div class="glow-blob blob-green"></div>
<div class="glow-blob blob-blue"></div>
<div class="glow-blob blob-yellow"></div>
<div class="glow-blob blob-red"></div>
<div class="glow-blob blob-purple"></div>
<div class="glow-blob blob-orange"></div>
<!-- Two extra blobs for the circled areas -->
<div class="glow-blob blob-upper"></div>
<div class="glow-blob blob-lower"></div>
</div>
<style>
.hover-zone {
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
z-index: 2;
}
.glow-container {
position: absolute;
bottom: -20%;
left: -30%;
width: 100%;
height: 300%;
pointer-events: none;
z-index: -1;
overflow: visible;
contain: layout paint;
}
.glow-blob {
position: absolute;
border-radius: 50%;
filter: blur(60px);
opacity: 0;
transform: scale(0.3) translate(0, 0);
transition:
opacity 1.2s cubic-bezier(0.4, 0, 0.2, 1),
transform 1.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.glow-container.active .glow-blob {
opacity: 1;
}
/* Green - largest, center of the glow: #00B95C */
.blob-green {
width: 500px;
height: 500px;
bottom: 0%;
left: 10%;
background: radial-gradient(circle at center, rgba(0, 185, 92, 1) 0%, rgba(0, 185, 92, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-green {
transform: scale(1) translate(20%, -30%);
}
/* Blue - upper area: #3186FF */
.blob-blue {
width: 400px;
height: 400px;
bottom: 8%;
left: 0;
background: radial-gradient(circle at center, rgba(49, 134, 255, 1) 0%, rgba(49, 134, 255, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-blue {
transform: scale(1) translate(50%, -80%);
}
/* Yellow - mid left: #FFE432 */
.blob-yellow {
width: 350px;
height: 350px;
bottom: 4%;
left: 5%;
background: radial-gradient(circle at center, rgba(255, 228, 50, 1) 0%, rgba(255, 228, 50, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-yellow {
transform: scale(1) translate(0%, -20%);
}
/* Red - accent, smaller: #FC413D */
.blob-red {
width: 280px;
height: 280px;
bottom: 12%;
left: 20%;
background: radial-gradient(circle at center, rgba(252, 65, 61, 1) 0%, rgba(252, 65, 61, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-red {
transform: scale(1) translate(30%, -60%);
}
/* Purple - upper right of glow area: #BD99FE */
.blob-purple {
width: 320px;
height: 320px;
bottom: 8%;
left: 25%;
background: radial-gradient(circle at center, rgba(189, 153, 254, 1) 0%, rgba(189, 153, 254, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-purple {
transform: scale(1) translate(60%, -30%);
}
/* Orange - small accent: #FBBC04 */
.blob-orange {
width: 250px;
height: 250px;
bottom: 2%;
left: 15%;
background: radial-gradient(circle at center, rgba(251, 188, 4, 1) 0%, rgba(251, 188, 4, 0.5) 40%, transparent 70%);
}
.glow-container.active .blob-orange {
transform: scale(1) translate(10%, -40%);
}
/* Upper blob - bottom left area */
.blob-upper {
width: 700px;
height: 700px;
bottom: -20%;
left: 10%;
background: radial-gradient(circle at center, rgba(49, 134, 255, 0.6) 0%, rgba(49, 134, 255, 0.3) 40%, transparent 70%);
}
.glow-container.active .blob-upper {
transform: scale(1) translate(20%, -10%);
}
/* Lower blob - bottom left corner */
.blob-lower {
width: 700px;
height: 700px;
bottom: -40%;
left: 25%;
background: radial-gradient(circle at center, rgba(0, 185, 92, 0.6) 0%, rgba(0, 185, 92, 0.3) 40%, transparent 70%);
}
.glow-container.active .blob-lower {
transform: scale(1) translate(10%, -30%);
}
@media (max-width: 768px) {
.hover-zone,
.glow-container {
display: none;
}
}
</style>

View File

@@ -0,0 +1,186 @@
<script lang="ts">
import type { PersonalityData } from '$lib/i18n';
interface Props {
personality: PersonalityData;
side: 'left' | 'right';
}
let { personality, side }: Props = $props();
</script>
<article class="personality-card side-{side}">
<!-- Blur layer with gradient mask -->
<div class="blur-layer"></div>
<!-- Content -->
<div class="card-content">
<div class="card-header">
<div class="icon">
{personality.name === 'Gemini' ? '娼' : '建'}
</div>
<div>
<h3 class="name">{personality.name}</h3>
<p class="tagline">{personality.tagline}</p>
</div>
</div>
<ul class="traits">
{#each personality.traits as trait}
<li class="trait">
<span class="bullet"></span>
{trait}
</li>
{/each}
</ul>
</div>
</article>
<style>
.personality-card {
position: relative;
min-height: 220px;
border-radius: 20px;
overflow: hidden;
}
.blur-layer {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.04);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 20px;
}
/* Gradient mask: blur fades towards outer edge */
.side-left .blur-layer {
mask-image: linear-gradient(to left, black 0%, black 40%, transparent 100%);
-webkit-mask-image: linear-gradient(to left, black 0%, black 40%, transparent 100%);
}
.side-right .blur-layer {
mask-image: linear-gradient(to right, black 0%, black 40%, transparent 100%);
-webkit-mask-image: linear-gradient(to right, black 0%, black 40%, transparent 100%);
}
.card-content {
position: relative;
z-index: 1;
padding: 1.5rem;
max-width: 280px;
}
.side-left .card-content {
margin-left: auto;
text-align: right;
padding-right: 1.75rem;
}
.side-left .card-header {
flex-direction: row-reverse;
text-align: right;
}
.side-left .traits {
align-items: flex-end;
}
.side-left .trait {
flex-direction: row-reverse;
}
.side-right .card-content {
margin-left: 0;
}
.card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.25rem;
}
.icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bold;
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
}
.name {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text-primary);
}
.tagline {
font-size: 0.875rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.5);
}
.traits {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.trait {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.9375rem;
color: var(--color-text-secondary);
}
.bullet {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.3);
}
@media (max-width: 768px) {
.blur-layer {
mask-image: none;
-webkit-mask-image: none;
}
.card-content {
max-width: none;
padding: 1.5rem;
}
.side-left .card-content,
.side-right .card-content {
margin-left: 0;
text-align: left;
padding-right: 1.5rem;
}
.side-left .card-header {
flex-direction: row;
text-align: left;
}
.side-left .trait {
flex-direction: row;
}
.side-left .traits {
align-items: flex-start;
}
}
</style>

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import { getI18n } from '$lib/i18n';
import PersonalityCard from './PersonalityCard.svelte';
import GeminiGlow from './GeminiGlow.svelte';
import ClaudeGlow from './ClaudeGlow.svelte';
const i18n = getI18n();
const t = $derived(i18n.t);
</script>
<section class="section">
<GeminiGlow />
<ClaudeGlow />
<div class="container">
<header class="text-center mb-12">
<h2 class="section-title">{t.personalityCards.sectionTitle}</h2>
<p class="section-subtitle">{t.personalityCards.sectionSubtitle}</p>
</header>
<div class="cards-grid">
<PersonalityCard personality={t.personalityCards.gemini} side="left" />
<PersonalityCard personality={t.personalityCards.claude} side="right" />
</div>
</div>
</section>
<style>
.container {
max-width: 72rem;
margin: 0 auto;
padding: 0 2rem;
}
.cards-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.section {
position: relative;
overflow: visible;
clip-path: inset(-100% -100% 0 -100%);
isolation: isolate; /* contain blend modes */
}
@media (max-width: 640px) {
.cards-grid {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.container {
padding: 0 1rem;
}
}
</style>

View File

@@ -0,0 +1,62 @@
<script lang="ts">
import { getI18n } from '$lib/i18n';
import PromptCard from './PromptCard.svelte';
const i18n = getI18n();
const t = $derived(i18n.t);
</script>
<section class="section">
<div class="max-w-6xl mx-auto">
<header class="text-center mb-12">
<h2 class="section-title">{t.promptBuilder.sectionTitle}</h2>
<p class="section-subtitle">{t.promptBuilder.sectionSubtitle}</p>
</header>
<div class="cards-grid">
{#each t.promptBuilder.presets as preset (preset.id)}
<PromptCard {preset} />
{/each}
</div>
<div class="setup-guide glass-card mt-8 p-6">
<div class="flex items-start gap-4">
<span class="text-2xl">+</span>
<div>
<h4 class="font-semibold text-[var(--color-text-primary)] mb-1">
{t.promptBuilder.setupTitle}
</h4>
<p class="text-sm text-[var(--color-text-secondary)]">
{t.promptBuilder.setupDescription}
</p>
</div>
</div>
</div>
</div>
</section>
<style>
.cards-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
}
@media (max-width: 1024px) {
.cards-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.cards-grid {
grid-template-columns: 1fr;
}
}
.setup-guide {
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
</style>

View File

@@ -0,0 +1,351 @@
<script lang="ts">
import { getI18n, type PromptPreset } from '$lib/i18n';
import { generateRaycastUrl, getPrompt } from '$lib/utils/raycast';
interface Props {
preset: PromptPreset;
}
let { preset }: Props = $props();
const i18n = getI18n();
const t = $derived(i18n.t);
let copied = $state(false);
const promptText = $derived(getPrompt(i18n.lang === 'ru' ? 'Russian' : 'English'));
const raycastUrl = $derived(
preset.id !== 'copy'
? generateRaycastUrl({
name: preset.name,
instructions: promptText,
model: preset.model,
reasoning_effort: preset.reasoning_effort,
creativity: 'none',
web_search: true,
})
: ''
);
async function copyPrompt() {
try {
await navigator.clipboard.writeText(promptText);
copied = true;
setTimeout(() => {
copied = false;
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}
function getIcon(iconName: string): string {
switch (iconName) {
case 'beaver':
return '匠'; // master craftsman / professional
case 'zap':
return '閃'; // flash / lightning
case 'clipboard':
return '写'; // copy / transcribe
default:
return '道';
}
}
const cardVariant = $derived(
preset.id === 'pro' ? 'pro' : preset.id === 'flash' ? 'flash' : 'copy'
);
let svgElement: SVGSVGElement | null = $state(null);
let isHovering = $state(false);
let isTransitioningOut = $state(false);
function handleMouseEnter() {
isHovering = true;
isTransitioningOut = false;
}
function handleMouseLeave() {
if (!svgElement) {
isHovering = false;
return;
}
// Grab current animated values
const computed = getComputedStyle(svgElement);
const currentTransform = computed.transform;
const currentStrokeWidth = computed.strokeWidth;
// Apply them as inline styles
svgElement.style.transform = currentTransform === 'none' ? '' : currentTransform;
svgElement.style.strokeWidth = currentStrokeWidth;
// Stop animation, start transition
isHovering = false;
isTransitioningOut = true;
// Remove inline styles after a frame to let transition kick in
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (svgElement) {
svgElement.style.transform = '';
svgElement.style.strokeWidth = '';
}
// Reset after transition completes
setTimeout(() => {
isTransitioningOut = false;
}, 1000);
});
});
}
</script>
{#snippet cardContent()}
<header class="card-header">
<div class="card-icon">
<span class="icon-text">{getIcon(preset.icon)}</span>
</div>
<h3 class="card-title">{preset.name}</h3>
<span class="action-button" aria-hidden="true">
{#if preset.id === 'copy'}
{#if copied}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"/>
</svg>
{:else}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>
{/if}
{:else}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 6l6 6-6 6"/>
</svg>
{/if}
</span>
</header>
<div class="divider"></div>
<p class="card-description">{preset.description}</p>
<!-- Decorative illustration -->
<div class="card-illustration" class:is-hovering={isHovering}>
{#if cardVariant === 'pro'}
<!-- Brain icon from Tabler Icons -->
<svg bind:this={svgElement} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="illustration-svg" class:breathing={isHovering}>
<path d="M15.5 13a3.5 3.5 0 0 0 -3.5 3.5v1a3.5 3.5 0 0 0 7 0v-1.8"/>
<path d="M8.5 13a3.5 3.5 0 0 1 3.5 3.5v1a3.5 3.5 0 0 1 -7 0v-1.8"/>
<path d="M17.5 16a3.5 3.5 0 0 0 0 -7h-.5"/>
<path d="M19 9.3v-2.8a3.5 3.5 0 0 0 -7 0"/>
<path d="M6.5 16a3.5 3.5 0 0 1 0 -7h.5"/>
<path d="M5 9.3v-2.8a3.5 3.5 0 0 1 7 0v10"/>
</svg>
{:else if cardVariant === 'flash'}
<!-- Lightning bolt -->
<svg bind:this={svgElement} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="illustration-svg" class:breathing={isHovering}>
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
{:else}
<!-- @ symbol -->
<svg bind:this={svgElement} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="illustration-svg" class:breathing={isHovering}>
<circle cx="12" cy="12" r="4"/>
<path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8"/>
</svg>
{/if}
</div>
{/snippet}
{#if preset.id === 'copy'}
<div
class="prompt-card variant-{cardVariant}"
role="button"
tabindex="0"
onclick={copyPrompt}
onkeydown={(e) => e.key === 'Enter' && copyPrompt()}
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
>
{@render cardContent()}
</div>
{:else}
<a
href={raycastUrl}
class="prompt-card variant-{cardVariant}"
target="_blank"
rel="noopener noreferrer"
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
>
{@render cardContent()}
</a>
{/if}
<style>
.prompt-card {
position: relative;
display: flex;
flex-direction: column;
padding: 1.5rem;
gap: 1rem;
text-decoration: none;
cursor: pointer;
min-height: 420px;
border-radius: 16px;
overflow: hidden;
}
/* Darkening overlay */
.prompt-card::before {
content: '';
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.25);
opacity: 0;
transition: opacity 1s ease;
z-index: 1;
pointer-events: none;
}
.prompt-card:hover::before {
opacity: 1;
}
/* Card variants with unique gradients */
.variant-pro {
background: linear-gradient(145deg, #191B43 0%, #0E0A2A 100%);
border: 1px solid rgba(25, 27, 67, 0.5);
}
.variant-flash {
background: linear-gradient(145deg, #122241 0%, #1E417A 100%);
border: 1px solid rgba(30, 65, 122, 0.5);
}
.variant-copy {
background: linear-gradient(145deg, #321134 0%, #260C28 100%);
border: 1px solid rgba(50, 17, 52, 0.5);
}
.card-header {
display: flex;
align-items: center;
gap: 0.75rem;
position: relative;
z-index: 2;
}
.card-icon {
width: 44px;
height: 44px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.icon-text {
font-size: 20px;
font-weight: bold;
color: white;
}
.card-title {
font-size: 1rem;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
flex: 1;
min-width: 0;
}
.action-button {
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.7);
flex-shrink: 0;
cursor: pointer;
transition: background-color 1s ease, color 1s ease;
}
.prompt-card:hover .action-button {
background: rgba(255, 255, 255, 0.15);
color: #fff;
}
.divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
position: relative;
z-index: 2;
}
.card-description {
font-size: 1rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
line-height: 1.7;
position: relative;
z-index: 2;
}
/* Decorative illustration - 50% of card, overflows edge */
.card-illustration {
position: absolute;
bottom: -25%;
right: -10%;
width: 85%;
height: 85%;
pointer-events: none;
opacity: 0.1;
transition: opacity 1s ease;
}
.card-illustration.is-hovering {
opacity: 0.18;
}
.illustration-svg {
width: 100%;
height: 100%;
stroke-width: 1.2;
transform: scale(1) translateY(0);
transition: transform 1s ease, stroke-width 1s ease;
}
.illustration-svg.breathing {
animation: breathe 3s ease-in-out infinite;
}
@keyframes breathe {
0%, 100% {
transform: scale(1) translateY(0);
stroke-width: 1.2;
}
50% {
transform: scale(1.05) translateY(-2%);
stroke-width: 1.8;
}
}
.variant-pro .illustration-svg {
stroke: rgba(147, 130, 255, 0.9);
}
.variant-flash .illustration-svg {
stroke: rgba(100, 180, 255, 0.9);
}
.variant-copy .illustration-svg {
stroke: rgba(255, 130, 200, 0.9);
}
</style>

View File

@@ -0,0 +1,43 @@
import { getContext, setContext } from 'svelte';
import type { Language, Translations } from './types';
import en from './translations/en';
import ru from './translations/ru';
const I18N_KEY = Symbol('i18n');
const translations: Record<Language, Translations> = { en, ru };
export interface I18nContext {
lang: Language;
t: Translations;
setLanguage: (lang: Language) => void;
}
export function createI18nContext(initialLang: Language = 'ru'): I18nContext {
let lang = $state<Language>(initialLang);
const t = $derived(translations[lang]);
function setLanguage(newLang: Language) {
if (newLang === 'en' || newLang === 'ru') {
lang = newLang;
}
}
return {
get lang() {
return lang;
},
get t() {
return t;
},
setLanguage
};
}
export function setI18nContext(ctx: I18nContext) {
setContext(I18N_KEY, ctx);
}
export function getI18n(): I18nContext {
return getContext<I18nContext>(I18N_KEY);
}

23
src/lib/i18n/index.ts Normal file
View File

@@ -0,0 +1,23 @@
// Type exports
export type { Language, Translations, PromptPreset, PersonalityData } from './types';
// Context exports (from svelte.ts file for runes support)
export { createI18nContext, setI18nContext, getI18n, type I18nContext } from './context.svelte';
// Translation data
import en from './translations/en';
import ru from './translations/ru';
import type { Language, Translations } from './types';
const translations: Record<Language, Translations> = { en, ru };
export const languages: Language[] = ['ru', 'en'];
export const defaultLanguage: Language = 'ru';
export function getTranslations(lang: Language): Translations {
return translations[lang] || translations[defaultLanguage];
}
export function isValidLanguage(lang: string): lang is Language {
return languages.includes(lang as Language);
}

View File

@@ -0,0 +1,91 @@
import type { Translations } from '../types';
const en: Translations = {
meta: {
title: 'Chief Beaver Officer',
description: 'AI agent for life management through Obsidian. Action cures fear.'
},
header: {
logo: 'Chief Beaver Officer'
},
hero: {
titleLine1: 'Your LifeOS',
titleLine2: 'manager.',
subtitle: 'AI agent, Obsidian, planning, tasks, decisions, mindset.',
philosophy: 'Action cures fear. Build, we\'ll think for you.'
},
fakeObsidian: {
sectionTitle: 'Explore the structure',
sectionSubtitle: 'Your second brain. Your rules.',
emptyState: 'Select a file to view',
caption: 'We don\'t give you files to download. Explore, understand, build your own.'
},
promptBuilder: {
sectionTitle: 'Get your Chief Beaver Officer',
sectionSubtitle: 'Import into Raycast or any interface',
openInRaycast: 'Open in Raycast',
copyPrompt: 'Copy prompt',
copied: 'Copied!',
setupTitle: 'Recommended: Semantic Notes Vault MCP',
setupDescription: 'Install via BRAT: aaronsb/obsidian-mcp-plugin. This connects manager to your vault.',
presets: [
{
id: 'pro',
name: 'Chief Beaver Pro',
description: 'Gemini 3 Pro (High) for Raycast, choose reasoning manually',
icon: 'beaver',
model: 'google-gemini-3-pro',
reasoning_effort: 'high'
},
{
id: 'flash',
name: 'Chief Beaver Flash',
description: 'Gemini 3 Flash (Minimal) for Raycast',
icon: 'zap',
model: 'google-gemini-3-flash',
reasoning_effort: 'minimal'
},
{
id: 'copy',
name: 'Copy Prompt',
description: 'Use in any AI client',
icon: 'clipboard',
model: '',
reasoning_effort: 'minimal'
}
]
},
personalityCards: {
sectionTitle: 'Recommended models',
sectionSubtitle: 'Different tools for different tasks',
gemini: {
name: 'Gemini 3 Pro/Flash',
tagline: 'For personal',
traits: [
'Daily planning',
'Life decisions',
'Emotional support',
'Journaling prompts',
'Your life coach'
],
color: 'aurora-2'
},
claude: {
name: 'Claude Opus 4.5',
tagline: 'For work',
traits: [
'Code review',
'Technical writing',
'Research & analysis',
'Project planning',
'Your senior colleague'
],
color: 'aurora-3'
}
},
footer: {
builtBy: 'h@kotikot.com'
}
};
export default en;

View File

@@ -0,0 +1,91 @@
import type { Translations } from '../types';
const ru: Translations = {
meta: {
title: 'Chief Beaver Officer',
description: 'AI-агент для управления жизнью через Obsidian. Action cures fear.'
},
header: {
logo: 'Chief Beaver Officer'
},
hero: {
titleLine1: 'Менеджер твоей',
titleLine2: 'ЖизньOS.',
subtitle: 'AI-агент, Obsidian, планирование, задачи, решения, майндсет.',
philosophy: 'Action cures fear. Build, we\'ll think for you.'
},
fakeObsidian: {
sectionTitle: 'Исследуй структуру',
sectionSubtitle: 'Твой второй мозг. Твои правила.',
emptyState: 'Выбери файл для просмотра',
caption: 'Мы не даём скачать готовое. Изучи, пойми, построй своё.'
},
promptBuilder: {
sectionTitle: 'Получи своего Менеджера Бобрения',
sectionSubtitle: 'Импортируй в Raycast или любой интерфейс',
openInRaycast: 'Открыть в Raycast',
copyPrompt: 'Скопировать промпт',
copied: 'Скопировано!',
setupTitle: 'Рекомендую Semantic Notes Vault MCP',
setupDescription: 'Установи через BRAT: aaronsb/obsidian-mcp-plugin. Это подключит менеджера к твоему vault.',
presets: [
{
id: 'pro',
name: 'Менеджер Pro',
description: 'Gemini 3 Pro (High) для Raycast, выбери reasoning вручную',
icon: 'beaver',
model: 'google-gemini-3-pro',
reasoning_effort: 'high'
},
{
id: 'flash',
name: 'Менеджер Flash',
description: 'Gemini 3 Flash (Minimal) для Raycast',
icon: 'zap',
model: 'google-gemini-3-flash',
reasoning_effort: 'minimal'
},
{
id: 'copy',
name: 'Скопировать промпт',
description: 'Используй в любом AI-клиенте',
icon: 'clipboard',
model: '',
reasoning_effort: 'minimal'
}
]
},
personalityCards: {
sectionTitle: 'Рекомендованные модели',
sectionSubtitle: 'Разные инструменты для разных задач',
gemini: {
name: 'Gemini 3 Pro/Flash',
tagline: 'Для личного',
traits: [
'Планирование дня',
'Жизненные решения',
'Эмоциональная поддержка',
'Journaling промпты',
'Твой life coach'
],
color: 'aurora-2'
},
claude: {
name: 'Claude Opus 4.5',
tagline: 'Для работы',
traits: [
'Code review',
'Техническое письмо',
'Исследования',
'Планирование проектов',
'Твой senior коллега'
],
color: 'aurora-3'
}
},
footer: {
builtBy: 'h@kotikot.com'
}
};
export default ru;

58
src/lib/i18n/types.ts Normal file
View File

@@ -0,0 +1,58 @@
export type Language = 'en' | 'ru';
export interface PromptPreset {
id: string;
name: string;
description: string;
icon: string;
model: string;
reasoning_effort: 'high' | 'minimal';
}
export interface PersonalityData {
name: string;
tagline: string;
traits: string[];
color: string;
}
export interface Translations {
meta: {
title: string;
description: string;
};
header: {
logo: string;
};
hero: {
titleLine1: string;
titleLine2: string;
subtitle: string;
philosophy: string;
};
fakeObsidian: {
sectionTitle: string;
sectionSubtitle: string;
emptyState: string;
caption: string;
};
promptBuilder: {
sectionTitle: string;
sectionSubtitle: string;
openInRaycast: string;
copyPrompt: string;
copied: string;
setupTitle: string;
setupDescription: string;
presets: PromptPreset[];
};
personalityCards: {
sectionTitle: string;
sectionSubtitle: string;
gemini: PersonalityData;
claude: PersonalityData;
};
footer: {
builtBy: string;
};
}

Some files were not shown because too many files have changed in this diff Show More