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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,15 @@ jobs:
# --summary detailed appends an end-of-log recap naming every action
# with status, duration, and cache hash, so a failed target is readable
# from the log tail without scrolling through interleaved task output.
- name: Run format
run: >-
moon run root:format-check

- name: Run affected tasks
run: >-
moon ci --include-relations --summary detailed :build demo:build
diffshub:build docs:build docs:build-trees-site path-store:build-demo
:test :typecheck trees:test-e2e root:lint root:lint-css
root:format-check

actions-pinned:
name: Actions pinned to SHA
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ tsconfig.tsbuildinfo
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/launch.json
!.vscode/settings.json
!.vscode/tasks.json
.idea
.DS_Store
*.suo
Expand Down
9 changes: 7 additions & 2 deletions .moon/workspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ vcs:
sync: true
hooks:
pre-commit:
# Typecheck the projects affected by staged files (and their
# dependents), then let lint-staged fix and re-stage style issues.
# Typecheck the projects affected by staged files (and their dependents).
# Some affected checks build committed generated files first. Run the
# repo formatter over the post-build worktree, including generated files
# that were not staged. lint-staged still runs after that because it only
# sees staged paths and re-stages formatter/linter fixes for files that
# are included in the commit.
- 'moon exec :typecheck --affected --status staged'
- 'moon run root:format'
- 'bun lint-staged'
pre-push:
# Git LFS guard: forwards the hook arguments and stdin to git-lfs.
Expand Down
1 change: 1 addition & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"recommendations": [
"YoavBls.pretty-ts-errors",
"oxc.oxc-vscode",
"pierrecomputer.pierre-theme",
"esbenp.prettier-vscode",
"TypeScriptTeam.native-preview",
"bradlc.vscode-tailwindcss",
Expand Down
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Theme Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}/packages/theme"],
"preLaunchTask": "moon: theme build"
},
{
"name": "Run Theme Extension Without Build",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}/packages/theme"]
}
]
}
13 changes: 13 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "moon: theme build",
"type": "shell",
"command": "moon",
"args": ["run", "theme:build"],
"group": "build",
"problemMatcher": []
}
]
}
4 changes: 2 additions & 2 deletions apps/diffshub/components/themeCatalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import { themes } from '@pierre/theming/themes';

