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
29 changes: 10 additions & 19 deletions .claude/skills/accessibility/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ This project builds on **shadcn/ui** primitives which provide strong built-in a1
All interactive elements without visible text must have an `aria-label`:

```typescript
// Icon buttons
<Button variant="ghost" aria-label="Home">
<Icon name="Home" />
</Button>

// Folder toggles
<div role="button" aria-expanded={isOpen} aria-label={`Folder: ${folder.name}`}>
```

Expand Down Expand Up @@ -45,11 +43,15 @@ Use the shadcn/ui `Label` component for proper form associations.
- **Escape**: Close dialogs, cancel editing, deselect
- **Tab**: Move focus between interactive elements

The `Input` component has built-in `onEnter` and `onEscape` props:

```typescript
// The Input component has built-in onEnter and onEscape props
<Input onEnter={save} onEscape={cancel} />
```

Everything else handles the keys itself:

// For non-Input elements, handle manually
```typescript
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
Expand Down Expand Up @@ -78,13 +80,11 @@ The dialog component already handles this — use `preventKeyboardPropagation` p
Use `sr-only` class for visually hidden but screen-reader-accessible text:

```typescript
// Close buttons with only an icon
<DialogClose>
<Cross2Icon />
<span className="sr-only">Close</span>
</DialogClose>

// Command palette title
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
Expand All @@ -95,19 +95,10 @@ Use `sr-only` class for visually hidden but screen-reader-accessible text:

Use proper semantic elements and ARIA roles:

```typescript
// Navigation
<nav aria-label="breadcrumb">

// Current page in breadcrumb
<span role="link" aria-disabled="true" aria-current="page">{children}</span>

// Decorative separators
<li role="presentation" aria-hidden="true">

// Alerts
<div role="alert">
```
- Navigation — `<nav aria-label="breadcrumb">`
- The current page in a breadcrumb — `<span role="link" aria-disabled="true" aria-current="page">`
- Decorative separators — `<li role="presentation" aria-hidden="true">`
- Alerts — `<div role="alert">`

## Focus Styling

Expand Down
10 changes: 6 additions & 4 deletions .claude/skills/analytics-tracking/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,17 @@ Pass a second argument for event-specific properties. The metadata is serialized

When a child component renders the interactive element but the parent owns the tracking context, prefer prop drilling or forwarding rest props so `data-tracking-id` reaches the DOM element. This is intentional — it keeps tracking declarative and avoids manual `track()` calls scattered across the codebase.

The parent passes the tracking attributes, and `ActionButton` forwards its rest props down the
`<TooltipButton>` → `<Button>` → DOM chain:

```tsx
// Parent passes tracking attributes
<ActionButton
tooltip="Export Pipeline"
icon="FileDown"
onClick={handleExport}
{...tracking("pipeline_editor.pipeline_actions.export_pipeline")}
/>;

// ActionButton forwards rest props to the underlying <TooltipButton> → <Button> → DOM
export const ActionButton = ({
tooltip,
onClick,
Expand Down Expand Up @@ -102,20 +103,21 @@ import { useAnalytics } from "@/providers/AnalyticsProvider";

const { track } = useAnalytics();

// Outcome — fires after success, not on click
const handleSave = async (name: string) => {
await savePipeline(name);
track("pipeline_editor.pipeline_actions.save_pipeline_as_completed");
};

// Impression — fires when dialog opens
useEffect(() => {
if (open) {
track("component_editor.save.already_exists_impression");
}
}, [open]);
```

The first is an outcome event — it fires after the save succeeds, not on click. The second is an
impression event, firing when the dialog opens.

## Event detail shape

The dispatched `CustomEvent` carries the following `detail` fields:
Expand Down
3 changes: 1 addition & 2 deletions .claude/skills/project-conventions/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: project-conventions
description: Project conventions for Tangle-UI including file structure, imports, code quality, and general rules. Use when writing new code, creating files, or organizing imports.
description: Project conventions for Tangle-UI including file structure, imports, comments, code quality, and general rules. Use when writing new code, editing existing code, adding or reviewing comments and JSDoc, creating files, or organizing imports.
---

