Skip to content
Open
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
1 change: 1 addition & 0 deletions .claude/commands/commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Co-Authored-By: Claude <noreply@anthropic.com>

## Steps

0. **Comprehension gate.** Unless the diff is trivial (pure docs/config/formatting/version bump/generated files), run the `/comprehension-check` skill first and only continue once the author has demonstrated understanding. Skip silently for trivial diffs.
1. `git status` + `git diff` (and `git diff --staged` if anything's staged), and **check the current branch is the right place for this commit**:
- **Never commit on `master`/`main`.** If you're on it, stop and **offer to create a feature branch** from the current HEAD — `git switch -c <type>/<short-desc>` carries the uncommitted changes onto the new branch — then commit there. Don't proceed on `master` even if the user didn't mention branching; confirm first.
- If you're on a feature branch, glance at its name. If it looks **unrelated** to the change you're about to commit, flag it and offer to branch off (so you don't pile an unrelated commit onto someone else's WIP); otherwise proceed.
Expand Down
68 changes: 68 additions & 0 deletions .claude/commands/comprehension-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
description: Quiz the author on the current diff's design before committing — a comprehension gate to prevent shipping code nobody understands
---

Gate a commit on the author demonstrating they understand the change. The point is to stop
"ok-looking" AI-generated code — and creeping architectural complexity — from landing when
the author can't actually explain it. This is a teaching gate, not a rubber stamp.

## 1. Read the diff

`git diff` and `git diff --staged`. Understand what actually changed.

## 2. Decide if a quiz is warranted

**Skip (say so and proceed straight to committing)** when the diff is only: Markdown/docs,
config, formatting/lint-only churn, version bumps, lockfiles, or generated files.

**Quiz** when the diff introduces or changes real logic: new abstractions/components,
control flow, data flow, state, public API/props, cross-package wiring, or anything with a
non-obvious "why". Scale the number of questions to the change: ~2 for a small focused diff,
up to ~5 for a large or architectural one.

Before writing questions, pick the 2-3 spots with the most hidden complexity or highest
"AI-slop risk" — clever one-liners, non-obvious control flow, React effect/memo dependency
arrays and referential stability, error/edge handling — and make at least one question target
each. Don't spend the quiz on the parts that were easy to write.

## 3. Ask — one or two questions at a time, in plain chat

Target the **decisions embedded in the code**, never syntax or trivia. Good angles:

- **Why this way?** — why this approach over the obvious alternative; what trade-off was made.
- **Blast radius** — what else depends on this; what breaks if it changes; who calls it.
- **Data/control flow** — where does this get invoked from, and what happens next.
- **Edge cases** — what inputs/states does it handle (or deliberately not), and why.
- **Integration** — how it fits the monorepo: cross-package deps, theme/tokens, i18n, v1/v2.

Rules:

- Ask questions **you (Claude) already know the answer to from the diff** — so you can grade.
- Before asking, note to yourself the specific points a complete answer must hit — the answer
key straight from the diff. Grade the reply against that key, not against how confident or
fluent it sounds.
- **Never reveal the answer in the question**, and don't hand the author hints before they try.
- Ask in the author's own words territory — "explain…", "why…", "what would happen if…".

## 4. Grade honestly

The gate is worthless if you rubber-stamp. Hold a real bar, but grade the _understanding_,
not the phrasing:

- **All parts, not most.** If a question has multiple parts, it passes only when _every_ part
is answered. Right on one part and silent/vague on another is **partial** → probe the missing
part before moving on. Don't average.
- **Correct & shows understanding** → acknowledge briefly, move on.
- **Coherent alternative that's actually right** (author knows something you didn't, or framed
it differently) → accept it. The goal is demonstrated understanding, not matching your words.
- **Vague / partial** → probe once more on the gap.
- **Wrong, or "I don't know"** → **teach**: explain the decision clearly with `file:line`
references, then ask a _fresh_ question on the same concept. Loop until the author can
explain it back in their own words. Don't move on until they can.

## 5. Pass

When the author has demonstrated understanding of every key decision, give a one-line summary
of what they showed they understand, then proceed to commit (follow `/commit`). If the author
wrote the code themselves and breezes through, that's a pass — the gate is aimed at code they
haven't internalized, not at making them jump through hoops.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ CLAUDE.local.md
!.claude/commands/slack-setup.md
!.claude/commands/implement.md
!.claude/commands/ticket.md
!.claude/commands/comprehension-check.md

