Ctrl K
ComunidadGuías…

agent_docs/code_style.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Code Style & Best Practices

> **Note**: Pre-commit hooks handle formatting automatically. Focus on implementation patterns.

## TypeScript

- Avoid `any` - use proper typing
- Avoid `as` casts, use `satisfies` or type inference whenever possible
- Use Zod schemas for runtime validation
- Define clear interfaces for data structures
- Implement proper error boundaries
- Define and import reusable named types instead of repeating verbose types

## Code Organization

- **Single Responsibility**: One clear purpose per component/function
- **File Size**: Max 300 lines - refactor when approaching limit
- **DRY**: Grep for an existing implementation before adding a type, schema,
  helper, or component - see "Before you add a type, schema, or helper" below
  (REQUIRED)
- **In-Context Learning**: Explore similar files before implementing

## Before you add a type, schema, or helper (REQUIRED)

**Grep for an existing implementation before you define a new type, Zod schema,
helper function, or React component.** Search for the _operation_ — splitting a
list, bucketing a timestamp, validating a URL, formatting a duration — not for
the name you were about to give it. Names differ; operations don't.

```bash
# Two or three words for the operation, as alternates. Runs in ~20ms.
grep -rniE "export (const|function|type|interface|enum) [a-zA-Z]*(bucket|interval|granular)" \
  packages/common-utils/src packages/app/src packages/api/src \
  --include='*.ts' --include='*.tsx'
```

Search order: the file you are editing, then `packages/common-utils/src/`, then
the rest of your package. `common-utils` has **no root barrel** — no
`src/index.ts`, and its `package.json` declares no entry point, which is why
every import is a deep `@hyperdx/common-utils/dist/...` path. `src/types.ts`
alone is ~2,850 lines and 280+ exports. You cannot discover what exists by
reading an index; grep is the index.

### Where shared code already lives

Nine modules carry ~90% of all cross-package imports:

| Import path | Holds |
|-------------|-------|
| `.../dist/types` | Every shared Zod schema and its inferred type: sources, alerts, dashboards, tiles, webhooks, chart configs, `SQLInterval`, `MetricsDataType`, `StacktraceFrame` |
| `.../dist/core/utils` | Time-bucket and granularity math (`toStartOfInterval`, `timeBucketByGranularity`, `convertGranularityToSeconds`), string splitting (`splitAndTrimCSV`, `splitAndTrimWithBracket`, `escapeSqlString`), `hashCode`, `parseJSON`, `formatDate` |
| `.../dist/core/metadata` | ClickHouse table/column introspection — `Metadata`, `getMetadata`, `parseKeyPath`, `unquoteIdentifier` |
| `.../dist/clickhouse` (`/node`, `/browser`) | Query client, `ChSql`/`chSql`/`concatChSql`, CH↔JS type conversion |
| `.../dist/core/renderChartConfig` | `ChartConfig` → SQL |
| `.../dist/filters` | Filter state, serialization, and validation (`FilterState`, `filtersToQuery`, `parseQuery`) |
| `.../dist/guards` | Chart-config and source type guards (`isBuilderChartConfig`, `isRawSqlChartConfig`, `isHeatmapCompatibleSource`) |
| `.../dist/validation` | `isValidUrl`, `isValidSlackUrl`, password validators |
| `.../dist/variables` | Dashboard variable resolution and template expansion |

Package-local homes, searched second: `packages/app/src/utils.ts`,
`packages/app/src/types.ts`, `packages/app/src/ChartUtils.tsx`,
`packages/api/src/utils/`.

### What to do once you find it

