john 2024-11-26 08:40:21

This commit is contained in:
John
2024-11-26 08:40:21 -05:00
parent f2cc718f49
commit 7043f78f1f
80 changed files with 14119 additions and 0 deletions

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"
]
}

22
web/README.md Normal file
View File

@@ -0,0 +1,22 @@
## 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](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.
### Todos
- [ ] 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/']
}
);

BIN
web/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 KiB

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.0.0",
"@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"
}
}

2918
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,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>

View File

@@ -0,0 +1,110 @@
import type { Particle } from '$lib/types/particle';
import { generateGradientColor } from '$lib/utils/colors';
export class ParticleSystem {
private particles: Particle[] = [];
private width: number;
private height: number;
private mouseX: number = 0;
private mouseY: number = 0;
private targetMouseX: number = 0;
private targetMouseY: number = 0;
constructor(
private readonly count: number,
private readonly baseSize: number,
private readonly speed: number,
width: number,
height: number
) {
this.width = width;
this.height = height;
this.mouseX = width / 2;
this.mouseY = height / 2;
this.targetMouseX = this.mouseX;
this.targetMouseY = this.mouseY;
this.initParticles();
}
private initParticles(): void {
this.particles = [];
for (let i = 0; i < this.count; i++) {
// Distribute particles across the entire width
const x = Math.random() * this.width;
// Distribute particles vertically around the middle with some variation
const yOffset = (Math.random() - 0.5) * 100;
this.particles.push({
x,
y: this.height / 2 + yOffset,
baseY: this.height / 2 + yOffset,
speed: (Math.random() - 0.5) * this.speed * 0.5, // Reduced base speed
angle: Math.random() * Math.PI * 2,
size: this.baseSize * (0.8 + Math.random() * 0.4),
color: generateGradientColor(this.height / 2 + yOffset, this.height),
velocityX: (Math.random() - 0.5) * this.speed // Reduced initial velocity
});
}
}
public updateDimensions(width: number, height: number): void {
this.width = width;
this.height = height;
this.mouseX = width / 2;
this.mouseY = height / 2;
this.targetMouseX = this.mouseX;
this.targetMouseY = this.mouseY;
this.initParticles();
}
public updateMousePosition(x: number, y: number): void {
this.targetMouseX = x;
this.targetMouseY = y;
}
public update(): void {
// Smooth mouse movement
this.mouseX += (this.targetMouseX - this.mouseX) * 0.05; // Slower mouse tracking
this.mouseY += (this.targetMouseY - this.mouseY) * 0.05;
this.particles.forEach(particle => {
// Update horizontal position with constant motion
particle.x += particle.velocityX;
// Wave motion
particle.angle += particle.speed;
const waveAmplitude = 30 * (this.mouseY / this.height); // Reduced amplitude
const frequencyFactor = (this.mouseX / this.width);
// Calculate vertical position with wave effect
particle.y = particle.baseY +
Math.sin(particle.angle * frequencyFactor + particle.x * 0.01) * // Slower wave
waveAmplitude;
// Update particle color based on position
particle.color = generateGradientColor(particle.y, this.height);
// Screen wrapping with position preservation
if (particle.x < 0) {
particle.x = this.width;
particle.baseY = this.height / 2 + (Math.random() - 0.5) * 100;
}
if (particle.x > this.width) {
particle.x = 0;
particle.baseY = this.height / 2 + (Math.random() - 0.5) * 100;
}
// Very subtle velocity adjustment to maintain spread
if (Math.abs(particle.velocityX) < 0.1) {
particle.velocityX += (Math.random() - 0.5) * 0.02;
}
// Gentle velocity dampening
particle.velocityX *= 0.99;
});
}
public getParticles(): Particle[] {
return this.particles;
}
}

View File

