Compare commits

...

13 Commits

Author SHA1 Message Date
github-actions[bot]
6bb3238e6d Update version to v1.4.113 and commit 2024-11-26 21:45:39 +00:00
Eugen Eisler
dfcd29593d Merge pull request #1166 from danielmiessler/dependabot/npm_and_yarn/web/npm_and_yarn-8ea39cdf54
build(deps-dev): bump @sveltejs/kit from 2.6.1 to 2.8.4 in /web in the npm_and_yarn group across 1 directory
2024-11-26 22:44:55 +01:00
dependabot[bot]
63b357168e build(deps-dev): bump @sveltejs/kit
Bumps the npm_and_yarn group with 1 update in the /web directory: [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit).


Updates `@sveltejs/kit` from 2.6.1 to 2.8.4
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.8.4/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-26 21:40:49 +00:00
github-actions[bot]
317a4309f7 Update version to v1.4.112 and commit 2024-11-26 21:40:17 +00:00
Eugen Eisler
eceb10b725 Merge pull request #1165 from johnconnor-sec/main
feat: Fabric Web UI
2024-11-26 22:39:16 +01:00
John Connor
9fa8634083 Update README.md 2024-11-26 09:13:04 -05:00
John Connor
a3ea63c1f9 Update Obsidian.md 2024-11-26 09:07:57 -05:00
John Connor
097b3eb0ba Update Obsidian.md 2024-11-26 09:07:30 -05:00
John
30f37ea633 john 2024-11-26 08:53:48 2024-11-26 08:54:12 -05:00
github-actions[bot]
23b495c8f7 Update version to v..1 and commit 2024-11-26 13:52:03 +00:00
John
e7f2d48437 john 2024-11-26 08:44:20 2024-11-26 08:50:31 -05:00
John
7043f78f1f john 2024-11-26 08:40:21 2024-11-26 08:50:31 -05:00
github-actions[bot]
f2cc718f49 Update version to v..1 and commit 2024-11-26 11:41:16 +00:00
144 changed files with 16179 additions and 3 deletions

168
.gitignore vendored
View File

@@ -22,7 +22,7 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
@@ -166,3 +166,169 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
web/node_modules
# Output
web/.output
web/.vercel
web/.svelte-kit
web/build
# OS
web/.DS_Store
web/Thumbs.db
# Env
web/.env
web/.env.*
web/!.env.example
web/!.env.test
# Vite
web/vite.config.js.timestamp-*
web/vite.config.ts.timestamp-*
# Created by https://www.toptal.com/developers/gitignore/api/node
# Edit at https://www.toptal.com/developers/gitignore?templates=node
### Node ###
# Logs
web/logs
web/*.log
web/npm-debug.log*
web/yarn-debug.log*
web/yarn-error.log*
web/lerna-debug.log*
web/.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
web/report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
web/pids
web/*.pid
web/*.seed
web/*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
web/lib-cov
# Coverage directory used by tools like istanbul
web/coverage
web/*.lcov
# nyc test coverage
web/.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
web/.grunt
# Bower dependency directory (https://bower.io/)
web/bower_components
# node-waf configuration
web/.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
web/node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web/web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
web/.npm
# Optional eslint cache
web/.eslintcache
# Optional stylelint cache
web/.stylelintcache
# Microbundle cache
web/.rpt2_cache/
web/.rts2_cache_cjs/
web/.rts2_cache_es/
web/.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
web/.env
web/.env.development.local
web/.env.test.local
web/.env.production.local
web/.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
web/.next
web/out
# Nuxt.js build / generate output
web/.nuxt
web/dist
# Gatsby files
web/.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
web/.vuepress/dist
# vuepress v2.x temp and cache directory
web/.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
web/.vscode-test
# yarn v2
web/.yarn/cache
web/.yarn/unplugged
web/.yarn/build-state.yml
web/.yarn/install-state.gz
web/.pnp.*
### Node Patch ###
# Serverless Webpack directories
web/.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
web/.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/node

View File

@@ -51,6 +51,7 @@ func Cli(version string) (err error) {
}
if currentFlags.Serve {
registry.ConfigureVendors()
err = restapi.Serve(registry, currentFlags.ServeAddress)
return
}

View File

@@ -1 +1 @@
"1.4.111"
"1.4.113"

211
restapi/chat.go Executable file
View File

@@ -0,0 +1,211 @@
package restapi
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
goopenai "github.com/sashabaranov/go-openai"
"github.com/danielmiessler/fabric/common"
"github.com/danielmiessler/fabric/core"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/gin-gonic/gin"
)
type ChatHandler struct {
registry *core.PluginRegistry
db *fsdb.Db
}
type PromptRequest struct {
UserInput string `json:"userInput"`
Vendor string `json:"vendor"`
Model string `json:"model"`
ContextName string `json:"contextName"`
PatternName string `json:"patternName"`
}
type ChatRequest struct {
Prompts []PromptRequest `json:"prompts"`
common.ChatOptions // Embed the ChatOptions from common package
}
type StreamResponse struct {
Type string `json:"type"` // "content", "error", "complete"
Format string `json:"format"` // "markdown", "mermaid", "plain"
Content string `json:"content"` // The actual content
}
func NewChatHandler(r *gin.Engine, registry *core.PluginRegistry, db *fsdb.Db) *ChatHandler {
handler := &ChatHandler{
registry: registry,
db: db,
}
r.POST("/chat", handler.HandleChat)
return handler
}
func (h *ChatHandler) HandleChat(c *gin.Context) {
var request ChatRequest
if err := c.BindJSON(&request); err != nil {
log.Printf("Error binding JSON: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request format: %v", err)})
return
}
log.Printf("Received chat request with %d prompts", len(request.Prompts))
// Set headers for SSE
c.Writer.Header().Set("Content-Type", "text/readystream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
c.Writer.Header().Set("X-Accel-Buffering", "no")
clientGone := c.Writer.CloseNotify()
for i, prompt := range request.Prompts {
select {
case <-clientGone:
log.Printf("Client disconnected")
return
default:
log.Printf("Processing prompt %d: Model=%s Pattern=%s Context=%s",
i+1, prompt.Model, prompt.PatternName, prompt.ContextName)
// Create chat channel for streaming
streamChan := make(chan string)
// Start chat processing in goroutine
go func(p PromptRequest) {
defer close(streamChan)
chatter, err := h.registry.GetChatter(p.Model, 2048, false, false)
if err != nil {
log.Printf("Error creating chatter: %v", err)
streamChan <- fmt.Sprintf("Error: %v", err)
return
}
chatReq := &common.ChatRequest{
Message: &goopenai.ChatCompletionMessage{
Role: "user",
Content: p.UserInput,
},
PatternName: p.PatternName,
ContextName: p.ContextName,
}
opts := &common.ChatOptions{
Model: p.Model,
Temperature: request.Temperature,
TopP: request.TopP,
FrequencyPenalty: request.FrequencyPenalty,
PresencePenalty: request.PresencePenalty,
}
session, err := chatter.Send(chatReq, opts)
if err != nil {
log.Printf("Error from chatter.Send: %v", err)
streamChan <- fmt.Sprintf("Error: %v", err)
return
}
if session == nil {
log.Printf("No session returned from chatter.Send")
streamChan <- "Error: No response from model"
return
}
// Get the last message from the session
lastMsg := session.GetLastMessage()
if lastMsg != nil {
streamChan <- lastMsg.Content
} else {
log.Printf("No message content in session")
streamChan <- "Error: No response content"
}
}(prompt)
// Read from streamChan and write to client
for content := range streamChan {
select {
case <-clientGone:
return
default:
if strings.HasPrefix(content, "Error:") {
response := StreamResponse{
Type: "error",
Format: "plain",
Content: content,
}
if err := writeSSEResponse(c.Writer, response); err != nil {
log.Printf("Error writing error response: %v", err)
return
}
} else {
response := StreamResponse{
Type: "content",
Format: detectFormat(content),
Content: content,
}
if err := writeSSEResponse(c.Writer, response); err != nil {
log.Printf("Error writing content response: %v", err)
return
}
}
}
}
// Signal completion of this prompt
completeResponse := StreamResponse{
Type: "complete",
Format: "plain",
Content: "",
}
if err := writeSSEResponse(c.Writer, completeResponse); err != nil {
log.Printf("Error writing completion response: %v", err)
return
}
}
}
}
func writeSSEResponse(w gin.ResponseWriter, response StreamResponse) error {
data, err := json.Marshal(response)
if err != nil {
return fmt.Errorf("error marshaling response: %v", err)
}
if _, err := fmt.Fprintf(w, "data: %s\n\n", string(data)); err != nil {
return fmt.Errorf("error writing response: %v", err)
}
w.(http.Flusher).Flush()
return nil
}
func detectFormat(content string) string {
if strings.HasPrefix(content, "graph TD") ||
strings.HasPrefix(content, "gantt") ||
strings.HasPrefix(content, "flowchart") ||
strings.HasPrefix(content, "sequenceDiagram") ||
strings.HasPrefix(content, "classDiagram") ||
strings.HasPrefix(content, "stateDiagram") {
return "mermaid"
}
if strings.Contains(content, "```") ||
strings.Contains(content, "#") ||
strings.Contains(content, "*") ||
strings.Contains(content, "_") ||
strings.Contains(content, "-") {
return "markdown"
}
return "plain"
}

124
restapi/configuration.go Executable file
View File

@@ -0,0 +1,124 @@
package restapi
import (
"fmt"
"net/http"
"os"
"strings"
"github.com/danielmiessler/fabric/plugins/db/fsdb"
"github.com/gin-gonic/gin"
)
// ConfigHandler defines the handler for configuration-related operations
type ConfigHandler struct {
db *fsdb.Db
// configurations *fsdb.EnvFilePath("$HOME/.config/fabric/.env")
}
func NewConfigHandler(r *gin.Engine, db *fsdb.Db) *ConfigHandler {
handler := &ConfigHandler{
db: db,
// configurations: db.Configurations,
}
r.GET("/config", handler.GetConfig)
r.POST("/config/update", handler.UpdateConfig)
return handler
}
func (h *ConfigHandler) GetConfig(c *gin.Context) {
if h.db == nil {
c.JSON(http.StatusNotFound, gin.H{"error": ".env file not found"})
return
}
if !h.db.IsEnvFileExists() {
c.JSON(http.StatusOK, gin.H{
"openai": "",
"anthropic": "",
"groq": "",
"mistral": "",
"gemini": "",
"ollama": "",
"openrouter": "",
"silicon": "",
})
return
}
err := h.db.LoadEnvFile()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
config := map[string]string{
"openai": os.Getenv("OPENAI_API_KEY"),
"anthropic": os.Getenv("ANTHROPIC_API_KEY"),
"groq": os.Getenv("GROQ_API_KEY"),
"mistral": os.Getenv("MISTRAL_API_KEY"),
"gemini": os.Getenv("GEMINI_API_KEY"),
"ollama": os.Getenv("OLLAMA_URL"),
"openrouter": os.Getenv("OPENROUTER_API_KEY"),
"silicon": os.Getenv("SILICON_API_KEY"),
}
c.JSON(http.StatusOK, config)
}
func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
if h.db == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database not initialized"})
return
}
var config struct {
OpenAIApiKey string `json:"openai_api_key"`
AnthropicApiKey string `json:"anthropic_api_key"`
GroqApiKey string `json:"groq_api_key"`
MistralApiKey string `json:"mistral_api_key"`
GeminiApiKey string `json:"gemini_api_key"`
OllamaURL string `json:"ollama_url"`
OpenRouterApiKey string `json:"openrouter_api_key"`
SiliconApiKey string `json:"silicon_api_key"`
}
if err := c.BindJSON(&config); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
envVars := map[string]string{
"OPENAI_API_KEY": config.OpenAIApiKey,
"ANTHROPIC_API_KEY": config.AnthropicApiKey,
"GROQ_API_KEY": config.GroqApiKey,
"MISTRAL_API_KEY": config.MistralApiKey,
"GEMINI_API_KEY": config.GeminiApiKey,
"OLLAMA_URL": config.OllamaURL,
"OPENROUTER_API_KEY": config.OpenRouterApiKey,
"SILICON_API_KEY": config.SiliconApiKey,
}
var envContent strings.Builder
for key, value := range envVars {
if value != "" {
envContent.WriteString(fmt.Sprintf("%s=%s\n", key, value))
os.Setenv(key, value)
}
}
// Save configuration to file
if err := h.db.SaveEnv(envContent.String()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if err := h.db.LoadEnvFile(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Configuration updated successfully"})
}

45
restapi/models.go Executable file
View File

@@ -0,0 +1,45 @@
package restapi
import (
"github.com/danielmiessler/fabric/plugins/ai"
"github.com/gin-gonic/gin"
)
type ModelsHandler struct {
vendorManager *ai.VendorsManager
}
func NewModelsHandler(r *gin.Engine, vendorManager *ai.VendorsManager) {
handler := &ModelsHandler{
vendorManager: vendorManager,
}
r.GET("/models/names", handler.GetModelNames)
}
func (h *ModelsHandler) GetModelNames(c *gin.Context) {
vendorsModels, err := h.vendorManager.GetModels()
if err != nil {
c.JSON(500, gin.H{"error": "Server failed to retrieve model names"})
return
}
response := make(map[string]interface{})
vendors := make(map[string][]string)
for _, groupItems := range vendorsModels.GroupsItems {
vendors[groupItems.Group] = groupItems.Items
}
response["models"] = h.getAllModelNames(vendorsModels)
response["vendors"] = vendors
c.JSON(200, response)
}
func (h *ModelsHandler) getAllModelNames(vendorsModels *ai.VendorsModels) []string {
var allModelNames []string
for _, groupItems := range vendorsModels.GroupsItems {
allModelNames = append(allModelNames, groupItems.Items...)
}
return allModelNames
}

View File

@@ -17,6 +17,9 @@ func Serve(registry *core.PluginRegistry, address string) (err error) {
NewPatternsHandler(r, fabricDb.Patterns)
NewContextsHandler(r, fabricDb.Contexts)
NewSessionsHandler(r, fabricDb.Sessions)
NewChatHandler(r, registry, fabricDb)
NewConfigHandler(r, fabricDb)
NewModelsHandler(r, registry.VendorManager)
// Start server
err = r.Run(address)

View File

@@ -1,3 +1,3 @@
package main
var version = "v1.4.111"
var version = "v1.4.113"

11
web/.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm", "pnpm" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

0
web/.npmrc Normal file
View File

4
web/.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

8
web/.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

120
web/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,120 @@
{
"prettier.documentSelectors": [
"**/*.svelte"
],
"tailwindCSS.classAttributes": [
"class",
"accent",
"active",
"animIndeterminate",
"aspectRatio",
"background",
"badge",
"bgBackdrop",
"bgDark",
"bgDrawer",
"bgLight",
"blur",
"border",
"button",
"buttonAction",
"buttonBack",
"buttonClasses",
"buttonComplete",
"buttonDismiss",
"buttonNeutral",
"buttonNext",
"buttonPositive",
"buttonTextCancel",
"buttonTextConfirm",
"buttonTextFirst",
"buttonTextLast",
"buttonTextNext",
"buttonTextPrevious",
"buttonTextSubmit",
"caretClosed",
"caretOpen",
"chips",
"color",
"controlSeparator",
"controlVariant",
"cursor",
"display",
"element",
"fill",
"fillDark",
"fillLight",
"flex",
"flexDirection",
"gap",
"gridColumns",
"height",
"hover",
"inactive",
"indent",
"justify",
"meter",
"padding",
"position",
"regionAnchor",
"regionBackdrop",
"regionBody",
"regionCaption",
"regionCaret",
"regionCell",
"regionChildren",
"regionChipList",
"regionChipWrapper",
"regionCone",
"regionContent",
"regionControl",
"regionDefault",
"regionDrawer",
"regionFoot",
"regionFootCell",
"regionFooter",
"regionHead",
"regionHeadCell",
"regionHeader",
"regionIcon",
"regionInput",
"regionInterface",
"regionInterfaceText",
"regionLabel",
"regionLead",
"regionLegend",
"regionList",
"regionListItem",
"regionNavigation",
"regionPage",
"regionPanel",
"regionRowHeadline",
"regionRowMain",
"regionSummary",
"regionSymbol",
"regionTab",
"regionTrail",
"ring",
"rounded",
"select",
"shadow",
"slotDefault",
"slotFooter",
"slotHeader",
"slotLead",
"slotMessage",
"slotMeta",
"slotPageContent",
"slotPageFooter",
"slotPageHeader",
"slotSidebarLeft",
"slotSidebarRight",
"slotTrail",
"spacing",
"text",
"track",
"transition",
"width",
"zIndex"
]
}