| Situation | Do this |
|-----------|---------|
| Canonical version exists and fits | Import it. Delete the copy you were writing. |
| Exists, but you want a local name | Alias-shim it — `packages/app/src/types.ts:9`, `export type NumberFormat = _NumberFormat;`. Never retype the definition. |
| Exists, but the shape is _almost_ right | Derive from it (`Pick`, `Omit`, `.extend()`, `Alert & { … }`) or parameterize the canonical one. One source of truth. |
| Shared logic between two components | Factor it into a sibling module — `packages/app/src/components/sourceSelectUtils.tsx:51` (`useFilteredSortedSourceItems`, shared by `SourceSelect` and `SourceMultiSelect`). |
| Two genuinely must coexist | Doc-comment _why_, and name the twin. `packages/common-utils/src/types.ts:571-582` does this for the Mongoose `IWebhook` (`packages/api/src/models/webhook.ts:17`) vs the JSON-serialized `WebhookSchema`. |

Not acceptable: a second definition with no alias, no doc comment explaining the
twin, and no test pinning the two together.

### Marking a whole-file port

When an entire file has to be copied across packages — `packages/cli` does this
for the components listed under "Web Frontend Alignment" in
[`packages/cli/AGENTS.md`](../packages/cli/AGENTS.md) — head it with an
`@source` tag naming the origin, one tag per origin file:

```ts
/**
 * Source helper functions.
 *
 * @source packages/app/src/source.ts
 */
```

`scripts/ci/ratchet.mjs` counts these so the total stays visible in CI. That
count is advisory, not a gate: annotating a port never fails the build, and
stripping the tag to go green hides the copy instead of removing it. Lower it by
moving the shared code into `common-utils`, then run `yarn ratchet:update`.

## React Patterns

- Functional components with hooks (not class components)
- Write small, focused components
- Extract reusable logic into custom hooks
- Define TypeScript interfaces for props
- Use proper keys for lists, memoization for expensive computations

## Mantine UI Components

The project uses Mantine UI with **custom variants** defined in `packages/app/src/theme/mantineTheme.ts`.

- Prefer Mantine components over custom-styled elements
- Prefer individual Mantine style props (eg. `m='xs'`) over raw styles (eg. `style={{ margin: '4px' }}`)

### Button & ActionIcon Variants (REQUIRED)

**ONLY use these variants for Button and ActionIcon components:**

| Variant | Use Case | Example |
|---------|----------|---------|
| `variant="primary"` | Primary actions (Submit, Save, Create, Run) | `<Button variant="primary">Save</Button>` |
| `variant="secondary"` | Secondary actions (Cancel, Clear, auxiliary actions) | `<Button variant="secondary">Cancel</Button>` |
| `variant="danger"` | Destructive actions (Delete, Remove, Rotate API Key) | `<Button variant="danger">Delete</Button>` |
| `variant="link"` | Link-style actions with no background or border (View Details, navigation-style CTAs) | `<Button variant="link">View Details</Button>` |
| `variant="subtle"` | Transparent background with hover highlight; for toolbar/utility controls that shouldn't draw attention until hovered (collapse toggles, close buttons, auxiliary actions) | `<Button variant="subtle">Filter</Button>` |

### Correct Usage

```tsx
<Button variant="primary">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="danger">Delete</Button>
<Button variant="subtle">Filter</Button>
<Button variant="link">View Details</Button>
<ActionIcon variant="primary">...</ActionIcon>
<ActionIcon variant="secondary">...</ActionIcon>
<ActionIcon variant="danger">...</ActionIcon>
<ActionIcon variant="link">...</ActionIcon>
<ActionIcon variant="subtle">...</ActionIcon>
```

### DO NOT USE (Forbidden Patterns)

```tsx
<Button variant="light" color="green">Save</Button>
<Button variant="light" color="gray">Cancel</Button>
<Button variant="light" color="red">Delete</Button>
<Button variant="outline" color="green">Save</Button>
<Button variant="outline" color="gray">Cancel</Button>
<Button variant="outline" color="red">Delete</Button>
<Button variant="filled" color="gray">Cancel</Button>
<Button variant="default">Cancel</Button>
<ActionIcon variant="light" color="red">...</ActionIcon>
<ActionIcon variant="filled" color="gray">...</ActionIcon>
```