# Playwright MCP
.playwright-mcp
5 changes: 4 additions & 1 deletion packages/__docs__/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import placeholderImage from './buildScripts/samplemedia/placeholder-image'
import ThemeColors from './src/ThemeColors'
// eslint-disable-next-line no-restricted-imports
import ColorTable from './src/ColorTable'
// eslint-disable-next-line no-restricted-imports
import { PropEditor } from './src/PropEditor'

import { additionalPrimitives, dataVisualization } from '@instructure/ui-themes'

Expand Down Expand Up @@ -107,7 +109,8 @@ const globals: Record<string, any> = {
additionalPrimitives,
dataVisualization,
ThemeColors,
ColorTable
ColorTable,
PropEditor
}

Object.keys(globals).forEach((key) => {
Expand Down
89 changes: 89 additions & 0 deletions packages/__docs__/src/Document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ import { Returns } from '../Returns'
import { ComponentTheme } from '../ComponentTheme'
import { TableOfContents } from '../TableOfContents'
import { Heading } from '../Heading'
import { PropEditor } from '../PropEditor'
import type { PropEditorSection, ReactDocgenProps } from '../PropEditor/props'
import {
shouldAutoInject,
getAutoInjectConfig,
getCustomPlayground,
isForceSimple
} from '../PropEditor/registry'

import { AppContext } from '../appContext'
import { navigateTo } from '../navigationUtils'
Expand Down Expand Up @@ -154,6 +162,84 @@ class Document extends Component<DocumentProps, DocumentState> {
) : null
}

renderPlayground(doc: DocDataType) {
const editor = this.getPlaygroundEditor(doc)
if (!editor) return null

return (
<View margin="x-large 0" display="block">
<Heading
level="h2"
as="h3"
id={`${doc.id}Playground`}
margin="0 0 small 0"
>
Playground
</Heading>
{editor}
</View>
)
}

// Which playground (if any) a component gets:
// - a curated composition, when a custom playground is registered;
// - the auto single-element form, for simple standalone components;
// - none, for compound components without a custom entry (they lean on
// their README examples) and anything the gate rejects.
getPlaygroundEditor(doc: DocDataType) {
const custom = getCustomPlayground(doc.id)

if (custom) {
const sections = custom.sections
.map<PropEditorSection | null>((section) => {
const sectionProps =
section.id === doc.id
? doc.props
: doc.children?.find((child) => child.id === section.id)?.props
return sectionProps
? {
id: section.id,
label: section.label,
props: sectionProps as ReactDocgenProps,
config: section.config
}
: null
})
.filter((section): section is PropEditorSection => section !== null)

// Bail if any section's metadata is missing rather than render a partial,
// broken composition.
if (sections.length !== custom.sections.length) return null

return (
<PropEditor
componentId={doc.id}
sections={sections}
template={custom.template}
/>
)
}

// Real compound components (whose children are composable subcomponents
// with dotted ids like `Menu.Item`) fall back to examples when they have no
// custom playground. Components whose only "children" are internal facades
// (non-dotted ids, e.g. Checkbox's `CheckboxFacade`) are not composed by
// consumers, so they still get the simple single-element form.
const hasSubcomponents = doc.children?.some((child) =>
child.id?.includes('.')
)
if (hasSubcomponents && !isForceSimple(doc.id)) return null
if (!shouldAutoInject(doc)) return null

return (
<PropEditor
componentId={doc.id}
props={doc.props as ReactDocgenProps}
config={getAutoInjectConfig(doc.id)}
/>
)
}

renderTheme(doc: DocDataType) {
const { themeVariables } = this.props
const { componentTheme } = this.state
Expand Down Expand Up @@ -467,6 +553,9 @@ import { ${importName} } from '${versionedPackageName}'`
{pageRef && <TableOfContents doc={doc} pageElement={pageRef} />}
{['.js', '.ts', '.tsx'].includes(doc.extension) && this.renderUsage()}
{this.renderDescription(doc, this.props.description)}
{/* Playground sits above the per-subcomponent details tabs so it always
applies to the whole component, not the selected tab. */}
{this.renderPlayground(doc)}
{details}
{this.renderEditOnGithub()}
{repository && layout !== 'small' && (
Expand Down
Loading
Loading