23
web/README.md Normal file
View File

@@ -0,0 +1,23 @@
## The Fabric Web App
[Installing](#Installing)|[Todos](#Todos)|[Collaborators](#Collaborators)
This is the web app for Fabric. It is built using [Svelte](https://svelte.dev/), [SkeletonUI](https://skeleton.dev/), and [Mdsvex](https://mdsvex.pngwn.io/).
The goal of this app is to not only provide a user interface for Fabric, but also a out-of-the-box website for those who want to get started with web development or blogging. The tech stack is minimal and (I hope) the code is easy to read and understand. One thing I kept in mind when making this app was to make it easy for beginners to get started with web development. You can use this app as a GUI interface for Fabric, a ready to go blog-site, or a website template for your own projects. I hope you find it useful!
![Preview](static/image.png)
### Installing
It can be installed by navigating to the `web` directory and using `npm install`, `pnpm install`, or your favorite package manager. Then simply run `npm run dev` or your equivalent command to start the app. *You will need to run fabric in a seperate terminal with the `fabric --serve` command.*
### Todos
- [ ] Remove dead code
- [ ] Add feat: Add copy button / load button to load blog posts to the Chat and Chat to Blog posts (system message or user?)
- [ ] Add feat: Jina button
- [ ] Fix save button to save markdown, not json
- [ ] Implement session handling
- [?] What will I do with the context?
- [ ] Add support for image uploads / file uploads
### Collaborators
There are many experienced developers in the Fabric community. If you'd like to collaborate on this project, please reach out to me on.

21
web/SECURITY.md Normal file
View File

@@ -0,0 +1,21 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.

38
web/STD-README.md Normal file
View File

@@ -0,0 +1,38 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

32
web/eslint.config.js Normal file
View File

@@ -0,0 +1,32 @@
import eslint from '@eslint/js';
import prettier from 'eslint-config-prettier';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: tseslint.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
);

19
web/jsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

4
web/markdown.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
/* declare module '*.md' {
const component: import('svelte').ComponentType;
export default component;
} */

102
web/my-custom-theme.ts Normal file
View File

@@ -0,0 +1,102 @@
import type { CustomThemeConfig } from '@skeletonlabs/tw-plugin';
export const myCustomTheme: CustomThemeConfig = {
name: 'my-custom-theme',
properties: {
// =~= Theme Properties =~=
"--theme-font-family-base": `system-ui`,
"--theme-font-family-heading": `system-ui`,
"--theme-font-color-base": "var(--color-primary-800)",
"--theme-font-color-dark": "var(--color-primary-300)",
"--theme-rounded-base": "9999px",
"--theme-rounded-container": "8px",
"--theme-border-base": "1px",
// =~= Theme On-X Colors =~=
"--on-primary": "0 0 0",
"--on-secondary": "0 0 0",
"--on-tertiary": "0 0 0",
"--on-success": "0 0 0",
"--on-warning": "0 0 0",
"--on-error": "0 0 0",
"--on-surface": "0 0 0",
// =~= Theme Colors =~=
// primary | #613bf7
"--color-primary-50": "231 226 254", // #e7e2fe
"--color-primary-100": "223 216 253", // #dfd8fd
"--color-primary-200": "216 206 253", // #d8cefd
"--color-primary-300": "192 177 252", // #c0b1fc
"--color-primary-400": "144 118 249", // #9076f9
"--color-primary-500": "97 59 247", // #613bf7
"--color-primary-600": "87 53 222", // #5735de
"--color-primary-700": "73 44 185", // #492cb9
"--color-primary-800": "58 35 148", // #3a2394
"--color-primary-900": "48 29 121", // #301d79
// secondary | #9de1ae
"--color-secondary-50": "240 251 243", // #f0fbf3
"--color-secondary-100": "235 249 239", // #ebf9ef
"--color-secondary-200": "231 248 235", // #e7f8eb
"--color-secondary-300": "216 243 223", // #d8f3df
"--color-secondary-400": "186 234 198", // #baeac6
"--color-secondary-500": "157 225 174", // #9de1ae
"--color-secondary-600": "141 203 157", // #8dcb9d
"--color-secondary-700": "118 169 131", // #76a983
"--color-secondary-800": "94 135 104", // #5e8768
"--color-secondary-900": "77 110 85", // #4d6e55
// tertiary | #3fa0a6
"--color-tertiary-50": "226 241 242", // #e2f1f2
"--color-tertiary-100": "217 236 237", // #d9eced
"--color-tertiary-200": "207 231 233", // #cfe7e9
"--color-tertiary-300": "178 217 219", // #b2d9db
"--color-tertiary-400": "121 189 193", // #79bdc1
"--color-tertiary-500": "63 160 166", // #3fa0a6
"--color-tertiary-600": "57 144 149", // #399095
"--color-tertiary-700": "47 120 125", // #2f787d
"--color-tertiary-800": "38 96 100", // #266064
"--color-tertiary-900": "31 78 81", // #1f4e51
// success | #37b3fc
"--color-success-50": "225 244 255", // #e1f4ff
"--color-success-100": "215 240 254", // #d7f0fe
"--color-success-200": "205 236 254", // #cdecfe
"--color-success-300": "175 225 254", // #afe1fe
"--color-success-400": "115 202 253", // #73cafd
"--color-success-500": "55 179 252", // #37b3fc
"--color-success-600": "50 161 227", // #32a1e3
"--color-success-700": "41 134 189", // #2986bd
"--color-success-800": "33 107 151", // #216b97
"--color-success-900": "27 88 123", // #1b587b
// warning | #d209f8
"--color-warning-50": "248 218 254", // #f8dafe
"--color-warning-100": "246 206 254", // #f6cefe
"--color-warning-200": "244 194 253", // #f4c2fd
"--color-warning-300": "237 157 252", // #ed9dfc
"--color-warning-400": "224 83 250", // #e053fa
"--color-warning-500": "210 9 248", // #d209f8
"--color-warning-600": "189 8 223", // #bd08df
"--color-warning-700": "158 7 186", // #9e07ba
"--color-warning-800": "126 5 149", // #7e0595
"--color-warning-900": "103 4 122", // #67047a
// error | #90df16
"--color-error-50": "238 250 220", // #eefadc
"--color-error-100": "233 249 208", // #e9f9d0
"--color-error-200": "227 247 197", // #e3f7c5
"--color-error-300": "211 242 162", // #d3f2a2
"--color-error-400": "177 233 92", // #b1e95c
"--color-error-500": "144 223 22", // #90df16
"--color-error-600": "130 201 20", // #82c914
"--color-error-700": "108 167 17", // #6ca711
"--color-error-800": "86 134 13", // #56860d
"--color-error-900": "71 109 11", // #476d0b
// surface | #46a1ed
"--color-surface-50": "227 241 252", // #e3f1fc
"--color-surface-100": "218 236 251", // #daecfb
"--color-surface-200": "209 232 251", // #d1e8fb
"--color-surface-300": "181 217 248", // #b5d9f8
"--color-surface-400": "126 189 242", // #7ebdf2
"--color-surface-500": "70 161 237", // #46a1ed
"--color-surface-600": "63 145 213", // #3f91d5
"--color-surface-700": "53 121 178", // #3579b2
"--color-surface-800": "42 97 142", // #2a618e
"--color-surface-900": "34 79 116", // #224f74
}
}

7397
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

53
web/package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "terminal-blog",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test": "vitest",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@skeletonlabs/skeleton": "^2.8.0",
"@skeletonlabs/tw-plugin": "^0.3.1",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.8.4",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@types/node": "^20.10.0",
"autoprefixer": "^10.4.16",
"lucide-svelte": "^0.309.0",
"mdsvex": "^0.11.0",
"postcss": "^8.4.32",
"postcss-load-config": "^5.0.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"svelte-youtube-embed": "^0.3.3",
"tailwindcss": "^3.3.6",
"typescript": "^5.0.0",
"vite": "^5.0.3",
"vite-plugin-tailwind-purgecss": "^0.2.0"
},
"type": "module",
"dependencies": {
"@floating-ui/dom": "^1.5.3",
"clsx": "^2.1.1",
"cn": "^0.1.1",
"date-fns": "^4.1.0",
"highlight.js": "^11.10.0",
"marked": "^15.0.1",
"rehype": "^13.0.2",
"rehype-autolink-headings": "^7.1.0",
"rehype-external-links": "^3.0.0",
"rehype-slug": "^6.0.0",
"rehype-unwrap-images": "^1.0.0",
"tailwind-merge": "^2.5.4",
"youtube-transcript": "^1.2.1"
}
}