**Link variant details**: Renders with no background, no border, and muted text color. On hover, text brightens to full contrast. Use for link-style CTAs that should blend into surrounding content (e.g., "View Details", "View Full Trace").

**Subtle variant details**: Transparent background with standard text color. On hover, a subtle background highlight appears (`--color-bg-hover`). This is the **default** ActionIcon variant. Use for toolbar icons, collapse toggles, close buttons, and utility controls that should stay unobtrusive but reveal interactivity on hover. Unlike `link`, `subtle` shows a hover background rather than changing text color.

**Note**: `variant="filled"` is still valid for **form inputs** (Select, TextInput, etc.), just not for Button/ActionIcon.

### Icon-Only Buttons → ActionIcon

**If a Button only contains an icon (no text), use ActionIcon instead:**

```tsx
// ❌ WRONG - Button with only an icon
<Button variant="secondary" px="xs">
  <IconRefresh size={18} />
</Button>

// ✅ CORRECT - Use ActionIcon for icon-only buttons
<ActionIcon variant="secondary" size="input-sm">
  <IconRefresh size={18} />
</ActionIcon>
```

This pattern cannot be enforced by ESLint and requires manual code review.

### Semantic component variants (Alert / Text / danger controls)

We ship **semantic variants** for `Alert`, `Text`, `Button`, and `ActionIcon` so callouts and status text are token-driven in both light and dark. **Prefer these over raw Mantine palette colors** (`color="yellow"`, `color="red"`, `c="green"`, etc.).

The variant → token mapping is centralized in `packages/app/src/theme/semanticVariants.ts` (the single source of truth, consumed by `theme/mantineTheme.ts`). See the Storybook stories `Components/Alert` (interactive `Playground`) and `Design tokens/Semantic variants` for the full visual matrix.

**`Alert`** — `info` | `success` | `warning` | `danger`. Renders a tinted `-subtle` background with the title, icon, **and body text** in the semantic color token:

```tsx
// ✅ token-driven, works in light and dark, meets WCAG AA
<Alert variant="warning" title="Heads up">This may take a while.</Alert>
<Alert variant="danger" title="Failed">Could not save the alert.</Alert>

// ❌ hardcoded Mantine palette — not theme-aware, inconsistent contrast
<Alert color="yellow" title="Heads up">...</Alert>
<Alert color="red" title="Failed">...</Alert>
```

**`Text`** — `danger` | `warning` | `success` for inline status/validation text:

```tsx
<Text variant="danger">Required field</Text>
<Text variant="success">Connection verified</Text>

// ❌ don't reach for raw palette colors for semantic status text
<Text c="red.5">Required field</Text>
```

**`Button` / `ActionIcon` `variant="danger"`** is a **soft** control: a tinted `--color-bg-danger-subtle` background (with hover) and semantic foreground, not a solid red fill. `warning`/`success` are intentionally **not** exposed as control variants — use them on `Text` and `Alert` only.

**Note**: Existing `<Alert color="...">` call sites are untouched; the semantic variants are opt-in. Prefer the variant for any **new** callout, and migrate nearby `color="..."` alerts when you touch them.

### Confirmation dialogs: use `useConfirm` (REQUIRED)

**Use `useConfirm` (`@/useConfirm`) for any "are you sure?" step. Do not
hand-roll a `<Modal>` with Cancel/Confirm buttons.** The provider is already
mounted app-wide in `pages/_app.tsx`, so there is no setup at the call site.

```tsx
const confirm = useConfirm();

const handleDelete = async () => {
  if (
    await confirm(
      <>
        Deleting {name} is <b>not reversible</b>.
      </>,
      'Delete',
      { variant: 'danger' },
    )
  ) {
    await deleteThing.mutateAsync({ id });
  }
};
```

- The message is a `ReactNode`, so it can carry emphasis and multiple sentences.
- Pass `{ variant: 'danger' }` for destructive actions; the confirm label
  defaults to `Confirm`.
