Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
38 changes: 38 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# CLAUDE.md

Read `CONTEXT.md` for the vocabulary. Every term in it has one meaning.

## UI is Radix Themes

Build every DOM-rendered control out of `@radix-ui/themes` components —
`Button`, `Spinner`, `Popover`, `Checkbox`, `Text`, and the rest. Reach for the
Radix component first and check its API before writing markup; a hand-rolled
`<button>` or `<div role="dialog">` is a bug in this codebase, not a shortcut.

Radix Themes is a peer dependency, so the host app supplies it and we cost them
no extra bytes by using more of it.

### Where the escape hatches actually are

- **Canvas cells.** Cells inside `@glideapps/glide-data-grid` are painted on a
`<canvas>`, so no DOM component reaches them. Draw those by hand. Their
_editors_ are real DOM and stay Radix.
- **Third-party pickers.** `react-day-picker`, `react-number-format`, and
`react-phone-number-input` fill gaps Radix has no component for. Wrap them in
Radix layout and Radix tokens so they match.

### Layout and colour

Tailwind utilities carry layout only, and every one is `dg:`-prefixed
(`dg:flex`, `dg:gap-2`). Colour comes from Radix tokens written as arbitrary
values — `dg:text-[var(--gray-11,#60646c)]` — never from a Tailwind palette
colour.

Using a Radix colour scale for the first time means adding its token CSS to
`src/theme/radixStyles.ts`, and adding the scale to `RADIX_BADGE_SCALES` in
`src/lib/grid/radixBadgePalette.ts` when a badge uses it. The two lists stay in
sync; a missing import warns at runtime.

## Appearance