2925
web/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

6
web/postcss.config.cjs Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

6
web/postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

22
web/rollup.config.js Normal file
View File

@@ -0,0 +1,22 @@
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
export default {
input: 'src/+page.js',
output: {
file: 'public/bundle.js',
format: 'iife',
name: 'app'
},
plugins: [
svelte({
// svelte options
extensions: [".svelte", ".svx", ".md"],
preprocess: mdsvex()
}),
resolve({
browser: true,
dedupe: ['svelte']
})
]
};

134
web/src/app.css Normal file
View File

@@ -0,0 +1,134 @@
/* @tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Go Mono', 'Fira Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Code', monospace;
--column-width: 42rem;
--column-margin-top: 4rem;
} */
/* Light theme variables */
/* :root {
--background: hsl(249, 81%, 85%);
--foreground: hsl(229, 65%, 29%);
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--primary: hsl(262.1 83.3% 57.8%);
--primary-foreground: hsl(274, 100%, 90%);
--secondary: hsl(173, 74%, 68%);
--secondary-foreground: hsl(195, 100%, 90%);
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--accent: hsl(220, 37%, 49%);
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 20% 98%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 262.1 83.3% 57.8%;
--radius: 0.5rem;
} */
/* Dark theme variables */
/* .dark {
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%;
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--ring: 263.4 70% 50.4%;
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
font-feature-settings: "rlig" 1, "calt" 1;
} */
/* Enhanced Typography */
/* h1, h2, h3, h4, h5, h6 {
@apply font-semibold tracking-tight;
}
h1 {
@apply text-4xl lg:text-5xl;
}
h2 {
@apply text-3xl lg:text-4xl;
}
h3 {
@apply text-2xl lg:text-3xl;
}
p {
@apply leading-7;
}
*/
/* Links */
/* a {
@apply text-primary hover:text-primary/80 transition-colors;
} */
/* Code blocks */
/* pre {
@apply p-4 rounded-lg bg-muted/50 font-mono text-sm;
}
code {
@apply font-mono text-sm;
}
*/
/* Terminal specific styles */
/* .terminal-window {
@apply rounded-lg border bg-card shadow-lg overflow-hidden;
}
.terminal-text {
@apply font-mono text-sm;
}
*/
/* Form elements */
/* input, textarea, select {
@apply rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
}
button {
@apply inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2;
}
} */
/* Custom scrollbar */
/* ::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-track {
@apply bg-muted;
}
::-webkit-scrollbar-thumb {
@apply bg-muted-foreground/30 rounded-full hover:bg-muted-foreground/50 transition-colors;
} */

9
web/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,9 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
interface PageData {}
// interface Error {}
// interface Platform {}
}

12
web/src/app.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" data-theme="my-custom-theme">
<div style="display: contents" class="h-full overflow-hidden">%sveltekit.body%</div>
</body>
</html>

25
web/src/app.postcss Normal file
View File

@@ -0,0 +1,25 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
html,
body {
@apply h-full overflow-hidden;
}
.terminal-output {
@apply font-mono text-sm;
}
.terminal-input {
@apply font-mono text-sm;
}
/* Skeleton theme overrides */
:root [data-theme='skeleton'] {
--theme-font-family-base: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--theme-font-family-heading: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

7
web/src/index.test.ts Normal file
View File

@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { cn } from "$lib/types/utils";
import { buttonVariants } from "./index.js";
let className = undefined;
export let variant = "";
export let size = "default";
export { className as class };
$: classes = cn(
buttonVariants.base,
buttonVariants.variants.variant[variant as keyof typeof buttonVariants.variants.variant],
buttonVariants.variants.size[size as keyof typeof buttonVariants.variants.size],
className
);
</script>
<button
class={classes}
type="button"
on:click
on:keydown
{...$$restProps}
>
<slot />
</button>

View File

@@ -0,0 +1,31 @@
import Root from "./button.svelte";
const buttonVariants = {
base: "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm",
outline: "border-input bg-background hover:bg-accent hover:text-accent-foreground border shadow-sm",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
};
export {
Root,
Root as Button,
buttonVariants
};

View File

@@ -0,0 +1,32 @@
<script lang="ts">
export let url: string = 'https://www.buymeacoffee.com/johnconnor.sec';
export let text: string = 'Buy me a coffee';
</script>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
class="text-sm px-3 py-1.5 btn variant-filled-tertiary hover:variant-filled-secondary transition-all duration-200 flex items-center gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="transition-transform duration-200 group-hover:rotate-12"
>
<path d="M17 8h1a4 4 0 1 1 0 8h-1" />
<path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
<line x1="6" y1="2" x2="6" y2="4" />
<line x1="10" y1="2" x2="10" y2="4" />
<line x1="14" y1="2" x2="14" y2="4" />
</svg>
{text}
</a>

View File

@@ -0,0 +1,6 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};

View File