- It resolves **exactly once**, so a double click on Confirm during the modal's
  exit transition cannot fire the action twice. A hand-rolled modal has to guard
  that itself.
- Test ids are shared and already exist: `confirm-modal`,
  `confirm-confirm-button`, `confirm-cancel-button`. **Do not invent per-flow
  confirm/cancel test ids** — E2E page objects key off the shared ones.

**Known limits.** It passes no `title` to the Modal and renders the body at
`size="sm" opacity={0.7}`, and CSS opacity applies to the whole subtree so a
nested `<Text>` cannot opt back out. If a flow genuinely needs a heading or
full-contrast body, **extend `useConfirm`** (an optional prop, applied to all
call sites) rather than forking a one-off modal.

**In component tests**, mock it — `ConfirmProvider` pulls in `next/router`,
which is not available in jsdom:

```tsx
jest.mock('@/useConfirm', () => ({ useConfirm: jest.fn() }));
```

Assert on the arguments (and render the message `ReactNode` if you need to check
the copy). Exercise the real dialog in E2E instead.

### EmptyState Component (REQUIRED)

**Use `EmptyState` (`@/components/EmptyState`) for all empty/no-data states.** Do not create ad-hoc inline empty states.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `icon` | `ReactNode` | — | Icon in the theme circle (hidden if not provided) |
| `title` | `string` | — | Heading text (headline style — no trailing period) |
| `description` | `ReactNode` | — | Subtext below the title |
| `children` | `ReactNode` | — | Actions (buttons, links) below description |
| `variant` | `"default" \| "card"` | `"default"` | `"card"` wraps in a bordered Paper |

```tsx
// ❌ BAD - ad-hoc inline empty states
<div className="text-center my-4 fs-8">No data</div>
<Text ta="center" c="dimmed">Nothing here</Text>

// ✅ GOOD - use the EmptyState component
<EmptyState
  icon={<IconBell size={32} />}
  title="No alerts created yet"
  description="Create alerts from dashboard charts or saved searches."
  variant="card"
/>
```

**Title copy**: Treat `title` as a short headline (like `Title` in the UI). Do **not** end it with a period. Use `description` for full sentences, which should use normal punctuation including a trailing period when appropriate. Match listing pages (e.g. dashboards and saved searches use parallel phrasing such as “No matching … yet” / “No … yet” without dots).

### Object names: use `InlineNameInput` (REQUIRED)

**Name or rename a dashboard, saved search, tile, or any other user-created
object with `InlineNameInput` (`@/components/InlineNameInput/InlineNameInput`).**
Do not build a pencil-button edit mode, a bordered name `TextInput` in a
header, or a "Name" field inside a save modal. See
[naming_objects.md](naming_objects.md) for which variant to use.

### Code snippets (REQUIRED)

**Do not render raw `<pre>`, ad-hoc `Paper` + monospace text, or unstyled `<code>`.** Use the existing Mantine/product components so snippets match Terraform export, onboarding, and Storybook guidelines.

| Kind | Component | When |
|------|-----------|------|
| **Inline code** | Mantine `<Code>` | Short tokens in prose (`Session`, a column name, a flag). |
| **Fenced / multi-line / copyable** | `CopySnippet` (`@/components/ClickStackOnboarding/CopySnippet`) | Install commands, HCL, JSX examples, any block the user might copy. Wraps `<Code block>` plus a Copy button. |

```tsx
// ✅ inline — Mantine Code
Create a source with <Code>Session</Code> type.

// ✅ fenced / copyable — CopySnippet (Code + Copy)
<CopySnippet
  label="Import block"
  snippet={`import { clickstack_dashboard } from "clickhouse/clickstack"`}
/>

// Label is optional when the surrounding heading already names the snippet
<CopySnippet snippet={USAGE} />

// ❌ BAD — raw pre / Paper chrome
<pre>{snippet}</pre>
<Paper bg="var(--color-bg-code)"><Text component="pre" ff="monospace">{snippet}</Text></Paper>
```

