feat: initial Crablo Control Center dashboard
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# Output
|
||||||
|
.output
|
||||||
|
.vercel
|
||||||
|
.netlify
|
||||||
|
.wrangler
|
||||||
|
/.svelte-kit
|
||||||
|
/build
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.test
|
||||||
|
|
||||||
|
# Vite
|
||||||
|
vite.config.js.timestamp-*
|
||||||
|
vite.config.ts.timestamp-*
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"svelte.svelte-vscode",
|
||||||
|
"bradlc.vscode-tailwindcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# sv
|
||||||
|
|
||||||
|
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||||
|
|
||||||
|
## Creating a project
|
||||||
|
|
||||||
|
If you're seeing this, you've probably already done this step. Congrats!
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# create a new project
|
||||||
|
npx sv create my-app
|
||||||
|
```
|
||||||
|
|
||||||
|
To recreate this project with the same configuration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# recreate this project
|
||||||
|
npx sv@0.15.2 create --template minimal --types ts --add tailwindcss="plugins:none" sveltekit-adapter="adapter:node" --install npm .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Developing
|
||||||
|
|
||||||
|
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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://svelte.dev/docs/kit/adapters) for your target environment.
|
||||||
Generated
+3043
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "crablo-dashboard",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/adapter-node": "^5.5.4",
|
||||||
|
"@sveltejs/kit": "^2.59.1",
|
||||||
|
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"postcss": "^8.4.24",
|
||||||
|
"svelte": "^5.55.5",
|
||||||
|
"svelte-check": "^3.8.6",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"vite": "^5.4.21"
|
||||||
|
},
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
// for information about these interfaces
|
||||||
|
declare global {
|
||||||
|
namespace App {
|
||||||
|
// interface Error {}
|
||||||
|
// interface Locals {}
|
||||||
|
// interface PageData {}
|
||||||
|
// interface PageState {}
|
||||||
|
// interface Platform {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="text-scale" content="scale" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
<body data-sveltekit-preload-data="hover">
|
||||||
|
<div style="display: contents">%sveltekit.body%</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1 @@
|
|||||||
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import './layout.css';
|
||||||
|
import { page } from '$app/state';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="shell">
|
||||||
|
<nav>
|
||||||
|
<a href="/" class="brand">🦀 crablo</a>
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/" class:active={page.url.pathname === '/'}>issues</a>
|
||||||
|
<a href="/agents" class:active={page.url.pathname === '/agents'}>agents</a>
|
||||||
|
<a href="/life" class:active={page.url.pathname === '/life'}>life</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
interface Issue {
|
||||||
|
title: string;
|
||||||
|
status: 'open' | 'resolved';
|
||||||
|
firstSeen?: string;
|
||||||
|
symptom?: string;
|
||||||
|
workaround?: string;
|
||||||
|
realFix?: string;
|
||||||
|
resolvedNote?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIssues(md: string): Issue[] {
|
||||||
|
const issues: Issue[] = [];
|
||||||
|
// Split on ### headers
|
||||||
|
const sections = md.split(/\n### /);
|
||||||
|
for (const section of sections.slice(1)) {
|
||||||
|
const lines = section.split('\n');
|
||||||
|
const title = lines[0].trim();
|
||||||
|
const body = lines.slice(1).join('\n');
|
||||||
|
|
||||||
|
const get = (key: string) => {
|
||||||
|
const m = body.match(new RegExp(`\\*\\*${key}\\*\\*:\\s*(.+)`));
|
||||||
|
return m ? m[1].replace(/\[\[.*?\]\]/g, s => s.replace(/\[\[.*?\/([^\]|]+).*?\]\]/, '$1')).trim() : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isResolved = md.includes('## Resolved') &&
|
||||||
|
md.indexOf('### ' + title) > md.indexOf('## Resolved');
|
||||||
|
|
||||||
|
issues.push({
|
||||||
|
title,
|
||||||
|
status: isResolved ? 'resolved' : 'open',
|
||||||
|
firstSeen: get('First seen'),
|
||||||
|
symptom: get('Symptom'),
|
||||||
|
workaround: get('Workaround'),
|
||||||
|
realFix: get('Real fix') || get('Fix'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
let issues: Issue[] = [];
|
||||||
|
try {
|
||||||
|
const md = readFileSync(
|
||||||
|
join(process.env.HOME || '/data/workspace', 'workspace/crablo/memory/active-issues.md'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
issues = parseIssues(md);
|
||||||
|
} catch (e) {
|
||||||
|
// file not found or parse error
|
||||||
|
}
|
||||||
|
|
||||||
|
return { issues };
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { data } = $props<{ data: { issues: Array<{
|
||||||
|
title: string; status: string; firstSeen?: string;
|
||||||
|
symptom?: string; workaround?: string; realFix?: string;
|
||||||
|
}> } }>();
|
||||||
|
|
||||||
|
let open = $derived(data.issues.filter((i: any) => i.status === 'open'));
|
||||||
|
let resolved = $derived(data.issues.filter((i: any) => i.status === 'resolved'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head><title>crablo // issues</title></svelte:head>
|
||||||
|
|
||||||
|
<h1>Active Issues <span class="dim">({open.length} open)</span></h1>
|
||||||
|
|
||||||
|
{#each open as issue}
|
||||||
|
<div class="card">
|
||||||
|
<div style="display:flex; align-items:center; gap:0.75rem; margin-bottom:0.6rem;">
|
||||||
|
<span class="tag open">open</span>
|
||||||
|
<span style="font-weight:500; font-size:0.95rem;">{issue.title}</span>
|
||||||
|
{#if issue.firstSeen}
|
||||||
|
<span class="dim" style="font-size:0.75rem; margin-left:auto;">{issue.firstSeen}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if issue.symptom}
|
||||||
|
<div class="label">symptom</div>
|
||||||
|
<div class="value" style="margin-bottom:0.5rem;">{issue.symptom}</div>
|
||||||
|
{/if}
|
||||||
|
{#if issue.workaround}
|
||||||
|
<div class="label">workaround</div>
|
||||||
|
<div class="value dim" style="margin-bottom:0.5rem;">{issue.workaround}</div>
|
||||||
|
{/if}
|
||||||
|
{#if issue.realFix}
|
||||||
|
<div class="label">real fix</div>
|
||||||
|
<div class="value" style="color: var(--amber);">{issue.realFix}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if resolved.length > 0}
|
||||||
|
<details style="margin-top:1.5rem;">
|
||||||
|
<summary>Resolved ({resolved.length})</summary>
|
||||||
|
<div style="margin-top:0.75rem;">
|
||||||
|
{#each resolved as issue}
|
||||||
|
<div class="card">
|
||||||
|
<span class="tag resolved">resolved</span>
|
||||||
|
<span style="font-size:0.9rem;">{issue.title}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if data.issues.length === 0}
|
||||||
|
<div class="dim" style="font-size:0.85rem; padding: 2rem 0;">no issues found — either everything's fine or the file didn't parse</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
let agents: any[] = [];
|
||||||
|
let defaults: any = {};
|
||||||
|
try {
|
||||||
|
const cfg = JSON.parse(readFileSync('/data/.openclaw/openclaw.json', 'utf-8'));
|
||||||
|
agents = cfg?.agents?.list || [];
|
||||||
|
defaults = cfg?.agents?.defaults || {};
|
||||||
|
} catch (e) {}
|
||||||
|
return { agents, defaults };
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
let { data } = $props<{ data: { agents: any[]; defaults: any } }>();
|
||||||
|
const thinking: Record<string, string> = { off: '—', low: '🔅', medium: '🔆', high: '🔆🔆', adaptive: '~' };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head><title>crablo // agents</title></svelte:head>
|
||||||
|
|
||||||
|
<h1>Agents <span class="dim">({data.agents.length})</span></h1>
|
||||||
|
|
||||||
|
<div class="grid-2">
|
||||||
|
{#each data.agents as agent}
|
||||||
|
<div class="card">
|
||||||
|
<div style="display:flex; align-items:baseline; gap:0.6rem; margin-bottom:0.6rem;">
|
||||||
|
<span style="font-weight:600; color:var(--amber);">{agent.name || agent.id}</span>
|
||||||
|
{#if agent.default}<span class="tag warn">default</span>{/if}
|
||||||
|
{#if agent.id !== (agent.name || agent.id)}<span class="dim" style="font-size:0.75rem;">{agent.id}</span>{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="label">primary model</div>
|
||||||
|
<div class="value" style="margin-bottom:0.4rem;">
|
||||||
|
<span class="pill primary">{agent.model?.primary || data.defaults?.model?.primary || '—'}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if agent.model?.fallbacks?.length}
|
||||||
|
<div class="label">fallbacks</div>
|
||||||
|
<div style="margin-bottom:0.4rem;">
|
||||||
|
{#each agent.model.fallbacks as fb}
|
||||||
|
<span class="pill">{fb}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if agent.thinkingDefault}
|
||||||
|
<div class="label">thinking</div>
|
||||||
|
<div class="value">{thinking[agent.thinkingDefault] || agent.thinkingDefault}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:2rem;">
|
||||||
|
<h2>Default fallback chain</h2>
|
||||||
|
<div style="margin-top:0.5rem;">
|
||||||
|
{#each (data.defaults?.model?.fallbacks || []) as fb, i}
|
||||||
|
<span class="pill">{i+1}. {fb}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500&display=swap');
|
||||||
|
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #080810;
|
||||||
|
--bg2: #0f0f1a;
|
||||||
|
--bg3: #16162a;
|
||||||
|
--border: #1e1e3a;
|
||||||
|
--amber: #f5a623;
|
||||||
|
--amber-dim: #7a5210;
|
||||||
|
--green: #39ff85;
|
||||||
|
--green-dim: #1a5c3a;
|
||||||
|
--red: #ff4455;
|
||||||
|
--red-dim: #5c1a20;
|
||||||
|
--blue: #4488ff;
|
||||||
|
--text: #d4d4e8;
|
||||||
|
--text-dim: #6666aa;
|
||||||
|
--mono: 'IBM Plex Mono', monospace;
|
||||||
|
--sans: 'IBM Plex Sans', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body { background: var(--bg); color: var(--text); font-family: var(--mono); height: 100%; }
|
||||||
|
|
||||||
|
.shell { display: flex; flex-direction: column; min-height: 100vh; }
|
||||||
|
|
||||||
|
nav {
|
||||||
|
display: flex; align-items: center; gap: 2rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg2);
|
||||||
|
position: sticky; top: 0; z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand { color: var(--amber); font-weight: 600; font-size: 1rem; text-decoration: none; letter-spacing: 0.05em; }
|
||||||
|
|
||||||
|
.nav-links { display: flex; gap: 1.5rem; }
|
||||||
|
.nav-links a { color: var(--text-dim); text-decoration: none; font-size: 0.85rem; letter-spacing: 0.08em; text-transform: uppercase; transition: color 0.15s; }
|
||||||
|
.nav-links a:hover, .nav-links a.active { color: var(--amber); }
|
||||||
|
|
||||||
|
main { flex: 1; padding: 2rem 1.5rem; max-width: 960px; margin: 0 auto; width: 100%; }
|
||||||
|
|
||||||
|
h1 { font-size: 1.1rem; font-weight: 600; color: var(--amber); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 1.5rem; }
|
||||||
|
h2 { font-size: 0.85rem; font-weight: 500; color: var(--text-dim); letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 1rem; }
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--bg2); border: 1px solid var(--border);
|
||||||
|
padding: 1rem 1.25rem; margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.card:hover { border-color: var(--amber-dim); }
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block; font-size: 0.7rem; padding: 0.15rem 0.5rem;
|
||||||
|
border: 1px solid; letter-spacing: 0.08em; text-transform: uppercase; margin-right: 0.4rem;
|
||||||
|
}
|
||||||
|
.tag.open { color: var(--red); border-color: var(--red-dim); background: rgba(255,68,85,0.06); }
|
||||||
|
.tag.resolved { color: var(--green); border-color: var(--green-dim); background: rgba(57,255,133,0.06); }
|
||||||
|
.tag.warn { color: var(--amber); border-color: var(--amber-dim); background: rgba(245,166,35,0.06); }
|
||||||
|
|
||||||
|
.label { font-size: 0.72rem; color: var(--text-dim); letter-spacing: 0.06em; margin-bottom: 0.25rem; }
|
||||||
|
.value { font-size: 0.88rem; color: var(--text); }
|
||||||
|
.dim { color: var(--text-dim); }
|
||||||
|
|
||||||
|
details > summary { cursor: pointer; list-style: none; color: var(--text-dim); font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.5rem 0; }
|
||||||
|
details > summary::before { content: '▶ '; }
|
||||||
|
details[open] > summary::before { content: '▼ '; }
|
||||||
|
|
||||||
|
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
|
||||||
|
@media (max-width: 600px) { .grid-2 { grid-template-columns: 1fr; } }
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: inline-block; font-size: 0.68rem; padding: 0.1rem 0.45rem;
|
||||||
|
background: var(--bg3); border: 1px solid var(--border); color: var(--text-dim);
|
||||||
|
letter-spacing: 0.05em; margin-right: 0.3rem; margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
.pill.primary { color: var(--amber); border-color: var(--amber-dim); }
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
const fitness = [
|
||||||
|
{ goal: 'Sign up for climbing gym', note: 'Miss it. Good cardio + social.', status: 'todo' },
|
||||||
|
{ goal: 'Regular cardio', note: 'Goal: a few times a week', status: 'todo' },
|
||||||
|
{ goal: 'Lifting', note: 'Get a little more jacked.', status: 'todo' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const recipes = [
|
||||||
|
{
|
||||||
|
name: 'Dandan Noodles',
|
||||||
|
note: 'Had as takeout. Need to make it again.',
|
||||||
|
ingredients: 'Ramen noodles, peanut butter sauce, ground beef, bok choy',
|
||||||
|
tags: ['easy', 'cheap', 'satisfying', 'noodles'],
|
||||||
|
status: 'want-to-make',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const health = [
|
||||||
|
{ label: 'Blood pressure', value: '~140/70', note: 'Hypertension. Primary motivation for fitness + lifestyle changes.' },
|
||||||
|
{ label: 'Oura ring', value: 'connected', note: 'Biometrics available. Explore HRV, sleep, readiness over time.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const farmshare = { day: 'Wednesday evenings', note: 'Plan weekly recipes around what comes in.' };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head><title>crablo // life</title></svelte:head>
|
||||||
|
|
||||||
|
<h1>Life</h1>
|
||||||
|
|
||||||
|
<section style="margin-bottom:2rem;">
|
||||||
|
<h2>🧗 Fitness</h2>
|
||||||
|
{#each fitness as item}
|
||||||
|
<div class="card" style="display:flex; gap:1rem; align-items:flex-start;">
|
||||||
|
<span style="color: {item.status === 'done' ? 'var(--green)' : 'var(--text-dim)'}; font-size:1.1rem;">
|
||||||
|
{item.status === 'done' ? '✓' : '○'}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<div class="value">{item.goal}</div>
|
||||||
|
{#if item.note}<div class="dim" style="font-size:0.8rem; margin-top:0.2rem;">{item.note}</div>{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom:2rem;">
|
||||||
|
<h2>🍜 Cooking</h2>
|
||||||
|
<div class="card" style="margin-bottom:1rem; border-color: var(--amber-dim);">
|
||||||
|
<div class="label">farmshare</div>
|
||||||
|
<div class="value">{farmshare.day} — <span class="dim">{farmshare.note}</span></div>
|
||||||
|
</div>
|
||||||
|
{#each recipes as r}
|
||||||
|
<div class="card">
|
||||||
|
<div style="display:flex; align-items:center; gap:0.75rem; margin-bottom:0.5rem;">
|
||||||
|
<span class="tag warn">{r.status}</span>
|
||||||
|
<span style="font-weight:500;">{r.name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="label">ingredients</div>
|
||||||
|
<div class="value" style="margin-bottom:0.4rem;">{r.ingredients}</div>
|
||||||
|
{#if r.note}<div class="dim" style="font-size:0.8rem;">{r.note}</div>{/if}
|
||||||
|
<div style="margin-top:0.5rem;">
|
||||||
|
{#each r.tags as tag}<span class="pill">{tag}</span>{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section style="margin-bottom:2rem;">
|
||||||
|
<h2>❤️ Health</h2>
|
||||||
|
{#each health as item}
|
||||||
|
<div class="card">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:baseline; margin-bottom:0.3rem;">
|
||||||
|
<span class="value">{item.label}</span>
|
||||||
|
<span style="color:var(--amber); font-size:0.85rem;">{item.value}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dim" style="font-size:0.8rem;">{item.note}</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>📋 On the list</h2>
|
||||||
|
{#each ['Clean the room', 'Start job search'] as item}
|
||||||
|
<div class="card">
|
||||||
|
<span style="color:var(--text-dim);">○</span>
|
||||||
|
<span style="margin-left:0.75rem;">{item}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# allow crawling everything by default
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import adapter from '@sveltejs/adapter-node';
|
||||||
|
|
||||||
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
const config = {
|
||||||
|
compilerOptions: {
|
||||||
|
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
|
||||||
|
runes: ({ filename }) => filename.split(/[/\\]/).includes('node_modules') ? undefined : true
|
||||||
|
},
|
||||||
|
kit: { adapter: adapter() }
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rewriteRelativeImportExtensions": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"moduleResolution": "bundler"
|
||||||
|
}
|
||||||
|
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||||
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
//
|
||||||
|
// To make changes to top-level options such as include and exclude, we recommend extending
|
||||||
|
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig({ plugins: [sveltekit()] });
|
||||||
Reference in New Issue
Block a user