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
23 changes: 23 additions & 0 deletions frontend/.claude/context/ui-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,29 @@ ComponentName/

Single-file components without their own styles can live as a single `.tsx` next to peers — no folder needed.

### SCSS: what belongs here

Write SCSS **only for what utilities and tokens cannot express**. Before adding a rule, ask whether a utility class on the element removes the need for it entirely.

**Do NOT re-declare in SCSS — use a className utility instead:**

| In SCSS you wrote | Use the utility |
|-------------------|-----------------|
| `color: var(--color-text-secondary)` | `text-secondary` (`bg-surface-*`, `border-*` likewise) |
| `border-radius: var(--radius-md)` | `rounded-md` |
| `box-shadow: var(--shadow-*)` | `shadow-*` |
| `display: flex; flex-direction: column; gap: 16px` | `d-flex flex-column gap-3` |
| `align-items: center; justify-content: space-between` | `align-items-center justify-content-between` |
| `padding: 16px` / `padding: 8px` | `p-3` / `p-2` |

The Bootstrap spacing scale is `1→4px 2→8px 3→16px 4→24px`. `gap-*`, `p-*`, `m-*` all follow it.

**SCSS is for the genuinely custom bits:** selected / hover / focus states, transitions, `grid-template-columns`, pseudo-elements, and one-off values that don't map to the scale.

**Off-scale spacing** (e.g. `12px`, `14px`, `2px`): prefer snapping to the `8/16` scale and using a utility. Keep the off-scale value in SCSS only when the design genuinely requires it — don't reach for custom SCSS just to avoid rounding.

Re-declaring tokens in SCSS typically inflates a stylesheet by ~⅓ over the utility-first equivalent and drifts from the design system.

## Storybook (Optional)

When working on complex or unfamiliar components, you can query Storybook MCP (`list-all-documentation`, then `get-documentation`) to discover existing components, their props, and visual examples. This is optional — for simple changes, grepping the codebase is fine.
Expand Down
42 changes: 42 additions & 0 deletions frontend/common/services/useMetric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Res } from 'common/types/responses'
import { Req } from 'common/types/requests'
import { service } from 'common/service'
import Utils from 'common/utils/utils'
import transformCorePaging from 'common/transformCorePaging'

export const metricService = service
.enhanceEndpoints({ addTagTypes: ['Metric'] })
.injectEndpoints({
endpoints: (builder) => ({
createMetric: builder.mutation<Res['metric'], Req['createMetric']>({
invalidatesTags: [{ id: 'LIST', type: 'Metric' }],
query: ({ body, environmentId }) => ({
body,
method: 'POST',
url: `environments/${environmentId}/experiment-metrics/`,
}),
}),
deleteMetric: builder.mutation<void, Req['deleteMetric']>({
invalidatesTags: [{ id: 'LIST', type: 'Metric' }],
query: ({ environmentId, metricId }) => ({
method: 'DELETE',
url: `environments/${environmentId}/experiment-metrics/${metricId}/`,
}),
}),
getMetrics: builder.query<Res['metrics'], Req['getMetrics']>({
providesTags: [{ id: 'LIST', type: 'Metric' }],
query: ({ environmentId, ...rest }) => ({
url: `environments/${environmentId}/experiment-metrics/?${Utils.toParam(
rest,
)}`,
}),
transformResponse: (res, _, req) => transformCorePaging(req, res),
}),
}),
})

export const {
useCreateMetricMutation,
useDeleteMetricMutation,
useGetMetricsQuery,
} = metricService
6 changes: 6 additions & 0 deletions frontend/common/theme/tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,5 +168,11 @@
"standard": { "cssVar": "--easing-standard", "value": "cubic-bezier(0.2, 0, 0.38, 0.9)", "description": "Default for most transitions. Smooth deceleration. Use for elements moving within the page." },
"entrance": { "cssVar": "--easing-entrance", "value": "cubic-bezier(0.0, 0, 0.38, 0.9)", "description": "Elements entering the viewport. Decelerates into resting position. Modals, toasts, slide-ins." },
"exit": { "cssVar": "--easing-exit", "value": "cubic-bezier(0.2, 0, 1, 0.9)", "description": "Elements leaving the viewport. Accelerates out of view. Closing modals, dismissing toasts." }
},
"font-weight": {
"regular": { "cssVar": "--font-weight-regular", "value": "400", "description": "Body copy, default text." },
"medium": { "cssVar": "--font-weight-medium", "value": "500", "description": "Subtle emphasis. Labels, secondary headings, table headers." },
"semibold": { "cssVar": "--font-weight-semibold", "value": "600", "description": "Strong emphasis. Card titles, selected states, section headings." },
"bold": { "cssVar": "--font-weight-bold", "value": "700", "description": "Maximum emphasis. Page titles, key figures." }
}
}
26 changes: 26 additions & 0 deletions frontend/common/theme/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,26 @@ export const easing: Record<string, TokenEntry> = {
value: 'var(--easing-standard)',
},
}
// Font-weight
export const fontWeight: Record<string, TokenEntry> = {
'bold': {
description: 'Maximum emphasis. Page titles, key figures.',
value: 'var(--font-weight-bold, 700)',
},
'medium': {
description: 'Subtle emphasis. Labels, secondary headings, table headers.',
value: 'var(--font-weight-medium, 500)',
},
'regular': {
description: 'Body copy, default text.',
value: 'var(--font-weight-regular, 400)',
},
'semibold': {
description:
'Strong emphasis. Card titles, selected states, section headings.',
value: 'var(--font-weight-semibold, 600)',
},
}

