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
6 changes: 3 additions & 3 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "theme-toggle/theme-toggle" }],
"commit": false,
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": ["@changesets/changelog-github", { "repo": "variableland/theme-toggle" }],
"commit": true,
"fixed": [],
"linked": [],
"access": "public",
Expand Down
5 changes: 5 additions & 0 deletions .changeset/initial-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vlandoss/theme-toggle": major
---

Initial stable release. Ships the `useColorMode` React hook and an inline pre-paint script for `light` / `dark` / `system` color modes. The `system` mode resolves against the OS `prefers-color-scheme` and tracks it live, the choice is persisted to `localStorage`, a `dark` class is toggled on `<html>`, and a `CustomEvent` is dispatched on every change.
38 changes: 27 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
name: CI

on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened, labeled]
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- name: ⬇️ Checkout repo
uses: actions/checkout@v4

- name: 🟢 Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm rr jsc
- run: pnpm rr tsc
- run: pnpm vitest run
- run: pnpm rr pack

- name: ⌛ Setup pnpm
uses: variableland/gh-actions/actions/setup-pnpm@main

- name: 💅 Check
run: pnpm rr check

- name: 🎭 Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium

- name: 🧪 Test
run: pnpm test

- name: 🔨 Build
run: pnpm rr pack
38 changes: 38 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Preview

on:
pull_request:
types: [opened, synchronize, reopened, labeled]
branches:
- main

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
preview:
runs-on: ubuntu-latest

if: ${{ startsWith(github.head_ref, 'feat') || startsWith(github.head_ref, 'fix') || contains(github.event.pull_request.labels.*.name, 'preview') }}

permissions:
id-token: write
contents: read

steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4

- name: 🟢 Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version

- name: ⌛ Setup CI
uses: variableland/gh-actions/actions/setup-pnpm@main

- name: 🚀 Preview release
uses: variableland/gh-actions/actions/preview-release@main
with:
npm_token: ${{ secrets.NPM_TOKEN }}
57 changes: 38 additions & 19 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,55 @@ name: Release

on:
push:
branches: [main]
branches:
- main

concurrency: ${{ github.workflow }}-${{ github.ref }}

permissions:
contents: write
pull-requests: write
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
release:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: write
packages: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- name: 🔑 Mint vland-bot token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.VLAND_BOT_APP_ID }}
private-key: ${{ secrets.VLAND_BOT_PRIVATE_KEY }}

- name: ⬇️ Checkout repo
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}

- name: 🟢 Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version
cache: pnpm
registry-url: https://registry.npmjs.org
- run: pnpm install --frozen-lockfile
- run: pnpm rr pack

- name: Create release PR or publish
- name: ⌛ Setup pnpm
uses: variableland/gh-actions/actions/setup-pnpm@main

- name: 🧑‍💻 Configure git user
run: |
git config user.name "vland-bot[bot]"
git config user.email "282456776+vland-bot[bot]@users.noreply.github.com"

- name: 🚀 Create release PR or publish versions
uses: changesets/action@v1
with:
commit: "chore: update versions"
title: "chore: update versions"
publish: pnpm changeset publish
commit: "chore: release"
title: "chore: release"
setupGitUser: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
LEFTHOOK: 0
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ node_modules
dist
.turbo
coverage
__screenshots__
.vitest-attachments

# editor / OS
.DS_Store
Expand Down
118 changes: 91 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,51 +1,115 @@
# theme-toggle
# @vlandoss/theme-toggle

