feat(global): implement main phase
This commit is contained in:
16
bun.lock
16
bun.lock
@@ -3,6 +3,10 @@
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "beaver-land",
|
||||
"dependencies": {
|
||||
"html2canvas": "^1.4.1",
|
||||
"marked": "^17.0.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.4.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -264,6 +268,8 @@
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
@@ -284,6 +290,8 @@
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="],
|
||||
|
||||
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
@@ -352,6 +360,8 @@
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
@@ -418,6 +428,8 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
|
||||
|
||||
"mdsvex": ["mdsvex@0.12.6", "", { "dependencies": { "@types/mdast": "^4.0.4", "@types/unist": "^2.0.3", "prism-svelte": "^0.4.7", "prismjs": "^1.17.1", "unist-util-visit": "^2.0.1", "vfile-message": "^2.0.4" }, "peerDependencies": { "svelte": "^3.56.0 || ^4.0.0 || ^5.0.0-next.120" } }, "sha512-pupx2gzWh3hDtm/iDW4WuCpljmyHbHi34r7ktOqpPGvyiM4MyfNgdJ3qMizXdgCErmvYC9Nn/qyjePy+4ss9Wg=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
@@ -506,6 +518,8 @@
|
||||
|
||||
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
|
||||
|
||||
"text-segmentation": ["text-segmentation@1.0.3", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
@@ -532,6 +546,8 @@
|
||||
|
||||
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||
|
||||
"utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="],
|
||||
|
||||
"vfile-message": ["vfile-message@2.0.4", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^2.0.0" } }, "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ=="],
|
||||
|
||||
"vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
|
||||
|
||||
@@ -36,5 +36,9 @@
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"vite": "^7.2.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"html2canvas": "^1.4.1",
|
||||
"marked": "^17.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
153
src/content/en/vault/AGENTS.md
Normal file
153
src/content/en/vault/AGENTS.md
Normal 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
|
||||
|
||||
---
|
||||
69
src/content/en/vault/README.md
Normal file
69
src/content/en/vault/README.md
Normal 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.
|
||||
@@ -0,0 +1,3 @@
|
||||
files from here live here
|
||||
|
||||
https://github.com/702573N/Obsidian-Tasks-Calendar
|
||||
14
src/content/en/vault/meta/templates/quickadd/event.md
Normal file
14
src/content/en/vault/meta/templates/quickadd/event.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
date_start: {{date}}
|
||||
date_end: {{date}}
|
||||
tags:
|
||||
aliases:
|
||||
---
|
||||
[[{{date}}]]
|
||||
## Brief Description
|
||||
|
||||
|
||||
## Event Progress
|
||||
|
||||
|
||||
## Notes
|
||||
18
src/content/en/vault/meta/templates/quickadd/media/anime.md
Normal file
18
src/content/en/vault/meta/templates/quickadd/media/anime.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
finished: false
|
||||
episodes:
|
||||
rating:
|
||||
genres:
|
||||
banner:
|
||||
aliases:
|
||||
link:
|
||||
tags:
|
||||
- media/anime
|
||||
---
|
||||
## Description
|
||||
|
||||
|
||||
## Opinion
|
||||
|
||||
|
||||
## Notes
|
||||
16
src/content/en/vault/meta/templates/quickadd/media/book.md
Normal file
16
src/content/en/vault/meta/templates/quickadd/media/book.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
finished: false
|
||||
pages:
|
||||
rating:
|
||||
genres:
|
||||
aliases:
|
||||
tags:
|
||||
- media/book
|
||||
---
|
||||
## Description
|
||||
|
||||
|
||||
## Opinion
|
||||
|
||||
|
||||
## Notes
|
||||
18
src/content/en/vault/meta/templates/quickadd/media/series.md
Normal file
18
src/content/en/vault/meta/templates/quickadd/media/series.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
finished: false
|
||||
episodes:
|
||||
rating:
|
||||
genres:
|
||||
banner:
|
||||
aliases:
|
||||
link:
|
||||
tags:
|
||||
- media/series
|
||||
---
|
||||
## Description
|
||||
|
||||
|
||||
## Opinion
|
||||
|
||||
|
||||
## Notes
|
||||
45
src/content/en/vault/meta/templates/quickadd/person.md
Normal file
45
src/content/en/vault/meta/templates/quickadd/person.md
Normal 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}}]]
|
||||
10
src/content/en/vault/meta/templates/quickadd/research.md
Normal file
10
src/content/en/vault/meta/templates/quickadd/research.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## Sources
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
- "#things/clothes"
|
||||
brand: {{VALUE:brand}}
|
||||
model: {{VALUE:model}}
|
||||
price:
|
||||
spent:
|
||||
fake: false
|
||||
link:
|
||||
banner:
|
||||
---
|
||||
# {{VALUE:brand}} {{VALUE:model}}
|
||||
@@ -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
|
||||
3
src/content/en/vault/meta/templates/quickadd/thought.md
Normal file
3
src/content/en/vault/meta/templates/quickadd/thought.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
aliases:
|
||||
---
|
||||
2
src/content/en/vault/meta/templates/readme.md
Normal file
2
src/content/en/vault/meta/templates/readme.md
Normal file
@@ -0,0 +1,2 @@
|
||||
when editing a template - remember that frontmatter must be edited in source mode
|
||||
otherwise {{these things}} break
|
||||
62
src/content/en/vault/meta/templates/templates/journal.md
Normal file
62
src/content/en/vault/meta/templates/templates/journal.md
Normal 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}}
|
||||
```
|
||||
4
src/content/en/vault/meta/templates/templates/note.md
Normal file
4
src/content/en/vault/meta/templates/templates/note.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
---
|
||||
7
src/content/en/vault/🌍 places/real/readme.md
Normal file
7
src/content/en/vault/🌍 places/real/readme.md
Normal 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
|
||||
6
src/content/en/vault/🌍 places/virtual/readme.md
Normal file
6
src/content/en/vault/🌍 places/virtual/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- sites/subtype
|
||||
---
|
||||
|
||||
here I store links to various sites by folders (categories)
|
||||
6
src/content/en/vault/🏄 events/_groups/readme.md
Normal file
6
src/content/en/vault/🏄 events/_groups/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- events/event_type
|
||||
---
|
||||
|
||||
reports by specific event types, date in filename
|
||||
6
src/content/en/vault/🏄 events/readme.md
Normal file
6
src/content/en/vault/🏄 events/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- events/event_type
|
||||
---
|
||||
|
||||
event reports, subfolders by year, date in filename
|
||||
9
src/content/en/vault/👤 people/celebrities/readme.md
Normal file
9
src/content/en/vault/👤 people/celebrities/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
aliases:
|
||||
- Full Name
|
||||
tags:
|
||||
- people/celebrities
|
||||
birthday: 0000-00-00
|
||||
---
|
||||
|
||||
any kind of celebrities
|
||||
9
src/content/en/vault/👤 people/personal/archive/readme.md
Normal file
9
src/content/en/vault/👤 people/personal/archive/readme.md
Normal 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
|
||||
9
src/content/en/vault/👤 people/personal/readme.md
Normal file
9
src/content/en/vault/👤 people/personal/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
aliases:
|
||||
- Full Name
|
||||
tags:
|
||||
- people/friend
|
||||
birthday: 0000-00-00
|
||||
---
|
||||
|
||||
friends, girlfriends, everyone I know
|
||||
9
src/content/en/vault/👤 people/professional/readme.md
Normal file
9
src/content/en/vault/👤 people/professional/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
aliases:
|
||||
- Full Name
|
||||
tags:
|
||||
- people/teacher/subject
|
||||
birthday: 0000-00-00
|
||||
---
|
||||
|
||||
teachers, colleagues
|
||||
6
src/content/en/vault/💻 education/readme.md
Normal file
6
src/content/en/vault/💻 education/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- study/subject
|
||||
---
|
||||
|
||||
lecture notes, subfolders by institutions/subjects
|
||||
8
src/content/en/vault/💻 projects/readme.md
Normal file
8
src/content/en/vault/💻 projects/readme.md
Normal 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
|
||||
6
src/content/en/vault/💻 skills/readme.md
Normal file
6
src/content/en/vault/💻 skills/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- solutions/area/more_specific
|
||||
---
|
||||
|
||||
quick problem solutions, notes on programs, things I know but forget
|
||||
7
src/content/en/vault/📅 days/readme.md
Normal file
7
src/content/en/vault/📅 days/readme.md
Normal 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
|
||||
7
src/content/en/vault/📆 boards/readme.md
Normal file
7
src/content/en/vault/📆 boards/readme.md
Normal 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
|
||||
2
src/content/en/vault/📆 planning/lists/readme.md
Normal file
2
src/content/en/vault/📆 planning/lists/readme.md
Normal 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
|
||||
8
src/content/en/vault/📆 planning/roadmap.md
Normal file
8
src/content/en/vault/📆 planning/roadmap.md
Normal file
@@ -0,0 +1,8 @@
|
||||
workspace, status for each area
|
||||
|
||||
# Priorities
|
||||
- priority tasks
|
||||
|
||||
# Different Areas
|
||||
## Various Projects
|
||||
- status of each and upcoming plans
|
||||
5
src/content/en/vault/📆 planning/tasks calendar.md
Normal file
5
src/content/en/vault/📆 planning/tasks calendar.md
Normal file
@@ -0,0 +1,5 @@
|
||||
```dataviewjs
|
||||
await dv.view("meta/scripts/taskscalendar", {pages: "", view: "week", firstDayOfWeek: "1", options: "style4"})
|
||||
```
|
||||
|
||||
renders a calendar
|
||||
11
src/content/en/vault/📆 planning/tasks.md
Normal file
11
src/content/en/vault/📆 planning/tasks.md
Normal file
@@ -0,0 +1,11 @@
|
||||
```tasks
|
||||
no due date
|
||||
not done
|
||||
```
|
||||
|
||||
```tasks
|
||||
not done
|
||||
has due date
|
||||
```
|
||||
|
||||
tasks get parsed here
|
||||
1
src/content/en/vault/📝 notes/readme.md
Normal file
1
src/content/en/vault/📝 notes/readme.md
Normal file
@@ -0,0 +1 @@
|
||||
for cases when you just need a text editor
|
||||
9
src/content/en/vault/📦 objects/physical/readme.md
Normal file
9
src/content/en/vault/📦 objects/physical/readme.md
Normal 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
|
||||
7
src/content/en/vault/📦 objects/virtual/apps/readme.md
Normal file
7
src/content/en/vault/📦 objects/virtual/apps/readme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
- apps/type/subtype
|
||||
---
|
||||
|
||||
notes about various programs (almost like places/virtual, but for programs)
|
||||
8
src/content/en/vault/📶 research/readme.md
Normal file
8
src/content/en/vault/📶 research/readme.md
Normal 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
|
||||
6
src/content/en/vault/📺 media/readme.md
Normal file
6
src/content/en/vault/📺 media/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- media/type
|
||||
---
|
||||
|
||||
description, notes and review of watched/read/listened
|
||||
6
src/content/en/vault/🧠 thoughts/_topics/readme.md
Normal file
6
src/content/en/vault/🧠 thoughts/_topics/readme.md
Normal 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
|
||||
7
src/content/en/vault/🧠 thoughts/readme.md
Normal file
7
src/content/en/vault/🧠 thoughts/readme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
tags:
|
||||
- thoughts/type
|
||||
---
|
||||
|
||||
ideas, manifestos, any longreads generated directly from the brain
|
||||
subfolders by year, date in filename
|
||||
152
src/content/ru/vault/AGENTS.md
Normal file
152
src/content/ru/vault/AGENTS.md
Normal 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. **Слушать запрос юзера** и навигировать по карте выше
|
||||
|
||||
---
|
||||
69
src/content/ru/vault/README.md
Normal file
69
src/content/ru/vault/README.md
Normal 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
|
||||
|
||||
удачи.
|
||||
@@ -0,0 +1,3 @@
|
||||
тут лежат файлы отсюда
|
||||
|
||||
https://github.com/702573N/Obsidian-Tasks-Calendar
|
||||
17
src/content/ru/vault/мета/шаблоны/quickadd/вещи/духи.md
Normal file
17
src/content/ru/vault/мета/шаблоны/quickadd/вещи/духи.md
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
- "#вещи/духи"
|
||||
designer: {{VALUE:designer}}
|
||||
model: {{VALUE:model}}
|
||||
price:
|
||||
link:
|
||||
notes:
|
||||
---
|
||||
# {{VALUE:designer}} {{VALUE:model}}
|
||||
|
||||
## Инфо
|
||||
|
||||
## Юзкейс
|
||||
|
||||
## Нанесение
|
||||
13
src/content/ru/vault/мета/шаблоны/quickadd/вещи/шмотка.md
Normal file
13
src/content/ru/vault/мета/шаблоны/quickadd/вещи/шмотка.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
- "#вещи/шмотки"
|
||||
brand: {{VALUE:brand}}
|
||||
model: {{VALUE:model}}
|
||||
price:
|
||||
spent:
|
||||
fake: false
|
||||
link:
|
||||
banner:
|
||||
---
|
||||
# {{VALUE:brand}} {{VALUE:model}}
|
||||
18
src/content/ru/vault/мета/шаблоны/quickadd/медиа/аниме.md
Normal file
18
src/content/ru/vault/мета/шаблоны/quickadd/медиа/аниме.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
досмотрел: false
|
||||
серий:
|
||||
оценка:
|
||||
жанры:
|
||||
banner:
|
||||
aliases:
|
||||
ссылка:
|
||||
tags:
|
||||
- медиа/аниме
|
||||
---
|
||||
## Описание
|
||||
|
||||
|
||||
## Мнение
|
||||
|
||||
|
||||
## Заметки
|
||||
16
src/content/ru/vault/мета/шаблоны/quickadd/медиа/книга.md
Normal file
16
src/content/ru/vault/мета/шаблоны/quickadd/медиа/книга.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
дочитал: false
|
||||
страниц:
|
||||
оценка:
|
||||
жанры:
|
||||
aliases:
|
||||
tags:
|
||||
- медиа/книга
|
||||
---
|
||||
## Описание
|
||||
|
||||
|
||||
## Мнение
|
||||
|
||||
|
||||
## Заметки
|
||||
18
src/content/ru/vault/мета/шаблоны/quickadd/медиа/сериал.md
Normal file
18
src/content/ru/vault/мета/шаблоны/quickadd/медиа/сериал.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
досмотрел: false
|
||||
серий:
|
||||
оценка:
|
||||
жанры:
|
||||
banner:
|
||||
aliases:
|
||||
ссылка:
|
||||
tags:
|
||||
- медиа/сериал
|
||||
---
|
||||
## Описание
|
||||
|
||||
|
||||
## Мнение
|
||||
|
||||
|
||||
## Заметки
|
||||
3
src/content/ru/vault/мета/шаблоны/quickadd/мысль.md
Normal file
3
src/content/ru/vault/мета/шаблоны/quickadd/мысль.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
aliases:
|
||||
---
|
||||
10
src/content/ru/vault/мета/шаблоны/quickadd/ресерч.md
Normal file
10
src/content/ru/vault/мета/шаблоны/quickadd/ресерч.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
## Источники
|
||||
|
||||
14
src/content/ru/vault/мета/шаблоны/quickadd/событие.md
Normal file
14
src/content/ru/vault/мета/шаблоны/quickadd/событие.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
date_start: {{date}}
|
||||
date_end: {{date}}
|
||||
tags:
|
||||
aliases:
|
||||
---
|
||||
[[{{date}}]]
|
||||
## Краткое описание
|
||||
|
||||
|
||||
## Ход события
|
||||
|
||||
|
||||
## Заметки
|
||||
45
src/content/ru/vault/мета/шаблоны/quickadd/человек.md
Normal file
45
src/content/ru/vault/мета/шаблоны/quickadd/человек.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
birthday: ""
|
||||
---
|
||||
# 👤 {{name}}
|
||||
## Основная информация
|
||||
- Возраст: `= date(today) - date(this.birthday)`
|
||||
- Знакомы из-за:
|
||||
|
||||
## Личная информация
|
||||
|
||||
|
||||
---
|
||||
## Психологический профиль
|
||||
### Состояние
|
||||
[[{{date}}]]
|
||||
-
|
||||
|
||||
### Характер и стиль общения
|
||||
[[{{date}}]]
|
||||
-
|
||||
|
||||
### Интересы и увлечения
|
||||
[[{{date}}]]
|
||||
-
|
||||
|
||||
---
|
||||
## Особенности взаимодействия
|
||||
|
||||
### Что важно помнить при общении:
|
||||
[[{{date}}]]
|
||||
-
|
||||
|
||||
#### Что ему/ей приятно:
|
||||
[[{{date}}]]
|
||||
-
|
||||
|
||||
#### Что неприятно:
|
||||
[[{{date}}]]
|
||||
-
|
||||
|
||||
---
|
||||
## Личное отношение
|
||||
[[{{date}}]]
|
||||
2
src/content/ru/vault/мета/шаблоны/readme.md
Normal file
2
src/content/ru/vault/мета/шаблоны/readme.md
Normal file
@@ -0,0 +1,2 @@
|
||||
когда редактируете шаблон - помните что frontmatter надо редактировать в source mode
|
||||
иначе слетают {{вот эти штуки}}
|
||||
62
src/content/ru/vault/мета/шаблоны/templates/дневник.md
Normal file
62
src/content/ru/vault/мета/шаблоны/templates/дневник.md
Normal 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}}
|
||||
```
|
||||
4
src/content/ru/vault/мета/шаблоны/templates/заметка.md
Normal file
4
src/content/ru/vault/мета/шаблоны/templates/заметка.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
---
|
||||
6
src/content/ru/vault/🌍 места/виртуальные/readme.md
Normal file
6
src/content/ru/vault/🌍 места/виртуальные/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- сайты/подтип
|
||||
---
|
||||
|
||||
тут я храню по папкам (категории) ссылки на разные сайты
|
||||
7
src/content/ru/vault/🌍 места/реальные/readme.md
Normal file
7
src/content/ru/vault/🌍 места/реальные/readme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
tags:
|
||||
- места/название_города
|
||||
---
|
||||
|
||||
тут я храню важные места и заметки к ним, подпапка на город/страну
|
||||
в основном у меня используется для backlinks, но иногда не только
|
||||
6
src/content/ru/vault/🏄 события/_группы/readme.md
Normal file
6
src/content/ru/vault/🏄 события/_группы/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- события/тип_события
|
||||
---
|
||||
|
||||
отчеты по конкретным видам событий, дата в названии файла
|
||||
6
src/content/ru/vault/🏄 события/readme.md
Normal file
6
src/content/ru/vault/🏄 события/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- события/тип_события
|
||||
---
|
||||
|
||||
отчеты по событиям, подпапки по годам, дата в названии файла
|
||||
9
src/content/ru/vault/👤 люди/личное/readme.md
Normal file
9
src/content/ru/vault/👤 люди/личное/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
aliases:
|
||||
- Полное Имя
|
||||
tags:
|
||||
- люди/друг
|
||||
birthday: 0000-00-00
|
||||
---
|
||||
|
||||
друзья, подруги, все кого знаю
|
||||
9
src/content/ru/vault/👤 люди/личное/архив/readme.md
Normal file
9
src/content/ru/vault/👤 люди/личное/архив/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
aliases:
|
||||
- Полное Имя
|
||||
tags:
|
||||
- люди/друг/архив
|
||||
birthday: 0000-00-00
|
||||
---
|
||||
|
||||
те, с кем больше не общаюсь. обычно получают смененный тег
|
||||
9
src/content/ru/vault/👤 люди/популярные/readme.md
Normal file
9
src/content/ru/vault/👤 люди/популярные/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
aliases:
|
||||
- Полное Имя
|
||||
tags:
|
||||
- люди/популярные
|
||||
birthday: 0000-00-00
|
||||
---
|
||||
|
||||
любого рода селебрити
|
||||
9
src/content/ru/vault/👤 люди/профессиональное/readme.md
Normal file
9
src/content/ru/vault/👤 люди/профессиональное/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
aliases:
|
||||
- Полное Имя
|
||||
tags:
|
||||
- люди/учитель/предмет
|
||||
birthday: 0000-00-00
|
||||
---
|
||||
|
||||
учителя, коллеги
|
||||
6
src/content/ru/vault/💻 навыки/readme.md
Normal file
6
src/content/ru/vault/💻 навыки/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- решения/сфера/конкретнее
|
||||
---
|
||||
|
||||
быстрые решения проблем, заметки по программам, то что я умею, но забываю
|
||||
6
src/content/ru/vault/💻 образование/readme.md
Normal file
6
src/content/ru/vault/💻 образование/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- учеба/предмет
|
||||
---
|
||||
|
||||
конспекты, подпапки по учебным заведениям/предметам
|
||||
8
src/content/ru/vault/💻 проекты/readme.md
Normal file
8
src/content/ru/vault/💻 проекты/readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
tags:
|
||||
- проекты/сфера
|
||||
---
|
||||
|
||||
подпапки активные/архив
|
||||
внутри подпапки по сферам деятельности, в них папки по проектам
|
||||
обычно внутри хаос со всей нужной инфой по проекту
|
||||
7
src/content/ru/vault/📅 дни/readme.md
Normal file
7
src/content/ru/vault/📅 дни/readme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
tags:
|
||||
- дни
|
||||
---
|
||||
|
||||
ежедневная заметка, сюда сконфигурирован плагин Calendar и ежедневные заметки
|
||||
рабочее поле на каждый день, мелкие мысли, заполнение ежедневной рутины, ход дня
|
||||
7
src/content/ru/vault/📆 доски/readme.md
Normal file
7
src/content/ru/vault/📆 доски/readme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
kanban-plugin: board
|
||||
---
|
||||
|
||||
kanban-доски по каждой из сфер (например: организация, работа, учеба, ресерчи)
|
||||
использую плагин Tasks, проставляю даты выполнения
|
||||
тут лежит только то что прямо сейчас в работе
|
||||
5
src/content/ru/vault/📆 планирование/задачи календарь.md
Normal file
5
src/content/ru/vault/📆 планирование/задачи календарь.md
Normal file
@@ -0,0 +1,5 @@
|
||||
```dataviewjs
|
||||
await dv.view("мета/скрипты/taskscalendar", {pages: "", view: "week", firstDayOfWeek: "1", options: "style4"})
|
||||
```
|
||||
|
||||
рендерится календарь
|
||||
11
src/content/ru/vault/📆 планирование/задачи.md
Normal file
11
src/content/ru/vault/📆 планирование/задачи.md
Normal file
@@ -0,0 +1,11 @@
|
||||
```tasks
|
||||
no due date
|
||||
not done
|
||||
```
|
||||
|
||||
```tasks
|
||||
not done
|
||||
has due date
|
||||
```
|
||||
|
||||
сюда парсятся задачи
|
||||
8
src/content/ru/vault/📆 планирование/роадмап.md
Normal file
8
src/content/ru/vault/📆 планирование/роадмап.md
Normal file
@@ -0,0 +1,8 @@
|
||||
рабочее поле, статусы по каждой из сфер
|
||||
|
||||
# Приоритеты
|
||||
- приоритетные задачи
|
||||
|
||||
# Разные сферы
|
||||
## Разные проекты
|
||||
- статус каждого из них и ближайшие планы
|
||||
2
src/content/ru/vault/📆 планирование/списки/readme.md
Normal file
2
src/content/ru/vault/📆 планирование/списки/readme.md
Normal file
@@ -0,0 +1,2 @@
|
||||
под каждую заметку из досок тут такая же заметка, но сюда кладутся задачи на будущее
|
||||
добавлена в список исключений в плагине Tasks
|
||||
1
src/content/ru/vault/📝 заметки/readme.md
Normal file
1
src/content/ru/vault/📝 заметки/readme.md
Normal file
@@ -0,0 +1 @@
|
||||
на случаи когда нужен просто текстовый редактор
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
aliases:
|
||||
tags:
|
||||
- приложения/тип/подтип
|
||||
---
|
||||
|
||||
заметки о разных программах (почти как места/виртуальные, только для программ)
|
||||
9
src/content/ru/vault/📦 объекты/материальное/readme.md
Normal file
9
src/content/ru/vault/📦 объекты/материальное/readme.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
tags:
|
||||
- "#вещи/тип_вещи"
|
||||
---
|
||||
|
||||
разные виды вещей, категоризованны по видам
|
||||
напирмер у меня есть папки "духи", "еда", "одежда", "техника"
|
||||
тут обычно гайды на всякую бытовую технику, под одежду беклинки для сбора фитов и тд
|
||||
напрямую рецепты тут не храню, советую Mealie для этого (в директории "еда" у меня стандартные списки закупок), но вы можете
|
||||
8
src/content/ru/vault/📶 ресерчи/readme.md
Normal file
8
src/content/ru/vault/📶 ресерчи/readme.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
tags:
|
||||
- ресерчи/сфера/задача
|
||||
---
|
||||
|
||||
подпапки на разные сферы
|
||||
заметки по источникам, вся собранная интересная информация
|
||||
обычно что-то отсюда в будущем уходит в "навыки" в концентрированном виде
|
||||
6
src/content/ru/vault/📺 медиа/readme.md
Normal file
6
src/content/ru/vault/📺 медиа/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- медиа/тип
|
||||
---
|
||||
|
||||
описание, заметки и отзыв о просмотренном/прочитанном/прослушанном
|
||||
6
src/content/ru/vault/🧠 мысли/_темы/readme.md
Normal file
6
src/content/ru/vault/🧠 мысли/_темы/readme.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- мысли/тема
|
||||
---
|
||||
|
||||
подпапки под каждую тему, например "философия". что-то, что не является навыком, но слишком большое, чтобы писать в дневник
|
||||
7
src/content/ru/vault/🧠 мысли/readme.md
Normal file
7
src/content/ru/vault/🧠 мысли/readme.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
tags:
|
||||
- мысли/тип
|
||||
---
|
||||
|
||||
идеи, манифесты, любые лонгриды сгенерированные напрямую из мозга
|
||||
подпапки по годам, дата в названии файла
|
||||
@@ -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
163
src/lib/assets/prompt.txt
Normal 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>
|
||||
530
src/lib/components/FakeObsidian/FakeObsidian.svelte
Normal file
530
src/lib/components/FakeObsidian/FakeObsidian.svelte
Normal 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>
|
||||
26
src/lib/components/FakeObsidian/FileTree.svelte
Normal file
26
src/lib/components/FakeObsidian/FileTree.svelte
Normal 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>
|
||||
112
src/lib/components/FakeObsidian/FileTreeItem.svelte
Normal file
112
src/lib/components/FakeObsidian/FileTreeItem.svelte
Normal 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>
|
||||
358
src/lib/components/FakeObsidian/MarkdownViewer.svelte
Normal file
358
src/lib/components/FakeObsidian/MarkdownViewer.svelte
Normal 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>
|
||||
14
src/lib/components/Footer.svelte
Normal file
14
src/lib/components/Footer.svelte
Normal 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>
|
||||
268
src/lib/components/Header.svelte
Normal file
268
src/lib/components/Header.svelte
Normal 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>
|
||||
374
src/lib/components/Hero/AuroraBackground.svelte
Normal file
374
src/lib/components/Hero/AuroraBackground.svelte
Normal 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>
|
||||
93
src/lib/components/Hero/Hero.svelte
Normal file
93
src/lib/components/Hero/Hero.svelte
Normal 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>
|
||||
664
src/lib/components/LiquidGlass.svelte
Normal file
664
src/lib/components/LiquidGlass.svelte
Normal 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>
|
||||
254
src/lib/components/PersonalityCards/ClaudeGlow.svelte
Normal file
254
src/lib/components/PersonalityCards/ClaudeGlow.svelte
Normal 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>
|
||||
182
src/lib/components/PersonalityCards/GeminiGlow.svelte
Normal file
182
src/lib/components/PersonalityCards/GeminiGlow.svelte
Normal 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>
|
||||
186
src/lib/components/PersonalityCards/PersonalityCard.svelte
Normal file
186
src/lib/components/PersonalityCards/PersonalityCard.svelte
Normal 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>
|
||||
57
src/lib/components/PersonalityCards/PersonalityCards.svelte
Normal file
57
src/lib/components/PersonalityCards/PersonalityCards.svelte
Normal 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>
|
||||
62
src/lib/components/PromptBuilder/PromptBuilder.svelte
Normal file
62
src/lib/components/PromptBuilder/PromptBuilder.svelte
Normal 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>
|
||||
351
src/lib/components/PromptBuilder/PromptCard.svelte
Normal file
351
src/lib/components/PromptBuilder/PromptCard.svelte
Normal 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>
|
||||
43
src/lib/i18n/context.svelte.ts
Normal file
43
src/lib/i18n/context.svelte.ts
Normal 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
23
src/lib/i18n/index.ts
Normal 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);
|
||||
}
|
||||
91
src/lib/i18n/translations/en.ts
Normal file
91
src/lib/i18n/translations/en.ts
Normal 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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user