Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/dark-mode-prime-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@viamrobotics/motion-tools': minor
---

Add `DarkMode` plugin
1 change: 1 addition & 0 deletions docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default defineConfig({
{
label: 'Plugins',
items: [
{ label: '<DarkMode />', link: '/plugins/dark-mode/' },
{ label: '<Debug />', link: '/plugins/debug/' },
{ label: '<DrawService />', link: '/plugins/draw-service/' },
{ label: '<MeasureTool />', link: '/plugins/measure-tool/' },
Expand Down
68 changes: 68 additions & 0 deletions docs/src/content/docs/plugins/dark-mode.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: <DarkMode />
description: Cycle the motion-tools visualizer between system, dark, and light themes, defaulting to the user's system preference.
---

`<DarkMode />` adds a theme button to the visualizer dashboard that cycles between three modes — **system**, **dark**, and **light**. It shows a sun/moon icon for system, a moon for dark, and a sun for light, and the choice is persisted across reloads.

In **system** mode (the default) the theme follows the user's operating-system preference (`prefers-color-scheme`) **live** — if the OS switches (for example a sunset auto-dark schedule), the visualizer follows along. **dark** and **light** pin the theme regardless of the OS. Each press of the button advances `system → dark → light → system`.

The visualizer follows the system preference on its own — mounting `<DarkMode />` is only needed to give users a manual override.

## Usage

Mount `<DarkMode />` anywhere inside `<Visualizer />`:

```svelte
<script lang="ts">
import { Visualizer } from '@viamrobotics/motion-tools'
import { DarkMode } from '@viamrobotics/motion-tools/plugins'
</script>

<div class="h-screen w-screen">
<Visualizer>
<DarkMode />
</Visualizer>
</div>
```

## How it works

`<Visualizer />` owns the theme, stored as a `system` / `dark` / `light` mode (persisted to local storage, default `system`):

- In `system` mode it sets **no** class on the document's root element, leaving the CSS `prefers-color-scheme` media query in charge — so the theme tracks the OS live.
- `dark` / `light` add a `.dark` / `.light` class to the root element that overrides the media query and pins the theme.

Two things respond to that state:

- **CSS** — the [`@viamrobotics/tailwind-config`](https://github.com/viamrobotics/prime/tree/main/packages/tailwind-config) dark theme reassigns its color tokens under the dark scope (the `prefers-color-scheme` media query in `system` mode, or the `.dark` class when pinned), so every semantic token (`text-default`, `bg-light`, `border-medium`, the status colors, …) flips with no markup changes. `color-scheme` follows the same class, so native controls and scrollbars match.
- **WebGL / JS surfaces** that can't read CSS tokens switch off the effective theme: the Tweakpane controls (`primeTheme` ↔ `primeThemeDark`), the scene backdrop and grid, the selection outline, and the measurement line.

## Reading the theme

Use `useDarkMode()` to read or cycle the theme from your own components anywhere inside `<Visualizer />`. Most code wants `isDark` — the resolved boolean, which already accounts for the OS in `system` mode:

```svelte
<script lang="ts">
import { useDarkMode, ThemeModes } from '@viamrobotics/motion-tools/plugins'

const darkMode = useDarkMode()
</script>

<p>Mode: {darkMode.current}</p>
<p>Currently {darkMode.isDark ? 'dark' : 'light'}</p>

<button onclick={() => darkMode.toggle()}> Cycle theme </button>

{#if darkMode.current === ThemeModes.System}
<span>Following the system preference</span>
{/if}
```

| Member | Type | Description |
| ---------- | ------------------------------- | ---------------------------------------------------------------------------------- |
| `current` | `'system' \| 'dark' \| 'light'` | The chosen mode. Compare against the exported `ThemeModes` constants. |
| `isDark` | `boolean` | The active theme — `true` when dark is showing (resolves `system` against the OS). |
| `toggle()` | `() => void` | Advance to the next mode: `system → dark → light → system`. |

`ThemeModes` (`{ System, Dark, Light }`) is exported for comparing `current`. Outside the provided context, the hook returns a light-mode default (`current: 'light'`, `isDark: false`) with a no-op `toggle()`.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@
"@eslint/js": "10.0.1",
"@playwright/test": "1.55.1",
"@sentry/sveltekit": "10.10.0",
"@skeletonlabs/skeleton": "3.2.0",
"@skeletonlabs/skeleton-svelte": "1.5.1",
"@sveltejs/adapter-static": "3.0.9",
"@sveltejs/kit": "2.60.1",
"@sveltejs/package": "2.5.7",
Expand All @@ -77,7 +75,8 @@
"@viamrobotics/prime-core": "0.1.5",
"@viamrobotics/sdk": "0.69.0",
"@viamrobotics/svelte-sdk": "1.2.2",
"@viamrobotics/tweakpane-config": "0.1.1",
"@viamrobotics/tailwind-config": "1.1.0",
"@viamrobotics/tweakpane-config": "0.2.1",
"@vitest/browser": "3.2.4",
"@vitest/coverage-v8": "^3.2.4",
"@zag-js/collapsible": "1.22.1",
Expand Down
Loading
Loading