The engine reads light or dark. It never decides it.
37 changes: 29 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,22 +157,43 @@ npm run lint
npm run build # dist/ — JS, .d.ts, and datagrid.css
```

`just check` runs all four in one go. The recipes live in
[`justfile`](./justfile) and need [`just`](https://github.com/casey/just)
installed; everything they call is a plain `npm` script, so `just` is a
convenience, never a requirement.

## Releasing

Versions ship from CI on a `v*` tag, authenticated with npm **trusted
publishing** (OIDC) — there is no `NPM_TOKEN` anywhere in this repo.

```bash
# bump package.json first, then:
git tag v0.1.1
git push origin main --tags
just release patch # or: minor, major
```

The workflow refuses to publish if the tag and `package.json` disagree.

npm cannot enable OIDC for a package that does not exist yet
([npm/cli#8544](https://github.com/npm/cli/issues/8544)), so the very first
version was published by hand. See the header of
That is the whole process. `just release` runs the checks, bumps
`package.json`, commits, tags, and pushes — in that order, so the tag always
points at the code that actually ships. It refuses to run from a dirty tree, a
branch other than `main`, a branch out of sync with `origin`, or a failing
`just check`.

Do not tag by hand. `git tag` on its own leaves `package.json` behind, and a
tag cut before a later fix keeps pointing at the old commit — CI then builds
code you already replaced. The workflow refuses to publish if the tag and
`package.json` disagree, but it cannot catch a stale tag that happens to match.

Two things had to be set up once, and neither lives in this repo:

- npm cannot enable OIDC for a package that does not exist yet
([npm/cli#8544](https://github.com/npm/cli/issues/8544)), so `0.1.0` was
published by hand.
- The package must list this repo as a trusted publisher on npmjs.com
(package → Settings → Trusted publisher → GitHub Actions, workflow
`publish.yml`, no environment). Without it the publish fails at the last
step with `404 Not Found - PUT`, which reads like a missing package but
means npm rejected the credential.

See the header of
[`.github/workflows/publish.yml`](.github/workflows/publish.yml).

## Licence
Expand Down
54 changes: 54 additions & 0 deletions demo/src/grid/RegionHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Badge, Button, Text } from "@radix-ui/themes";
import { REGIONS } from "./api/types";

/**
* What the demo hangs off each group header, to exercise
* `grouping.header`.
*
* Deliberately DIFFERENT per region rather than one component parameterised by
* it. The point of the hook is that a host can decide per group, and a single
* shape for every group would not prove that.
*/
export function RegionHeader({ region }: { region: number }) {
const label = REGIONS[region]?.label ?? String(region);

if (region === 0) {
return (
<>
<Text size="1" color="gray">
Invoiced
</Text>
<Badge size="1" color="cyan" variant="soft">
62%
</Badge>
</>
);
}

if (region === 1) {
return (
<>
<Text size="1" color="gray">
Region value
</Text>
<Text size="2" weight="medium">
$4,218,900
</Text>
<Button size="1" variant="soft" color="amber">
{`Export ${label}`}
</Button>
</>
);
}

return (
<>
<Badge size="1" color="red" variant="soft">
3 price breaks
</Badge>
<Button size="1" variant="soft" color="plum">
Review
</Button>
</>
);
}
10 changes: 6 additions & 4 deletions demo/src/grid/testGrid.ts → demo/src/grid/testGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
updateTestRow,
} from "./api/testRowQueries";
import { regionCell, testGridCells } from "./testGridCells";
import { RegionHeader } from "./RegionHeader";

export const GRID_NAME = "testGrid";

Expand Down Expand Up @@ -70,10 +71,6 @@ export const testGridDescriptor: GridDescriptor<TestRow, number, number> = {
"updatedAt",
"active",
],
// The default is "editable columns only", which would leave `id`, `sector`,
// `region`, and the derived `value` unsortable. Sorting a read-only column
// is a thing this page exists to exercise.
sortable: () => true,
},
grouping: {
field: "region",
Expand All @@ -86,6 +83,11 @@ export const testGridDescriptor: GridDescriptor<TestRow, number, number> = {
// The header reads its colour from the very cell that draws the column, so
// a group's header text and that group's badges cannot drift apart.
color: (region) => regionCell.colorOf(region),

// The whole reason group headers left the canvas. Each region gets a
// DIFFERENT set of components, which is what the hook is for.
header: (region) => <RegionHeader region={region} />,
headerHeight: 44,
},
api: {
fetchRows: fetchTestRows,
Expand Down
7 changes: 4 additions & 3 deletions demo/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineConfig } from "vite";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { fileURLToPath } from "node:url";
import { defineConfig } from "vite";

const here = (p: string) => fileURLToPath(new URL(p, import.meta.url));

Expand All @@ -13,10 +14,10 @@ const here = (p: string) => fileURLToPath(new URL(p, import.meta.url));
*/
export default defineConfig({
root: here("."),
plugins: [react()],
plugins: [react(), tailwindcss()],
resolve: {
alias: [
{ find: "@matthewhsu1/datagrid/datagrid.css", replacement: here("../src/datagrid.css") },
{ find: "@matthewhsu1/datagrid/datagrid.css", replacement: here("../src/datagrid.src.css") },
{
find: "@matthewhsu1/datagrid/radix-styles",
replacement: here("../src/theme/radixStyles.ts"),
Expand Down
65 changes: 65 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Show every command in this file.
default:
@just --list

# Run every check that CI runs.
check:
npm run lint
npm run typecheck
npm test
npm run build

# Show the version here, the newest tag, and what npm has.
status:
#!/usr/bin/env bash
set -euo pipefail
git fetch origin --prune --tags --quiet
pkg="$(node -p "require('./package.json').version")"
tag="$(git describe --tags --abbrev=0 2>/dev/null || echo none)"
published="$(npm view "$(node -p "require('./package.json').name")" version 2>/dev/null || echo none)"
echo "package.json : $pkg"
echo "newest tag : $tag"
echo "on npm : $published"

# Publish a new version. LEVEL is patch, minor, or major.
release level="patch":
#!/usr/bin/env bash
set -euo pipefail

case "{{level}}" in
patch|minor|major) ;;
*) echo "error: level must be patch, minor, or major"; exit 1 ;;
esac

# A tag is only correct if it points at code that is already on GitHub.
# Everything below refuses to tag until that is true.
branch="$(git rev-parse --abbrev-ref HEAD)"
if [ "$branch" != "main" ]; then
echo "error: you are on '$branch'. Switch to main first."
exit 1
fi

if [ -n "$(git status --porcelain)" ]; then
echo "error: you have uncommitted changes. Commit or stash them first."
git status --short
exit 1
fi

git fetch origin --prune --tags
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then
echo "error: main and origin/main are different. Push or pull first."
exit 1
fi

# Never tag code that does not build. A bad tag cannot be unpublished.
just check

# npm version bumps package.json, commits, and tags -- in that order,
# on the newest commit. Do not do these steps by hand.
new="$(npm version {{level}} -m 'chore: release %s')"

git push origin main
git push origin "$new"

echo ""
echo "pushed $new. GitHub Actions is publishing it now."
Loading
Loading