Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<meta name="theme-color" content="#06111d" />
<meta name="theme-color" content="#090909" />
<title>KNeuron</title>
</head>
<body>
Expand Down
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"quality:full": "npm run quality && cargo check --manifest-path src-tauri/Cargo.toml"
},
"dependencies": {
"@fontsource/barlow": "^5.3.0",
"@fontsource/barlow-semi-condensed": "^5.3.0",
"@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-shell": "^2.4.0",
"lucide-react": "^0.468.0",
Expand Down
Binary file added public/brain-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/devices/brainaccess-maxi.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/devices/brainlink-lite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/modules/cortex/thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/modules/miner/thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/modules/neuorrun/thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"decorations": false,
"shadow": true,
"theme": "Dark",
"backgroundColor": "#030A12"
"backgroundColor": "#090909"
}
],
"security": {
Expand Down
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ export default function App() {
}
}

const activeSidebarPage: ShellPage = route.kind === "shell" ? route.page : "dashboard";
const activeSidebarPage: ShellPage | null = route.kind === "shell" ? route.page : null;

const isModuleOpen = route.kind === "module";

Expand Down
36 changes: 17 additions & 19 deletions src/components/BrainLogo.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
export function BrainLogo({ size = 54 }: { size?: number }) {
interface BrainLogoProps {
size?: number;
}

export function BrainLogo({ size = 54 }: BrainLogoProps) {
return (
<svg width={size} height={size} viewBox="0 0 64 64" fill="none" aria-hidden="true">
<path
d="M31 10c-5-7-15-3-14 5-7 0-10 9-5 13-6 5-2 14 5 14-2 8 9 13 14 6V10Z"
stroke="currentColor"
strokeWidth="3"
/>
<path
d="M33 10c5-7 15-3 14 5 7 0 10 9 5 13 6 5 2 14-5 14 2 8-9 13-14 6V10Z"
stroke="currentColor"
strokeWidth="3"
/>
<path
d="M23 17c6 1 4 7 1 9m-7-2c7 0 8 7 5 11m8-16c-5 2-5 8-2 11m-8 13c2-6 7-6 10-4M41 17c-6 1-4 7-1 9m7-2c-7 0-8 7-5 11m-8-16c5 2 5 8 2 11m8 13c-2-6-7-6-10-4"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
/>
</svg>
<img
src="/brain-logo.png"
width={size}
height={size}
alt=""
aria-hidden="true"
draggable={false}
style={{
display: "block",
objectFit: "contain",
}}
/>
);
}
58 changes: 32 additions & 26 deletions src/components/DevicePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,75 @@
import { MonitorOff } from "lucide-react";

import { useDevice } from "../features/device/useDevice";

import { ModuleThumbnail } from "./ModuleThumbnail";

/**
* Right-hand panel with the active device.
*/
export function DevicePanel() {
const { snapshot, streamInfoByDeviceId } = useDevice();

const activeDevice = snapshot.activeDeviceInfo;

const activeStatus = snapshot.activeStatus;

const state = activeStatus?.state ?? "disconnected";

const streamInfo = snapshot.activeDeviceId
? streamInfoByDeviceId[snapshot.activeDeviceId]
: undefined;

return (
<aside className="device-panel">
<div className="device-panel__header">
<span className="eyebrow">DEVICE</span>

<aside className="device-panel" aria-label="Device">
<header className="device-panel__header">
<span className="eyebrow">Device</span>
<h2>Device</h2>
</div>
</header>

{!activeDevice ? (
<div className="device-panel__empty">
<span className="device-panel__dot" />
<MonitorOff size={64} strokeWidth={1.25} />

<strong>No device connected</strong>

<p>Select a device from the Device page.</p>
<span>Select a device from the Device page.</span>
</div>
) : (
<div className="device-panel__active">
<div className="device-panel__status">
<span
className={`device-panel__dot device-panel__dot--${activeStatus?.state ?? "disconnected"}`}
/>

<span>{activeStatus?.state ?? "disconnected"}</span>
</div>
<ModuleThumbnail
src={activeDevice.thumbnail}
name={activeDevice.name}
className="device-panel__thumbnail"
/>

<strong className="device-panel__name">{activeDevice.name}</strong>

{activeDevice.model && <span className="device-panel__model">{activeDevice.model}</span>}
<span className="device-panel__state">
<span className={`device-panel__dot device-panel__dot--${state}`} />
{state}
</span>

<div className="device-panel__details">
<dl className="device-panel__details">
<div>
<span>Type</span>

<strong>{activeDevice.kind}</strong>
<dt>Type</dt>
<dd>{activeDevice.kind}</dd>
</div>

{streamInfo && (
<>
<div>
<span>Rate</span>

<strong>{streamInfo.sampleRateHz} Hz</strong>
<dt>Rate</dt>
<dd>{streamInfo.sampleRateHz} Hz</dd>
</div>

<div>
<span>Channels</span>

<strong>{streamInfo.channels.length}</strong>
<dt>Channels</dt>
<dd>{streamInfo.channels.length}</dd>
</div>
</>
)}
</div>
</dl>
</div>
)}
</aside>
Expand Down
26 changes: 11 additions & 15 deletions src/components/ModuleThumbnail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,28 @@ import { useState } from "react";
interface ModuleThumbnailProps {
src?: string;
name: string;
/**
* Extra class for placement, e.g. on device cards.
*/
className?: string;
}

/**
* Displays a module thumbnail with a deterministic fallback.
* Displays a module or device thumbnail.
*
* A broken or missing thumbnail must never break the launcher layout.
* Renders nothing when no image is set or it fails to load, so the layout
* closes the gap instead of showing a placeholder.
*/
export function ModuleThumbnail({ src, name }: ModuleThumbnailProps) {
export function ModuleThumbnail({ src, name, className }: ModuleThumbnailProps) {
const [imageFailed, setImageFailed] = useState(false);

const showFallback = !src || imageFailed;

if (showFallback) {
return (
<div
className="module-thumbnail module-thumbnail--fallback"
aria-label={`${name} thumbnail unavailable`}
>
<span>{name.charAt(0).toUpperCase()}</span>
</div>
);
if (!src || imageFailed) {
return null;
}

return (
<img
className="module-thumbnail"
className={["module-thumbnail", className].filter(Boolean).join(" ")}
src={src}
alt={`${name} thumbnail`}
draggable={false}
Expand Down
44 changes: 23 additions & 21 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { Cpu, LayoutDashboard, Settings } from "lucide-react";
import type { ShellPage } from "../types/navigation";
import { LayoutGrid, Monitor, Settings } from "lucide-react";
import type { LucideIcon } from "lucide-react";

import { APP_CONFIG } from "../config/appConfig";
import type { ShellPage } from "../types/navigation";

const items: Array<{ id: ShellPage; label: string; icon: typeof LayoutDashboard }> = [
{ id: "dashboard", label: "Dashboard", icon: LayoutDashboard },
{ id: "device", label: "Device", icon: Cpu },
const items: Array<{ id: ShellPage; label: string; icon: LucideIcon }> = [
{ id: "dashboard", label: "Dashboard", icon: LayoutGrid },
{ id: "device", label: "Device", icon: Monitor },
{ id: "settings", label: "Settings", icon: Settings },
];

/**
* Left navigation with the app version at the bottom.
*
* `page` is null while a module is open, so no item is marked current.
*/
export function Sidebar({
page,
onNavigate,
}: {
page: ShellPage;
page: ShellPage | null;
onNavigate: (page: ShellPage) => void;
}) {
return (
Expand All @@ -21,28 +28,23 @@ export function Sidebar({
{items.map(({ id, label, icon: Icon }) => (
<button
key={id}
type="button"
className={page === id ? "nav-item active" : "nav-item"}
aria-current={page === id ? "page" : undefined}
onClick={() => onNavigate(id)}
>
<Icon size={21} />
<span>{label}</span>
<Icon size={24} strokeWidth={1.5} />
{label}
</button>
))}
</nav>
<div className="wave" aria-hidden="true">
<svg viewBox="0 0 240 170" preserveAspectRatio="none">
{Array.from({ length: 13 }, (_, i) => (
<path
key={i}
d={`M-10 ${72 + i * 5} C 45 ${25 + i * 4}, 82 ${145 - i * 2}, 145 ${91 + i * 2} S 225 ${58 + i * 3}, 255 ${95 + i * 2}`}
/>
))}
</svg>

<div className="sidebar__footer">
<strong>
{APP_CONFIG.name} v{APP_CONFIG.version}
</strong>
<span>Built for Brain-Computer Interface</span>
</div>
<strong>
{APP_CONFIG.name} v{APP_CONFIG.version}
</strong>
<span>Built for Brain-Computer Interface</span>
</aside>
);
}
22 changes: 13 additions & 9 deletions src/components/TitleBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { APP_CONFIG } from "../config/appConfig";

const isTauri = () => "__TAURI_INTERNALS__" in window;

/**
* Top bar: wordmark and window controls.
*/
export function TitleBar() {
const run = async (action: "minimize" | "maximize" | "close") => {
if (!isTauri()) return;
Expand All @@ -17,23 +20,24 @@ export function TitleBar() {
return (
<header className="titlebar" data-tauri-drag-region>
<div className="brand" data-tauri-drag-region>
<span className="brand-logo">
<BrainLogo size={46} />
</span>
<div data-tauri-drag-region>
<strong>{APP_CONFIG.name}</strong>
<span>Modular Brain-Computer Interface</span>
<BrainLogo size={44} />
<div className="brand__text" data-tauri-drag-region>
<strong data-tauri-drag-region>{APP_CONFIG.name}</strong>
<span data-tauri-drag-region>Brain-Computer Interface</span>
</div>
</div>

<div className="titlebar__drag" data-tauri-drag-region />

<div className="window-controls">
<button aria-label="Minimize" onClick={() => void run("minimize")}>
<Minus size={18} />
<Minus size={16} />
</button>
<button aria-label="Maximize" onClick={() => void run("maximize")}>
<Square size={15} />
<Square size={13} />
</button>
<button aria-label="Close" className="close" onClick={() => void run("close")}>
<X size={18} />
<X size={16} />
</button>
</div>
</header>
Expand Down
2 changes: 1 addition & 1 deletion src/config/appConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Runtime user preferences belong in SettingsStore.
*/
export const APP_CONFIG = {
name: "KNeuron",
name: "Neuron",

version: "0.2.7",

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const DEVICE_INFO: EEGDeviceInfo = {
transport: "bluetooth",
manufacturer: "BrainAccess",
model: "MAXI 009",
thumbnail: "/devices/brainaccess-maxi.png",
};

/**
Expand Down
1 change: 1 addition & 0 deletions src/core/devices/adapters/brainlink/BrainLinkAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const DEVICE_INFO: EEGDeviceInfo = {
transport: "bluetooth",
manufacturer: "Macrotellect",
model: "BL002 V2.0",
thumbnail: "/devices/brainlink-lite.png",
};

/**
Expand Down
Loading