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
17 changes: 17 additions & 0 deletions .changeset/sticky-header-stacking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'rowkit': patch
---

Fix a `sticky` column losing its own header in `DataTable`.

Every header cell sat on the same `z-sticky` layer, so at equal z-index the
later cells in the DOM painted over the pinned one. Scrolling right slid the
neighbouring header straight across the pinned column's heading, while the
pinned body cells below stayed put — the column kept its data and lost its name.

The header row now establishes one stacking context and the pinned cell is
ordered inside it. No API change.

If your application places rowkit under a **fixed header of its own**, note that
`--z-index-sticky` is `100`: a table's sticky header will paint over any chrome
below that. Raise your header above it.
68 changes: 68 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# AGENTS.md

Instructions for coding agents working **on** rowkit.

> Looking for the API of the library itself? That is
> [`packages/ui/AGENTS.md`](./packages/ui/AGENTS.md) — generated from the source,
> shipped inside the npm package, and describing how to *use* the components.
> This file is about changing them.

## Read first

- **[`CLAUDE.md`](./CLAUDE.md)** — the working agreement: stack, hard rules,
repository layout, definition of done. It is the authority; where this file and
that one disagree, that one wins.
- **[`docs/conventions.md`](./docs/conventions.md)** — before designing anything
with a public surface. Prop naming, state ownership, event and slot shapes and
the recurring accessibility patterns are decided there, once, for every
component.
- **[`ROADMAP.md`](./ROADMAP.md)** — before proposing a component. The scope is
twelve, and that is a decision rather than a stage.

## Commands

```bash
pnpm build # run first in a fresh clone; workspace deps resolve through dist
pnpm test # unit, component and browser tests
pnpm lint # eslint, type-aware
pnpm typecheck # vue-tsc, strict
pnpm format # prettier
pnpm size # bundle budget, brotli

pnpm docs:props # regenerate the props tables after touching a prop or its JSDoc
pnpm docs:agents # regenerate packages/ui/AGENTS.md, likewise
```

`pnpm build` before anything else is not optional. The playground, the docs and
the type checker all resolve `rowkit` through `packages/ui/dist`, and an unbuilt
workspace produces a wall of confusing type errors rather than one clear one.

## Three things that are true here and not everywhere

**Backward compatibility matters.** rowkit is a published package with semver and
changesets, not an application. Breaking a public API is a deliberate act that
needs a major-version changeset and a reason, never a convenience taken while
doing something else. Generic agent guidance often says the opposite; it is wrong
for this repository.

**The failure mode is silence, not errors.** A Tailwind class that matches no
utility, a token in the wrong theme namespace, a `@source` path that resolves
nowhere, a slot prop named `name`: no error, no warning, wrong pixels. Verify
rendering and computed styles rather than assuming a green build means a correct
page. Several tests in `packages/ui/src/styles/` exist because of exactly this.

**Generated files are generated.** The props tables in `docs/components/*.md` and
`packages/ui/AGENTS.md` are derived from prop types and JSDoc. Editing them by
hand fails CI; run the command above and commit the result.

## Before opening a pull request

Everything in the definition of done in `CLAUDE.md`, plus a changeset for any
public API change:

```bash
pnpm changeset
```