**SQL query previews** (rendered ClickHouse SQL with highlighting) still use `SQLPreview` / `ChartSQLPreview` — those are editors, not snippet chrome.

Storybook: `Components/Code` (live `InlineCode`, `FencedBlocks`, and `Usage`). Guidelines markdown maps fenced blocks → `CopySnippet` and inline `` `code` `` → `<Code>` automatically. Follow the same split in app UI.

### Chart cards (ChartCard)

**Use `ChartCard` (`@/components/charts/ChartCard`) to wrap a chart in a card.**
It is a bordered surface with the same header treatment as a custom dashboard
tile (a full-bleed divider under the title). It replaces the older `ChartBox`;
don't hand-roll a bordered `<div>`/`<Paper>` around a chart.

`ChartCard` renders the card **chrome only**. The header divider is drawn only
when a descendant renders a `ChartContainer` with a `title` (or `toolbarItems`) —
`ChartCard` supplies the `ChartContainerCardHeaderProvider` that switches that
header into card mode — so put a chart that renders a `ChartContainer` inside it
(`DBTimeChart`, `DBTableChart`, `DBHeatmapChart`, `DBListBarChart`, …). Content
with its own heading (e.g. a bespoke table card) should still route that heading
through a titled `ChartContainer` rather than a bare `Text`, so it gets the same
card header — divider and top padding included — instead of sitting flush against
the top border. The tile-level controls (fullscreen, line/bar display switcher,
kebab menu) belong to dashboard tiles and are intentionally **not** part of
`ChartCard`.

| Prop | Type | Description |
|------|------|-------------|
| `children` | `ReactNode` | The chart, usually a `DB*Chart` (or a titled `ChartContainer`) |
| `style` | `CSSProperties` | Sizing/overflow override — pass a fixed `height`, or `flex: 1; height: 100%` to fill a flex row (`paddingInline` is pinned to keep the divider aligned) |
| `data-testid` | `string` | Test hook |

```tsx
// ✅ GOOD — shared card chrome, consistent with dashboard tiles
<ChartCard style={{ height: 350 }}>
  <DBTimeChart title="Request Latency" config={config} />
</ChartCard>

// ❌ BAD — hand-rolled card that drifts from the dashboard look
<Box style={{ border: '1px solid var(--color-border)', borderRadius: 4 }}>
  <DBTimeChart title="Request Latency" config={config} />
</Box>
```

**Give it a height.** `ChartCard` is `width: 100%` and fills its parent, so the
parent (or a `style={{ height }}`) must define the height. For equal-width
side-by-side charts (e.g. the RED row) use
`style={{ flex: 1, minWidth: 0, minHeight: 0, height: '100%' }}` inside a
`Flex`. See the `Charts/ChartCard` Storybook stories for the variants.

## UI text: use sentence case

All user-facing text uses **sentence case** — capitalize only the **first word**
and any proper nouns/acronyms. Do **not** use Title Case (capitalizing every
significant word).

This applies to every string a user reads: field labels, buttons, tab/menu
items, headings, section titles, modal titles, placeholders, tooltips, table
column headers, empty states, and toast/notification copy.

| Title Case (avoid) | Sentence case (use) |
|--------------------|---------------------|
| `Data Source`      | `Data source`       |
| `Chart Name`       | `Chart name`        |
| `Add Series`       | `Add series`        |
| `Count of Events`  | `Count of events`   |
| `Save Changes`     | `Save changes`      |
| `Delete Dashboard` | `Delete dashboard`  |

**Keep the original casing of proper nouns, product names, and acronyms**
anywhere in the string — sentence case only changes the words around them:
HyperDX, ClickHouse, ClickStack, OpenTelemetry/OTel, Lucene, SQL, PromQL,
MongoDB, Kubernetes, JSON, CSV, URL, ID, API, MCP, CPU, P95. For example:
`Search your events w/ Lucene`, `Edit SQL`, `Copy as cURL`, `View in ClickHouse`.