# Project Conventions
Expand Down Expand Up @@ -28,7 +28,6 @@ React + TypeScript application for building and running ML pipelines using drag
- Use Prettier for formatting
- Write tests using Vitest for unit tests, Playwright for E2E
- Use descriptive variable and function names
- Add JSDoc comments for complex functions
- Prefer early returns to reduce nesting

## Comments & Documentation
Expand Down
8 changes: 4 additions & 4 deletions .claude/skills/react-patterns/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ description: React and React Compiler patterns for this project. Use when writin
```typescript
// ComponentName/ComponentName.tsx — import directly, no index.ts barrel
interface ComponentNameProps {
// props
...
}

export const ComponentName = ({ }: ComponentNameProps) => {
// component logic
...
return (
// JSX
...
);
};
```
Expand All @@ -42,7 +42,7 @@ export const ComponentName = ({ }: ComponentNameProps) => {
const Context = createContext<ContextType | null>(null);

export const Provider = ({ children }: { children: ReactNode }) => {
// provider logic
...
return <Context.Provider value={value}>{children}</Context.Provider>;
};

Expand Down
1 change: 0 additions & 1 deletion .claude/skills/tangle-domain/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ Arbitrary key-value metadata (`Record<string, unknown>`) on components, tasks, i
Sensitive values (API keys, credentials) stored securely in the backend and referenced by name in task arguments via `SecretArgument`. Resolved at runtime — never embedded in pipeline definitions or exports.

```typescript
// SecretArgument structure
{
secret: {
name: "my-api-key";
Expand Down
6 changes: 4 additions & 2 deletions .claude/skills/tanstack-query/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ This project uses TanStack Query v5 for all server state management.
Use **hierarchical array-based keys**. For domains with multiple related queries, use a query key factory:

```typescript
// Query key factory pattern (preferred for grouped queries)
export const SecretsQueryKeys = {
All: () => ["secrets"] as const,
Id: (id: string) => ["secrets", id] as const,
} as const;
```

Standalone queries just use a simple key:

// Simple keys for standalone queries
```typescript
queryKey: ["pipeline-run", rootExecutionId];
queryKey: ["execution-details", rootExecutionId];
queryKey: ["component", "hydrate", componentQueryKey];
Expand Down
1 change: 0 additions & 1 deletion .claude/skills/tanstack-router/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ const indexRoute = createRoute({
component: Editor,
});

// Assemble tree
const appRouteTree = mainLayout.addChildren([
indexRoute,
quickStartRoute,
Expand Down
25 changes: 15 additions & 10 deletions .claude/skills/vitest-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,12 @@ vi.mock("@/utils/localforage", () => ({
saveComponent: vi.fn(),
}));

// React component mock
vi.mock("@monaco-editor/react", () => ({
default: ({ defaultValue }: { defaultValue: string }) => (
<pre data-testid="monaco-mock">{defaultValue}</pre>
),
}));

// Provider/context mock
vi.mock("@/providers/ComponentSpecProvider", () => ({
useComponentSpec: () => ({
componentSpec: mockSpec,
Expand All @@ -135,7 +133,7 @@ vi.mock("@/providers/ComponentSpecProvider", () => ({

```typescript
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
// ... test code
// ...
expect(consoleSpy).toHaveBeenCalledWith("Error:", expect.any(Error));
```

Expand Down Expand Up @@ -184,25 +182,34 @@ beforeEach(() => {
});

afterEach(() => {
cleanup(); // Testing Library cleanup
queryClient.clear(); // Clear query cache if using QueryClient
cleanup();
queryClient.clear();
vi.restoreAllMocks();
});
```

Only call `queryClient.clear()` when the suite uses a `QueryClient`.

## Assertion Patterns

DOM:

```typescript
// DOM assertions
expect(screen.getByTestId("submit")).toBeInTheDocument();
expect(screen.queryByTestId("hidden")).not.toBeInTheDocument();
```

Mocks:

// Mock assertions
```typescript
expect(mockFn).toHaveBeenCalledWith(url);
expect(mockFn).toHaveBeenCalledTimes(1);
expect(mockFn).not.toHaveBeenCalled();
```

// Partial matching
Partial matching:

```typescript
expect(mockSave).toHaveBeenCalledWith({
id: expect.stringMatching(/^component-\w+$/),
createdAt: expect.any(Number),
Expand All @@ -223,12 +230,10 @@ expect(mockCallback).toHaveBeenCalled();
## Async Testing

```typescript
// Wait for state updates
await waitFor(() => {
expect(screen.getByTestId("content")).toBeInTheDocument();
});

// Assert on promises
await expect(promise).resolves.toBe(expectedValue);
```

Expand Down
9 changes: 3 additions & 6 deletions .cursorrules
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ This is a React + TypeScript application for building and running Machine Learni
- Use Prettier for formatting
- Write tests using Vitest for unit tests, Playwright for E2E
- Use descriptive variable and function names
- Add JSDoc comments for complex functions
- Prefer early returns to reduce nesting

### API & Data
Expand Down Expand Up @@ -416,11 +415,9 @@ export const useContext = () => {

## Comments & Documentation

- Use JSDoc for public APIs
- Add comments for complex business logic
- **Explain "why" not "what" in comments**
- **Keep comments up to date with code changes**
- Avoid writing redundant comments for functions and variables that are self-explanatory
The comments policy lives in `CLAUDE.md` (root) — that is the single source of truth. In short:
code must be self-explanatory, default to no comment, comment only a non-obvious _why_, never
comment interface fields or props (rename them instead).

## Specific Project Patterns

Expand Down
26 changes: 26 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Tangle-UI

## Comments

Code must be self-explanatory. Default to **no comment**.

- A comment is justified only when it states a non-obvious **why** the code and its names cannot: a
workaround, a race condition, an external-system quirk, a subtle ordering constraint.
- Never narrate **what** the code does. No step-by-step narration (`// fetch the data`), no
section-divider banners (`// ─── Helpers ───`), no restating a name.
- Never comment `interface` fields, `type` members, component `Props`, or config-object fields —
leading or trailing, `//` or `/** */`. A field that needs a comment to explain what it is has a
**naming problem**: rename it. (Only exception: the prop contract of a shared design-system
primitive in `src/components/ui/`.)
- Prefer renaming, extracting a well-named function, or a named constant over adding a comment.
- JSDoc only for genuinely complex functions and shared public APIs — never as a substitute for a
good name.
- A stale comment is worse than none: update comments with the code, and delete ones you invalidate.

Removing a comment that violates the above is always in scope for a change you are already making.

## Everything else

Project conventions live in `.claude/skills/` — start with `project-conventions`, plus
`typescript-standards`, `react-patterns`, `ui-primitives`, `tangle-domain`, `tanstack-query`,
`tanstack-router`, `vitest-testing`, `e2e-testing`, `accessibility`, and `open-source`.
2 changes: 0 additions & 2 deletions docs/generic-overlay-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,6 @@ type ProfileLinkReplacements = {
userId: string;
};

// userProfile comes from an API hook (e.g., useFetchUserProfile)
export function resolveGreetingReplacements(
metadata: Record<string, unknown>,
userProfile: { name: string; retriesUsed: number },
Expand Down Expand Up @@ -300,7 +299,6 @@ export const StatusOverlaySection = ({
profileLink: resolveProfileLinkReplacements(userId),
};

// Pass null for displayFor when no conditional rendering is needed
const hydrated = filterAndHydrateSchema(schema, allReplacements);
return <Composer schema={hydrated} />;
};
Expand Down
Loading
Loading