export const docsThemeCatalog = createThemeCatalog({
themes,
defaultLightThemeName: 'pierre-light',
defaultDarkThemeName: 'pierre-dark',
defaultLightThemeName: 'pierre-light-soft',
defaultDarkThemeName: 'pierre-dark-soft',
});
2 changes: 1 addition & 1 deletion apps/diffshub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"dependencies": {
"@pierre/diffs": "workspace:*",
"@pierre/icons": "catalog:",
"@pierre/theme": "catalog:",
"@pierre/theme": "workspace:*",
"@pierre/theming": "workspace:*",
"@pierre/trees": "workspace:*",
"@pierre/truncate": "workspace:*",
Expand Down
3 changes: 3 additions & 0 deletions apps/diffshub/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
{
"path": "../../packages/truncate/tsconfig.json"
},
{
"path": "../../packages/theme/tsconfig.json"
},
{
"path": "../../packages/theming/tsconfig.json"
}
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"@icons-pack/react-simple-icons": "catalog:",
"@pierre/diffs": "workspace:*",
"@pierre/icons": "catalog:",
"@pierre/theme": "catalog:",
"@pierre/theme": "workspace:*",
"@pierre/theming": "workspace:*",
"@pierre/tree-test-data": "workspace:*",
"@pierre/trees": "workspace:*",
Expand Down
565 changes: 546 additions & 19 deletions bun.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ minimumReleaseAge = 604800 # 7 days in seconds
minimumReleaseAgeExcludes = [
"@types/bun",
"@pierre/icons",
"@pierre/theme",
"@pierre/vscode-icons",
# moon is exact-pinned everywhere it is consumed (.prototools, vercel.json),
# so the age gate adds no protection and would block fresh moon releases.
Expand Down
1 change: 1 addition & 0 deletions moon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ language: 'typescript'
# dist-producing package and lint orders itself after their builds.
dependsOn:
- 'diffs'
- 'theme'
- 'theming'
- 'trees'
- 'truncate'
Expand Down
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"@octokit/rest": "22.0.0",
"@pierre/icons": "0.5.0",
"@pierre/storage": "0.0.10",
"@pierre/theme": "1.0.3",
"@pierre/vscode-icons": "0.0.9",
"@playwright/test": "1.51.1",
"@radix-ui/react-avatar": "1.1.10",
Expand All @@ -39,12 +38,14 @@
"@shikijs/transformers": "4.2.0",
"@tailwindcss/postcss": "4.1.13",
"@types/bun": "1.3.12",
"@types/culori": "4.0.1",
"@types/hast": "3.0.4",
"@types/jsdom": "28.0.1",
"@types/mdast": "4.0.4",
"@types/node": "^20",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@vscode/vsce": "3.2.2",
"@typescript/native-preview": "7.0.0-dev.20260128.1",
"@vscode/web-custom-data": "0.6.3",
"@vitejs/plugin-react": "5.0.3",
Expand All @@ -55,6 +56,7 @@
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"culori": "4.0.2",
"diff": "8.0.3",
"hast-util-heading-rank": "3.0.0",
"hast-util-to-html": "9.0.5",
Expand All @@ -67,6 +69,7 @@
"mitata": "1.0.34",
"next": "16.2.3",
"next-mdx-remote": "6.0.0",
"ovsx": "1.0.0",
"oxfmt": "0.27.0",
"oxlint": "1.42.0",
"oxlint-tsgolint": "0.11.3",
Expand Down Expand Up @@ -132,6 +135,9 @@
"oxfmt --write",
"oxlint --type-aware --tsconfig tsconfig.oxlint.json --fix"
],
"**/*.{json,jsonc,yml,yaml}": [
"oxfmt --write"
],
"**/*.{css,pcss,postcss}": [
"stylelint --fix"
]
Expand Down
2 changes: 1 addition & 1 deletion packages/diffs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"prepublishOnly": "moon run diffs:prepublish"
},
"dependencies": {
"@pierre/theme": "catalog:",
"@pierre/theme": "workspace:*",
"@pierre/theming": "workspace:*",
"@shikijs/transformers": "^3.0.0 || ^4.0.0",
"diff": "catalog:",
Expand Down
6 changes: 6 additions & 0 deletions packages/theme/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
node_modules/
build
dist
preview
*.vsix
19 changes: 19 additions & 0 deletions packages/theme/.vscodeignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
node_modules/**
src/**
scripts/**
test/**
preview/**
.github/**
.vscode/**
zed/**

ACCESSIBILITY.md
CONTRIBUTING.md
DISPLAY-P3.md
.gitignore
README.md.bak
moon.yml
tsconfig.json
*.tsbuildinfo
*.vsix
.DS_Store
139 changes: 139 additions & 0 deletions packages/theme/ACCESSIBILITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Accessibility

## Color-Vision-Deficiency (CVD) themes

Pierre ships four themes designed for people with a **color vision deficiency
(CVD)**, the condition commonly called "color blindness." This document explains
what CVD is and how the themes are engineered around it.

| `name` | label | type | targets |
| -------------------------------------- | -------------------------------------- | ----- | ----------- |
| `pierre-light-protanopia-deuteranopia` | Pierre Light Protanopia & Deuteranopia | light | red-green |
| `pierre-light-tritanopia` | Pierre Light Tritanopia | light | blue-yellow |
| `pierre-dark-protanopia-deuteranopia` | Pierre Dark Protanopia & Deuteranopia | dark | red-green |
| `pierre-dark-tritanopia` | Pierre Dark Tritanopia | dark | blue-yellow |

---

### What is CVD?

The retina senses color with three cone types — **L** (long / "red"), **M**
(medium / "green") and **S** (short / "blue") wavelengths. When one type is
missing or shifted, colors that differ mainly along that cone's axis collapse
into the same perceived color. CVD affects roughly **8% of men and 0.5% of
women**.

The three dichromacies (the strongest, "complete" forms) we target:

| Type | Missing cone | Confuses | **Preserved** axis (what can still be told apart) |
| ---------------- | ------------ | ---------------------------------- | ------------------------------------------------- |
| **Protanopia** | L ("red") | red ↔ green | **blue ↔ orange/yellow** + luminance |
| **Deuteranopia** | M ("green") | red ↔ green | **blue ↔ orange/yellow** + luminance |
| **Tritanopia** | S ("blue") | blue ↔ green (and yellow ↔ violet) | **red ↔ cyan/teal** + luminance |

> Tritanopia is loosely called "blue-yellow," but blue and yellow differ a lot
> in _luminance_ (which is preserved), so they stay apparent. The pair that
> truly collapses is **blue ↔ green**.

The key consequence for a code editor: a normal theme encodes the most important
signals — **added vs deleted**, **pass vs fail**, **error vs warning** — as
**red vs green**. To a protanope or deuteranope (the most common CVD), red and
green look nearly identical, so those signals become ambiguous.

---

### How the themes are engineered

Four principles, each enforced or made checkable by the build:

1. **Identical chrome = family fit.** Each CVD theme reuses the base
`light`/`dark` roles for `bg`, `fg`, and `border` **verbatim**. Windows,
text, and borders are pixel-for-pixel identical to Pierre Light / Pierre Dark
— that is what keeps them recognizably "Pierre." Only the chromatic roles
(`accent`, `states`, `syntax`, `ansi`) change.

2. **Signals ride the preserved axis.** Every meaningful color is re-mapped onto
the hue axis that the target deficiency keeps:
- **Protan/Deutan:** positive/added → **blue**, negative/deleted →
**orange**.
- **Tritan:** positive/added → **teal/cyan**, negative/deleted →
**red/vermillion**.

3. **Luminance is the backup channel.** Under dichromacy there are only ~2
usable hue poles + luminance, but ~20 chromatic roles. Where two roles must
share a pole (e.g. several "cool" syntax tokens), they are separated by
**luminance** (different palette stops) — the channel CVD users rely on most.

4. **Reuse the existing palette.** All colors come from scales already in
`src/palettes.ts` (`blue`, `orange`, `teal`, `vermillion`, `magenta`, …). No
off-brand hues were invented.

#### Role mapping (the actual choices)

Stops shift lighter on dark backgrounds (mirroring how `dark` shifts vs
`light`).

**Protan/Deutan — axis blue ↔ orange:**

| Role | Light | Dark | Why |
| --------------------------- | ------------- | ------------- | --------------------------------------------- |
| `accent.primary` / `link` | blue 500 | blue 500 | keep Pierre blue (brand) |
| `success` (added) | blue 700 | blue 300 | positive → blue, luminance-split from accent |
| `danger` (deleted/error) | orange 700 | orange 400 | negative → orange |
| `warn` | yellow 500 | yellow 300 | bright caution; big luminance gap from danger |
| `info` | cyan 700 | cyan 400 | cool side; pairs against merge |
| `merge` | violet 700 | violet 400 | blue-violet conflict color |
| `ansi.red` / `ansi.green` | orange / blue | orange / blue | terminal pass/fail separable |
| `syntax.string` (=inserted) | blue 800 | blue 300 | added pole |
| `syntax.tag` (=deleted) | orange 700 | orange 400 | deleted pole |

**Tritanopia — axis red ↔ cyan/teal:**

| Role | Light | Dark | Why |
| --------------------------- | ----------------- | ----------------- | ----------------------------------------------------- |
| `accent.primary` / `link` | blue 500 | blue 500 | reads cyan-blue, clearly ≠ red |
| `success` (added) | teal 700 | teal 300 | positive → teal/cyan |
| `danger` (deleted/error) | vermillion 600 | vermillion 400 | negative → red (preserved) |
| `warn` | amber 600 | amber 400 | caution; ΔE-separated from danger |
| `info` | blue 600 | blue 400 | cyan side |
| `merge` | magenta 700 | magenta 400 | reddish-purple — tritan-safe, far from blue _and_ red |
| `ansi.red` / `ansi.green` | vermillion / teal | vermillion / teal | terminal pass/fail separable |
| `syntax.string` (=inserted) | teal 700 | teal 300 | added pole |
| `syntax.tag` (=deleted) | vermillion 600 | vermillion 400 | deleted pole |

---

### The objective test

A hard gate in `test/` simulates every chromatic role for each deficiency —
under _both_ the linear-RGB and gamma-sRGB Machado (2009) conventions — and
fails the build if any must-distinguish pair stops being separable (CIEDE2000
ΔE) or legible (WCAG contrast). It enforces the tiers and contrast policy below.
For how to run and read the report, see
[CONTRIBUTING.md](CONTRIBUTING.md#testing).

#### Tiers — graded by _what carries the signal when color fails_

Under dichromacy not every pair can be hue-unique, so we gate hardest where
color is the _only_ cue and lean on the editor's built-in non-color cues
elsewhere:

| Tier | Bar | What | Why this bar |
| ----- | -------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| **1** | ΔE ≥ 11 | diff add/delete backgrounds, diff inserted/deleted **text**, merge-conflict backgrounds, terminal red/green | color is the **sole** disambiguator — no glyph fallback |
| **2** | ΔE ≥ 8 | diagnostics (error/warn/info), core syntax (keyword/string/variable), comment-vs-code | color **plus** a non-color cue (icon shapes; position) |
| **3** | advisory | git-tree clique, extended syntax | every git entry has an **M/A/D/U/C letter badge**; syntax has bold/italic + position. Reported, not gated. |

#### Contrast policy

We hold the themes to WCAG bars, but only the bar that fits how each color
renders:

- **Body text** (editor foreground) → **4.5:1** (SC 1.4.3 normal text).
- **Syntax tokens & meaningful signal colors** → **3:1** (SC 1.4.11 UI / large
text), checked normal **and** after simulation.
- **Report-only** (printed, never fails the build): colors whose canonical/brand
hue is intrinsically high-luminance and that base Pierre itself keeps bright —
`accent.primary`/`link` (brand blue), `warn` (caution yellow/amber), and the
decorative ansi colors. Their _distinguishability_ is gated; their raw
contrast is not.
Loading