Only sentence-case **static UI chrome**. Never rewrite dynamic/user data (column
names, tag values, log/trace content, source or dashboard names a user typed) —
render those verbatim.

## Semantic design tokens (prefer over raw Mantine colors)

The UI is built with **Mantine components**, but **colors and surfaces** should follow the **semantic CSS custom properties** in our themes (`--color-*`, etc.), not ad-hoc Mantine palette values. Those tokens are defined in `packages/app/src/theme/_tokens.scss`, align with a **Click UI**–style system, and are shared by every deployment. They are the path toward a shared design system even while Mantine remains the component layer.

- **Do**: Use Mantine for layout, components, and spacing; use **semantic tokens** for themed backgrounds, text colors, borders, and states (e.g. `style={{ color: 'var(--color-text-muted)' }}` or `style={{ border: '1px solid var(--color-border)' }}`).
- **Do not**: Rely on raw Mantine color props for app chrome and content when a semantic token exists — e.g. `c="gray.5"`, `bg="dark.7"`, or arbitrary `color="blue.4"` for surfaces that should match the rest of the product.
- **Reference**: `packages/app/src/theme/semanticColorsGrouped.ts` (token names), `packages/app/src/theme/_tokens.scss`, and Storybook (`SemanticColors` and related theme stories) for a visual map.

Mantine theme overrides in `packages/app/src/theme/**` may map Mantine’s scale to our palette; that does not replace using **`var(--color-...)`** in new styling where you need explicit color control.

## Click UI (incremental adoption)

`@clickhouse/click-ui` is a dependency of `packages/app`. `ThemeWrapper` mounts
`ClickUIProvider` inside `MantineProvider` with `persistTheme={false}`, so it
follows the app color mode and never writes its own theme to localStorage.
Storybook and `renderWithMantine` in tests get the provider for free.

- **Do**: Prefer a click-ui component over a new Mantine one when click-ui has
  an equivalent for a simple primitive (`Link`, `Text`, `Title`, `Container`,
  `Badge`, `Separator`, `Spacer`, `Icon`). Import from `@clickhouse/click-ui`.
- **Do not**: Mix both libraries inside one small component, and do not
  replace working Mantine code outside the change you were asked to make.
- **`Container` is not a `Box`**: click-ui has no plain-wrapper equivalent of
  Mantine `Box`. `Container` is a flex layout, closer to `Group` or `Stack`. It
  defaults to `display="flex"`, `orientation="horizontal"`, and
  `alignItems="center"`, so a direct swap puts the children in one row. It also
  has no `m`/`p`/`w`-style props, only `gap` and `padding` tokens. To replace a
  `Box` that only carries attributes such as `id` or `data-testid`, use a plain
  HTML element. To replace a `Box` that uses Mantine style props, keep the
  `Box`, or set `Container` props to match the layout and check it visually.
- **Styling**: click-ui styles live in the `clickui` CSS cascade layer, so any
  unlayered app or Mantine rule wins over them. Override `--click-*` variables
  outside the layer if a token must change.
- **First use**: `packages/app/src/components/ContactSupportText.tsx` and the
  `Click UI/Link` story in `packages/app/src/stories/ClickUI.stories.tsx`.

**Chart and visualization colors are a separate, more specific contract** — they have their own categorical, semantic, and heatmap palettes wired through `packages/app/src/utils.ts` helpers. Don't hard-code series colors or reuse `--color-text-*` for charts. See [`data_viz_colors.md`](./data_viz_colors.md) before touching anything that renders data.

## Refactoring

- Edit files directly - don't create `component-v2.tsx` copies
- Look for duplicate code across the affected area
- Verify all callers and integrations after changes
- Refactor to improve clarity or reduce complexity, not just to change

## File Naming and Organization

- Clear, descriptive names following package conventions
- Avoid "temp", "refactored", "improved" in permanent filenames
- Put related components in a single directory