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

Renamed from `@vlandoss/theme-toggle`. The package now lives at [withvariablelab/theme-toggle](https://github.com/withvariablelab/theme-toggle); `@vlandoss/theme-toggle` will not receive further releases.

**Breaking changes**

- Install and import `@variablelab/theme-toggle` instead of `@vlandoss/theme-toggle` (the inline script moves to `https://unpkg.com/@variablelab/theme-toggle/dist/initial.js`).
- The `localStorage` key is now `@variablelab/theme-toggle.colorMode` (was `@vlandoss/theme-toggle.colorMode`).
- The change event is now `@variablelab/theme-toggle.colorModeChange` (was `@vlandoss/theme-toggle.colorModeChange`). Update any `document.addEventListener(...)` listeners, or read the name from `window.__COLOR_MODE__.CUSTOM_EVENT`.

**Automatic migration**

Users keep their saved color mode. On init, both the inline script and `useColorMode` check for the 1.x key: if `@variablelab/theme-toggle.colorMode` is absent and `@vlandoss/theme-toggle.colorMode` exists, its value is copied to the new key. A value already stored under the new key always wins. Either way, the old key is removed.
29 changes: 26 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
branches:
- main

permissions:
contents: read # checkout

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand All @@ -15,15 +18,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

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

- name: ⌛ Setup pnpm
uses: variableland/gh-actions/actions/setup-pnpm@main
# First-party action, deliberately tracked at @main.
uses: withvariablelab/gh-actions/actions/setup-pnpm@main # zizmor: ignore[unpinned-uses]

- name: 💅 Check
run: pnpm rr check
Expand All @@ -36,3 +42,20 @@ jobs:

- name: 🔨 Build
run: pnpm rr pack

zizmor:
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

# Audits .github/ (the repo has no other workflows or actions). Passing the
# repo root keeps the annotation paths repo-relative so they land on the diff.
- name: 🌈 Audit workflows (zizmor)
env:
# Read-only token so zizmor also runs its online audits
# (known-vulnerable-actions, impostor-commit, ref-confusion).
GH_TOKEN: ${{ github.token }}
run: pipx run --spec zizmor==1.30.1 zizmor --format=github .
94 changes: 66 additions & 28 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
branches:
- main

# The org default GITHUB_TOKEN is read-only; each job asks for exactly what it needs.
permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Expand All @@ -17,68 +20,103 @@ jobs:
release:
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
# Pushes, the version PR and the GitHub releases go through the vland-bot
# token minted below, not GITHUB_TOKEN.
permissions:
id-token: write
contents: write
packages: write
pull-requests: write
id-token: write # npm trusted publishing (OIDC)
contents: read # checkout
steps:
- 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
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
token: ${{ steps.app-token.outputs.token }}
# changesets/action writes ~/.netrc with its GITHUB_TOKEN (the
# vland-bot token below) and pushes through it.
persist-credentials: false

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

- name: ⌛ Setup pnpm
uses: variableland/gh-actions/actions/setup-pnpm@main
# First-party action, deliberately tracked at @main.
uses: withvariablelab/gh-actions/actions/setup-pnpm@main # zizmor: ignore[unpinned-uses]

- name: 🔑 Mint vland-bot token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.VLAND_BOT_CLIENT_ID }}
private-key: ${{ secrets.VLAND_BOT_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write

- name: 🧑‍💻 Configure git user
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
run: |
git config user.name "vland-bot[bot]"
git config user.email "282456776+vland-bot[bot]@users.noreply.github.com"
user_id="$(gh api "/users/${APP_SLUG}[bot]" --jq .id)"
git config user.name "${APP_SLUG}[bot]"
git config user.email "${user_id}+${APP_SLUG}[bot]@users.noreply.github.com"

- name: 🚀 Create release PR or publish versions
uses: changesets/action@v1
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0
with:
commit: "chore: update versions"
title: "chore: update versions"
publish: pnpm changeset publish
setupGitUser: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# The bot opens the version PR, so CI runs on it.
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
LEFTHOOK: 0

# Runs the PR's code and publishes the preview to npm via OIDC. It never sees
# the vland-bot key; it only hands the published package list to the next job.
preview:
if: ${{ github.event_name == 'pull_request' && (startsWith(github.head_ref, 'feat') || startsWith(github.head_ref, 'fix') || contains(github.event.pull_request.labels.*.name, 'preview')) }}
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
id-token: write # npm trusted publishing (OIDC)
contents: read # checkout
outputs:
packages: ${{ steps.preview.outputs.packages }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

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

- name: ⌛ Setup CI
uses: variableland/gh-actions/actions/setup-pnpm@main
# First-party action, deliberately tracked at @main.
uses: withvariablelab/gh-actions/actions/setup-pnpm@main # zizmor: ignore[unpinned-uses]

- name: 🚀 Preview release
uses: variableland/gh-actions/actions/preview-release@main
- name: 🚀 Publish preview
id: preview
# First-party action, deliberately tracked at @main.
uses: withvariablelab/gh-actions/actions/preview-release@main # zizmor: ignore[unpinned-uses]
with:
npm_token: ${{ secrets.NPM_TOKEN }}
mode: publish
# Keep this explicit while the repo has a project .npmrc: preview-release
# auto-detects any .npmrc as token-only, and there is no NPM_TOKEN here.
publish_mode: oidc-only

# Never checks out or runs PR code: the reusable workflow mints the vland-bot
# token (pull-requests: write, this repo only) and upserts the PR comment.
preview-comment:
needs: preview
if: ${{ needs.preview.result == 'success' }}
permissions: {}
# First-party reusable workflow, deliberately tracked at @main.
uses: withvariablelab/gh-actions/.github/workflows/preview-comment.yml@main # zizmor: ignore[unpinned-uses]
with:
action: preview-release
packages: ${{ needs.preview.outputs.packages }}
secrets:
VLAND_BOT_PRIVATE_KEY: ${{ secrets.VLAND_BOT_PRIVATE_KEY }}
18 changes: 18 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Supply-chain guard: never resolve a dependency version published less than
# 24 h ago (most malicious releases are pulled within the first hour). Requires
# pnpm >= 10.16.
#
# No exclude list on purpose: our own scopes (@variablelab/*, @rrlab/*) wait
# 24 h too. In .npmrc, `minimum-release-age-exclude[]` breaks whenever pnpm is
# launched through npm (`npx pnpm`, `npm exec`, `npm run`). npm re-exports the
# array as a single string, and pnpm 10.30.3 then exempts every package. To
# adopt a fresh own release right away, pass
# `--config.minimum-release-age=0` to that one `pnpm add`.
#
# This lives here instead of pnpm-workspace.yaml because @rrlab/cli (through
# @vlandoss/clibuddy 0.7.1) treats any repo with a pnpm-workspace.yaml as a
# monorepo, and `rr tsc` then fails ("Invalid workspace patterns") or, with
# `packages: ["."]`, silently skips this package. Once
# https://github.com/withvariablelab/dx/issues/249 ships, move it there with
# minimumReleaseAgeExclude, and delete this file.
minimum-release-age=1440
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# @vlandoss/theme-toggle
# @variablelab/theme-toggle

## 1.0.0

### Major Changes

- [#2](https://github.com/variableland/theme-toggle/pull/2) [`dd42ff8`](https://github.com/variableland/theme-toggle/commit/dd42ff8bd1ca2b96c6484f8a323d17aa916e158a) Thanks [@rqbazan](https://github.com/rqbazan)! - 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.
- [#2](https://github.com/withvariablelab/theme-toggle/pull/2) [`dd42ff8`](https://github.com/withvariablelab/theme-toggle/commit/dd42ff8bd1ca2b96c6484f8a323d17aa916e158a) Thanks [@rqbazan](https://github.com/rqbazan)! - 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.
33 changes: 23 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @vlandoss/theme-toggle
# @variablelab/theme-toggle

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).
Expand All @@ -12,7 +12,7 @@ sets the theme **before first paint** to avoid the flash of the wrong theme (FOU
## Installation

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

## Usage
Expand All @@ -26,7 +26,7 @@ Load the prebuilt IIFE **before your app renders** — ideally as the first thin

```html
<head>
<script src="https://unpkg.com/@vlandoss/theme-toggle/dist/initial.js"></script>
<script src="https://unpkg.com/@variablelab/theme-toggle/dist/initial.js"></script>
<!-- ...rest of head... -->
</head>
```
Expand All @@ -38,7 +38,7 @@ exposes its runtime contract on `window.__COLOR_MODE__`.
### 2. React hook (read & control the mode)

```tsx
import { useColorMode } from "@vlandoss/theme-toggle";
import { useColorMode } from "@variablelab/theme-toggle";

function ThemeSwitch() {
const { colorMode, setColorMode } = useColorMode();
Expand Down Expand Up @@ -81,17 +81,17 @@ body {

## How it works

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

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

```ts
document.addEventListener("@vlandoss/theme-toggle.colorModeChange", (event) => {
document.addEventListener("@variablelab/theme-toggle.colorModeChange", (event) => {
console.log(event.detail.colorMode); // "light" | "dark" | "system"
});
```
Expand All @@ -100,6 +100,19 @@ 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.

## Upgrading from `@vlandoss/theme-toggle` 1.x

The package was renamed to `@variablelab/theme-toggle` in 2.0.0:

1. Replace the dependency and imports: `pnpm remove @vlandoss/theme-toggle && pnpm add @variablelab/theme-toggle`,
and update the inline script URL to `https://unpkg.com/@variablelab/theme-toggle/dist/initial.js`.
2. Listeners of the change event must use the new name, `@variablelab/theme-toggle.colorModeChange`
(or read it from `window.__COLOR_MODE__.CUSTOM_EVENT`).

Saved preferences carry over automatically. On init, if the old `@vlandoss/theme-toggle.colorMode`
key exists, its value is copied to the new storage key, unless the new key already holds a value
(that value wins). The old key is then removed either way.

## Development

```sh
Expand Down
2 changes: 1 addition & 1 deletion mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ _.path = ["{{config_root}}/node_modules/.bin"]

[tools]
node = "24.16.0"
pnpm = "10.6.4"
pnpm = "10.30.3"
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "@vlandoss/theme-toggle",
"name": "@variablelab/theme-toggle",
"version": "1.0.0",
"description": "Light/dark/system color-mode toggle for React with a no-flash inline script",
"homepage": "https://github.com/variableland/theme-toggle#readme",
"homepage": "https://github.com/withvariablelab/theme-toggle#readme",
"bugs": {
"url": "https://github.com/variableland/theme-toggle/issues"
"url": "https://github.com/withvariablelab/theme-toggle/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/variableland/theme-toggle.git"
"url": "git+https://github.com/withvariablelab/theme-toggle.git"
},
"license": "MIT",
"author": "rcrd <rcrd@variable.land>",
Expand Down Expand Up @@ -62,5 +62,5 @@
"react": ">=18",
"react-dom": ">=18"
},
"packageManager": "pnpm@10.6.4"
"packageManager": "pnpm@10.30.3"
}
11 changes: 9 additions & 2 deletions src/const.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export const COLOR_MODE_EVENT = "@vlandoss/theme-toggle.colorModeChange";
export const COLOR_MODE_EVENT = "@variablelab/theme-toggle.colorModeChange";

export const COLOR_MODE_STORAGE_KEY = "@vlandoss/theme-toggle.colorMode";
export const COLOR_MODE_STORAGE_KEY = "@variablelab/theme-toggle.colorMode";

/**
* Storage key written by `@vlandoss/theme-toggle` 1.x. On init its value is
* moved to {@link COLOR_MODE_STORAGE_KEY} unless that key is already set, and
* the legacy key is removed (see `migrateLegacyColorMode`).
*/
export const LEGACY_COLOR_MODE_STORAGE_KEY = "@vlandoss/theme-toggle.colorMode";

export const COLOR_MODE = {
DARK: "dark",
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from "react";
import { COLOR_MODE, COLOR_MODE_EVENT, COLOR_MODE_STORAGE_KEY, type ColorMode } from "./const.ts";
import { migrateLegacyColorMode } from "./migrate.ts";

export function useColorMode() {
const [colorMode, setColorMode] = React.useState<ColorMode>(() => {
Expand All @@ -8,6 +9,8 @@ export function useColorMode() {
}

try {
migrateLegacyColorMode();

const raw = localStorage.getItem(COLOR_MODE_STORAGE_KEY);

if (!raw) {
Expand Down
3 changes: 3 additions & 0 deletions src/initial.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { COLOR_MODE, COLOR_MODE_EVENT, COLOR_MODE_STORAGE_KEY } from "./const.ts";
import { migrateLegacyColorMode } from "./migrate.ts";

function getColorMode() {
migrateLegacyColorMode();

const persistedColorMode = localStorage.getItem(COLOR_MODE_STORAGE_KEY);

return typeof persistedColorMode === "string" ? persistedColorMode : COLOR_MODE.SYSTEM;
Expand Down
Loading
Loading