// =============================================================================
// Flat token constants — semantic tokens as CSS value strings.
Expand Down Expand Up @@ -233,3 +253,9 @@ export const easingEntrance =
export const easingExit = 'var(--easing-exit, cubic-bezier(0.2, 0, 1, 0.9))'
export const easingStandard =
'var(--easing-standard, cubic-bezier(0.2, 0, 0.38, 0.9))'

// Font-weight
export const fontWeightBold = 'var(--font-weight-bold, 700)'
export const fontWeightMedium = 'var(--font-weight-medium, 500)'
export const fontWeightRegular = 'var(--font-weight-regular, 400)'
export const fontWeightSemibold = 'var(--font-weight-semibold, 600)'
17 changes: 17 additions & 0 deletions frontend/common/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import {
StageActionBody,
ChangeRequest,
ExperimentStatus,
MetricAggregation,
MetricDirection,
MetricDefinition,
FlagsmithValue,
TagStrategy,
FeatureType,
Expand Down Expand Up @@ -1014,5 +1017,19 @@ export type Req = {
}
experimentAction: { environmentId: string; experimentId: number }
deleteExperiment: { environmentId: string; experimentId: number }
getMetrics: PagedRequest<{
environmentId: string
}>
createMetric: {
environmentId: string
body: {
name: string
description: string
aggregation: MetricAggregation
direction: MetricDirection
definition: MetricDefinition
}
}
deleteMetric: { environmentId: string; metricId: number }
// END OF TYPES
}
22 changes: 22 additions & 0 deletions frontend/common/types/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,26 @@ export type ExperimentStatus = 'created' | 'running' | 'paused' | 'completed'

export type ExperimentStatusCounts = Record<ExperimentStatus, number>

export type MetricAggregation = 'count' | 'sum' | 'mean' | 'occurrence'

export type MetricDirection = 'up' | 'down' | 'informational'

export type MetricDefinition = {
version: number
event: string
}

export type Metric = {
id: number
name: string
description: string
aggregation: MetricAggregation
direction: MetricDirection
definition: MetricDefinition
created_at: string
updated_at: string
}

export type ExperimentFeature = {
id: number
name: string
Expand Down Expand Up @@ -1379,5 +1399,7 @@ export type Res = {
status_counts?: ExperimentStatusCounts
}
experiment: Experiment
metric: Metric
metrics: PagedResponse<Metric>
// END OF TYPES
}
50 changes: 50 additions & 0 deletions frontend/documentation/TokenReference.generated.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,56 @@ export const AllTokens: StoryObj = {
</tr>
</tbody>
</table>
<h3>Font-weight</h3>
<table className='docs-table'>
<thead>
<tr>
<th>Token</th>
<th>Value</th>
<th>Usage</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>--font-weight-regular</code>
</td>
<td>
<code>400</code>
</td>
<td>Body copy, default text.</td>
</tr>
<tr>
<td>
<code>--font-weight-medium</code>
</td>
<td>
<code>500</code>
</td>
<td>Subtle emphasis. Labels, secondary headings, table headers.</td>
</tr>
<tr>
<td>
<code>--font-weight-semibold</code>
</td>
<td>
<code>600</code>
</td>
<td>
Strong emphasis. Card titles, selected states, section headings.
</td>
</tr>
<tr>
<td>
<code>--font-weight-bold</code>
</td>
<td>
<code>700</code>
</td>
<td>Maximum emphasis. Page titles, key figures.</td>
</tr>
</tbody>
</table>

<h3>Dark mode shadows</h3>
<p>
Expand Down
Loading
Loading