Describe the change the way a consumer reading a changelog would want it
described — what changed and what it means for them, not which files moved.
17 changes: 17 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export default defineConfig({
nav: [
{ text: 'Guide', link: '/introduction' },
{ text: 'Components', link: '/components/button' },
{ text: 'Patterns', link: '/patterns/data-table-page' },
{ text: 'Storybook', link: 'https://storybook.rowkit.dev' },
{ text: 'Decisions', link: '/decisions/001-typescript-pin' },
{
text: 'v0.x',
Expand Down Expand Up @@ -103,6 +105,21 @@ export default defineConfig({
{ text: 'Tooltip', link: '/components/tooltip' },
],
},
{
text: 'Patterns',
items: [
{ text: 'A data table page', link: '/patterns/data-table-page' },
{ text: 'Forms', link: '/patterns/forms' },
{ text: 'Loading states', link: '/patterns/loading-states' },
],
},
{
text: 'Project',
items: [
{ text: 'Roadmap', link: '/roadmap' },
{ text: 'Contributing', link: '/contributing' },
],
},
{
text: 'Decisions',
items: [
Expand Down
2 changes: 1 addition & 1 deletion docs/.vitepress/theme/components/DemoBox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const alignment = { start: 'items-start', center: 'items-center', end: 'items-en

<template>
<div
class="my-6 flex gap-4 overflow-x-auto rounded-lg border border-border bg-surface p-6"
class="rk-demo my-6 flex gap-4 overflow-x-auto rounded-lg border border-border bg-surface p-6"
:class="layout === 'stack' ? 'flex-col' : ['flex-wrap', alignment[align]]"
>
<slot />
Expand Down
13 changes: 13 additions & 0 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { inject as injectAnalytics } from '@vercel/analytics'
import type { Theme } from 'vitepress'
import DefaultTheme from 'vitepress/theme'
import * as rowkit from 'rowkit'
Expand Down Expand Up @@ -29,5 +30,17 @@ export default {
app.component('DemoBox', DemoBox)
app.component('ColorScale', ColorScale)
app.component('TokenGrid', TokenGrid)

/*
* Vercel Analytics, guarded because `enhanceApp` runs during the static
* build as well as in the browser. `inject()` writes a `<script>` into
* `document.head`, so calling it server-side fails the docs build rather
* than the page — the same shape as the SSR traps the overlay demos hit.
*
* Cookieless and without personal data, so it needs no consent banner.
*/
if (typeof window !== 'undefined') {
injectAnalytics()
}
},
} satisfies Theme
108 changes: 108 additions & 0 deletions docs/.vitepress/theme/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,114 @@
@source '../../components';
@source '../../*.md';

/*
* A table inside a demo is a table, not a block.
*
* VitePress sets `.vp-doc table { display: block; overflow-x: auto }` so a long
* markdown table scrolls instead of stretching the page. A block-level table
* generates an anonymous inner table box that is shrink-to-fit, so `width: 100%`
* sizes the *block* and leaves the cells at their content width — the table
* measures full width in JavaScript while rendering with several hundred pixels
* of empty space to its right.
*
* `DataTable` brings its own scroll container, so it neither needs nor wants
* VitePress's treatment.
*/
/*
* Controls inside a demo keep the styling their component gave them.
*
* VitePress's default theme ships an **unlayered** reset:
*
* button, input, optgroup, select, textarea { border: 0; padding: 0; … }
* button { background-color: transparent; background-image: none }
*
* Tailwind v4 puts every utility in `@layer utilities`, and unlayered CSS beats
* layered CSS whatever the order or the specificity. So `bg-primary-solid`,
* `px-4` and `border` lost on every button and input in a demo, while `h-9` and
* `rounded-md` — properties the reset does not name — came through. The result
* was a live demo of an invisible button.
*
* `all: revert-layer` from an unlayered rule rolls every property back to the
* layered value underneath — the component's own utility where it set one, and
* Tailwind's preflight default where it did not.
*
* `all`, rather than a list of properties, because a list has to guess which
* property each utility writes and gets it wrong. Reverting `padding` restores
* the *physical* longhands, while `px-4` writes `padding-inline`; the unlayered
* shorthand then beat the layered logical property and the controls came back
* with correct backgrounds, correct borders and no padding at all.
*
* The blunt alternative, `@import 'tailwindcss' important`, makes every utility
* important and collides with Tailwind's own `container` utility — a class
* VitePress uses for its layout — and for important declarations layer order is
* reversed, so a layered `!important` beats an unlayered one and the collision
* is unwinnable.
*/
.rk-demo :is(button, input, optgroup, select, textarea) {
all: revert-layer;
}

.rk-demo table {
display: table;
margin: 0;
overflow: visible;
}

/*
* The markdown below the hero shares the hero's lines.
*
* Two differences to reconcile. The hero and the feature cards cap at 1152px
* while the markdown container caps at 1280px, and the markdown container adds
* its own padding, which `box-sizing: border-box` then subtracts from that cap.
*
* `content-box` makes the cap describe the content rather than the box, so the
* padding stays — it is the only gutter on a narrow screen — while the content
* edges line up with the cards above at every width.
*/
/*
* The gutter is re-declared rather than inherited so the cap can subtract it:
* under `content-box` the max-width no longer accounts for padding, so without
* the `min()` the box grows past a narrow viewport by exactly twice the gutter.
*/
.VPHome .vp-doc.container {
--rk-gutter: 24px;

box-sizing: content-box;
max-width: min(1152px, 100% - 2 * var(--rk-gutter));
padding-inline: var(--rk-gutter);
}

@media (min-width: 640px) {
.VPHome .vp-doc.container {
--rk-gutter: 48px;
}
}

@media (min-width: 960px) {
.VPHome .vp-doc.container {
--rk-gutter: 64px;
}
}

/*
* VitePress's chrome outranks anything inside a demo.
*
* Its navbar sits at z-index 20; rowkit's `--z-index-sticky` is 100, because
* that scale is designed for an application where rowkit *is* the chrome. In the
* docs it is the guest, so a `DataTable` with a sticky header scrolled its
* header straight over the site nav and the search box.
*
* The same applies to any app embedding rowkit under a fixed header: its header
* has to sit above `--z-index-sticky`, and this is the one-line version of that.
*/
:root {
--vp-z-index-local-nav: 1010;
--vp-z-index-nav: 1020;
--vp-z-index-layout-top: 1030;
--vp-z-index-backdrop: 1040;
--vp-z-index-sidebar: 1050;
}

:root {
--vp-c-brand-1: var(--color-primary-700);
--vp-c-brand-2: var(--color-primary-600);
Expand Down
7 changes: 7 additions & 0 deletions docs/components/data-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ the data lands — the header and the column widths are already correct.
Sort a column and switch to `loading`: the sort survives, because the table
never owned it.

::: warning Under a fixed header of your own
`--z-index-sticky` is `100`, because rowkit's scale assumes rowkit is the page's
chrome. A sticky table header will therefore paint over any application header
below that layer — raise yours above it. This site does exactly that for
VitePress's navbar.
:::

## Anatomy

| Part | Purpose |
Expand Down
Loading