@@ -0,0 +1,192 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { goto } from '$app/navigation';
let mounted = false;
let currentCommand = '';
let commandHistory: string[] = [];
let showCursor = true;
let terminalContent = '';
let typing = false;
const pages = {
home: 'Welcome to Fabric\n\nType `help` to see available commands.',
about: 'About Fabric',
chat: 'Enter `chat` to start a chat session.',
posts: 'Enter `posts` to view blog posts.',
tags: 'Enter `tags` to view tags.',
contact: 'Enter `contact` to view contact info.',
help: `Available commands:
- help: Show this help message
- about: Navigate to About page
- chat: Start a chat session
- posts: View all blog posts
- tags: Browse content by tags
- contact: Get in touch
- clear: Clear the terminal
- ls: List available pages`,
};
// Simulate typing effect
async function typeContent(content: string) {
typing = true;
terminalContent = '';
for (const char of content) {
terminalContent += char;
await new Promise(resolve => setTimeout(resolve, 20));
}
typing = false;
}
function handleCommand(cmd: string) {
commandHistory = [...commandHistory, cmd];
switch (cmd) {
case 'clear':
terminalContent = '';
break;
case 'help':
typeContent(pages.help);
break;
case 'about':
goto('/about');
break;
case 'chat':
goto('/chat');
break;
case 'posts':
goto('/posts');
break;
case 'tags':
goto('/tags');
break;
case 'contact':
goto('/contact');
break;
case 'ls':
typeContent(Object.keys(pages).join('\n'));
break;
default:
const page = cmd.slice(3);
if (pages[page]) {
typeContent(pages[page]);
} else {
typeContent(`Error: Page '${page}' not found`);
}
}
}
function handleKeydown(event: KeyboardEvent) {
if (typing) return;
if (event.key === 'Enter') {
handleCommand(currentCommand.trim());
currentCommand = '';
}
}
onMount(() => {
mounted = true;
setInterval(() => {
showCursor = !showCursor;
}, 500);
// Initial content
typeContent(pages.home);
});
</script>
<div class="min-h-[calc(100vh-4rem)] pt-20 pb-8 px-4">
<div class="container mx-auto max-w-4xl">
<div class="terminal-window backdrop-blur-sm">
<!-- Terminal header -->
<div class="terminal-header flex items-center gap-2 px-4 py-2 border-b border-gray-700/50">
<div class="flex gap-2">
<div class="w-3 h-3 rounded-full bg-red-500/80"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500/80"></div>
<div class="w-3 h-3 rounded-full bg-green-500/80"></div>
</div>
<span class="text-sm text-gray-400 ml-2">me@localhost</span>
</div>
<!-- Terminal content -->
<div class="p-6">
<!-- Terminal output -->
<div class="mb-4 whitespace-pre-wrap terminal-text leading-relaxed">{terminalContent}</div>
<!-- Command input -->
{#if mounted}
<div class="flex items-center command-input" in:fade={{ duration: 200 }}>
<span class="mr-2 terminal-prompt font-bold">$</span>
<!-- {#if showCursor}
<span class="animate-blink terminal-text">▋</span>
{/if} -->
<input
type="text"
bind:value={currentCommand}
on:keydown={handleKeydown}
class="flex-1 bg-transparent border-none outline-none terminal-text caret-primary-500"
placeholder="Type a command..."
/>
</div>
{/if}
<!-- Command history -->
<!-- <div class="mt-6 space-y-1 text-sm text-gray-500">
{#each commandHistory as cmd}
<div class="terminal-text opacity-60">
<span class="terminal-prompt font-bold mr-2">$</span>
{cmd}
</div>
{/each}
</div> -->
</div>
</div>
</div>
</div>
<style>
.terminal-window {
@apply rounded-lg border border-gray-700/50 bg-gray-900/95 shadow-2xl;
box-shadow: 0 0 60px -15px rgba(0, 0, 0, 0.3);
}
.terminal-text {
@apply font-mono text-green-400/90;
}
.terminal-prompt {
@apply text-blue-400/90;
}
input::placeholder {
@apply text-gray-600;
}
/* .animate-blink {
animation: blink 1s step-end infinite;
flex-col: 1;
}*/
@keyframes blink {
50% {
opacity: 0;
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-track {
@apply bg-gray-800/50 rounded-full;
}
::-webkit-scrollbar-thumb {
@apply bg-gray-600/50 rounded-full hover:bg-gray-500/50 transition-colors;
}
</style>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="container flex min-h-[400px] flex-col items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl">
{$page.status}: {$page.error?.message || 'Something went wrong'}
</h1>
<p class="mt-4 text-muted-foreground">
{#if $page.status === 404}
Sorry, we couldn't find the page you're looking for.
{:else}
An error occurred while loading the page. Please try again later.
{/if}
</p>
<div class="mt-8">
<a
href="/home"
class="inline-flex items-center justify-center rounded-md bg-primary px-8 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Try Again
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
<script>
import Content from './README.md'
</script>
{#if Content}
<article class="container max-w-3xl">
<div class="space-y-4">
<svelte:component this={Content} />
</div>
</article>
{:else}
<div class="container py-12">
<h1 class="mb-8 text-3xl font-bold">Sorry</h1>
<div class="flex min-h-[400px] items-center justify-center text-center">
<p class="text-lg font-medium">Nothing found</p>
<p class="mt-2 text-sm text-muted-foreground">Check back later for new content.</p>
</div>
</div>
{/if}

View File

@@ -0,0 +1,5 @@
import { dev } from '$app/environment';
export const csr = dev;
export const prerender = true;

View File

@@ -0,0 +1,496 @@
---
title: README
description: fabric is an open-source framework for augmenting humans using AI. It provides a modular framework for solving specific problems using a crowdsourced set of AI prompts that can be used anywhere.
date: 2024-1-12
updated: 2024-11-22
---
The UI for Fabric can be found [here](/chat).
<div align="center" style="">
<img src="./src/lib/images/fabric-logo.gif" alt="fabriclogo" width="400" height="400"/>
# `fabric`
![Static Badge](https://img.shields.io/badge/mission-human_flourishing_via_AI_augmentation-purple)![Github top language](https://img.shields.io/github/languages/top/danielmiessler/fabric)![GitHub last commit](https://img.shields.io/github/last-commit/danielmiessler/fabric)
![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)
[MIT Open Source License](https://opensource.org/licenses/MIT)
</div>
<div align="center">
<p class="align center">
<h2><code>fabric</code> is an open-source framework for augmenting humans using AI.</h2>
[Updates](#updates) •
[What and Why](#whatandwhy) •
[Philosophy](#philosophy) •
[Installation](#Installation) •
[Usage](#Usage) •
[Examples](#examples) •
[Just Use the Patterns](#just-use-the-patterns) •
[Custom Patterns](#custom-patterns) •
[Helper Apps](#helper-apps) •
[Meta](#meta)
</div>
## Navigation
- [`fabric`](#fabric)
- [Navigation](#navigation)
- [Updates](#updates)
- [What and why](#what-and-why)
- [Intro videos](#intro-videos)
- [Philosophy](#philosophy)
- [Breaking problems into components](#breaking-problems-into-components)
- [Too many prompts](#too-many-prompts)
- [Installation](#installation)
- [Get Latest Release Binaries](#get-latest-release-binaries)
- [From Source](#from-source)
- [Environment Variables](#environment-variables)
- [Setup](#setup)
- [Add aliases for all patterns](#add-aliases-for-all-patterns)
- [Save your files in markdown using aliases](#save-your-files-in-markdown-using-aliases)
- [Migration](#migration)
- [Upgrading](#upgrading)
- [Usage](#usage)
- [Our approach to prompting](#our-approach-to-prompting)
- [Examples](#examples)
- [Just use the Patterns](#just-use-the-patterns)
- [Custom Patterns](#custom-patterns)
- [Helper Apps](#helper-apps)
- [`to_pdf`](#to_pdf)
- [`to_pdf` Installation](#to_pdf-installation)
- [pbpaste](#pbpaste)
- [Meta](#meta)
- [Primary contributors](#primary-contributors)
<br />
## Updates
> [!NOTE]
November 8, 2024
> * **Multimodal Support**: You can now us `-a` (attachment) for Multimodal submissions to OpenAI models that support it. Example: `fabric -a https://path/to/image "Give me a description of this image."`
## What and why
Since the start of 2023 and GenAI we've seen a massive number of AI applications for accomplishing tasks. It's powerful, but _it's not easy to integrate this functionality into our lives._
<div align="center">
<h4>In other words, AI doesn't have a capabilities problem—it has an <em>integration</em> problem.</h4>
</div>
Fabric was created to address this by enabling everyone to granularly apply AI to everyday challenges.
## Intro videos
Keep in mind that many of these were recorded when Fabric was Python-based, so remember to use the current [install instructions](#Installation) below.
* [Network Chuck](https://www.youtube.com/watch?v=UbDyjIIGaxQ)
* [David Bombal](https://www.youtube.com/watch?v=vF-MQmVxnCs)
* [My Own Intro to the Tool](https://www.youtube.com/watch?v=wPEyyigh10g)
* [More Fabric YouTube Videos](https://www.youtube.com/results?search_query=fabric+ai)
## Philosophy
> AI isn't a thing; it's a _magnifier_ of a thing. And that thing is **human creativity**.
We believe the purpose of technology is to help humans flourish, so when we talk about AI we start with the **human** problems we want to solve.
### Breaking problems into components
Our approach is to break problems into individual pieces (see below) and then apply AI to them one at a time. See below for some examples.
<img width="2078" alt="augmented_challenges" src="https://github.com/danielmiessler/fabric/assets/50654/31997394-85a9-40c2-879b-b347e4701f06">
### Too many prompts
Prompts are good for this, but the biggest challenge I faced in 2023——which still exists today—is **the sheer number of AI prompts out there**. We all have prompts that are useful, but it's hard to discover new ones, know if they are good or not, _and manage different versions of the ones we like_.
One of <code>fabric</code>'s primary features is helping people collect and integrate prompts, which we call _Patterns_, into various parts of their lives.
Fabric has Patterns for all sorts of life and work activities, including:
- Extracting the most interesting parts of YouTube videos and podcasts
- Writing an essay in your own voice with just an idea as an input
- Summarizing opaque academic papers
- Creating perfectly matched AI art prompts for a piece of writing
- Rating the quality of content to see if you want to read/watch the whole thing
- Getting summaries of long, boring content
- Explaining code to you
- Turning bad documentation into usable documentation
- Creating social media posts from any content input
- And a million more…
## Installation
To install Fabric, you can use the latest release binaries or install it from the source.
### Get Latest Release Binaries
```bash
# Windows:
curl -L https://github.com/danielmiessler/fabric/releases/latest/download/fabric-windows-amd64.exe > fabric.exe && fabric.exe --version
# MacOS (arm64):
curl -L https://github.com/danielmiessler/fabric/releases/latest/download/fabric-darwin-arm64 > fabric && chmod +x fabric && ./fabric --version
# MacOS (amd64):
curl -L https://github.com/danielmiessler/fabric/releases/latest/download/fabric-darwin-amd64 > fabric && chmod +x fabric && ./fabric --version
# Linux (amd64):
curl -L https://github.com/danielmiessler/fabric/releases/latest/download/fabric-linux-amd64 > fabric && chmod +x fabric && ./fabric --version
# Linux (arm64):
curl -L https://github.com/danielmiessler/fabric/releases/latest/download/fabric-linux-arm64 > fabric && chmod +x fabric && ./fabric --version
```
### From Source
To install Fabric, [make sure Go is installed](https://go.dev/doc/install), and then run the following command.
```bash
# Install Fabric directly from the repo
go install github.com/danielmiessler/fabric@latest
```
### Environment Variables
You may need to set some environment variables in your `~/.bashrc` on linux or `~/.zshrc` file on mac to be able to run the `fabric` command. Here is an example of what you can add:
For Intel based macs or linux
```bash
# Golang environment variables
export GOROOT=/usr/local/go
export GOPATH=$HOME/go
# Update PATH to include GOPATH and GOROOT binaries
export PATH=$GOPATH/bin:$GOROOT/bin:$HOME/.local/bin:$PATH
```
for Apple Silicon based macs
```bash
# Golang environment variables
export GOROOT=$(brew --prefix go)/libexec
export GOPATH=$HOME/go
export PATH=$GOPATH/bin:$GOROOT/bin:$HOME/.local/bin:$PATH
```
### Setup
Now run the following command
```bash
# Run the setup to set up your directories and keys
fabric --setup
```
If everything works you are good to go.
### Add aliases for all patterns
In order to add aliases for all your patterns and use them directly as commands ie. `summarize` instead of `fabric --pattern summarize`
You can add the following to your `.zshrc` or `.bashrc` file.
```bash
# Loop through all files in the ~/.config/fabric/patterns directory
for pattern_file in $HOME/.config/fabric/patterns/*; do
# Get the base name of the file (i.e., remove the directory path)
pattern_name=$(basename "$pattern_file")
# Create an alias in the form: alias pattern_name="fabric --pattern pattern_name"
alias_command="alias $pattern_name='fabric --pattern $pattern_name'"
# Evaluate the alias command to add it to the current shell
eval "$alias_command"
done
yt() {
local video_link="$1"
fabric -y "$video_link" --transcript
}
```
This also creates a `yt` alias that allows you to use `yt https://www.youtube.com/watch?v=4b0iet22VIk` to get your transcripts.
#### Save your files in markdown using aliases
If in addition to the above aliases you would like to have the option to save the output to your favourite markdown note vault like Obsidian then instead of the above add the following to your `.zshrc` or `.bashrc` file:
```bash
# Define the base directory for Obsidian notes
obsidian_base="/path/to/obsidian"
# Loop through all files in the ~/.config/fabric/patterns directory
for pattern_file in ~/.config/fabric/patterns/*; do
# Get the base name of the file (i.e., remove the directory path)
pattern_name=$(basename "$pattern_file")
# Unalias any existing alias with the same name
unalias "$pattern_name" 2>/dev/null
# Define a function dynamically for each pattern
eval "
$pattern_name() {
local title=\$1
local date_stamp=\$(date +'%Y-%m-%d')
local output_path=\"\$obsidian_base/\${date_stamp}-\${title}.md\"
# Check if a title was provided
if [ -n \"\$title\" ]; then
# If a title is provided, use the output path
fabric --pattern \"$pattern_name\" -o \"\$output_path\"
else
# If no title is provided, use --stream
fabric --pattern \"$pattern_name\" --stream
fi
}
"
done
yt() {
local video_link="$1"
fabric -y "$video_link" --transcript
}
```
This will allow you to use the patterns as aliases like in the above for example `summarize` instead of `fabric --pattern summarize --stream`, however if you pass in an extra argument like this `summarize "my_article_title"` your output will be saved in the destination that you set in `obsidian_base="/path/to/obsidian"` in the following format `YYYY-MM-DD-my_article_title.md` where the date gets autogenerated for you.
You can tweak the date format by tweaking the `date_stamp` format.
### Migration
If you have the Legacy (Python) version installed and want to migrate to the Go version, here's how you do it. It's basically two steps: 1) uninstall the Python version, and 2) install the Go version.
```bash
# Uninstall Legacy Fabric
pipx uninstall fabric
# Clear any old Fabric aliases
(check your .bashrc, .zshrc, etc.)
# Install the Go version
go install github.com/danielmiessler/fabric@latest
# Run setup for the new version. Important because things have changed
fabric --setup
```
Then [set your environmental variables](#environmental-variables) as shown above.
### Upgrading
The great thing about Go is that it's super easy to upgrade. Just run the same command you used to install it in the first place and you'll always get the latest version.
```bash
go install github.com/danielmiessler/fabric@latest
```
## Usage
Once you have it all set up, here's how to use it.
```bash
fabric -h
```
```bash
Usage:
fabric [OPTIONS]
Application Options:
-p, --pattern= Choose a pattern from the available patterns
-v, --variable= Values for pattern variables, e.g. -v=#role:expert -v=#points:30"
-C, --context= Choose a context from the available contexts
--session= Choose a session from the available sessions
-a, --attachment= Attachment path or URL (e.g. for OpenAI image recognition messages)
-S, --setup Run setup for all reconfigurable parts of fabric
-t, --temperature= Set temperature (default: 0.7)
-T, --topp= Set top P (default: 0.9)
-s, --stream Stream
-P, --presencepenalty= Set presence penalty (default: 0.0)
-r, --raw Use the defaults of the model without sending chat options (like temperature etc.) and use the user role instead of the system role for patterns.
-F, --frequencypenalty= Set frequency penalty (default: 0.0)
-l, --listpatterns List all patterns
-L, --listmodels List all available models
-x, --listcontexts List all contexts
-X, --listsessions List all sessions
-U, --updatepatterns Update patterns
-c, --copy Copy to clipboard
-m, --model= Choose model
-o, --output= Output to file
--output-session Output the entire session (also a temporary one) to the output file
-n, --latest= Number of latest patterns to list (default: 0)
-d, --changeDefaultModel Change default model
-y, --youtube= YouTube video "URL" to grab transcript, comments from it and send to chat
--transcript Grab transcript from YouTube video and send to chat (it used per default).
--comments Grab comments from YouTube video and send to chat
-g, --language= Specify the Language Code for the chat, e.g. -g=en -g=zh
-u, --scrape_url= Scrape website URL to markdown using Jina AI
-q, --scrape_question= Search question using Jina AI
-e, --seed= Seed to be used for LMM generation
-w, --wipecontext= Wipe context
-W, --wipesession= Wipe session
--printcontext= Print context
--printsession= Print session
--readability Convert HTML input into a clean, readable view
--dry-run Show what would be sent to the model without actually sending it
--version Print current version
Help Options:
-h, --help Show this help message
```
## Our approach to prompting
Fabric _Patterns_ are different than most prompts you'll see.
- **First, we use `Markdown` to help ensure maximum readability and editability**. This not only helps the creator make a good one, but also anyone who wants to deeply understand what it does. _Importantly, this also includes the AI you're sending it to!_
Here's an example of a Fabric Pattern.
```bash
https://github.com/danielmiessler/fabric/blob/main/patterns/extract_wisdom/system.md
```
<img width="1461" alt="pattern-example" src="https://github.com/danielmiessler/fabric/assets/50654/b910c551-9263-405f-9735-71ca69bbab6d">
- **Next, we are extremely clear in our instructions**, and we use the Markdown structure to emphasize what we want the AI to do, and in what order.
- **And finally, we tend to use the System section of the prompt almost exclusively**. In over a year of being heads-down with this stuff, we've just seen more efficacy from doing that. If that changes, or we're shown data that says otherwise, we will adjust.
## Examples
> The following examples use the macOS `pbpaste` to paste from the clipboard. See the [pbpaste](#pbpaste) section below for Windows and Linux alternatives.
Now let's look at some things you can do with Fabric.
1. Run the `summarize` Pattern based on input from `stdin`. In this case, the body of an article.
```bash
pbpaste | fabric --pattern summarize
```
2. Run the `analyze_claims` Pattern with the `--stream` option to get immediate and streaming results.
```bash
pbpaste | fabric --stream --pattern analyze_claims
```
3. Run the `extract_wisdom` Pattern with the `--stream` option to get immediate and streaming results from any Youtube video (much like in the original introduction video).
```bash
fabric -y "https://youtube.com/watch?v=uXs-zPc63kM" --stream --pattern extract_wisdom
```
4. Create patterns- you must create a .md file with the pattern and save it to ~/.config/fabric/patterns/[yourpatternname].
## Just use the Patterns
<img width="1173" alt="fabric-patterns-screenshot" src="https://github.com/danielmiessler/fabric/assets/50654/9186a044-652b-4673-89f7-71cf066f32d8">
<br />
<br />
If you're not looking to do anything fancy, and you just want a lot of great prompts, you can navigate to the [`/patterns`](https://github.com/danielmiessler/fabric/tree/main/patterns) directory and start exploring!
We hope that if you used nothing else from Fabric, the Patterns by themselves will make the project useful.
You can use any of the Patterns you see there in any AI application that you have, whether that's ChatGPT or some other app or website. Our plan and prediction is that people will soon be sharing many more than those we've published, and they will be way better than ours.
The wisdom of crowds for the win.
## Custom Patterns
You may want to use Fabric to create your own custom Patterns—but not share them with others. No problem!
Just make a directory in `~/.config/custompatterns/` (or wherever) and put your `.md` files in there.
When you're ready to use them, copy them into:
```
~/.config/fabric/patterns/
```
You can then use them like any other Patterns, but they won't be public unless you explicitly submit them as Pull Requests to the Fabric project. So don't worry—they're private to you.
This feature works with all openai and ollama models but does NOT work with claude. You can specify your model with the -m flag
## Helper Apps
Fabric also makes use of some core helper apps (tools) to make it easier to integrate with your various workflows. Here are some examples:
### `to_pdf`
`to_pdf` is a helper command that converts LaTeX files to PDF format. You can use it like this:
```bash
to_pdf input.tex
```
This will create a PDF file from the input LaTeX file in the same directory.
You can also use it with stdin which works perfectly with the `write_latex` pattern:
```bash
echo "ai security primer" | fabric --pattern write_latex | to_pdf
```
This will create a PDF file named `output.pdf` in the current directory.
### `to_pdf` Installation
To install `to_pdf`, install it the same way as you install Fabric, just with a different repo name.
```bash
go install github.com/danielmiessler/fabric/plugins/tools/to_pdf@latest
```
Make sure you have a LaTeX distribution (like TeX Live or MiKTeX) installed on your system, as `to_pdf` requires `pdflatex` to be available in your system's PATH.
## pbpaste
The [examples](#examples) use the macOS program `pbpaste` to paste content from the clipboard to pipe into `fabric` as the input. `pbpaste` is not available on Windows or Linux, but there are alternatives.
On Windows, you can use the PowerShell command `Get-Clipboard` from a PowerShell command prompt. If you like, you can also alias it to `pbpaste`. If you are using classic PowerShell, edit the file `~\Documents\WindowsPowerShell\.profile.ps1`, or if you are using PowerShell Core, edit `~\Documents\PowerShell\.profile.ps1` and add the alias,
```powershell
Set-Alias pbpaste Get-Clipboard
```
On Linux, you can use `xclip -selection clipboard -o` to paste from the clipboard. You will likely need to install `xclip` with your package manager. For Debian based systems including Ubuntu,
```sh
sudo apt update
sudo apt install xclip -y
```
You can also create an alias by editing `~/.bashrc` or `~/.zshrc` and adding the alias,
```sh
alias pbpaste='xclip -selection clipboard -o'
```
## Meta
> [!NOTE]
> Special thanks to the following people for their inspiration and contributions!
- _Jonathan Dunn_ for being the absolute MVP dev on the project, including spearheading the new Go version, as well as the GUI! All this while also being a full-time medical doctor!
- _Caleb Sima_ for pushing me over the edge of whether to make this a public project or not.
- _Eugen Eisler_ and _Frederick Ros_ for their invaluable contributions to the Go version
- _Joel Parish_ for super useful input on the project's Github directory structure..
- _Joseph Thacker_ for the idea of a `-c` context flag that adds pre-created context in the `./config/fabric/` directory to all Pattern queries.
- _Jason Haddix_ for the idea of a stitch (chained Pattern) to filter content using a local model before sending on to a cloud model, i.e., cleaning customer data using `llama2` before sending on to `gpt-4` for analysis.
- _Andre Guerra_ for assisting with numerous components to make things simpler and more maintainable.
### Primary contributors
<a href="https://github.com/danielmiessler"><img src="https://avatars.githubusercontent.com/u/50654?v=4" alt="Daniel Miessler" title="Daniel Miessler" width="50" height="50"></a>
<a href="https://github.com/xssdoctor"><img src="https://avatars.githubusercontent.com/u/9218431?v=4" alt="Jonathan Dunn" title="Jonathan Dunn" width="50" height="50"></a>
<a href="https://github.com/sbehrens"><img src="https://avatars.githubusercontent.com/u/688589?v=4" alt="Scott Behrens" title="Scott Behrens" width="50" height="50"></a>
<a href="https://github.com/agu3rra"><img src="https://avatars.githubusercontent.com/u/10410523?v=4" alt="Andre Guerra" title="Andre Guerra" width="50" height="50"></a>
`fabric` was created by <a href="https://danielmiessler.com/subscribe" target="_blank">Daniel Miessler</a> in January of 2024.
<br />
<a href="https://twitter.com/intent/user?screen_name=danielmiessler">
![X (formerly Twitter) Follow](https://img.shields.io/twitter/follow/danielmiessler)
</a>

View File

@@ -0,0 +1,18 @@
<script>
import { initializeStores } from "@skeletonlabs/skeleton";
initializeStores();
</script>
<div class="chat-layout">
<slot />
</div>
<style>
.chat-layout {
display: flex;
flex-direction: column;
/* height: 100vh;
max-height: 100vh;
overflow: hidden; */
}
</style>

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import ChatHeader from './ChatHeader.svelte';
import ChatInput from "./ChatInput.svelte";
import ChatMessages from "./ChatMessages.svelte";
import ModelConfig from "./ModelConfig.svelte";
//import { Button } from "$lib/components/ui/button";
//import { setSystemPrompt } from "$lib/store/chat";
// import { Select } from "$lib/components/ui/select";
// import { Save, Code, Share2, MoreHorizontal } from 'lucide-svelte';
// import { onMount } from "svelte";
</script>
<ChatHeader />
<div class="flex-1 container mx-auto p-4">
<div class="grid grid-cols-1 lg:grid-cols-[minmax(400px,_600px),minmax(400px,_600px),300px] gap-4">
<div class="space-y-4 order-2 lg:order-1 h-full overflow-y flex-1">
<ChatInput />
</div>
<div class="border container max-h-[680px] rounded-lg bg-muted/50 p-4 order-1 lg:order-2 h-full">
<ChatMessages />
</div>
<div class="space-y-2 order-3">
<ModelConfig />
</div>
</div>
</div>
<!-- NOTE : This is for the mobile view
<div class="border container max-h-[660px] rounded-lg bg-muted/50 p-4 order-1 lg:order-2 h-full overflow-auto overflow-y-hidden overflow-x-hidden">
<ChatMessages />
</div> -->

View File

@@ -0,0 +1,5 @@
import { dev } from '$app/environment';
export const csr = dev;
export const prerender = true;

View File

@@ -0,0 +1,34 @@
// For the Youtube API
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getTranscript } from '$lib/services/transcriptService';
export const POST: RequestHandler = async ({ request }) => {
try {
const body = await request.json();
console.log('Received request body:', body);
const { url } = body;
if (!url) {
return json({ error: 'URL is required' }, { status: 400 });
}
console.log('Fetching transcript for URL:', url);
const transcriptData = await getTranscript(url);
console.log('Successfully fetched transcript, preparing response');
const response = json(transcriptData);
// Log the actual response being sent
const responseText = JSON.stringify(transcriptData);
console.log('Sending response (first 200 chars):', responseText.slice(0, 200) + '...');
return response;
} catch (error) {
console.error('Server error:', error);
return json(
{ error: error instanceof Error ? error.message : 'Failed to fetch transcript' },
{ status: 500 }
);
}
};

View File

@@ -0,0 +1,22 @@
<!-- Moved to ModelConfig.svelte -->
<!-- <script lang="ts">
import { onMount } from 'svelte';
import { availableModels, modelConfig, loadAvailableModels } from '$lib/store/model-config';
onMount(async () => {
await loadAvailableModels();
});
</script>
<select
class="flex h-9 w-full items-center justify-between rounded-md border border-input bg-background 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 appearance-none"
bind:value={$modelConfig.model}
>
<option value="">Select a model...</option>
{#each $availableModels as model}
<option value={model.name}>
{model.name} ({model.vendor})
</option>
{/each}
</select> -->

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { patterns } from "$lib/types/chat/patterns";
import { patternAPI } from "$lib/types/chat/patterns";
import { Select } from "$lib/components/ui/select";
import { onMount } from "svelte";
let selectedPreset = "";
$: if (selectedPreset) {
console.log('Pattern selected:', selectedPreset);
patternAPI.selectPattern(selectedPreset);
}
onMount(async () => {
await patternAPI.loadPatterns();
});
</script>
<header class="border-b">
<div class="container mx-auto p-3">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h1 class="text-xl font-semibold">Chat</h1>
<div class="flex flex-wrap items-center gap-2 w-full sm:w-auto">
<Select
class="w-full font-bold sm:w-[200px]"
bind:value={selectedPreset}
>
<option value="">Load a pattern...</option>
{#each $patterns as pattern}
<option value={pattern.Name}>{pattern.Description}</option>
{/each}
</Select>
</div>
</div>
</div>
</header>

View File

@@ -0,0 +1,77 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Textarea } from "$lib/components/ui/textarea";
import { Label } from "$lib/components/ui/label";
import { currentSession, setSession, sendMessage } from '$lib/store/chat';
import { systemPrompt } from '$lib/types/chat/patterns';
import { onMount } from 'svelte';
let userInput = "";
async function handleSubmit() {
if (!userInput.trim()) return;
try {
await sendMessage($systemPrompt.trim() + userInput.trim());
} catch (error) {
console.error('Chat submission error:', error);
}
}
function handleSetSession(name: string | null) {
setSession(name);
}
onMount(() => {
console.log('ChatInput mounted, current system prompt:', $systemPrompt);
});
</script>
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<Button
variant="outline"
size="sm"
on:click={() => handleSetSession(null)}
>
Clear Session
</Button>
{#if !$currentSession}
<Button
variant="outline"
size="sm"
on:click={() => handleSetSession('new-session-' + Date.now())}
>
New Session
</Button>
{/if}
</div>
<div class="flex flex-col h-full">
<div class="space-y-2 flex-none">
<Label class="p-1 font-bold">System Prompt</Label>
<Textarea
value={$systemPrompt}
on:input={(e) => systemPrompt}
placeholder="Enter system instructions..."
class="min-h-[200px] resize-none bg-background"
/>
</div>
<div class="space-y-2 flex-1 py-1">
<Label class="p-1 font-bold">User Input</Label>
<Textarea
bind:value={userInput}
placeholder="Enter your message..."
class="h-[calc(80vh-24rem)] resize-none bg-background"
/>
</div>
<Button on:click={handleSubmit} variant="outline" size="sm" class="mt-2">Submit</Button>
</div>
</div>
<style>
.flex-col {
min-height: 0;
}
</style>

View File

@@ -0,0 +1,100 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { RotateCcw, Trash2, Save } from 'lucide-svelte';
import { chatState, clearMessages, revertLastMessage, currentSession } from '$lib/store/chat';
import { afterUpdate } from 'svelte';
let sessionName: string | null = null;
let messagesContainer: HTMLDivElement;
currentSession.subscribe(value => {
sessionName = value;
});
afterUpdate(() => {
if (messagesContainer) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
});
function saveChat() {
const chatData = JSON.stringify($chatState.messages, null, 2);
const blob = new Blob([chatData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'chat-history.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
</script>
<div class="chat-messages-wrapper flex flex-col h-full">
<div class="flex justify-between items-center mb-4 flex-none">
<span class="text-sm font-medium">Chat History</span>
<div class="flex gap-2">
<Button variant="outline" size="icon" on:click={revertLastMessage}>
<RotateCcw class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" on:click={clearMessages}>
<Trash2 class="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" on:click={saveChat}>
<Save class="h-4 w-4" />
</Button>
</div>
</div>
<div class="messages-container flex-1" bind:this={messagesContainer}>
<div class="messages-content space-y-4">
{#each $chatState.messages as message}
<div class="message-item whitespace-pre-wrap text-sm {message.role === 'assistant' ? 'pl-4' : 'font-medium'} transition-all">
<span class="text-xs tertiary uppercase">{message.role}:</span>
{message.content}
</div>
{/each}
{#if $chatState.isStreaming}
<div class="pl-4 text-tertiary-700 animate-pulse"></div>
{/if}
</div>
</div>
</div>
<style>
.chat-messages-wrapper {
display: flex;
flex-direction: column;
min-height: 0;
}
.messages-container {
position: relative;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--color-primary-500) transparent;
}
.messages-content {
padding-bottom: 1rem;
padding-right: 0.5rem;
}
.message-item {
margin-bottom: 0.5rem;
}
.messages-container::-webkit-scrollbar {
width: 2px;
}
.messages-container::-webkit-scrollbar-track {
background: transparent;
}
.messages-container::-webkit-scrollbar-thumb {
background-color: var(--color-primary-500);
border-radius: 2px;
}
</style>

View File

@@ -0,0 +1,61 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Select } from "$lib/components/ui/select";
import { Label } from "$lib/components/ui/label";
import { Slider } from "$lib/components/ui/slider";
import { modelConfig, availableModels, loadAvailableModels } from "$lib/store/model-config";
import Transcripts from "./Transcripts.svelte";
onMount(async () => {
await loadAvailableModels();
});
//Debugging
//$: console.log('Current available models:', $availableModels);
//$: console.log('Current model config:', $modelConfig);
</script>
<div class="space-y-4">
<div class="space-y-2">
<Label>Model</Label>
<Select bind:value={$modelConfig.model}>
<option value="">Default Model</option>
{#each $availableModels as model (model.name)}
<option value={model.name}>{model.vendor} - {model.name}</option>
{/each}
</Select>
</div>
<div class="space-y-2">
<Label>Temperature ({$modelConfig.temperature.toFixed(1)})</Label>
<Slider
bind:value={$modelConfig.temperature}
min={0}
max={2}
step={0.1}
/>
</div>
<div class="space-y-2">
<Label>Maximum Length ({$modelConfig.maxLength})</Label>
<Slider
bind:value={$modelConfig.maxLength}
min={1}
max={4000}
step={1}
/>
</div>
<div class="space-y-2">
<Label>Top P ({$modelConfig.top_p.toFixed(2)})</Label>
<Slider
bind:value={$modelConfig.top_p}
min={0}
max={1}
step={0.01}
/>
</div>
<div class="space-y-2">
<Transcripts />
</div>
</div>

View File

@@ -0,0 +1,76 @@
<!-- WIP to be included in ChatInput or ChatHeader -->
<!--
<script lang="ts">
import { onMount } from 'svelte';
import { sessionAPI } from '$lib/types/chat/sessions';
import { currentSession } from '$lib/store/chat';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
let sessions: string[] = [];
let newSessionName = '';
onMount(async () => {
try {
sessions = await sessionAPI.getNames();
} catch (error) {
console.error('Failed to load sessions:', error);
}
});
async function createSession() {
if (!newSessionName.trim()) return;
try {
await sessionAPI.save(newSessionName, JSON.stringify({
name: newSessionName,
messages: []
}));
sessions = await sessionAPI.getNames();
currentSession.set(newSessionName);
newSessionName = '';
} catch (error) {
console.error('Failed to create session:', error);
}
}
async function deleteSession(name: string) {
try {
await sessionAPI.delete(name);
sessions = await sessionAPI.getNames();
if ($currentSession === name) {
currentSession.set(null);
}
} catch (error) {
console.error('Failed to delete session:', error);
}
}
</script>
<div class="space-y-4">
<div class="flex gap-2">
<Input bind:value={newSessionName} placeholder="New session name" />
<Button on:click={createSession}>Create</Button>
</div>
<div class="space-y-2">
{#each sessions as session}
<div class="flex items-center justify-between p-2 border rounded">
<button
class="flex-1 text-left"
on:click={() => currentSession.set(session)}
>
{session}
</button>
<Button
variant="destructive"
size="sm"
on:click={() => deleteSession(session)}
>
Delete
</Button>
</div>
{/each}
</div>
</div> -->

View File

@@ -0,0 +1,165 @@
<script lang='ts'>
import { getToastStore } from '@skeletonlabs/skeleton';
import { Button } from "$lib/components/ui/button";
import Input from '$components/ui/input/input.svelte';
import { getModalStore, type ModalSettings } from '@skeletonlabs/skeleton';
let url = '';
let transcript = '';
let loading = false;
let error = '';
let title = '';
const toastStore = getToastStore();
const modalStore = getModalStore();
// Event dispatcher to send transcript to parent
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
async function fetchTranscript() {
if (!isValidYouTubeUrl(url)) {
error = 'Please enter a valid YouTube URL';
toastStore.trigger({
message: error,
background: 'variant-filled-error'
});
return;
}
loading = true;
error = '';
try {
const response = await fetch('/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ url })
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to fetch transcript');
}
const data = await response.json();
console.log('Parsed response data:', data);
transcript = data.transcript;
title = data.title;
// Show modal with transcript
const modal: ModalSettings = {
type: 'component',
title: 'YouTube Transcript',
body: transcript,
buttonTextCancel: 'Close',
buttonTextSubmit: 'Use in Chat',
response: (r: boolean) => {
if (r) {
dispatch('transcript', transcript);
}
}
};
modalStore.trigger(modal);
toastStore.trigger({
message: 'Transcript fetched successfully!',
background: 'variant-filled-success'
});
} catch (err) {
error = err.message;
toastStore.trigger({
message: error,
background: 'variant-filled-error'
});
} finally {
loading = false;
}
}
function isValidYouTubeUrl(url) {
const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+$/;
return pattern.test(url);
}
async function copyToClipboard() {
try {
await navigator.clipboard.writeText(transcript);
toastStore.trigger({
message: 'Transcript copied to clipboard!',
background: 'variant-filled-success'
});
} catch (err) {
toastStore.trigger({
message: 'Failed to copy transcript',
background: 'variant-filled-error'
});
}
}
</script>
<div class="flex gap-2">
<Input
type="text"
bind:value={url}
placeholder="Enter YouTube URL"
class="flex-1 rounded-md border bg-background px-4"
disabled={loading}
/>
<Button
variant="outline"
on:click={fetchTranscript}
disabled={loading || !url}
>
{#if loading}
<div class="spinner-border" />
{:else}
Fetch Transcript
{/if}
</Button>
</div>
{#if error}
<div class="bg-destructive/15 text-destructive rounded-lg p-2">{error}</div>
{/if}
{#if transcript}
<div class="space-y-4 border rounded-lg p-4 bg-muted/50 h-96">
<div class="flex justify-between items-center">
<h3 class="text-xs font-semibold">{title || 'Transcript'}</h3>
<Button
variant="outline"
size="sm"
on:click={copyToClipboard}
>
Copy to Clipboard
</Button>
</div>
<textarea
class="w-full text-xs rounded-md border bg-background px-3 py-2 resize-none h-72"
readonly
value={transcript}
></textarea>
</div>
{/if}
<style>
.spinner-border {
width: 1rem;
height: 1rem;
border: 2px solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spin 0.75s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="container flex min-h-[400px] flex-col items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl">
{$page.status}: {$page.error?.message || 'Something went wrong'}
</h1>
<p class="mt-4 text-muted-foreground">
{#if $page.status === 404}
Sorry, we couldn't find the post you're looking for.
{:else}
An error occurred while loading the post. Please try again later.
{/if}
</p>
<div class="mt-8">
<a
href="/"
class="inline-flex items-center justify-center rounded-md bg-primary px-8 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Back to Home
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
<script>
import Content from './Contact.md'
</script>
{#if Content}
<article class="container max-w-3xl">
<div class="space-y-4">
<svelte:component this={Content} />
</div>
</article>
{:else}
<div class="container py-12">
<h1 class="mb-8 text-3xl font-bold">Sorry</h1>
<div class="flex min-h-[400px] items-center justify-center text-center">
<p class="text-lg font-medium">Nothing found</p>
<p class="mt-2 text-sm text-muted-foreground">Check back later for new content.</p>
</div>
</div>
{/if}

View File

@@ -0,0 +1,5 @@
import { dev } from '$app/environment';
export const csr = dev;
export const prerender = true;

View File

@@ -0,0 +1,32 @@
---
title: "Contact"
description: "Default Contact Page"
date: 2024-11-24
---
<div class="flex flex-col items-center justify-center">
<div class="container flex flex-col items-center justify-center gap-6">
<h1 class="text-3xl font-bold tracking-tight text-white sm:text-5xl"><i>{title}</i></h1>
</div>
<p class="text-center text-muted-foreground md:text-lg">
{description}
</p>
</div>
Place your contact info here: ...
<div class="flex items-center space-x-4">
<a href="https://github.com/johnconnor-sec" class="inline-block">
<img src="https://github.com/johnconnor-sec.png" alt="John Connor" class="w-12 h-12 rounded-full">
</a>
<a href="https://x.com/Noob73286788366" class="inline-block">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-twitter-x" viewBox="0 0 16 16">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z"/>
</svg>
</a>
<a href="https://medium.com/@j0hnc0nn0r" class="inline-block">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-medium" viewBox="0 0 16 16">
<path d="M9.025 8c0 2.485-2.02 4.5-4.513 4.5A4.506 4.506 0 0 1 0 8c0-2.486 2.02-4.5 4.512-4.5A4.506 4.506 0 0 1 9.025 8m4.95 0c0 2.34-1.01 4.236-2.256 4.236S9.463 10.339 9.463 8c0-2.34 1.01-4.236 2.256-4.236S13.975 5.661 13.975 8M16 8c0 2.096-.355 3.795-.794 3.795-.438 0-.793-1.7-.793-3.795 0-2.096.355-3.795.794-3.795.438 0 .793 1.699.793 3.795"/>
</svg>
</a>
</div>

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="container flex min-h-[400px] flex-col items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl">
{$page.status}: {$page.error?.message || 'Something went wrong'}
</h1>
<p class="mt-4 text-muted-foreground">
{#if $page.status === 404}
Sorry, we couldn't find the posts you're looking for.
{:else}
An error occurred while loading the posts. Please try again later.
{/if}
</p>
<div class="mt-8">
<a
href="/posts"
class="inline-flex items-center justify-center rounded-md bg-primary px-8 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Try Again
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import Content from './Obsidian.md'
</script>
{#if Content}
<article class="container max-w-3xl">
<div class="space-y-4">
<svelte:component this={Content} />
</div>
</article>
{:else}
<div class="container py-12">
<h1 class="mb-8 text-3xl font-bold">Sorry</h1>
<div class="flex min-h-[400px] items-center justify-center text-center">
<p class="text-lg font-medium">Nothing found</p>
<p class="mt-2 text-sm text-muted-foreground">Check back later for new content.</p>
</div>
</div>
{/if}

View File

@@ -0,0 +1,5 @@
import { dev } from '$app/environment';
export const csr = dev;
export const prerender = true;

View File

@@ -0,0 +1,63 @@
---
title: Obsidian
description: Create and manage your notes with Obsidian!
date: 2024-11-16
tags: [type/note,obsidian]
---
<div align="center">
<a href="https://obsidian.md" target="_blank" rel="noopener noreferrer">
<img src="/obsidian-logo.png" alt="Obsidian Logo" style="max-width: 20%; height: auto;" />
</a>
</div>
While you don't need to use Obsidian to write your blog posts, it's a great tool for managing your notes.
## What is Obsidian?
Obsidian is a powerful knowledge base that works on top of a local folder of plain text Markdown files. It's designed for creating and managing interconnected notes.
### Key Features
- 📝 **Plain Text** - All notes are stored as local Markdown files, i.e. they last forever
- 🔗 **Bidirectional Linking** - Create connections between notes like wikilinks
- 🎨 **Graph View** - Visualize your knowledge network with knowledge graphs
- 🧩 **Plugin System** - Extend functionality with community plugins
### Example Note Structure
```markdown
# Project Planning
## Goals
- Define project scope
- Set milestones
- Assign resources
## Links
[[Resources]]
[[Timeline]]
[[Team Members]]
```
### Why Use Obsidian?
1. **Privacy First** - Your data stays on your device
2. **Future Proof** - Plain text files never become obsolete
3. **Flexible** - Adapt it to your workflow
4. **Community Driven** - Rich ecosystem of themes and plugins
## Getting Started
1. **Install Obsidian** - Download from [obsidian.md](https://obsidian.md/)
2. **Create a Vault** - Create a local folder for your notes. Try opening a vault in the `src/lib/content` directory.
3. **Start Writing** - Create your first note
## Next Steps
1. **Organize Your Notes** - Create folders for potential posts. All posts are currently placed in the `posts` folder of the `src/lib/content/{posts}` directory. This can be chaged in the `src/lib/content/posts/index.ts` file.
2. **Develop Templates** - Develop templates for your posts
3. **Develop SvelteKit Components** - Build components for displaying content using metadata and templates
4. **Publish Notes** - Drag notes into specific folders in Obsidian to publish them
5. **Test and Refine Workflow** - Regularly test the publishing process by running the SvelteKit development server
6. **Share and Collaborate**
Check out some examples of the posts you can make over at [Posts](/posts)

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="container flex min-h-[400px] flex-col items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl">
{$page.status}: {$page.error?.message || 'Something went wrong'}
</h1>
<p class="mt-4 text-muted-foreground">
{#if $page.status === 404}
Sorry, we couldn't find the posts you're looking for.
{:else}
An error occurred while loading the posts. Please try again later.
{/if}
</p>
<div class="mt-8">
<a
href="/posts"
class="inline-flex items-center justify-center rounded-md bg-primary px-8 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Try Again
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { formatDistance } from 'date-fns';
import type { PageData } from './$types';
import { Paginator } from '@skeletonlabs/skeleton'
// import Spinner from '$lib/components/ui/spinner/spinner.svelte';
export let data: PageData;
$: posts = data.posts;
let visible: boolean = true;
let message: string = "No posts found";
</script>
<div class="container py-12">
<h1 class="mb-4 text-3xl font-bold">Blog Posts</h1>
<p class="text-sm mb-8 font-small">This blog is maintained in an Obsidian Vault</p>
{#if posts.length === 0}
{#if !visible}
<aside class="alert variant-ghost">
<div>(icon)</div>
<slot:fragment href="./+error.svelte" />
<div class="alert-actions">(buttons)</div>
</aside>
{/if}
{:else}
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each posts as post}
<article class="card card-hover group relative rounded-lg border p-6 hover:bg-muted/50">
<a href="/posts/{post.slug}" class="absolute inset-0">
<span class="sr-only">View {post.meta.title}</span>
</a>
<div class="flex flex-col justify-between space-y-4">
<div class="space-y-2">
<h2 class="text-xl font-semibold tracking-tight">{post.meta.title}</h2>
<p class="text-muted-foreground">{post.meta.description}</p>
</div>
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
<time datetime={post.meta.date}>
{formatDistance(new Date(post.meta.date), new Date(), { addSuffix: true })}
</time>
{#if post.meta.tags.length > 0}
<span class="text-xs"></span>
<div class="flex flex-wrap gap-2">
{#each post.meta.tags as 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-secondary"
>
{tag}
</a>
{/each}
</div>
{/if}
</div>
</div>
</article>
{/each}
<!-- <Paginator records={posts} limit={6} buttonClass="btn" /> -->
</div>
{/if}
</div>

View File

@@ -0,0 +1,42 @@
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
// import type { PostMetadata } from '$lib/types';
export const load: PageLoad = async () => {
try {
const postFiles = import.meta.glob('/src/lib/content/posts/*.{md,svx}', { eager: true });
if (Object.keys(postFiles).length === 0) {
return {
posts: []
};
}
const posts = Object.entries(postFiles).map(([path, post]: [string, any]) => {
const slug = path.split('/').pop()?.replace(/\.(md|svx)$/, '');
return {
slug,
meta: {
title: post.metadata?.title || 'Untitled',
date: post.metadata?.date || new Date().toISOString(),
created: post.metadata?.created || new Date().toISOString(),
description: post.metadata?.description || '',
tags: post.metadata?.tags || [],
aliases: post.metadata?.aliases || [],
lead: post.metadata?.lead || '',
updated: post.metadata?.updated || new Date().toISOString(),
author: post.metadata?.author || 'John Connor',
}
};
});
posts.sort((a, b) => new Date(b.meta.date).getTime() - new Date(a.meta.date).getTime());
return {
posts
};
} catch (e) {
console.error('Failed to load posts:', e);
throw error(500, 'Failed to load posts');
}
};

View File

@@ -0,0 +1,26 @@
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="container flex min-h-[400px] flex-col items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-bold tracking-tighter sm:text-5xl">
{$page.status}: {$page.error?.message || 'Something went wrong'}
</h1>
<p class="mt-4 text-muted-foreground">
{#if $page.status === 404}
Sorry, we couldn't find the post you're looking for.
{:else}
An error occurred while loading the post. Please try again later.
{/if}
</p>
<div class="mt-8">
<a
href="/posts"
class="inline-flex items-center justify-center rounded-md bg-primary px-8 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Back to Posts
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,54 @@
<script lang="ts">
import { formatDistance } from 'date-fns';
import type { PageData } from './$types';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
export let data: PageData;
$: ({ content: Content, meta } = data);
$: formattedDate = formatDistance(new Date(meta.date), new Date(), { addSuffix: true });
</script>
<article class="container max-w-3xl py-6 lg:py-2">
{#await Content}
<div class="flex min-h-[400px] items-center justify-center">
<div class="flex items-center gap-2">
<Spinner class="h-6 w-6" />
<span class="text-sm text-muted-foreground">Loading post...</span>
</div>
</div>
{:then Content}
<!-- <div class="space-y-4">
<h1 class="inline-block text-4xl font-bold lg:text-5xl">{meta.title}</h1>
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
<time datetime={meta.date}>{formattedDate}</time>
<span class="text-xs">•</span>
<div class="flex items-center space-x-2">
{#each meta.tags as 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-secondary"
>
{tag}
</a>
{/each}
</div>
</div>
</div>
<hr class="my-8" /> -->
<div class="prose prose-slate dark:prose-invert">
<svelte:component this={Content} />
</div>
{:catch error}
<div class="flex min-h-[400px] flex-col items-center justify-center text-center">
<p class="text-lg font-medium">Failed to load post</p>
<p class="mt-2 text-sm text-muted-foreground">{error.message}</p>
<a
href="/posts"
class="mt-4 inline-flex items-center justify-center rounded-md bg-primary px-8 py-2 text-sm font-medium text-primary-foreground ring-offset-background transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
Back to Posts
</a>
</div>
{/await}
</article>

View File

@@ -0,0 +1,25 @@
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
try {
const post = await import(`$lib/content/posts/${params.slug}.md`);
return {
content: post.default,
meta: {
title: post.metadata.title,
date: post.metadata.date,
description: post.metadata.description,
tags: post.metadata.tags || [],
updated: post.metadata.updated || new Date().toISOString(),
author: post.metadata.author || '',
lead: post.metadata.lead || '',
reference: post.metadata.reference || '',
}
};
} catch (e) {
console.error(`Failed to load post ${params.slug}:`, e);
throw error(404, `Could not find post ${params.slug}`);
}
};

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import type { PageData } from './$types';
// import TagList from '$components/ui/tag-list/TagList.svelte';
export let data: PageData;
$: tags = data.tags;
$: postsCount = data.postsCount;
</script>
<div class="container py-12">
<h1 class="mb-8 text-3xl font-bold">{Object.keys(tags).length} Tags in {postsCount} Posts</h1>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{#each Object.entries(tags) as [tag, posts]}
<a
href="/tags/{tag}"
class="flex items-center justify-between rounded-lg border p-4 hover:bg-muted/50"
>
<span class="text-lg font-semibold">{tag}</span>
<span class="text-sm text-muted-foreground">{posts.length} posts</span>
<!-- WIP -->
<!-- <TagList tags={posts.map((post) => post.meta.tags).flat()} /> -->
</a>
{/each}
</div>
</div>

View File

@@ -0,0 +1,34 @@
import type { PageLoad } from './$types';
import type { PostMetadata } from '$lib/types/types';
export const load: PageLoad = async () => {
const postFiles = import.meta.glob('/src/lib/content/posts/*.{md,svx}', { eager: true });
const posts = Object.entries(postFiles).map(([path, post]: [string, any]) => {
const slug = path.split('/').pop()?.replace(/\.(md|svx)$/, '');
return {
slug,
meta: {
title: post.metadata.title,
date: post.metadata.date,
description: post.metadata.description,
tags: post.metadata.tags || []
}
};
});
const tags = posts.reduce((acc, post) => {
post.meta.tags.forEach((tag: string) => {
if (!acc[tag]) {
acc[tag] = [];
}
acc[tag].push(post);
});
return acc;
}, {} as Record<string, PostMetadata[]>);
return {
tags,
postsCount: posts.length
};
};

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { formatDistance } from 'date-fns';
import type { PageData } from './$types';
export let data: PageData;
$: ({ tag, posts } = data);
</script>
<div class="container py-12">
<div class="mb-8 flex items-center justify-between">
<h1 class="text-3xl font-bold">Posts tagged with "{tag}"</h1>
<a href="/tags" class="text-sm text-muted-foreground hover:text-foreground">← Back to tags</a>
</div>
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each posts as post}
<article class="group relative rounded-lg border p-6 hover:bg-muted/50">
<a href="/posts/{post.slug}" class="absolute inset-0">
<span class="sr-only">View {post.meta.title}</span>
</a>
<div class="flex flex-col justify-between space-y-4">
<div class="space-y-2">
<h2 class="text-xl font-semibold tracking-tight">{post.meta.title}</h2>
<p class="text-muted-foreground">{post.meta.description}</p>
</div>
<div class="flex items-center space-x-4 text-sm text-muted-foreground">
<time datetime={post.meta.date}>
{formatDistance(new Date(post.meta.date), new Date(), { addSuffix: true })}
</time>
</div>
</div>
</article>
{/each}
</div>
</div>

View File

@@ -0,0 +1,30 @@
import { error } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
const postFiles = import.meta.glob('/src/lib/content/posts/*.{md,svx}', { eager: true });
const posts = Object.entries(postFiles).map(([path, post]: [string, any]) => {
const slug = path.split('/').pop()?.replace(/\.(md|svx)$/, '');
return {
slug,
meta: {
title: post.metadata.title,
date: post.metadata.date,
description: post.metadata.description,
tags: post.metadata.tags || []
}
};
});
const tagPosts = posts.filter((post) => post.meta.tags.includes(params.tag));
if (tagPosts.length === 0) {
throw error(404, `Tag "${params.tag}" not found`);
}
return {
tag: params.tag,
posts: tagPosts
};
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 MiB

BIN
web/static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 MiB

BIN
web/static/obsidian-logo.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

3
web/static/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

58
web/svelte.config.js Normal file
View File

@@ -0,0 +1,58 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
import { mdsvex } from 'mdsvex';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeExternalLinks from 'rehype-external-links';
import rehypeUnwrapImages from 'rehype-unwrap-images';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
/** @type {import('mdsvex').MdsvexOptions} */
const mdsvexOptions = {
extensions: ['.md', '.svx'],
layout: {
_: join(__dirname, './src/lib/layouts/post.svelte')
},
highlight: {
theme: {
dark: 'github-dark',
light: 'github-light'
}
},
rehypePlugins: [
rehypeSlug,
rehypeUnwrapImages,
[rehypeAutolinkHeadings, {
behavior: 'wrap'
}],
[rehypeExternalLinks, {
target: '_blank',
rel: ['noopener', 'noreferrer']
}]
]
};
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
alias: {
$components: join(__dirname, 'src/lib/components'),
$lib: join(__dirname, 'src/lib'),
$styles: join(__dirname, 'src/styles'),
$utils: join(__dirname, 'src/lib/utils')
}
},
extensions: ['.svelte', '.md', '.svx'],
preprocess: [
vitePreprocess(),
mdsvex(mdsvexOptions)
]
};
export default config;

111
web/tailwind.config.ts Normal file
View File

@@ -0,0 +1,111 @@
import { join } from 'path';
import type { Config } from 'tailwindcss';
import forms from '@tailwindcss/forms';
import typography from '@tailwindcss/typography';
import { skeleton } from '@skeletonlabs/tw-plugin';
import { myCustomTheme } from './my-custom-theme.ts'
export default {
darkMode: 'class',
content: [
'./src/**/*.{html,js,svelte,svx,md,ts}',
join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts,svx,md}')
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
fontFamily: {
mono: ['Fira Code', 'monospace'],
},
animation: {
fabGradient: 'fabGradient 15s ease infinite',
blink: 'blink 1s step-end infinite',
},
keyframes: {
fabGradient: {
'0%, 100%': { 'background-size': '200% 200%, background-position: left center' },
'50%': { 'background-size': '200% 200%, background-position: right center' },
},
blink: {
'0%, 100%': { opacity: 1 },
'50%': { opacity: 0 },
},
},
typography: {
DEFAULT: {
css: {
'code::before': {
content: '""'
},
'code::after': {
content: '""'
}
}
}
}
},
},
plugins: [
forms,
typography,
skeleton({
themes: {
preset: [
{
name: 'skeleton',
enhancements: true
},
{
name: 'modern',
enhancements: true
}
],
custom: [
myCustomTheme
]
}
})
]
} satisfies Config;

19
web/tsconfig.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
}

17
web/vite.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import { purgeCss } from 'vite-plugin-tailwind-purgecss';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { mdsvex } from 'mdsvex';
export default defineConfig({
plugins: [sveltekit(), mdsvex(), purgeCss()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});