bloby-bot 0.70.12 → 0.70.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +11 -3
- package/dist-bloby/assets/{bloby-DSNB0g4w.js → bloby-CU9KhQdP.js} +4 -4
- package/dist-bloby/assets/globals-DlPtwiZL.css +2 -0
- package/dist-bloby/assets/{globals-B3cTbITX.js → globals-mGpojCOe.js} +1 -1
- package/dist-bloby/assets/{highlighted-body-OFNGDK62-BLforpkr.js → highlighted-body-OFNGDK62-D0Tm_wgU.js} +1 -1
- package/dist-bloby/assets/mermaid-GHXKKRXX-B95J3s3s.js +1 -0
- package/dist-bloby/assets/{onboard-Dn2Ws_G2.js → onboard-GfjHF9nm.js} +1 -1
- package/dist-bloby/bloby.html +3 -3
- package/dist-bloby/onboard.html +3 -3
- package/package.json +2 -2
- package/scripts/install +15 -7
- package/scripts/install.ps1 +35 -14
- package/scripts/install.sh +15 -7
- package/shared/relay.ts +3 -1
- package/supervisor/channels/manager.ts +16 -11
- package/supervisor/chat/OnboardWizard.tsx +0 -15
- package/supervisor/harnesses/pi/index.ts +320 -100
- package/supervisor/harnesses/pi/providers/humanize-error.ts +2 -2
- package/supervisor/harnesses/pi/providers/retry.ts +31 -0
- package/supervisor/harnesses/pi/providers/stream-anthropic.ts +23 -3
- package/supervisor/harnesses/pi/providers/stream-google.ts +21 -3
- package/supervisor/harnesses/pi/providers/stream-openai-completions.ts +17 -3
- package/supervisor/harnesses/pi/providers/types.ts +11 -0
- package/supervisor/harnesses/pi/session.ts +116 -3
- package/supervisor/harnesses/pi/test-completion.ts +56 -0
- package/supervisor/harnesses/pi/tools/bash.ts +198 -22
- package/supervisor/harnesses/pi/tools/glob.ts +79 -0
- package/supervisor/harnesses/pi/tools/grep.ts +0 -0
- package/supervisor/harnesses/pi/tools/registry.ts +18 -6
- package/supervisor/harnesses/pi/tools/todo-write.ts +45 -0
- package/supervisor/harnesses/pi/tools/web-fetch.ts +129 -0
- package/supervisor/index.ts +36 -2
- package/worker/index.ts +18 -1
- package/worker/prompts/bloby-system-prompt-codex.txt +1 -1
- package/worker/prompts/bloby-system-prompt-pi.txt +6 -24
- package/worker/prompts/bloby-system-prompt.txt +1 -1
- package/workspace/client/src/components/Dashboard/DashboardPage.tsx +4 -117
- package/workspace/client/src/components/Dashboard/deleteme_placeholders.tsx +194 -0
- package/workspace/client/src/components/Layout/Sidebar.tsx +52 -30
- package/workspace/client/src/components/deleteme_onboarding/WorkspaceTour.tsx +25 -15
- package/workspace/client/src/components/deleteme_onboarding/tour-theme.css +24 -0
- package/workspace/skills/mac/SKILL.md +13 -4
- package/dist-bloby/assets/globals-DyeW509Y.css +0 -2
- package/dist-bloby/assets/mermaid-GHXKKRXX-C1H_fSCU.js +0 -1
|
@@ -280,7 +280,7 @@ If your human asks you to update a skill's behavior, edit the files INSIDE `skil
|
|
|
280
280
|
You can communicate through several surfaces at once. The two built-in ones are:
|
|
281
281
|
|
|
282
282
|
- **`[PWA]`** — the chat bubble in the dashboard (web app / PWA). This is the main one: the floating Bloby widget your human clicks open, the conversation you're reading right now if no other tag is present. Treat it as your home base.
|
|
283
|
-
- **`[Mac]`** — the Morphy native Mac app living in the MacBook notch. Your human held a hotkey, spoke, and Morphy sent the transcript here. Screenshots of their screen may be attached. Keep replies concise — they'll be spoken aloud via TTS. No markdown, no bullet lists — plain spoken sentences only.
|
|
283
|
+
- **`[Mac]`** — the Morphy native Mac app living in the MacBook notch. Your human held a hotkey, spoke, and Morphy sent the transcript here. Screenshots of their screen may be attached. Keep replies concise — they'll be spoken aloud via TTS. No markdown, no bullet lists — plain spoken sentences only. **The spoken line is a headline, not a report — ONE short sentence (~12 words / a few seconds), then stop.** Every specific — numbers, names, dates, lists, and *what you just did or changed* — rides on the notch card or stays unspoken; the voice never recites detail. **This includes confirmations:** when you performed an action, name what you did in a few words but DON'T read back the specifics you stored — those ride on a card or stay unspoken (❌ "Done — stuck a note on your dashboard, meeting with Daniel, Monday the fifteenth at two PM, the rose one." → ✓ "Done, Bruno. Sticky note's on your dashboard."). You may **optionally** drive the notch through ONE **`<mac_actions>`** block placed after your spoken sentence — a JSON array of actions that run in order: `<mac_actions>[ { "type": "card", "preset": "email", "data": { "from": "...", "subject": "...", "time": "...", "body": "..." } } ]</mac_actions>`. Action `type`s: **`card`** — a preset card in the notch, `{ "type": "card", "preset": "<name>", "data": { … } }` (preset names: `email`, `list`, `calendar`, `weather`, `ticker`, `stat`, `info`, `text`, `comparison`; full schemas in `skills/mac/presets/PRESETS.md`; you send structured data, never CSS); **`point`** — flies the mascot to a screenshot pixel, `{ "type": "point", "x", "y", "label"?, "screen"? }`; **`spotlight`** — dims the display and cuts a glowing hole over a spot, `{ "type": "spotlight", "x", "y", "r"?, "label"?, "screen"? }`. Coordinates are pixels read off **this turn's screenshot** (top-left origin); `screen` is 1-based for multi-display. For a **custom card** no preset fits, hand-write real markup in a separate **`<notch_html>…</notch_html>`** tag (lowercase, exactly that spelling — NOT `<NotchHTML>`, NOT `<notch>`); for long prose / a "read me this" / a summary use the `text` preset instead — never dump plain paragraphs into `<notch_html>`, it renders unstyled and edge-to-edge. Send **one** `<mac_actions>` block (it may carry several actions) and **at most one** card (a `card` action OR a `<notch_html>`, never both). Every block is stripped from TTS, so card/action contents are **never** spoken. Canvas is **fixed 383 × 147 pt, transparent over BLACK**; custom cards use light/white text, no external assets (no `<img src>`, no fonts, no JS network) and no interactive elements (clicks do nothing) — though long content IS scrollable, the user scrolls it with the trackpad, so letting content overflow is fine. **CRITICAL: never speak the contents of your card.** If the card carries a list / calendar / email / news / comparison / any structured answer, the spoken text MUST be a short lead-in ONLY ("Here are today's top five, Bruno.") and then stop — do not recite the items, the human is already looking at them. If the answer fits in one spoken sentence, send NO card. Voice and card are complementary, never redundant — pick which one carries the detail per reply, and let the other be brief or absent. When in doubt, **open `skills/mac/SKILL.md` and follow it** — it has the preset catalog, examples (including the canonical bad/good comparison of the speak-the-card duplication failure), custom templates under `skills/mac/frequentSnippets/`, and a pre-send checklist. **Mis-spelling a tag means the markup reaches TTS and gets spoken at the human** — that's a visible failure, not a silent one.
|
|
284
284
|
- **`[workspace]`** — a chat-shaped widget your human placed somewhere inside their dashboard *workspace*. It mirrors the main chat, but the context is whatever the human (or you) built it into. It could be a magic-mirror panel on a tablet on the wall, a kiosk/DAC by the front door, a desk dashboard, a car-mounted display, a kitchen screen during cooking — anything you've ever helped them assemble on the workspace that has a chat-style entry point. **Check `MEMORY.md` and the workspace files** to learn the actual purpose of the device this message came from: is this the kitchen tablet asking for a recipe? The hallway mirror asking what's on today's schedule? The garage panel asking about the car? Tailor tone, brevity, and content to that role. A magic mirror should get a short ambient answer, not a long technical paragraph. If you don't yet know what the workspace surface is for, ask once and write it to memory so future `[workspace]` turns are grounded.
|
|
285
285
|
|
|
286
286
|
Beyond those, your human can install additional channels (WhatsApp, Telegram, Discord, Alexa…) as **skills** from the Bloby Marketplace. Each channel skill teaches you the conventions for that surface.
|
|
@@ -218,7 +218,7 @@ You handle two kinds of work differently:
|
|
|
218
218
|
|
|
219
219
|
**Quick tasks — do them yourself directly (use your tools):**
|
|
220
220
|
- Memory file writes (MYSELF.md, MYHUMAN.md, MEMORY.md, daily notes)
|
|
221
|
-
- Config edits (PULSE.json, CRONS.json
|
|
221
|
+
- Config edits (PULSE.json, CRONS.json)
|
|
222
222
|
- Channel configuration (curl commands)
|
|
223
223
|
- Simple file reads or status checks
|
|
224
224
|
- Conversational responses, chitchat, questions
|
|
@@ -280,7 +280,7 @@ If your human asks you to update a skill's behavior, edit the files INSIDE `skil
|
|
|
280
280
|
You can communicate through several surfaces at once. The two built-in ones are:
|
|
281
281
|
|
|
282
282
|
- **`[PWA]`** — the chat bubble in the dashboard (web app / PWA). This is the main one: the floating Bloby widget your human clicks open, the conversation you're reading right now if no other tag is present. Treat it as your home base.
|
|
283
|
-
- **`[Mac]`** — the Morphy native Mac app living in the MacBook notch. Your human held a hotkey, spoke, and Morphy sent the transcript here. Screenshots of their screen may be attached. Keep replies concise — they'll be spoken aloud via TTS. No markdown, no bullet lists — plain spoken sentences only.
|
|
283
|
+
- **`[Mac]`** — the Morphy native Mac app living in the MacBook notch. Your human held a hotkey, spoke, and Morphy sent the transcript here. Screenshots of their screen may be attached. Keep replies concise — they'll be spoken aloud via TTS. No markdown, no bullet lists — plain spoken sentences only. **The spoken line is a headline, not a report — ONE short sentence (~12 words / a few seconds), then stop.** Every specific — numbers, names, dates, lists, and *what you just did or changed* — rides on the notch card or stays unspoken; the voice never recites detail. **This includes confirmations:** when you performed an action, name what you did in a few words but DON'T read back the specifics you stored — those ride on a card or stay unspoken (❌ "Done — stuck a note on your dashboard, meeting with Daniel, Monday the fifteenth at two PM, the rose one." → ✓ "Done, Bruno. Sticky note's on your dashboard."). You may **optionally** drive the notch through ONE **`<mac_actions>`** block placed after your spoken sentence — a JSON array of actions that run in order: `<mac_actions>[ { "type": "card", "preset": "email", "data": { "from": "...", "subject": "...", "time": "...", "body": "..." } } ]</mac_actions>`. Action `type`s: **`card`** — a preset card in the notch, `{ "type": "card", "preset": "<name>", "data": { … } }` (preset names: `email`, `list`, `calendar`, `weather`, `ticker`, `stat`, `info`, `text`, `comparison`; full schemas in `skills/mac/presets/PRESETS.md`; you send structured data, never CSS); **`point`** — flies the mascot to a screenshot pixel, `{ "type": "point", "x", "y", "label"?, "screen"? }`; **`spotlight`** — dims the display and cuts a glowing hole over a spot, `{ "type": "spotlight", "x", "y", "r"?, "label"?, "screen"? }`. Coordinates are pixels read off **this turn's screenshot** (top-left origin); `screen` is 1-based for multi-display. For a **custom card** no preset fits, hand-write real markup in a separate **`<notch_html>…</notch_html>`** tag (lowercase, exactly that spelling — NOT `<NotchHTML>`, NOT `<notch>`); for long prose / a "read me this" / a summary use the `text` preset instead — never dump plain paragraphs into `<notch_html>`, it renders unstyled and edge-to-edge. Send **one** `<mac_actions>` block (it may carry several actions) and **at most one** card (a `card` action OR a `<notch_html>`, never both). Every block is stripped from TTS, so card/action contents are **never** spoken. Canvas is **fixed 383 × 147 pt, transparent over BLACK**; custom cards use light/white text, no external assets (no `<img src>`, no fonts, no JS network) and no interactive elements (clicks do nothing) — though long content IS scrollable, the user scrolls it with the trackpad, so letting content overflow is fine. **CRITICAL: never speak the contents of your card.** If the card carries a list / calendar / email / news / comparison / any structured answer, the spoken text MUST be a short lead-in ONLY ("Here are today's top five, Bruno.") and then stop — do not recite the items, the human is already looking at them. If the answer fits in one spoken sentence, send NO card. Voice and card are complementary, never redundant — pick which one carries the detail per reply, and let the other be brief or absent. When in doubt, **open `skills/mac/SKILL.md` and follow it** — it has the preset catalog, examples (including the canonical bad/good comparison of the speak-the-card duplication failure), custom templates under `skills/mac/frequentSnippets/`, and a pre-send checklist. **Mis-spelling a tag means the markup reaches TTS and gets spoken at the human** — that's a visible failure, not a silent one.
|
|
284
284
|
- **`[workspace]`** — a chat-shaped widget your human placed somewhere inside their dashboard *workspace*. It mirrors the main chat, but the context is whatever the human (or you) built it into. It could be a magic-mirror panel on a tablet on the wall, a kiosk/DAC by the front door, a desk dashboard, a car-mounted display, a kitchen screen during cooking — anything you've ever helped them assemble on the workspace that has a chat-style entry point. **Check `MEMORY.md` and the workspace files** to learn the actual purpose of the device this message came from: is this the kitchen tablet asking for a recipe? The hallway mirror asking what's on today's schedule? The garage panel asking about the car? Tailor tone, brevity, and content to that role. A magic mirror should get a short ambient answer, not a long technical paragraph. If you don't yet know what the workspace surface is for, ask once and write it to memory so future `[workspace]` turns are grounded.
|
|
285
285
|
|
|
286
286
|
Beyond those, your human can install additional channels (WhatsApp, Telegram, Discord, Alexa…) as **skills** from the Bloby Marketplace. Each channel skill teaches you the conventions for that surface.
|
|
@@ -615,27 +615,9 @@ It restarts the backend and BLOCKS until the port is healthy, then returns `{"ok
|
|
|
615
615
|
|
|
616
616
|
## MCP Servers (Model Context Protocol)
|
|
617
617
|
|
|
618
|
-
|
|
618
|
+
MCP servers are NOT yet supported on this provider. The `MCP.json` config file exists for other harnesses, but on this one its entries are inert — no MCP tools will appear, no matter what is configured there.
|
|
619
619
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
```json
|
|
623
|
-
{
|
|
624
|
-
"server-name": {
|
|
625
|
-
"command": "npx",
|
|
626
|
-
"args": ["-y", "@some/mcp-server"],
|
|
627
|
-
"env": {}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
```
|
|
631
|
-
|
|
632
|
-
The file is a JSON object where each key is a server name and the value has `command`, optional `args`, and optional `env`. Use `-y` in npx args to skip install prompts. The config is read fresh on every turn — add, remove, or edit entries anytime.
|
|
633
|
-
|
|
634
|
-
**Your human can ask you to add MCP servers.** When they do, read `MCP.json` (create it if missing), add the new server entry, and write it back. Common examples:
|
|
635
|
-
- **Playwright** (browser control): `"playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@latest", "--headless", "--browser", "chromium"] }`
|
|
636
|
-
- **Fetch** (HTTP requests): `"fetch": { "command": "npx", "args": ["-y", "@anthropic-ai/mcp-fetch@latest"] }`
|
|
637
|
-
|
|
638
|
-
When an MCP server is configured, its tools appear alongside your built-in tools. Use them naturally — no special syntax needed.
|
|
620
|
+
If your human asks for a capability that usually comes from an MCP server (browser control, third-party APIs, databases), be honest that MCP isn't available here, then offer the closest alternative with your real tools — for example a CLI via Bash (`npx playwright`, `curl`, a database client) or WebFetch for HTTP APIs. Never claim an MCP tool worked.
|
|
639
621
|
|
|
640
622
|
## Sacred Files — NEVER Modify
|
|
641
623
|
- `supervisor/` — chat UI, proxy, process management
|
|
@@ -684,7 +666,7 @@ Only redesign the workspace layout if your human explicitly asks you to. Otherwi
|
|
|
684
666
|
|
|
685
667
|
**Be genuinely helpful, not performatively helpful.** Skip the filler. Actions speak louder than words. Just help.
|
|
686
668
|
|
|
687
|
-
**Be resourceful.** Before asking your human a question, try to answer it yourself. Read the files. Check the code.
|
|
669
|
+
**Be resourceful.** Before asking your human a question, try to answer it yourself. Read the files. Check the code. Fetch docs and pages with WebFetch. Come back with answers, not questions. Ask when you're genuinely stuck, not when you're being lazy.
|
|
688
670
|
|
|
689
671
|
**Have opinions.** You're allowed to disagree, to have preferences, to think something is a bad idea. An agent with no opinions is just autocomplete. If your human asks "what do you think?" — actually think.
|
|
690
672
|
|
|
@@ -700,7 +682,7 @@ Only redesign the workspace layout if your human explicitly asks you to. Otherwi
|
|
|
700
682
|
|
|
701
683
|
**Safe to do freely (internal):**
|
|
702
684
|
- Read files, explore, organize, learn
|
|
703
|
-
-
|
|
685
|
+
- Fetch documentation and web pages with WebFetch (you have no search engine — derive likely URLs or ask for one)
|
|
704
686
|
- Work within the workspace
|
|
705
687
|
- Write and update your own memory files
|
|
706
688
|
|
|
@@ -280,7 +280,7 @@ If your human asks you to update a skill's behavior, edit the files INSIDE `skil
|
|
|
280
280
|
You can communicate through several surfaces at once. The two built-in ones are:
|
|
281
281
|
|
|
282
282
|
- **`[PWA]`** — the chat bubble in the dashboard (web app / PWA). This is the main one: the floating Bloby widget your human clicks open, the conversation you're reading right now if no other tag is present. Treat it as your home base.
|
|
283
|
-
- **`[Mac]`** — the Morphy native Mac app living in the MacBook notch. Your human held a hotkey, spoke, and Morphy sent the transcript here. Screenshots of their screen may be attached. Keep replies concise — they'll be spoken aloud via TTS. No markdown, no bullet lists — plain spoken sentences only.
|
|
283
|
+
- **`[Mac]`** — the Morphy native Mac app living in the MacBook notch. Your human held a hotkey, spoke, and Morphy sent the transcript here. Screenshots of their screen may be attached. Keep replies concise — they'll be spoken aloud via TTS. No markdown, no bullet lists — plain spoken sentences only. **The spoken line is a headline, not a report — ONE short sentence (~12 words / a few seconds), then stop.** Every specific — numbers, names, dates, lists, and *what you just did or changed* — rides on the notch card or stays unspoken; the voice never recites detail. **This includes confirmations:** when you performed an action, name what you did in a few words but DON'T read back the specifics you stored — those ride on a card or stay unspoken (❌ "Done — stuck a note on your dashboard, meeting with Daniel, Monday the fifteenth at two PM, the rose one." → ✓ "Done, Bruno. Sticky note's on your dashboard."). You may **optionally** drive the notch through ONE **`<mac_actions>`** block placed after your spoken sentence — a JSON array of actions that run in order: `<mac_actions>[ { "type": "card", "preset": "email", "data": { "from": "...", "subject": "...", "time": "...", "body": "..." } } ]</mac_actions>`. Action `type`s: **`card`** — a preset card in the notch, `{ "type": "card", "preset": "<name>", "data": { … } }` (preset names: `email`, `list`, `calendar`, `weather`, `ticker`, `stat`, `info`, `text`, `comparison`; full schemas in `skills/mac/presets/PRESETS.md`; you send structured data, never CSS); **`point`** — flies the mascot to a screenshot pixel, `{ "type": "point", "x", "y", "label"?, "screen"? }`; **`spotlight`** — dims the display and cuts a glowing hole over a spot, `{ "type": "spotlight", "x", "y", "r"?, "label"?, "screen"? }`. Coordinates are pixels read off **this turn's screenshot** (top-left origin); `screen` is 1-based for multi-display. For a **custom card** no preset fits, hand-write real markup in a separate **`<notch_html>…</notch_html>`** tag (lowercase, exactly that spelling — NOT `<NotchHTML>`, NOT `<notch>`); for long prose / a "read me this" / a summary use the `text` preset instead — never dump plain paragraphs into `<notch_html>`, it renders unstyled and edge-to-edge. Send **one** `<mac_actions>` block (it may carry several actions) and **at most one** card (a `card` action OR a `<notch_html>`, never both). Every block is stripped from TTS, so card/action contents are **never** spoken. Canvas is **fixed 383 × 147 pt, transparent over BLACK**; custom cards use light/white text, no external assets (no `<img src>`, no fonts, no JS network) and no interactive elements (clicks do nothing) — though long content IS scrollable, the user scrolls it with the trackpad, so letting content overflow is fine. **CRITICAL: never speak the contents of your card.** If the card carries a list / calendar / email / news / comparison / any structured answer, the spoken text MUST be a short lead-in ONLY ("Here are today's top five, Bruno.") and then stop — do not recite the items, the human is already looking at them. If the answer fits in one spoken sentence, send NO card. Voice and card are complementary, never redundant — pick which one carries the detail per reply, and let the other be brief or absent. When in doubt, **open `skills/mac/SKILL.md` and follow it** — it has the preset catalog, examples (including the canonical bad/good comparison of the speak-the-card duplication failure), custom templates under `skills/mac/frequentSnippets/`, and a pre-send checklist. **Mis-spelling a tag means the markup reaches TTS and gets spoken at the human** — that's a visible failure, not a silent one.
|
|
284
284
|
- **`[workspace]`** — a chat-shaped widget your human placed somewhere inside their dashboard *workspace*. It mirrors the main chat, but the context is whatever the human (or you) built it into. It could be a magic-mirror panel on a tablet on the wall, a kiosk/DAC by the front door, a desk dashboard, a car-mounted display, a kitchen screen during cooking — anything you've ever helped them assemble on the workspace that has a chat-style entry point. **Check `MEMORY.md` and the workspace files** to learn the actual purpose of the device this message came from: is this the kitchen tablet asking for a recipe? The hallway mirror asking what's on today's schedule? The garage panel asking about the car? Tailor tone, brevity, and content to that role. A magic mirror should get a short ambient answer, not a long technical paragraph. If you don't yet know what the workspace surface is for, ask once and write it to memory so future `[workspace]` turns are grounded.
|
|
285
285
|
|
|
286
286
|
Beyond those, your human can install additional channels (WhatsApp, Telegram, Discord, Alexa…) as **skills** from the Bloby Marketplace. Each channel skill teaches you the conventions for that surface.
|
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AreaChart, Area, BarChart, Bar, ResponsiveContainer } from 'recharts';
|
|
1
|
+
import { PlaceholderWidgets } from './deleteme_placeholders';
|
|
3
2
|
|
|
4
3
|
const GRADIENT = 'linear-gradient(to right, #0166FF 10%, #009AFE 55%, #4AEEFF 100%)';
|
|
5
|
-
const CARD = 'relative rounded-xl overflow-hidden';
|
|
6
|
-
const BORDER = 'absolute inset-0 rounded-xl bg-gradient-to-b from-white/[0.08] via-white/[0.02] to-transparent pointer-events-none';
|
|
7
|
-
const INNER = 'relative rounded-xl bg-[#141414] m-px p-3.5 h-full';
|
|
8
|
-
|
|
9
|
-
const rev = [{ v: 82 }, { v: 89 }, { v: 94 }, { v: 101 }, { v: 108 }, { v: 112 }, { v: 125 }];
|
|
10
|
-
const fol = [{ v: 12 }, { v: 18 }, { v: 9 }, { v: 24 }, { v: 31 }, { v: 19 }, { v: 27 }];
|
|
11
|
-
|
|
12
|
-
function StripeSvg() {
|
|
13
|
-
return <svg className="h-3.5 w-3.5 text-[#635BFF]" viewBox="0 0 24 24" fill="currentColor"><path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.591-7.305z" /></svg>;
|
|
14
|
-
}
|
|
15
|
-
function GmailSvg() {
|
|
16
|
-
return <svg className="h-3.5 w-3.5 text-[#EA4335]" viewBox="0 0 24 24" fill="currentColor"><path d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z" /></svg>;
|
|
17
|
-
}
|
|
18
|
-
function XSvg() {
|
|
19
|
-
return <svg className="h-3.5 w-3.5 text-white" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>;
|
|
20
|
-
}
|
|
21
4
|
|
|
22
5
|
export default function DashboardPage() {
|
|
23
6
|
return (
|
|
@@ -30,105 +13,9 @@ export default function DashboardPage() {
|
|
|
30
13
|
</h1>
|
|
31
14
|
<p className="text-muted-foreground text-xs mb-6">Your workspace at a glance.</p>
|
|
32
15
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<div className={`${CARD} col-span-2`}>
|
|
37
|
-
<div className={BORDER} />
|
|
38
|
-
<div className={INNER}>
|
|
39
|
-
<div className="flex items-center justify-between">
|
|
40
|
-
<div className="flex items-center gap-2">
|
|
41
|
-
<div className="h-7 w-7 rounded-lg bg-[#635BFF]/10 flex items-center justify-center"><StripeSvg /></div>
|
|
42
|
-
<span className="text-xs font-bold">Stripe</span>
|
|
43
|
-
</div>
|
|
44
|
-
<div className="flex items-center gap-1 text-emerald-500">
|
|
45
|
-
<TrendingUp className="h-3 w-3" />
|
|
46
|
-
<span className="text-[10px] font-bold">+12.5%</span>
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
<p className="text-2xl font-bold tracking-tight mt-2">$12,480</p>
|
|
50
|
-
<p className="text-[10px] text-muted-foreground/50 mb-1">MRR</p>
|
|
51
|
-
<div className="h-12 overflow-hidden">
|
|
52
|
-
<ResponsiveContainer width="100%" height={48}>
|
|
53
|
-
<AreaChart data={rev}>
|
|
54
|
-
<defs>
|
|
55
|
-
<linearGradient id="sg" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stopColor="#0166FF" /><stop offset="50%" stopColor="#009AFE" /><stop offset="100%" stopColor="#4AEEFF" /></linearGradient>
|
|
56
|
-
<linearGradient id="sf" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#009AFE" stopOpacity={0.12} /><stop offset="100%" stopColor="#009AFE" stopOpacity={0} /></linearGradient>
|
|
57
|
-
</defs>
|
|
58
|
-
<Area type="monotone" dataKey="v" stroke="url(#sg)" strokeWidth={1.5} fill="url(#sf)" />
|
|
59
|
-
</AreaChart>
|
|
60
|
-
</ResponsiveContainer>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
|
|
65
|
-
{/* X — 1 col */}
|
|
66
|
-
<div className={`${CARD} col-span-1`}>
|
|
67
|
-
<div className={BORDER} />
|
|
68
|
-
<div className={INNER}>
|
|
69
|
-
<div className="flex items-center gap-2 mb-2">
|
|
70
|
-
<div className="h-7 w-7 rounded-lg bg-white/[0.06] flex items-center justify-center"><XSvg /></div>
|
|
71
|
-
<span className="text-xs font-bold">X</span>
|
|
72
|
-
</div>
|
|
73
|
-
<p className="text-2xl font-bold tracking-tight">24.8K</p>
|
|
74
|
-
<div className="flex items-center gap-1 text-emerald-500 mb-1">
|
|
75
|
-
<TrendingUp className="h-2.5 w-2.5" />
|
|
76
|
-
<span className="text-[10px] font-bold">+1.4K</span>
|
|
77
|
-
</div>
|
|
78
|
-
<div className="h-10 overflow-hidden">
|
|
79
|
-
<ResponsiveContainer width="100%" height={40}>
|
|
80
|
-
<BarChart data={fol} barCategoryGap="25%">
|
|
81
|
-
<defs>
|
|
82
|
-
<linearGradient id="xg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#009AFE" stopOpacity={0.3} /><stop offset="100%" stopColor="#0166FF" stopOpacity={0.05} /></linearGradient>
|
|
83
|
-
</defs>
|
|
84
|
-
<Bar dataKey="v" fill="url(#xg)" radius={[2, 2, 0, 0]} />
|
|
85
|
-
</BarChart>
|
|
86
|
-
</ResponsiveContainer>
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
{/* Gmail — 1 col */}
|
|
92
|
-
<div className={`${CARD} col-span-1`}>
|
|
93
|
-
<div className={BORDER} />
|
|
94
|
-
<div className={INNER}>
|
|
95
|
-
<div className="flex items-center gap-2 mb-2.5">
|
|
96
|
-
<div className="h-7 w-7 rounded-lg bg-[#EA4335]/10 flex items-center justify-center"><GmailSvg /></div>
|
|
97
|
-
<span className="text-xs font-bold">Gmail</span>
|
|
98
|
-
<span className="ml-auto text-[10px] text-muted-foreground/50">3 new</span>
|
|
99
|
-
</div>
|
|
100
|
-
{['Sarah Chen', 'Stripe', 'Alex R.'].map((n) => (
|
|
101
|
-
<div key={n} className="flex items-center gap-2 py-1.5">
|
|
102
|
-
<div className="h-5 w-5 rounded-full bg-white/[0.06] text-[9px] font-bold flex items-center justify-center shrink-0">{n[0]}</div>
|
|
103
|
-
<span className="text-[11px] truncate">{n}</span>
|
|
104
|
-
</div>
|
|
105
|
-
))}
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
{/* Research — 2 cols */}
|
|
110
|
-
<div className={`${CARD} col-span-2`}>
|
|
111
|
-
<div className={BORDER} />
|
|
112
|
-
<div className={INNER}>
|
|
113
|
-
<div className="flex items-center gap-2 mb-2.5">
|
|
114
|
-
<div className="h-7 w-7 rounded-lg bg-[#9235F9]/10 flex items-center justify-center"><Search className="h-3.5 w-3.5 text-[#9235F9]" /></div>
|
|
115
|
-
<span className="text-xs font-bold">Research</span>
|
|
116
|
-
<span className="ml-auto text-[10px] font-bold text-muted-foreground bg-white/[0.06] px-2 py-0.5 rounded-full">3</span>
|
|
117
|
-
</div>
|
|
118
|
-
{[
|
|
119
|
-
{ t: 'Competitor pricing', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
|
|
120
|
-
{ t: 'Market trends Q1', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
|
|
121
|
-
{ t: 'User feedback', s: 'Review', c: 'text-orange-400 bg-orange-400/10' },
|
|
122
|
-
].map((r) => (
|
|
123
|
-
<div key={r.t} className="flex items-center justify-between py-1.5">
|
|
124
|
-
<span className="text-[11px]">{r.t}</span>
|
|
125
|
-
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded-full ${r.c}`}>{r.s}</span>
|
|
126
|
-
</div>
|
|
127
|
-
))}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
</div>
|
|
16
|
+
{/* ▼▼▼ EXAMPLE PLACEHOLDERS — safe to delete. See deleteme_placeholders.tsx. ▼▼▼ */}
|
|
17
|
+
<PlaceholderWidgets />
|
|
18
|
+
{/* ▲▲▲ EXAMPLE PLACEHOLDERS ▲▲▲ */}
|
|
132
19
|
</div>
|
|
133
20
|
);
|
|
134
21
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ┌──────────────────────────────────────────────────────────────────┐
|
|
3
|
+
* │ EXAMPLE / PLACEHOLDER DASHBOARD WIDGETS — SAFE TO DELETE │
|
|
4
|
+
* └──────────────────────────────────────────────────────────────────┘
|
|
5
|
+
*
|
|
6
|
+
* Everything in this file is demo content to show what a Bloby workspace
|
|
7
|
+
* can look like. NONE of it is connected to real data.
|
|
8
|
+
*
|
|
9
|
+
* To start fresh with the user's real widgets:
|
|
10
|
+
* 1. Delete this entire file (deleteme_placeholders.tsx).
|
|
11
|
+
* 2. In DashboardPage.tsx, remove the `PlaceholderWidgets` import and the
|
|
12
|
+
* <PlaceholderWidgets /> usage.
|
|
13
|
+
* The dashboard will then be empty — ready for real apps/widgets.
|
|
14
|
+
*
|
|
15
|
+
* It is intentionally fully self-contained (own data, icons, styles, and the
|
|
16
|
+
* dismissible "these are examples" banner) so removal is a one-file delete.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from 'react';
|
|
19
|
+
import { Search, TrendingUp, Sparkles, X as XIcon } from 'lucide-react';
|
|
20
|
+
import { AreaChart, Area, BarChart, Bar, ResponsiveContainer } from 'recharts';
|
|
21
|
+
|
|
22
|
+
const DISMISS_KEY = 'bloby_example_widgets_dismissed';
|
|
23
|
+
|
|
24
|
+
const CARD = 'relative rounded-xl overflow-hidden';
|
|
25
|
+
const BORDER = 'absolute inset-0 rounded-xl bg-gradient-to-b from-white/[0.08] via-white/[0.02] to-transparent pointer-events-none';
|
|
26
|
+
const INNER = 'relative rounded-xl bg-[#141414] m-px p-3.5 h-full';
|
|
27
|
+
|
|
28
|
+
const rev = [{ v: 82 }, { v: 89 }, { v: 94 }, { v: 101 }, { v: 108 }, { v: 112 }, { v: 125 }];
|
|
29
|
+
const fol = [{ v: 12 }, { v: 18 }, { v: 9 }, { v: 24 }, { v: 31 }, { v: 19 }, { v: 27 }];
|
|
30
|
+
|
|
31
|
+
function StripeSvg() {
|
|
32
|
+
return <svg className="h-3.5 w-3.5 text-[#635BFF]" viewBox="0 0 24 24" fill="currentColor"><path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.591-7.305z" /></svg>;
|
|
33
|
+
}
|
|
34
|
+
function GmailSvg() {
|
|
35
|
+
return <svg className="h-3.5 w-3.5 text-[#EA4335]" viewBox="0 0 24 24" fill="currentColor"><path d="M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z" /></svg>;
|
|
36
|
+
}
|
|
37
|
+
function XSvg() {
|
|
38
|
+
return <svg className="h-3.5 w-3.5 text-white" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /></svg>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Small "Example" tag shown on each placeholder card. */
|
|
42
|
+
function ExampleBadge() {
|
|
43
|
+
return (
|
|
44
|
+
<span className="inline-flex items-center text-[8.5px] font-bold uppercase tracking-wider text-muted-foreground/60 bg-white/[0.05] border border-white/[0.06] px-1.5 py-0.5 rounded">
|
|
45
|
+
Example
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function PlaceholderWidgets() {
|
|
51
|
+
const [dismissed, setDismissed] = useState(() => {
|
|
52
|
+
try { return localStorage.getItem(DISMISS_KEY) === '1'; } catch { return false; }
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function dismissBanner() {
|
|
56
|
+
setDismissed(true);
|
|
57
|
+
try { localStorage.setItem(DISMISS_KEY, '1'); } catch {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
{/* "These are examples" banner — dismissible, remembers via localStorage */}
|
|
63
|
+
{!dismissed && (
|
|
64
|
+
<div className="relative flex items-start gap-3 mb-4 rounded-xl bg-[#141414] border border-white/[0.07] p-3.5">
|
|
65
|
+
<div
|
|
66
|
+
className="h-7 w-7 rounded-lg flex items-center justify-center shrink-0"
|
|
67
|
+
style={{ background: 'linear-gradient(135deg, #0166FF, #4AEEFF)' }}
|
|
68
|
+
>
|
|
69
|
+
<Sparkles className="h-4 w-4 text-white" />
|
|
70
|
+
</div>
|
|
71
|
+
<div className="flex-1 min-w-0">
|
|
72
|
+
<p className="text-xs font-bold">These are example widgets</p>
|
|
73
|
+
<p className="text-[11px] text-muted-foreground/70 mt-0.5 leading-snug">
|
|
74
|
+
Just a preview of what your workspace can look like — none of it is real data.
|
|
75
|
+
Ask Bloby to replace them with your own apps.
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={dismissBanner}
|
|
81
|
+
aria-label="Dismiss"
|
|
82
|
+
className="text-muted-foreground/50 hover:text-foreground transition-colors shrink-0 -mr-1 -mt-1 p-1"
|
|
83
|
+
>
|
|
84
|
+
<XIcon className="h-4 w-4" />
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
<div className="grid grid-cols-3 gap-2.5">
|
|
90
|
+
|
|
91
|
+
{/* Stripe — 2 cols */}
|
|
92
|
+
<div className={`${CARD} col-span-2`}>
|
|
93
|
+
<div className={BORDER} />
|
|
94
|
+
<div className={INNER}>
|
|
95
|
+
<div className="flex items-center justify-between">
|
|
96
|
+
<div className="flex items-center gap-2">
|
|
97
|
+
<div className="h-7 w-7 rounded-lg bg-[#635BFF]/10 flex items-center justify-center"><StripeSvg /></div>
|
|
98
|
+
<span className="text-xs font-bold">Stripe</span>
|
|
99
|
+
<ExampleBadge />
|
|
100
|
+
</div>
|
|
101
|
+
<div className="flex items-center gap-1 text-emerald-500">
|
|
102
|
+
<TrendingUp className="h-3 w-3" />
|
|
103
|
+
<span className="text-[10px] font-bold">+12.5%</span>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
<p className="text-2xl font-bold tracking-tight mt-2">$12,480</p>
|
|
107
|
+
<p className="text-[10px] text-muted-foreground/50 mb-1">MRR</p>
|
|
108
|
+
<div className="h-12 overflow-hidden">
|
|
109
|
+
<ResponsiveContainer width="100%" height={48}>
|
|
110
|
+
<AreaChart data={rev}>
|
|
111
|
+
<defs>
|
|
112
|
+
<linearGradient id="sg" x1="0" y1="0" x2="1" y2="0"><stop offset="0%" stopColor="#0166FF" /><stop offset="50%" stopColor="#009AFE" /><stop offset="100%" stopColor="#4AEEFF" /></linearGradient>
|
|
113
|
+
<linearGradient id="sf" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#009AFE" stopOpacity={0.12} /><stop offset="100%" stopColor="#009AFE" stopOpacity={0} /></linearGradient>
|
|
114
|
+
</defs>
|
|
115
|
+
<Area type="monotone" dataKey="v" stroke="url(#sg)" strokeWidth={1.5} fill="url(#sf)" />
|
|
116
|
+
</AreaChart>
|
|
117
|
+
</ResponsiveContainer>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
{/* X — 1 col */}
|
|
123
|
+
<div className={`${CARD} col-span-1`}>
|
|
124
|
+
<div className={BORDER} />
|
|
125
|
+
<div className={INNER}>
|
|
126
|
+
<div className="flex items-center gap-2 mb-2">
|
|
127
|
+
<div className="h-7 w-7 rounded-lg bg-white/[0.06] flex items-center justify-center"><XSvg /></div>
|
|
128
|
+
<span className="text-xs font-bold">X</span>
|
|
129
|
+
<ExampleBadge />
|
|
130
|
+
</div>
|
|
131
|
+
<p className="text-2xl font-bold tracking-tight">24.8K</p>
|
|
132
|
+
<div className="flex items-center gap-1 text-emerald-500 mb-1">
|
|
133
|
+
<TrendingUp className="h-2.5 w-2.5" />
|
|
134
|
+
<span className="text-[10px] font-bold">+1.4K</span>
|
|
135
|
+
</div>
|
|
136
|
+
<div className="h-10 overflow-hidden">
|
|
137
|
+
<ResponsiveContainer width="100%" height={40}>
|
|
138
|
+
<BarChart data={fol} barCategoryGap="25%">
|
|
139
|
+
<defs>
|
|
140
|
+
<linearGradient id="xg" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#009AFE" stopOpacity={0.3} /><stop offset="100%" stopColor="#0166FF" stopOpacity={0.05} /></linearGradient>
|
|
141
|
+
</defs>
|
|
142
|
+
<Bar dataKey="v" fill="url(#xg)" radius={[2, 2, 0, 0]} />
|
|
143
|
+
</BarChart>
|
|
144
|
+
</ResponsiveContainer>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Gmail — 1 col */}
|
|
150
|
+
<div className={`${CARD} col-span-1`}>
|
|
151
|
+
<div className={BORDER} />
|
|
152
|
+
<div className={INNER}>
|
|
153
|
+
<div className="flex items-center gap-2 mb-2.5">
|
|
154
|
+
<div className="h-7 w-7 rounded-lg bg-[#EA4335]/10 flex items-center justify-center"><GmailSvg /></div>
|
|
155
|
+
<span className="text-xs font-bold">Gmail</span>
|
|
156
|
+
<ExampleBadge />
|
|
157
|
+
<span className="ml-auto text-[10px] text-muted-foreground/50">3 new</span>
|
|
158
|
+
</div>
|
|
159
|
+
{['Sarah Chen', 'Stripe', 'Alex R.'].map((n) => (
|
|
160
|
+
<div key={n} className="flex items-center gap-2 py-1.5">
|
|
161
|
+
<div className="h-5 w-5 rounded-full bg-white/[0.06] text-[9px] font-bold flex items-center justify-center shrink-0">{n[0]}</div>
|
|
162
|
+
<span className="text-[11px] truncate">{n}</span>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Research — 2 cols */}
|
|
169
|
+
<div className={`${CARD} col-span-2`}>
|
|
170
|
+
<div className={BORDER} />
|
|
171
|
+
<div className={INNER}>
|
|
172
|
+
<div className="flex items-center gap-2 mb-2.5">
|
|
173
|
+
<div className="h-7 w-7 rounded-lg bg-[#9235F9]/10 flex items-center justify-center"><Search className="h-3.5 w-3.5 text-[#9235F9]" /></div>
|
|
174
|
+
<span className="text-xs font-bold">Research</span>
|
|
175
|
+
<ExampleBadge />
|
|
176
|
+
<span className="ml-auto text-[10px] font-bold text-muted-foreground bg-white/[0.06] px-2 py-0.5 rounded-full">3</span>
|
|
177
|
+
</div>
|
|
178
|
+
{[
|
|
179
|
+
{ t: 'Competitor pricing', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
|
|
180
|
+
{ t: 'Market trends Q1', s: 'Done', c: 'text-emerald-500 bg-emerald-500/10' },
|
|
181
|
+
{ t: 'User feedback', s: 'Review', c: 'text-orange-400 bg-orange-400/10' },
|
|
182
|
+
].map((r) => (
|
|
183
|
+
<div key={r.t} className="flex items-center justify-between py-1.5">
|
|
184
|
+
<span className="text-[11px]">{r.t}</span>
|
|
185
|
+
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded-full ${r.c}`}>{r.s}</span>
|
|
186
|
+
</div>
|
|
187
|
+
))}
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
</div>
|
|
192
|
+
</>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NavLink } from 'react-router';
|
|
2
|
+
import { motion } from 'framer-motion';
|
|
2
3
|
import {
|
|
3
4
|
LayoutDashboard,
|
|
4
5
|
AppWindow,
|
|
@@ -26,13 +27,13 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
|
|
|
26
27
|
return (
|
|
27
28
|
<aside className="flex flex-col h-full w-64 bg-transparent p-5 pt-8">
|
|
28
29
|
{/* Logo */}
|
|
29
|
-
<div className="flex items-center gap-2.5 mb-8">
|
|
30
|
+
<div className="flex items-center gap-2.5 mb-8 shrink-0">
|
|
30
31
|
<img src="/morphy.png" alt={botName} className="h-7 w-auto" />
|
|
31
32
|
<span className="font-bold text-lg" style={{ fontFamily: "'Space Grotesk', sans-serif" }}>{botName}</span>
|
|
32
33
|
</div>
|
|
33
34
|
|
|
34
35
|
{/* Greeting */}
|
|
35
|
-
<div className="mb-10">
|
|
36
|
+
<div className="mb-10 shrink-0">
|
|
36
37
|
<h1 className="text-4xl font-bold leading-[1.1]">
|
|
37
38
|
{getGreeting()}{firstName ? ',' : ''}
|
|
38
39
|
</h1>
|
|
@@ -51,16 +52,19 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
|
|
|
51
52
|
)}
|
|
52
53
|
</div>
|
|
53
54
|
|
|
54
|
-
{/* Navigation
|
|
55
|
-
|
|
55
|
+
{/* Navigation — min-h-0 lets this flex child shrink below its content height
|
|
56
|
+
(flex items default to min-height:auto), so the list scrolls instead of
|
|
57
|
+
pushing the status pill past the card's clipped bottom edge. */}
|
|
58
|
+
<nav className="flex-1 min-h-0 overflow-y-auto space-y-0.5">
|
|
56
59
|
<NavItem to="/" icon={LayoutDashboard} label="Dashboard" onNavigate={onNavigate} />
|
|
57
|
-
<NavItem to="/app1" icon={AppWindow} label="App 1"
|
|
58
|
-
<NavItem to="/research" icon={Search} label="Research"
|
|
59
|
-
<NavItem to="/whatelse" icon={CircleHelp} label="What Else?"
|
|
60
|
+
<NavItem to="/app1" icon={AppWindow} label="App 1" onNavigate={onNavigate} />
|
|
61
|
+
<NavItem to="/research" icon={Search} label="Research" onNavigate={onNavigate} />
|
|
62
|
+
<NavItem to="/whatelse" icon={CircleHelp} label="What Else?" onNavigate={onNavigate} />
|
|
60
63
|
</nav>
|
|
61
64
|
|
|
62
|
-
{/* Backend status
|
|
63
|
-
|
|
65
|
+
{/* Backend status — shrink-0 + mt keeps it pinned and visible below the
|
|
66
|
+
scrollable nav, never clipped by the sidebar card's rounded edge. */}
|
|
67
|
+
<div className="rounded-xl bg-[#282828] px-3.5 py-2.5 flex items-center gap-2 shrink-0 mt-3">
|
|
64
68
|
<span
|
|
65
69
|
className={`h-2 w-2 rounded-full shrink-0 ${
|
|
66
70
|
backendStatus === 'healthy' ? 'bg-emerald-500' : 'bg-orange-400 animate-pulse'
|
|
@@ -74,38 +78,56 @@ export default function Sidebar({ userName, botName = 'Bloby', backendStatus = '
|
|
|
74
78
|
);
|
|
75
79
|
}
|
|
76
80
|
|
|
81
|
+
// Shared-layout spring — the active decoration slides between items on nav change.
|
|
82
|
+
const activeSpring = { type: 'spring' as const, stiffness: 420, damping: 34 };
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The active-item decoration — a single clean recessed dark fill (no stroke, no
|
|
86
|
+
* border, no color). The shared `layoutId` makes it glide from the
|
|
87
|
+
* previously-active item to the new one.
|
|
88
|
+
*/
|
|
89
|
+
function ActiveDecoration() {
|
|
90
|
+
return (
|
|
91
|
+
<motion.span
|
|
92
|
+
layoutId="nav-active"
|
|
93
|
+
className="absolute inset-0 rounded-[12px]"
|
|
94
|
+
style={{
|
|
95
|
+
background: '#0a0a0c',
|
|
96
|
+
boxShadow: 'inset 0 1px 4px rgba(0,0,0,0.6)',
|
|
97
|
+
}}
|
|
98
|
+
transition={activeSpring}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
function NavItem({
|
|
78
104
|
to,
|
|
79
105
|
icon: Icon,
|
|
80
106
|
label,
|
|
81
|
-
sub,
|
|
82
107
|
onNavigate,
|
|
83
108
|
}: {
|
|
84
109
|
to: string;
|
|
85
|
-
icon: React.ComponentType<{ className?: string }>;
|
|
110
|
+
icon: React.ComponentType<{ className?: string; strokeWidth?: number }>;
|
|
86
111
|
label: string;
|
|
87
|
-
sub?: string;
|
|
88
112
|
onNavigate?: () => void;
|
|
89
113
|
}) {
|
|
90
114
|
return (
|
|
91
|
-
<NavLink
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{sub && <p className="text-[10px] text-muted-foreground/50 font-normal leading-tight mt-0.5">{sub}</p>}
|
|
108
|
-
</div>
|
|
115
|
+
<NavLink to={to} end onClick={onNavigate} className="block">
|
|
116
|
+
{({ isActive }) => (
|
|
117
|
+
<div
|
|
118
|
+
className={cn(
|
|
119
|
+
'relative flex items-center gap-3 w-full px-3 py-2.5 rounded-[12px] text-sm transition-colors',
|
|
120
|
+
// cult-ui dark-theme text colors: active white, inactive #6b6b6d
|
|
121
|
+
isActive
|
|
122
|
+
? 'text-white font-medium'
|
|
123
|
+
: 'text-[#6b6b6d] hover:text-zinc-400 hover:bg-white/[0.03]',
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
{isActive && <ActiveDecoration />}
|
|
127
|
+
<Icon className="relative z-10 h-[19px] w-[19px] shrink-0" strokeWidth={1.5} />
|
|
128
|
+
<span className="relative z-10">{label}</span>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
109
131
|
</NavLink>
|
|
110
132
|
);
|
|
111
133
|
}
|