@@ -0,0 +1,33 @@
<script lang="ts">
import { cn } from "$lib/types/utils";
let className: string | undefined = undefined;
export let value = undefined;
export { className as class };
export let readonly = undefined;
</script>
<input
class={cn(
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex h-9 w-full rounded-md border bg-transparent px-3 py-1 text-sm shadow-lg transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>

View File

@@ -0,0 +1,6 @@
import Root from "./label.svelte";
export {
Root,
Root as Label
};

View File

@@ -0,0 +1,15 @@
<script>
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export { className as class };
</script>
<label
class={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...$$restProps}
>
<slot />
</label>

View File

@@ -0,0 +1,6 @@
import Root from "./select.svelte";
export {
Root,
Root as Select
};

View File

@@ -0,0 +1,30 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
import { getContext } from "svelte";
let className = undefined;
export let sideOffset = 4;
export let position = "popper";
</script>
<div
class={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
>
<SelectPrimitive.Content {position} {sideOffset} {...$$restProps}>
<div
class={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
<slot />
</div>
</SelectPrimitive.Content>
</div>

View File

@@ -0,0 +1,16 @@
<script>
import { cn } from "$lib/utils";
let className = undefined;
export let value;
export let disabled = false;
export { className as class };
</script>
<option
{value}
{disabled}
class={cn("relative flex w-full cursor-default select-none py-1.5 pl-2 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className)}
>
<slot />
</option>

View File

@@ -0,0 +1,10 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Label class={cn("px-2 py-1.5 text-sm variant-filled-secondary font-semibold", className)} {...$$restProps}>
<slot />
</SelectPrimitive.Label>

View File

@@ -0,0 +1,8 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Separator class={cn("bg-muted -mx-1 my-1 h-px", className)} {...$$restProps} />

View File

@@ -0,0 +1,20 @@
<script>
import { Select as SelectPrimitive } from "bits-ui";
import { ChevronDown } from "lucide-svelte";
import { cn } from "$lib/utils";
import { getContext } from "svelte";
let className = undefined;
export { className as class };
</script>
<SelectPrimitive.Trigger
class={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...$$restProps}
>
<slot />
<ChevronDown class="h-4 w-4 opacity-50" />
</SelectPrimitive.Trigger>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { Select as SelectPrimitive } from "bits-ui";
import { cn } from "$lib/utils";
type $$Props = SelectPrimitive.ValueProps;
let className: $$Props["class"] = undefined;
export { className as class };
export let placeholder: $$Props["placeholder"] = undefined;
</script>
<SelectPrimitive.Value
class={cn("text-sm", className)}
{placeholder}
{...$$restProps}
>
<slot />
</SelectPrimitive.Value>

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import { cn } from "$lib/types/utils.ts";
import { ChevronDown } from "lucide-svelte";
export let value: any = undefined;
export let disabled = false;
let className = undefined;
export { className as class };
</script>
<div class="relative">
<select
{disabled}
bind:value
class={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm shadow-lg ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 appearance-none",
className
)}
{...$$restProps}
>
<slot />
</select>
<ChevronDown class="absolute right-3 top-2.5 h-4 w-4 opacity-50 pointer-events-none" />
</div>

View File

@@ -0,0 +1,6 @@
import Root from "./slider.svelte";
export {
Root,
//
Root as Slider,
};

View File

@@ -0,0 +1,89 @@
<script>
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export let value = 0;
export let min = 0;
export let max = 100;
export let step = 1;
export { className as class };
let sliderEl;
let isDragging = false;
$: percentage = ((value - min) / (max - min)) * 100;
function handleMouseDown(e) {
isDragging = true;
updateValue(e);
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleMouseUp);
}
function handleMouseMove(e) {
if (!isDragging) return;
updateValue(e);
}
function handleMouseUp() {
isDragging = false;
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleMouseUp);
}
function updateValue(e) {
if (!sliderEl) return;
const rect = sliderEl.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const rawValue = min + (max - min) * pos;
const steppedValue = Math.round(rawValue / step) * step;
value = Math.max(min, Math.min(max, steppedValue));
}
function handleKeyDown(e) {
const step = e.shiftKey ? 10 : 1;
switch (e.key) {
case 'ArrowLeft':
case 'ArrowDown':
e.preventDefault();
value = Math.max(min, value - step);
break;
case 'ArrowRight':
case 'ArrowUp':
e.preventDefault();
value = Math.min(max, value + step);
break;
case 'Home':
e.preventDefault();
value = min;
break;
case 'End':
e.preventDefault();
value = max;
break;
}
}
</script>
<div
bind:this={sliderEl}
class={cn("relative flex w-full touch-none select-none items-center", className)}
on:mousedown={handleMouseDown}
on:keydown={handleKeyDown}
role="slider"
tabindex="0"
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={value}
>
<div class="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
<div
class="absolute h-full bg-primary transition-all"
style="width: {percentage}%"
/>
</div>
<div
class="absolute h-4 w-4 rounded-full border border-secondary bg-primary-500 shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
style="left: calc({percentage}% - 0.5rem)"
/>
</div>

View File

@@ -0,0 +1,9 @@
<script lang="ts">
import { cn } from '$lib/types/utils';
import { Loader2 } from 'lucide-svelte';
let className: string | undefined = undefined;
export { className as class };
</script>
<Loader2 class={cn('h-4 w-4 animate-spin', className)} {...$$restProps} />

View File

@@ -0,0 +1,66 @@
<script lang="ts">
import { ChevronLeft, ChevronRight } from 'lucide-svelte';
import { slide } from 'svelte/transition';
import { cn } from '$lib/types/utils';
export let tags: string[] = [];
export let tagsPerPage = 5;
export let className: string | undefined = undefined;
let currentPage = 0;
let containerWidth: number;
$: totalPages = Math.ceil(tags.length / tagsPerPage);
$: startIndex = currentPage * tagsPerPage;
$: endIndex = Math.min(startIndex + tagsPerPage, tags.length);
$: visibleTags = tags.slice(startIndex, endIndex);
$: canGoBack = currentPage > 0;
$: canGoForward = currentPage < totalPages - 1;
function nextPage() {
if (canGoForward) {
currentPage++;
}
}
function prevPage() {
if (canGoBack) {
currentPage--;
}
}
</script>
<div class={cn('relative flex items-center gap-2', className)} bind:clientWidth={containerWidth}>
{#if totalPages > 1 && canGoBack}
<button
on:click={prevPage}
class="flex h-6 w-6 items-center justify-center rounded-md border bg-background hover:bg-muted"
transition:slide
>
<ChevronLeft class="h-4 w-4" />
<span class="sr-only">Previous page</span>
</button>
{/if}
<div class="flex flex-wrap gap-2">
{#each visibleTags as tag (tag)}
<a
href="/tags/{tag}"
class="inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-semibold transition-colors hover:bg-muted"
>
{tag}
</a>
{/each}
</div>
{#if totalPages > 1 && canGoForward}
<button
on:click={nextPage}
class="flex h-6 w-6 items-center justify-center rounded-md border bg-background hover:bg-muted"
transition:slide
>
<ChevronRight class="h-4 w-4" />
<span class="sr-only">Next page</span>
</button>
{/if}
</div>

View File

@@ -0,0 +1,6 @@
import Root from "./textarea.svelte";
export {
Root,
//
Root as Textarea,
};

View File

@@ -0,0 +1,29 @@
<script>
import { cn } from "$lib/types/utils.ts";
let className = undefined;
export let value = undefined;
export { className as class };
export let readonly = undefined;
</script>
<textarea
class={cn(
"border-input placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[60px] w-full rounded-md border bg-transparent px-3 py-2 text-sm shadow-lg focus-visible:outline-none focus-visible:ring-1 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:paste
on:input
{...$$restProps}
></textarea>

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,30 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"properties": false,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": false,
"sync": false
}

View File

@@ -0,0 +1,5 @@
{
"folder": "templates",
"dateFormat": "YYYY-MM-DD",
"timeFormat": "HH:mm"
}

View File

@@ -0,0 +1,8 @@
{
"types": {
"aliases": "aliases",
"cssclasses": "multitext",
"tags": "tags",
"updated": "datetime"
}
}

View File

@@ -0,0 +1,173 @@
{
"main": {
"id": "d2e57b203fabd791",
"type": "split",
"children": [
{
"id": "bede7cd0fb75a7df",
"type": "tabs",
"children": [
{
"id": "c588c8d12c5f7702",
"type": "leaf",
"state": {
"type": "markdown",
"state": {
"file": "templates/{{title}}.md",
"mode": "source",
"source": false
},
"icon": "lucide-file",
"title": "{{title}}"
}
}
]
}
],
"direction": "vertical"
},
"left": {
"id": "a69ef8c1dea71399",
"type": "split",
"children": [
{
"id": "99030135b6260693",
"type": "tabs",
"children": [
{
"id": "bbeb4ea8d01ce855",
"type": "leaf",
"state": {
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical"
},
"icon": "lucide-folder-closed",
"title": "Files"
}
},
{
"id": "afc5509c38fa5543",
"type": "leaf",
"state": {
"type": "search",
"state": {
"query": "",
"matchingCase": false,
"explainSearch": false,
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical"
},
"icon": "lucide-search",
"title": "Search"
}
},
{
"id": "53c950f1571616a8",
"type": "leaf",
"state": {
"type": "bookmarks",
"state": {},
"icon": "lucide-bookmark",
"title": "Bookmarks"
}
}
]
}
],
"direction": "horizontal",
"width": 300
},
"right": {
"id": "74142d853a5fb911",
"type": "split",
"children": [
{
"id": "065cd0d2b52977db",
"type": "tabs",
"children": [
{
"id": "398f4b2bc7fb48c1",
"type": "leaf",
"state": {
"type": "backlink",
"state": {
"file": "posts/welcome.md",
"collapseAll": false,
"extraContext": false,
"sortOrder": "alphabetical",
"showSearch": false,
"searchQuery": "",
"backlinkCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-coming-in",
"title": "Backlinks for welcome"
}
},
{
"id": "655e694ad24637c7",
"type": "leaf",
"state": {
"type": "outgoing-link",
"state": {
"file": "posts/welcome.md",
"linksCollapsed": false,
"unlinkedCollapsed": true
},
"icon": "links-going-out",
"title": "Outgoing links from welcome"
}
},
{
"id": "eba769dfb90abcb3",
"type": "leaf",
"state": {
"type": "tag",
"state": {
"sortOrder": "frequency",
"useHierarchy": true
},
"icon": "lucide-tags",
"title": "Tags"
}
},
{
"id": "2bcc1385d707df56",
"type": "leaf",
"state": {
"type": "outline",
"state": {
"file": "posts/welcome.md"
},
"icon": "lucide-list",
"title": "Outline of welcome"
}
}
]
}
],
"direction": "horizontal",
"width": 300,
"collapsed": true
},
"left-ribbon": {
"hiddenItems": {
"switcher:Open quick switcher": false,
"graph:Open graph view": false,
"canvas:Create new canvas": false,
"daily-notes:Open today's daily note": false,
"templates:Insert template": false,
"command-palette:Open command palette": false
}
},
"active": "c588c8d12c5f7702",
"lastOpenFiles": [
"posts/welcome.md",
"templates/{{title}}.md",
"posts/SkeletonUI.md",
"posts/getting-started.md",
"posts/extract_wisdom.md"
]
}

View File

@@ -0,0 +1,226 @@
---
title: SkeletonUI
tags:
- svelte
- styling
- skeletonui
date: 2023-01-17
---
SkeletonUI is a comprehensive UI toolkit that integrates seamlessly with SvelteKit and Tailwind CSS, enabling developers to build adaptive and accessible web interfaces efficiently.
SkeletonUI offers a comprehensive suite of components to enhance your Svelte applications. Below is a categorized list of these components, presented in Svelte syntax:
```svelte
<!-- Layout Components -->
<AppShell />
<AppBar />
<Sidebar />
<Footer />
<!-- Navigation Components -->
<NavMenu />
<Breadcrumbs />
<Tabs />
<Pagination />
<!-- Form Components -->
<Button />
<Input />
<Select />
<Textarea />
<Checkbox />
<Radio />
<Switch />
<Slider />
<FileUpload />
<!-- Data Display Components -->
<Card />
<Avatar />
<Badge />
<Chip />
<Divider />
<Table />
<List />
<Accordion />
<ProgressBar />
<Rating />
<Tag />
<!-- Feedback Components -->
<Alert />
<Modal />
<Toast />
<Popover />
<Tooltip />
<!-- Media Components -->
<Image />
<Video />
<Icon />
<!-- Utility Components -->
<Spinner />
<SkeletonLoader />
<Placeholder />
```
For detailed information on each component, including their properties and usage examples, please refer to the official SkeletonUI documentation.
___
Below is an expanded cheat sheet to assist you in leveraging SkeletonUI within your SvelteKit projects.
**1\. Installation**
To set up SkeletonUI in a new SvelteKit project, follow these steps:
- **Create a new SvelteKit project**:
```bash
npx sv create my-skeleton-app
cd my-skeleton-app
npm install
```
- **Install SkeletonUI packages**:
```bash
npm install -D @skeletonlabs/skeleton@next @skeletonlabs/skeleton-svelte@next
```
- **Configure Tailwind CSS**:
In your `tailwind.config.js` file, add the following:
```javascript
import { skeleton, contentPath } from '@skeletonlabs/skeleton/plugin';
import * as themes from '@skeletonlabs/skeleton/themes';
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
contentPath(import.meta.url, 'svelte')
],
theme: {
extend: {},
},
plugins: [
skeleton({
themes: [themes.cerberus, themes.rose]
})
]
};
```
- **Set the active theme**:
In your `src/app.html`, set the `data-theme` attribute on the `<body>` tag:
```html
<body data-theme="cerberus">
<!-- Your content -->
</body>
```
For detailed installation instructions, refer to the official SkeletonUI documentation.
**2\. Components**
SkeletonUI offers a variety of pre-designed components to accelerate your development process. Here's how to use some of them:
- **Button**:
```svelte
<script>
import { Button } from '@skeletonlabs/skeleton-svelte';
</script>
<Button on:click={handleClick}>Click Me</Button>
```
- **Card**:
```svelte
<script>
import { Card } from '@skeletonlabs/skeleton-svelte';
</script>
<Card>
<h2>Card Title</h2>
<p>Card content goes here.</p>
</Card>
```
- **Form Input**:
```svelte
<script>
import { Input } from '@skeletonlabs/skeleton-svelte';
let inputValue = '';
</script>
<Input bind:value={inputValue} placeholder="Enter text" />
```
For a comprehensive list of components and their usage, consult the SkeletonUI components documentation.
**3\. Theming**
SkeletonUI's theming system allows for extensive customization:
- **Applying a Theme**:
Set the desired theme in your `tailwind.config.js` and `app.html` as shown in the installation steps above.
- **Switching Themes Dynamically**:
To enable dynamic theme switching, you can modify the `data-theme` attribute programmatically:
```svelte
<script>
function switchTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
</script>
<button on:click={() => switchTheme('rose')}>Switch to Rose Theme</button>
```
- **Customizing Themes**:
You can create custom themes by defining your own color palettes and settings in the `tailwind.config.js` file.
For more information on theming, refer to the SkeletonUI theming guide.
**4\. Utilities**
SkeletonUI provides several utility functions and actions to enhance your SvelteKit application:
- **Table of Contents**:
Automatically generate a table of contents based on page headings:
```svelte
<script>
import { TableOfContents, tocCrawler } from '@skeletonlabs/skeleton-svelte';
</script>
<div use:tocCrawler>
<TableOfContents />
<!-- Your content with headings -->
</div>
```
- **Transitions and Animations**:
Utilize built-in transitions for smooth animations:
```svelte
<script>
import { fade } from '@skeletonlabs/skeleton-svelte';
let visible = true;
</script>
{#if visible}
<div transition:fade>
Fading content
</div>
{/if}
```
For a full list of utilities and their usage, explore the SkeletonUI utilities documentation.
This cheat sheet provides a foundational overview to help you start integrating SkeletonUI into your SvelteKit projects. For more detailed information and advanced features, please refer to the official SkeletonUI documentation.
https://www.skeleton.dev/docs/introduction

View File

@@ -0,0 +1,135 @@
---
title: Using Markdown in Svelte
description: Learn how to use your markdown documents in Svelte Applications!
date: 2023-12-22
tags: [markdown, svelte,web-dev, documentation]
---
[Mdsvex](https://mdsvex.pngwn.io/docs#install-it)
Here are some examples illustrating how to use Mdsvex in a Svelte application:
**Example 1**: Basic Markdown with Svelte Component
Create a file named example.svx:
markdown
```markdown
---
title: "Interactive Markdown Example"
---
<script>
import Counter from '../components/Counter.svelte';
</script>
# {title}
This is an example of combining Markdown with a Svelte component:
<Counter />
```
In this example:
- The frontmatter (--- sections) defines variables like title.
- A Svelte component Counter is imported and used inside the Markdown.
**Example 2**: Custom Layouts with Mdsvex
Assuming you have a layout component at src/lib/layouts/BlogLayout.svelte:
svelte
```text
<!-- BlogLayout.svelte -->
<script>
export let title;
</script>
<div class="blog-post">
<h1>{title}</h1>
<slot />
</div>
```
Now, to use this layout in your Markdown:
**markdown**
```markdown
---
title: "My Favorite Layout"
layout: "../lib/layouts/BlogLayout.svelte"
---
## Markdown with Custom Layout
This Markdown file will be wrapped by the `BlogLayout`.
```
**Example 3:** Using Frontmatter Variables in Markdown
**markdown**
```markdown
---
author: "John Doe"
date: "2024-11-15"
---
# Blog Post
By {author} on {date}
Here's some markdown content. You can reference frontmatter values directly in the body.
```
**Example 4**: Interactive Elements in Markdown
markdown
```markdown
---
title: "Interactive Chart"
---
<script>
import { Chart } from '../components/Chart.svelte';
</script>
# {title}
Below is an interactive chart:
<Chart />
```
## Setting Up Mdsvex
To make these work, you need to configure your SvelteKit project:
1. Install Mdsvex:
```bash
npm install -D mdsvex
```
2. Configure SvelteKit:
In your svelte.config.js:
```javascript
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { mdsvex } from 'mdsvex';
/** @type {import('mdsvex').MdsvexOptions} */
const mdsvexOptions = {
extensions: ['.svx'],
};
/** @type {import('@sveltejs/kit').Config} */
const config = {
extensions: ['.svelte', '.svx'],
preprocess: [
vitePreprocess(),
mdsvex(mdsvexOptions),
],
kit: {
adapter: adapter()
}
};
export default config;
```
3. Create a Route for Markdown Files:
Place your .svx files in the src/routes directory or subdirectories, and SvelteKit will automatically handle them as routes.
These examples show how you can integrate Mdsvex into your Svelte application, combining the simplicity of Markdown with the interactivity of Svelte components. Remember, any Svelte component you want to use within Markdown must be exported from a .svelte file and imported in your .svx file.

View File

@@ -0,0 +1,66 @@
---
title: Extract Wisdom
date: 2024-01-01
description:
tags: [patterns, fabric]
---
# IDENTITY and PURPOSE
You extract surprising, insightful, and interesting information from text content. You are interested in insights related to the purpose and meaning of life, human flourishing, the role of technology in the future of humanity, artificial intelligence and its affect on humans, memes, learning, reading, books, continuous improvement, and similar topics.
Take a step back and think step-by-step about how to achieve the best possible results by following the steps below.
# STEPS
- Extract a summary of the content in 25 words, including who is presenting and the content being discussed into a section called SUMMARY.
- Extract 20 to 50 of the most surprising, insightful, and/or interesting ideas from the input in a section called IDEAS:. If there are less than 50 then collect all of them. Make sure you extract at least 20.
- Extract 10 to 20 of the best insights from the input and from a combination of the raw input and the IDEAS above into a section called INSIGHTS. These INSIGHTS should be fewer, more refined, more insightful, and more abstracted versions of the best ideas in the content.
- Extract 15 to 30 of the most surprising, insightful, and/or interesting quotes from the input into a section called QUOTES:. Use the exact quote text from the input.
- Extract 15 to 30 of the most practical and useful personal habits of the speakers, or mentioned by the speakers, in the content into a section called HABITS. Examples include but aren't limited to: sleep schedule, reading habits, things they always do, things they always avoid, productivity tips, diet, exercise, etc.
- Extract 15 to 30 of the most surprising, insightful, and/or interesting valid facts about the greater world that were mentioned in the content into a section called FACTS:.
- Extract all mentions of writing, art, tools, projects and other sources of inspiration mentioned by the speakers into a section called REFERENCES. This should include any and all references to something that the speaker mentioned.
- Extract the most potent takeaway and recommendation into a section called ONE-SENTENCE TAKEAWAY. This should be a 15-word sentence that captures the most important essence of the content.
- Extract the 15 to 30 of the most surprising, insightful, and/or interesting recommendations that can be collected from the content into a section called RECOMMENDATIONS.
# OUTPUT INSTRUCTIONS
- Only output Markdown.
- Write the IDEAS bullets as exactly 15 words.
- Write the RECOMMENDATIONS bullets as exactly 15 words.
- Write the HABITS bullets as exactly 15 words.
- Write the FACTS bullets as exactly 15 words.
- Write the INSIGHTS bullets as exactly 15 words.
- Extract at least 25 IDEAS from the content.
- Extract at least 10 INSIGHTS from the content.
- Extract at least 20 items for the other output sections.
- Do not give warnings or notes; only output the requested sections.
- You use bulleted lists for output, not numbered lists.
- Do not repeat ideas, quotes, facts, or resources.
- Do not start items with the same opening words.
- Ensure you follow ALL these instructions when creating your output.
# INPUT
INPUT:

View File

@@ -0,0 +1,60 @@
---
title: Getting Started with SvelteKit
date: 2024-11-01
---
# Getting Started with SvelteKit
SvelteKit is a framework for building web applications of all sizes, with a beautiful development experience and flexible filesystem-based routing.
## Why SvelteKit?
- Zero-config setup
- Filesystem-based routing
- Server-side rendering
- Hot module replacement
```bash
npx sv create my-app
cd my-app
npm install
```
**Install SkeletonUI**
```bash
npm i -D @skeletonlabs/skeleton@next @skeletonlabs/skeleton-svelte@next
```
**Configure Tailwind CSS**
```tailwind.config
import type { Config } from 'tailwindcss';
import { skeleton, contentPath } from '@skeletonlabs/skeleton/plugin';
import * as themes from '@skeletonlabs/skeleton/themes';
export default {
content: [
'./src/**/*.{html,js,svelte,ts}',
contentPath(import.meta.url, 'svelte')
],
theme: {
extend: {},
},
plugins: [
skeleton({
// NOTE: each theme included will be added to your CSS bundle
themes: [ themes.cerberus, themes.rose ]
})
]
} satisfies Config
```
**Start the dev server**
```bash
npm run dev
```
Read more at https://svelte.dev, https://next.skeleton.dev/docs/get-started/installation/sveltekit, and https://www.skeleton.dev/docs/introduction

View File

@@ -0,0 +1,18 @@
---
title: Welcome to Your Blog
description: First post on my new SvelteKit blog
date: 2024-01-17
tags: [welcome, blog]
---
This is the first post of your new blog, powered by [SvelteKit](/posts/getting-started), [Obsidian](/obsidian), and [Fabric](/about). I'm excited to share this project with you, and I hope you find it useful for your own writing and experiences.
This part of the application is edited in <a href="http://localhost:5173/obsidian" name="Obsidian">Obsidian</a>.
## What to Expect
- Updates on Incorporating Fabric into your workflow
- How to use Obsidian to manage you notes and workflows
- How to use Fabric and Obsidian to write and publish
- More ways to use Obsidian and Fabric together!
Stay tuned for more content!

View File

@@ -0,0 +1,7 @@
---
title: Your Title Here
date:
description: Post description
updated:
---
{{Content}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -0,0 +1,50 @@
<script lang="ts">
import { formatDistance } from 'date-fns';
import TagList from '$components/ui/tag-list/TagList.svelte';
/** @type {string} */
export let title;
/** @type {string} */
export let date;
/** @type {string} */
export let description;
/** @type {string} */
export let tags = [];
/** @type {string}*/
export let updated;
/** @type {string}**/
export let reference;
</script>
<article class="prose prose-slate mx-auto max-w-3xl dark:prose-invert py-12">
<header class="mb-8 not-prose">
{#if title}
<h1 class="mb-2 text-4xl font-bold">{title}</h1>
{/if}
{#if description}
<p class="mb-4 text-lg text-muted-foreground">{description}</p>
{/if}
{#if date}
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
<time datetime={date}>{formatDistance(new Date(date), new Date(), { addSuffix: true })}</time>
{#if tags?.length}
<span class="text-xs"></span>
<TagList {tags} className="flex-1" />
{/if}
{#if updated}
<span class="text-xs"></span>
<time datetime={updated}>Updated {formatDistance(new Date(updated), new Date(), { addSuffix: true })}</time>
{/if}
{#if reference}
<span class="text-xs"></span>
<a href={reference}>Reference</a>
{/if}
</div>
{/if}
</header>
<div class="mt-8">
<slot />
</div>
</article>

View File

@@ -0,0 +1,24 @@
import { compile } from 'mdsvex';
export interface Post {
slug: string;
title: string;
date: string;
content?: any;
}
const modules = import.meta.glob('../content/posts/*.md' + '../../routes/**/*.md', { eager: true });
export const posts: Post[] = Object.entries(modules).map(([path, module]: [string, any]) => {
const slug = path.split('/').pop()?.replace('.md', '') || '';
return {
slug,
title: module.metadata?.title || slug,
date: module.metadata?.date || new Date().toISOString().split('T')[0],
content: module.default
};
});
export async function getPost(slug: string) {
return posts.find(p => p.slug === slug) || null;
}

View File

@@ -0,0 +1,38 @@
import { YoutubeTranscript } from 'youtube-transcript';
export interface TranscriptResponse {
transcript: string;
title: string;
}
export async function getTranscript(url: string): Promise<TranscriptResponse> {
try {
const videoId = extractVideoId(url);
if (!videoId) {
throw new Error('Invalid YouTube URL');
}
const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId);
const transcript = transcriptItems
.map(item => item.text)
.join(' ');
const transcriptTitle = transcriptItems
.map(item => item.text)
.join('');
// TODO: Add title fetching
return {
transcript,
title: videoId // Just returning the video ID as title
};
} catch (error) {
console.error('Transcript fetch error:', error);
throw new Error('Failed to fetch transcript');
}
}
function extractVideoId(url: string): string | null {
const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
return match ? match[1] : null;
}

129
web/src/lib/store/chat.ts Normal file
View File

@@ -0,0 +1,129 @@
import { writable, get } from 'svelte/store';
import type { ChatRequest, StreamResponse, ChatState, Message } from '$lib/types/interfaces/chat-interface';
import { chatApi } from '$lib/types/chat/chat';
import { modelConfig } from './model-config';
import { systemPrompt } from '$lib/types/chat/patterns';
export const currentSession = writable<string | null>(null);
export const chatState = writable<ChatState>({
messages: [],
isStreaming: false
});
export const setSession = (sessionName: string | null) => {
currentSession.set(sessionName);
if (!sessionName) {
clearMessages();
}
};
export const clearMessages = () => {
chatState.update(state => ({ ...state, messages: [] }));
};
export const revertLastMessage = () => {
chatState.update(state => ({
...state,
messages: state.messages.slice(0, -1)
}));
};
export async function sendMessage(userInput: string, systemPromptText?: string) {
// Guard against streaming state
const currentState = get(chatState);
if (currentState.isStreaming) {
console.log('Message submission blocked - already streaming');
return;
}
// Update chat state
chatState.update((state) => ({
...state,
messages: [...state.messages, { role: 'user', content: userInput }],
isStreaming: true
}));
try {
const config = get(modelConfig);
const sessionName = get(currentSession);
const request: ChatRequest = {
prompts: [{
userInput: userInput,
systemPrompt: systemPromptText || get(systemPrompt),
model: Array.isArray(config.model) ? config.model.join(',') : config.model,
vendor: '',
patternName: '',
}],
temperature: config.temperature,
top_p: config.top_p,
frequency_penalty: 0,
presence_penalty: 0
};
const stream = await chatApi.streamChat(request);
const reader = stream.getReader();
let assistantMessage: Message = {
role: 'assistant',
content: ''
};
let isCancelled = false;
while (!isCancelled) {
const { done, value } = await reader.read();
if (done) break;
// Check if we're still streaming before processing
const currentState = get(chatState);
if (!currentState.isStreaming) {
isCancelled = true;
break;
}
const response = value as StreamResponse;
switch (response.type) {
case 'content':
assistantMessage.content += response.content += `\n`;
chatState.update(state => ({
...state,
messages: [
...state.messages.slice(0, -1),
{...assistantMessage}
]
}));
break;
case 'error':
throw new Error(response.content);
case 'complete':
break;
}
}
if (isCancelled) {
throw new Error('Stream cancelled');
}
} catch (error) {
console.error('Chat error:', error);
// Only add error message if still streaming
const currentState = get(chatState);
if (currentState.isStreaming) {
chatState.update(state => ({
...state,
messages: [...state.messages, {
role: 'assistant',
content: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`
}]
}));
}
} finally {
chatState.update(state => ({
...state,
isStreaming: false
}));
}
}
export type { StreamResponse };

View File

@@ -0,0 +1,58 @@
import { writable, derived } from 'svelte/store';
import { modelsApi } from '$lib/types/chat/models';
import { configApi } from '$lib/types/chat/config';
import type { VendorModel } from '$lib/types/interfaces/model-interface';
import type { ModelConfig } from '$lib/types/interfaces/model-interface';
export const modelConfig = writable<ModelConfig>({
model: [],
temperature: 0.7,
maxLength: 2000,
top_p: 0.9,
frequency: 1
});
export const availableModels = writable<VendorModel[]>([]);
// Initialize available models
export async function loadAvailableModels() {
try {
const models = await modelsApi.getAvailable();
console.log('Load models:', models);
const uniqueModels = [...new Map(models.map(model => [model.name, model])).values()];
availableModels.set(uniqueModels);
} catch (error) {
console.error('Client failed to load available models:', error);
availableModels.set([]);
}
}
// Initialize config
export async function initializeConfig() {
try {
const config = await configApi.get();
const safeConfig: ModelConfig = {
...config,
model: Array.isArray(config.model) ? config.model :
typeof config.model === 'string' ? (config.model as string).split(',') : []
};
modelConfig.set(safeConfig);
} catch (error) {
console.error('Failed to load config:', error);
}
}
/* modelConfig.subscribe(async (config) => {
try {
const configRecord: Record<string, string> = {
model: config.model.toString(),
temperature: config.temperature.toString(),
maxLength: config.maxLength.toString(),
top_p: config.top_p.toString(),
frequency: config.frequency.toString()
};
// await configApi.update(configRecord);
} catch (error) {
console.error('Failed to update config:', error);
}
}); */

View File

@@ -0,0 +1,25 @@
import { writable } from 'svelte/store';
function createThemeStore() {
const { subscribe, set, update } = writable<'light' | 'dark'>('dark');
return {
subscribe,
toggleTheme: () => update(theme => {
const newTheme = theme === 'light' ? 'dark' : 'light';
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', newTheme === 'dark');
}
return newTheme;
}),
setTheme: (theme: 'light' | 'dark') => {
set(theme);
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', theme === 'dark');
}
}
};
}
export const theme = createThemeStore();
export const toggleTheme = theme.toggleTheme;

View File

@@ -0,0 +1,131 @@
// import type { ModelConfig } from '$lib/types/model-types';
import type { StorageEntity } from '$lib/types/interfaces/storage-interface';
interface APIErrorResponse {
error: string;
}
interface APIResponse<T> {
data?: T;
error?: string;
}
// Define and export the base api object
export const api = {
async fetch<T>(endpoint: string, options: RequestInit = {}): Promise<APIResponse<T>> {
try {
const response = await fetch(`/api${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
...options.headers,
},
});
if (!response.ok) {
const errorData = await response.json() as APIErrorResponse;
return { error: errorData.error || response.statusText };
}
const data = await response.json();
return { data };
} catch (error) {
return { error: error instanceof Error ? error.message : 'Unknown error occurred' };
}
},
get: <T>(fetch: typeof window.fetch, endpoint: string) => api.fetch<T>(endpoint),
post: <T>(fetch: typeof window.fetch, endpoint: string, data: unknown) =>
api.fetch<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data),
}),
put: <T>(fetch: typeof window.fetch, endpoint: string, data?: unknown) =>
api.fetch<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
}),
delete: <T>(fetch: typeof window.fetch, endpoint: string) =>
api.fetch<T>(endpoint, {
method: 'DELETE',
}),
stream: async function*(fetch: typeof window.fetch, endpoint: string, data: unknown): AsyncGenerator<string> {
const response = await fetch(`/api${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('Response body is null');
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield decoder.decode(value);
}
}
};
export function createStorageAPI<T extends StorageEntity>(entityType: string) {
return {
// Get a specific entity by name
async get(name: string): Promise<T> {
const response = await api.fetch<T>(`/api${entityType}/${name}`);
if (response.error) throw new Error(response.error);
return response.data as T;
},
// Get all entity names
async getNames(): Promise<string[]> {
const response = await api.fetch<string[]>(`/api${entityType}/names`);
if (response.error) throw new Error(response.error);
return response.data || [];
},
// Delete an entity
async delete(name: string): Promise<void> {
const response = await api.fetch(`/api${entityType}/${name}`, {
method: 'DELETE',
});
if (response.error) throw new Error(response.error);
},
// Check if an entity exists
async exists(name: string): Promise<boolean> {
const response = await api.fetch<boolean>(`/api${entityType}/exists/${name}`);
if (response.error) throw new Error(response.error);
return response.data || false;
},
// Rename an entity
async rename(oldName: string, newName: string): Promise<void> {
const response = await api.fetch(`/api${entityType}/rename/${oldName}/${newName}`, {
method: 'PUT',
});
if (response.error) throw new Error(response.error);
},
// Save an entity
async save(name: string, content: string | object): Promise<void> {
const body = typeof content === 'string' ? content : JSON.stringify(content);
const response = await api.fetch(`/api${entityType}/${name}`, {
method: 'POST',
body,
});
if (response.error) throw new Error(response.error);
},
};
}

View File

@@ -0,0 +1,59 @@
// import { api } from '$lib/types/base';
import type { ChatRequest, StreamResponse } from '$lib/types/interfaces/chat-interface';
// Create a chat API client
export const chatApi = {
async streamChat(request: ChatRequest): Promise<ReadableStream<StreamResponse>> {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('Response body is null');
let buffer = '';
return new ReadableStream({
async start(controller) {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += new TextDecoder().decode(value);
const messages = buffer
.split('\n\n')
.filter(msg => msg.startsWith('data: '));
// Process complete messages
if (messages.length > 1) {
// Keep the last (potentially incomplete) chunk in the buffer
buffer = messages.pop() || '';
for (const msg of messages) {
controller.enqueue(JSON.parse(msg.slice(6)) as StreamResponse);
}
}
}
} catch (error) {
controller.error(error);
} finally {
// Process any remaining complete messages in the buffer
if (buffer.startsWith('data: ')) {
controller.enqueue(JSON.parse(buffer.slice(6)) as StreamResponse);
}
controller.close();
reader.releaseLock();
}
},
});
},
};

View File

@@ -0,0 +1,25 @@
import type { ModelConfig } from '$lib/types/interfaces/model-interface';
import { api } from './base';
export const configApi = {
async get(): Promise<ModelConfig> {
const response = await api.fetch<ModelConfig>('/config');
if (response.error) throw new Error(response.error);
return response.data || {
model: [],
temperature: 0.7,
top_p: 0.9,
frequency: 1,
maxLength: 2000
};
},
/* async update(config: Record<string, string>) {
const response = await api.fetch('config/update', {
method: 'POST',
body: JSON.stringify(config),
});
if (response.error) throw new Error(response.error);
return response;
} */
};

View File

@@ -0,0 +1,11 @@
import { api } from './base';
import type { Context } from '$lib/types/interfaces/context-interface';
export const contextAPI = {
async getAvailable(): Promise<Context[]> {
const response = await api.fetch<Context[]>('/contexts/names');
return response.data || [];
}
}
// TODO: add context element somewhere in the UI

View File

@@ -0,0 +1,40 @@
import { api } from './base';
import type { VendorModel, ModelsResponse } from '$lib/types/interfaces/model-interface';
export const modelsApi = {
async getAvailable(): Promise<VendorModel[]> {
const response = await api.fetch<ModelsResponse>('/models/names');
console.log("Client raw API response:", response)
if (response.error) {
console.error("Client couldn't fetch models:", response.error);
throw new Error(response.error);
}
if (!response.data) {
console.error('No data received from models API');
return [];
}
const vendorsData = response.data.vendors || {};
const result: VendorModel[] = [];
for (const [vendor, models] of Object.entries(vendorsData)) {
for (const model of models) {
result.push({
name: model,
vendor: vendor
});
}
}
console.log('Available models:', result);
return result;
},
/* async getNames(): Promise<string[]> {
const response = await api.fetch<ModelsResponse>('/models/names');
if (response.error) throw new Error(response.error);
return response.data?.models || [];
} */
};

View File

@@ -0,0 +1,68 @@
import { createStorageAPI } from './base';
import type { Pattern } from '$lib/types/interfaces/pattern-interface';
import { get, writable } from 'svelte/store';
export const patterns = writable<Pattern[]>([]);
export const systemPrompt = writable<string>('');
export const setSystemPrompt = (prompt: string) => {
console.log('Setting system prompt:', prompt);
systemPrompt.set(prompt);
console.log('Current system prompt:', get(systemPrompt));
};
export const patternAPI = {
...createStorageAPI<Pattern>('patterns'),
async loadPatterns() {
try {
const response = await fetch(`/api/patterns/names`);
const data = await response.json();
console.log("Load Patterns:", data);
// Create an array of promises to fetch all pattern contents
const patternsPromises = data.map(async (pattern: string) => {
try {
const patternResponse = await fetch(`/api/patterns/${pattern}`);
const patternData = await patternResponse.json();
return {
Name: pattern,
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
Pattern: patternData.Pattern
};
} catch (error) {
console.error(`Failed to load pattern ${pattern}:`, error);
return {
Name: pattern,
Description: pattern.charAt(0).toUpperCase() + pattern.slice(1),
Pattern: ""
};
}
});
// Wait for all pattern contents to be fetched
const loadedPatterns = await Promise.all(patternsPromises);
console.log("Patterns with content:", loadedPatterns);
patterns.set(loadedPatterns);
return loadedPatterns;
} catch (error) {
console.error('Failed to load patterns:', error);
patterns.set([]);
return [];
}
},
selectPattern(patternName: string) {
const allPatterns = get(patterns);
console.log('Selecting pattern:', patternName);
const selectedPattern = allPatterns.find(p => p.Name === patternName);
if (selectedPattern) {
console.log('Found pattern content:', selectedPattern.Pattern);
setSystemPrompt(selectedPattern.Pattern.trim());
} else {
console.log('No pattern found for name:', patternName);
setSystemPrompt('');
}
console.log('System prompt store value after setting:', get(systemPrompt));
}
};

View File

@@ -0,0 +1,4 @@
import { createStorageAPI } from './base';
import type { Session } from '$lib/types/interfaces/session-interface';
export const sessionAPI = createStorageAPI<Session>('sessions');

View File

@@ -0,0 +1,31 @@
export interface ChatRequest {
prompts: {
userInput: string;
systemPrompt: string;
model: string;
vendor: string;
// contextName: string;
patternName: string;
// sessionName: string;
}[];
temperature: number;
top_p: number;
frequency_penalty: number;
presence_penalty: number;
}
export interface Message {
role: 'system' | 'user' | 'assistant';
content: string;
}
export interface ChatState {
messages: Message[];
isStreaming: boolean;
}
export interface StreamResponse {
type: 'content' | 'error' | 'complete';
format: 'markdown' | 'mermaid' | 'plain';
content: string;
}

View File

@@ -0,0 +1,4 @@
export interface Context {
name: string;
content: string;
}

View File

@@ -0,0 +1,24 @@
export interface VendorModel {
name: string;
vendor: string;
}
export interface ModelsResponse {
models: string[];
vendors: Record<string, string[]>;
}
export interface ModelConfig {
model: string[];
temperature: number;
top_p: number;
maxLength: number;
frequency: number;
presence: number;
}
/* export type ModelSelect = {
vendor: string;
name: string;
}*/

View File

@@ -0,0 +1,5 @@
export interface Pattern {
Name: string;
Description: string;
Pattern: string; // | object
}

View File

@@ -0,0 +1,6 @@
import type { Message } from "$lib/types/interfaces/chat-interface";
export interface Session {
name: string;
content: Message[];
}

View File

@@ -0,0 +1,5 @@
export interface StorageEntity {
Name: string;
Description: string;
Pattern: string | object;
}

View File

@@ -0,0 +1,10 @@
export interface Particle {
x: number;
y: number;
baseY: number;
speed: number;
angle: number;
size: number;
color: string;
velocityX: number;
}

View File

@@ -0,0 +1,10 @@
export interface Post {
title: string;
date: string;
description: string;
tags: string[];
}
export interface PostMetadata extends Post {
slug: string;
}

View File

@@ -0,0 +1,56 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { cubicOut } from 'svelte/easing';
import type { TransitionConfig } from 'svelte/transition';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
type FlyAndScaleParams = {
y?: number;
x?: number;
start?: number;
duration?: number;
};
export const flyAndScale = (
node: Element,
params: FlyAndScaleParams = { y: -8, x: 0, start: 0.95, duration: 150 }
): TransitionConfig => {
const style = getComputedStyle(node);
const transform = style.transform === 'none' ? '' : style.transform;
const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => {
const [minA, maxA] = scaleA;
const [minB, maxB] = scaleB;
const percentage = (valueA - minA) / (maxA - minA);
const valueB = percentage * (maxB - minB) + minB;
return valueB;
};
const styleToString = (style: Record<string, number | string | undefined>): string => {
return Object.keys(style).reduce((str, key) => {
if (style[key] === undefined) return str;
return str + `${key}:${style[key]};`;
}, '');
};
return {
duration: params.duration ?? 200,
delay: 0,
css: (t) => {
const y = scaleConversion(t, [0, 1], [params.y ?? 5, 0]);
const x = scaleConversion(t, [0, 1], [params.x ?? 0, 0]);
const scale = scaleConversion(t, [0, 1], [params.start ?? 0.95, 1]);
return styleToString({
transform: `${transform} translate3d(${x}px, ${y}px, 0) scale(${scale})`,
opacity: t
});
},
easing: cubicOut
};
};

View File

@@ -0,0 +1,4 @@
export function validateYouTubeUrl(url:string) {
const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/;
return pattern.test(url);
}

View File

@@ -0,0 +1,12 @@
export function createParticleGradient(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
size: number,
color: string
): CanvasGradient {
const gradient = ctx.createRadialGradient(x, y, 0, x, y, size);
gradient.addColorStop(0, color);
gradient.addColorStop(1, 'transparent');
return gradient;
}

View File

@@ -0,0 +1,4 @@
export function generateGradientColor(y: number, height: number): string {
const hue = (y / height) * 60 + 200; // Blue to purple range
return `hsla(${hue}, 70%, 60%, 0.8)`;
}

View File

@@ -0,0 +1,49 @@
<script>
import '../app.postcss';
import { AppShell, Toast } from '@skeletonlabs/skeleton';
import Footer from './Footer.svelte';
import Header from './Header.svelte';
import { initializeStores } from '@skeletonlabs/skeleton';
import { getToastStore } from '@skeletonlabs/skeleton';
import { onMount } from 'svelte';
// Initialize stores
initializeStores();
const toastStore = getToastStore();
onMount(() => {
toastStore.trigger({
message: "👋 Welcome to the site! I'm still working on it... ",
background: 'variant-filled-primary',
timeout: 15000,
hoverable: true
});
});
</script>
<Toast position="t" />
<AppShell class="relative">
<div class="fixed inset-0 bg-gradient-to-br from-primary-500/20 via-tertiary-500/20 to-secondary-500/20 -z-10"></div>
<svelte:fragment slot="header">
<Header />
<div class="bg-gradient-to-b variant-gradient-primary-tertiary opacity-20 h-2 py-4">
</div>
</svelte:fragment>
<main class="mx-auto p-4">
<slot />
</main>
<svelte:fragment slot="footer">
<Footer />
</svelte:fragment>
</AppShell>
<style>
main {
/*height: calc( 100vh - 2rem ); /* Adjust based on header/footer height */
padding: 2rem;
box-sizing: border-box;
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,3 @@
import '../app.postcss';
export const prerender = true;

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import Terminal from './Terminal.svelte';
import Fabric from './Fabric.svelte';
</script>
<div class="">
<Terminal />
<div class="absolute inset-0 -z-10 overflow-hidden h-96">
<Fabric />
</div>
</div>

1
web/src/routes/+page.ts Normal file
View File

@@ -0,0 +1 @@
export const prerender = true;

View File

@@ -0,0 +1,139 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { ParticleSystem } from './ParticleSystem';
import { createParticleGradient } from '$lib/utils/canvas';
export let particleCount = 100;
export let particleSize = 3;
export let particleSpeed = 0.5;
export let connectionDistance = 100;
let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D;
let animationFrame: number;
let particleSystem: ParticleSystem;
let isMouseOver = false;
let browser = false;
function handleMouseMove(event: MouseEvent) {
if (!isMouseOver) return;
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
particleSystem?.updateMousePosition(x, y);
}
function handleMouseEnter() {
isMouseOver = true;
}
function handleMouseLeave() {
isMouseOver = false;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
particleSystem?.updateMousePosition(centerX, centerY);
}
function handleResize() {
if (!browser) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
particleSystem?.updateDimensions(canvas.width, canvas.height);
}
function drawConnections() {
const particles = particleSystem.getParticles();
ctx.lineWidth = 0.5;
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < connectionDistance) {
const alpha = 1 - (distance / connectionDistance);
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.15})`; // Slightly reduced opacity
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
}
function drawParticles() {
const particles = particleSystem.getParticles();
particles.forEach(particle => {
const gradient = createParticleGradient(
ctx,
particle.x,
particle.y,
particle.size,
particle.color
);
ctx.beginPath();
ctx.fillStyle = gradient;
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
ctx.fill();
});
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
particleSystem.update();
drawConnections();
drawParticles();
animationFrame = requestAnimationFrame(animate);
}
onMount(() => {
browser = true;
if (!browser) return;
ctx = canvas.getContext('2d')!;
handleResize();
particleSystem = new ParticleSystem(
particleCount,
particleSize,
particleSpeed,
canvas.width,
canvas.height
);
window.addEventListener('resize', handleResize);
animationFrame = requestAnimationFrame(animate);
});
onDestroy(() => {
if (!browser) return;
window.removeEventListener('resize', handleResize);
cancelAnimationFrame(animationFrame);
});
</script>
<canvas
bind:this={canvas}
on:mousemove={handleMouseMove}
on:mouseenter={handleMouseEnter}
on:mouseleave={handleMouseLeave}
class="particle-wave"
/>
<style>
.particle-wave {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
}

View File

@@ -0,0 +1,16 @@
<script>
const year = new Date().getFullYear();
import BuyMeCoffee from "$lib/components/ui/buymeacoffee/BuyMeCoffee.svelte";
</script>
<footer class="border-t bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div class="container flex h-14 items-center justify-between px-4">
<p class="text-sm text-muted-foreground">
Built in {year} by @johnconnor-sec
</p>
<nav class="flex items-center gap-4 ">
<BuyMeCoffee url="https://www.buymeacoffee.com/johnconnor.sec" />
</nav>
</div>
</footer>

View File

@@ -0,0 +1,125 @@
<script lang="ts">
import { page } from '$app/stores';
import { Sun, Moon, Menu, X, Github } from 'lucide-svelte';
import { Avatar } from '@skeletonlabs/skeleton';
import { fade } from 'svelte/transition';
import { theme, toggleTheme } from '$lib/store/theme';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import Fabric from './Fabric.svelte'
let isMenuOpen = false;
function goToGithub() {
window.open('https://github.com/danielmiessler/fabric', '_blank');
}
function toggleMenu() {
isMenuOpen = !isMenuOpen;
}
$: currentPath = $page.url.pathname;
$: isDarkMode = $theme === 'dark';
const navItems = [
{ href: '/', label: 'Home' },
{ href: '/posts', label: 'Posts' },
{ href: '/tags', label: 'Tags' },
{ href: '/chat', label: 'Chat' },
//{ href: '/obsidian', label: 'Obsidian' },
{ href: '/contact', label: 'Contact' },
{ href: '/about', label: 'About' },
];
onMount(() => {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
theme.setTheme(prefersDark ? 'dark' : 'light');
});
</script>
<header class="fixed top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div class="container flex h-16 items-center justify-between px-4">
<div class="flex items-center gap-4">
<Avatar
src="/src/lib/images/fabric-logo.png"
width="w-10"
rounded="rounded-full"
class="border-2 border-primary/20"
/>
<a href="/" class="flex items-center">
<span class="text-lg font-semibold">Fabric</span>
</a>
</div>
<!-- Desktop Navigation -->
<nav class="hidden flex-1 px-8 md:flex">
<ul class="flex items-center space-x-8">
{#each navItems as { href, label }}
<li>
<a
{href}
class="text-sm font-medium transition-colors hover:text-primary {currentPath === href ? 'text-primary' : 'text-foreground/60'}"
>
{label}
</a>
</li>
{/each}
</ul>
</nav>
<div class="flex items-center gap-2">
<button
on:click={goToGithub}
class="inline-flex h-9 w-9 items-center justify-center rounded-md border bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
aria-label="GitHub"
>
<Github class="h-4 w-4" />
<span class="sr-only">GitHub</span>
</button>
<button
on:click={toggleTheme}
class="inline-flex h-9 w-9 items-center justify-center rounded-md border bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
aria-label="Toggle theme"
>
{#if isDarkMode}
<Sun class="h-4 w-4" />
{:else}
<Moon class="h-4 w-4" />
{/if}
<span class="sr-only">Toggle theme</span>
</button>
<!-- Mobile Menu Button -->
<button
class="inline-flex h-9 w-9 items-center justify-center rounded-md border bg-background text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground md:hidden"
on:click={toggleMenu}
aria-expanded={isMenuOpen}
aria-label="Toggle menu"
>
{#if isMenuOpen}
<X class="h-4 w-4" />
{:else}
<Menu class="h-4 w-4" />
{/if}
</button>
</div>
</div>
<!-- Mobile Navigation -->
{#if isMenuOpen}
<div class="container md:hidden" transition:fade={{ duration: 200 }}>
<nav class="flex flex-col space-y-4 p-4">
{#each navItems as { href, label }}
<a
{href}
class="text-base font-medium transition-colors hover:text-primary {currentPath === href ? 'text-primary' : 'text-foreground/60'}"
on:click={() => (isMenuOpen = false)}
>
{label}
</a>
{/each}
</nav>
</div>
{/if}
</header>

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