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
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 @@ -997,5 +1000,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
68 changes: 45 additions & 23 deletions frontend/scripts/generate-tokens.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,19 @@ const json = JSON.parse(
// Helpers
// ---------------------------------------------------------------------------

const kebabToCamel = (s) =>
s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())
const kebabToCamel = (s) => s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase())

const sorted = (obj) =>
Object.entries(obj).sort(([a], [b]) => a.localeCompare(b, undefined, { numeric: true }))
Object.entries(obj).sort(([a], [b]) =>
a.localeCompare(b, undefined, { numeric: true }),
)

const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
const lightVal = (e) => e.light ?? e.value
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1)

const NON_COLOUR = ['radius', 'shadow', 'duration', 'easing']
const DESCRIBED = ['radius', 'shadow', 'duration', 'easing']
const NON_COLOUR = ['radius', 'shadow', 'duration', 'easing', 'font-weight']
const DESCRIBED = ['radius', 'shadow', 'duration', 'easing', 'font-weight']
// Chart colours are like colour tokens (light/dark) but not under "color"
const CHART_CATEGORY = 'chart'

Expand Down Expand Up @@ -82,7 +83,9 @@ function toPrimitiveRef(val) {
if (hexMatch) return hexMatch

// rgba match → oklch relative colour
const rgbaMatch = val.match(/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/)
const rgbaMatch = val.match(
/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/,
)
if (rgbaMatch) {
const rgb = `${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]}`
const alpha = rgbaMatch[4]
Expand Down Expand Up @@ -161,7 +164,7 @@ function buildScssLines() {
rootLines.push('')
}

return { rootLines, darkLines }
return { darkLines, rootLines }
}

function buildTsDescribedLines() {
Expand All @@ -170,7 +173,9 @@ function buildTsDescribedLines() {
if (!json[cat]) continue
const lines = []
lines.push(`// ${cap(cat)}`)
lines.push(`export const ${cat}: Record<string, TokenEntry> = {`)
lines.push(
`export const ${kebabToCamel(cat)}: Record<string, TokenEntry> = {`,
)
for (const [key, e] of sorted(json[cat])) {
const v = esc(makeCssVar(e.cssVar, lightVal(e)))
const d = esc(e.description || '')
Expand Down Expand Up @@ -210,14 +215,22 @@ function cssVarToConstName(cssVar) {
function buildFlatConstants() {
const lines = []

lines.push('// =============================================================================')
lines.push(
'// =============================================================================',
)
lines.push('// Flat token constants — semantic tokens as CSS value strings.')
lines.push('// Use directly in any context that accepts a CSS value:')
lines.push('// <Bar fill={colorChart1} /> (recharts prop)')
lines.push('// style={{ color: colorTextSecondary }} (inline style)')
lines.push('// border: `1px solid ${colorBorderDefault}` (template strings)')
lines.push('// var() resolves at render; theme toggle updates colours via CSS cascade.')
lines.push('// =============================================================================')
lines.push(
'// border: `1px solid ${colorBorderDefault}` (template strings)',
)
lines.push(
'// var() resolves at render; theme toggle updates colours via CSS cascade.',
)
lines.push(
'// =============================================================================',
)
lines.push('')

// Colour tokens under json.color (border, icon, surface, text)
Expand Down Expand Up @@ -297,7 +310,7 @@ function buildTableRows(title, entries, opts = {}) {
// ---------------------------------------------------------------------------

function generateScss() {
const { rootLines, darkLines } = buildScssLines()
const { darkLines, rootLines } = buildScssLines()

const header = [
'// =============================================================================',
Expand Down Expand Up @@ -356,18 +369,20 @@ function generateMcpStory() {
if (json[CHART_CATEGORY]) {
const chartData = Object.values(json[CHART_CATEGORY]).map((e) => ({
cssVar: e.cssVar,
value: e.light,
description: e.description || '',
value: e.light,
}))
tables.push(...buildTableRows('Chart colours', chartData, { showDescription: true }))
tables.push(
...buildTableRows('Chart colours', chartData, { showDescription: true }),
)
}

for (const cat of DESCRIBED) {
if (!json[cat]) continue
const data = Object.values(json[cat]).map((e) => ({
cssVar: e.cssVar,
value: lightVal(e),
description: e.description || '',
value: lightVal(e),
}))
tables.push(...buildTableRows(cap(cat), data, { showDescription: true }))
}
Expand Down Expand Up @@ -397,7 +412,7 @@ function generateMcpStory() {
' >',
...tables,
'',
" <h3>Dark mode shadows</h3>",
' <h3>Dark mode shadows</h3>',
' <p>',
' Dark mode overrides use stronger opacity (0.20-0.40 vs 0.05-0.20).',
' Higher elevation surfaces should use lighter backgrounds',
Expand Down Expand Up @@ -431,10 +446,10 @@ function generateUtilities() {

// Colour utilities
const colourMappings = {
surface: { prefix: 'bg-surface', property: 'background-color' },
text: { prefix: 'text', property: 'color' },
border: { prefix: 'border', property: 'border-color' },
icon: { prefix: 'icon', property: null }, // special: color + fill
icon: { prefix: 'icon', property: null },
surface: { prefix: 'bg-surface', property: 'background-color' },
text: { prefix: 'text', property: 'color' }, // special: color + fill
}

for (const [category, entries] of Object.entries(json.color)) {
Expand All @@ -445,7 +460,9 @@ function generateUtilities() {
for (const [key, e] of sorted(entries)) {
const cls = `${mapping.prefix}-${key}`
if (category === 'icon') {
lines.push(`.${cls} { color: var(${e.cssVar}); fill: var(${e.cssVar}); }`)
lines.push(
`.${cls} { color: var(${e.cssVar}); fill: var(${e.cssVar}); }`,
)
} else {
lines.push(`.${cls} { ${mapping.property}: var(${e.cssVar}); }`)
}
Expand Down Expand Up @@ -493,7 +510,10 @@ function generateUtilities() {
const scssPath = resolve(ROOT, 'web/styles/_tokens.scss')
const utilitiesPath = resolve(ROOT, 'web/styles/_token-utilities.scss')
const tsPath = resolve(ROOT, 'common/theme/tokens.ts')
const storyPath = resolve(ROOT, 'documentation/TokenReference.generated.stories.tsx')
const storyPath = resolve(
ROOT,
'documentation/TokenReference.generated.stories.tsx',
)

writeFileSync(scssPath, generateScss(), 'utf-8')
writeFileSync(utilitiesPath, generateUtilities(), 'utf-8')
Expand Down Expand Up @@ -523,5 +543,7 @@ console.log(` ${utilitiesPath}`)
console.log(` ${tsPath}`)
console.log(` ${storyPath}`)
console.log(
` ${colorCount} colour + ${nonColorCount} non-colour = ${colorCount + nonColorCount} tokens`,
` ${colorCount} colour + ${nonColorCount} non-colour = ${
colorCount + nonColorCount
} tokens`,
)
Loading
Loading