A standalone TypeScript library scaffolded with [`@vlandoss/vland`](https://github.com/variableland/dx/tree/main/vland/cli).
Light/dark/system color-mode toggle for React apps, with a tiny inline script that
sets the theme **before first paint** to avoid the flash of the wrong theme (FOUC).

## Install
- Three modes: `light`, `dark`, and `system` (defaults to `system`).
- `system` resolves against the OS `prefers-color-scheme` and tracks it live.
- Persists the user's choice in `localStorage`.
- Applies a `dark` class on `<html>` so you can style with plain CSS.
- Emits a `CustomEvent` on every change so non-React code can react too.

## Installation

```sh
pnpm add theme-toggle
pnpm add @vlandoss/theme-toggle
```

## Usage

```ts
import { greet } from "theme-toggle";
The package ships **two pieces** that work together.

### 1. Inline script (prevents the theme flash)

greet("vland");
Load the prebuilt IIFE **before your app renders** — ideally as the first thing in
`<head>`, blocking, so the correct `dark` class is on `<html>` before the page paints.

```html
<head>
<script src="https://unpkg.com/@vlandoss/theme-toggle/dist/initial.js"></script>
<!-- ...rest of head... -->
</head>
```

## Set up tooling
On load it reads the stored mode (or `system` by default), toggles the `dark` class
on `<html>` — resolving `system` against the OS preference — persists the mode, and
exposes its runtime contract on `window.__COLOR_MODE__`.

This project uses [`rr`](https://github.com/variableland/dx/tree/main/run-run) (the `@rrlab/cli`) as the single entry point for lint, format, type-check, and build. The pre-configured `mise.toml` puts `./node_modules/.bin` on your `PATH`, so `rr` is invokable directly from the project root.
### 2. React hook (read & control the mode)

```sh
pnpm install # also pulls in @rrlab/cli
rr plugins add biome # lint + format (writes biome.json)
rr plugins add ts # type-check (writes tsconfig.json)
rr plugins add tsdown # bundle (writes tsdown.config.ts)
```tsx
import { useColorMode } from "@vlandoss/theme-toggle";

function ThemeSwitch() {
const { colorMode, setColorMode } = useColorMode();
// colorMode -> "light" | "dark" | "system"

return (
<select value={colorMode} onChange={(e) => setColorMode(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
);
}
```

Each `rr plugins add` records the plugin in `run-run.config.mts` and scaffolds its config file. Re-run any time you want a different toolset.
Calling `setColorMode` persists the choice to `localStorage`, toggles the `dark` class
on `<html>`, and dispatches the change event. While `colorMode` is `system`, the hook
also follows the OS preference live as it changes.

## Develop
## Styling

```sh
rr jsc # lint + format check
rr jsc --fix # auto-fix
rr tsc # type-check
rr pack # build to dist/
vitest run # tests
Style the two themes with the `dark` class that gets toggled on `<html>`:

```css
:root {
--bg: white;
--fg: black;
}

.dark {
--bg: black;
--fg: white;
}

body {
background: var(--bg);
color: var(--fg);
}
```

## Release
## How it works

| Concern | Value |
| -------------- | ------------------------------------------ |
| DOM class | `dark` on `document.documentElement` |
| Storage key | `@vlandoss/theme-toggle.colorMode` |
| Change event | `@vlandoss/theme-toggle.colorModeChange` |

The change event is a `CustomEvent` dispatched on `document`, with the new value in
`event.detail.colorMode`:

This package uses [Changesets](https://github.com/changesets/changesets). As a developer you only need to describe the change; the publish step is run by CI.
```ts
document.addEventListener("@vlandoss/theme-toggle.colorModeChange", (event) => {
console.log(event.detail.colorMode); // "light" | "dark" | "system"
});
```

The inline script also publishes these names on `window.__COLOR_MODE__`
(`STORAGE_KEY`, `CUSTOM_EVENT`, `COLOR_MODE`) so other scripts can read them without
importing the package.

## Development

```sh
pnpm changeset # describe a change
pnpm changeset version # bump versions + changelog (locally, when needed)
pnpm install
pnpm test # functional tests run in a real browser via Vitest Browser Mode + Playwright
```

CI invokes `pnpm changeset publish` on `main`. `prepublishOnly` runs `rr pack` automatically before publish.
The suite in `test/functional` covers both entry points: the `useColorMode` hook
(`src/index.ts`) and the inline script (`src/initial.ts`).

## License

MIT
4 changes: 4 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.1/schema.json",
"extends": ["@rrlab/biome-config"]
}
2 changes: 1 addition & 1 deletion lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ pre-commit:
pre-push:
jobs:
- name: test
run: pnpm vitest run
run: pnpm test
Loading
Loading