# 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) | `` | | `variant="secondary"` | Secondary actions (Cancel, Clear, auxiliary actions) | `` | | `variant="danger"` | Destructive actions (Delete, Remove, Rotate API Key) | `` | | `variant="link"` | Link-style actions with no background or border (View Details, navigation-style CTAs) | `` | | `variant="subtle"` | Transparent background with hover highlight; for toolbar/utility controls that shouldn't draw attention until hovered (collapse toggles, close buttons, auxiliary actions) | `` | ### Correct Usage ```tsx ... ... ... ... ... ``` ### DO NOT USE (Forbidden Patterns) ```tsx ... ... ``` **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 // ✅ CORRECT - Use ActionIcon for icon-only buttons ``` 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 This may take a while. Could not save the alert. // ❌ hardcoded Mantine palette — not theme-aware, inconsistent contrast ... ... ``` **`Text`** — `danger` | `warning` | `success` for inline status/validation text: ```tsx Required field Connection verified // ❌ don't reach for raw palette colors for semantic status text Required field ``` **`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 `` 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 `` 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 not reversible. , '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 `` 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
No data
Nothing here // ✅ GOOD - use the EmptyState component } 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 `
`, ad-hoc `Paper` + monospace text, or unstyled ``.** Use the existing Mantine/product components so snippets match Terraform export, onboarding, and Storybook guidelines.

| Kind | Component | When |
|------|-----------|------|
| **Inline code** | Mantine `` | 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 `` plus a Copy button. |

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

// ✅ fenced / copyable — CopySnippet (Code + Copy)


// Label is optional when the surrounding heading already names the snippet


// ❌ BAD — raw pre / Paper chrome
{snippet}
{snippet} ``` **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` `` → `` 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 `
`/`` 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 // ❌ BAD — hand-rolled card that drifts from the dashboard look ``` **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