diff --git a/apps/web/package.json b/apps/web/package.json index f699ef717..ea99fa20a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,6 +17,7 @@ "@fluentui/react-charts": "^9.3.22", "@fluentui/react-components": "^9.74.4", "@fluentui/react-icons": "^2.0.334", + "@fontsource-variable/cascadia-code": "^5.3.0", "@hookform/resolvers": "^5.5.7", "@react-router/node": "8.3.0", "d3-shape": "^3.2.0", @@ -46,6 +47,7 @@ "@types/prismjs": "^1.26.6", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", + "@unocss/postcss": "^66.7.5", "happy-dom": "^20.11.1", "unocss": "^66.7.5", "vite": "^8.1.5" diff --git a/apps/web/postcss.config.ts b/apps/web/postcss.config.ts new file mode 100644 index 000000000..9807c93ab --- /dev/null +++ b/apps/web/postcss.config.ts @@ -0,0 +1,25 @@ +import UnoCSS from '@unocss/postcss'; + +// UnoCSS generates through PostCSS rather than `unocss/vite`, whose global mode +// emits nothing under React Router: it keys its `vite:css-post` handle by the +// top-level `build.outDir`, while React Router sets `outDir` only per +// environment (`dist/client`, `dist/server`) and opts into +// `builder.sharedConfigBuild`, which is what otherwise re-resolves the config +// per environment. The lookup misses and the layer placeholder ships as the +// entire stylesheet. +// https://github.com/unocss/unocss/issues/4990 +// https://github.com/unocss/unocss/blob/e28a47c557fe179935a37a4fbeb650292d0d1d5a/packages-integrations/vite/src/modes/global/build.ts#L128-L182 +// +// That plugin's per-module mode does emit, but generates one sheet per module, +// and concatenating them in module-graph order breaks the cascade: a breakpoint +// variant can land ahead of the base utility it has to override, and since a +// media query adds no specificity the base utility wins at every width. Rule +// order is a property of a single `generate()` call, which is what one PostCSS +// pass over the whole content set gives us. +// +// `cwd` resolves both uno.config.ts discovery and the `content.filesystem` +// globs, and defaults to the build process's working directory. Pinning it here +// keeps a build launched from the workspace root scanning the same files as one +// launched from this package. +// https://github.com/unocss/unocss/blob/e28a47c557fe179935a37a4fbeb650292d0d1d5a/packages-integrations/postcss/src/esm.ts#L21-L110 +export default { plugins: [UnoCSS({ cwd: import.meta.dirname })] }; diff --git a/apps/web/src/components/api-docs/api-docs-content.tsx b/apps/web/src/components/api-docs/api-docs-content.tsx index abfcbb975..fd8e734a7 100644 --- a/apps/web/src/components/api-docs/api-docs-content.tsx +++ b/apps/web/src/components/api-docs/api-docs-content.tsx @@ -1,5 +1,5 @@ import { ArrowUpRight16Regular } from '@fluentui/react-icons'; -import { useState } from 'react'; +import { Fragment, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { apiDocsEndpoints, apiDocsExamples, apiDocsGroups, authCurlExample, type ApiDocsExampleId } from './api-docs-data'; @@ -13,6 +13,7 @@ const { AccordionHeader, AccordionItem, AccordionPanel, + Badge, Link, MessageBar, MessageBarBody, @@ -65,23 +66,29 @@ export function ApiDocsContent() { {t('dashboard.apiDocs.endpointsTitle')} {t('dashboard.apiDocs.endpointsDescription')} - {apiDocsGroups.map(group =>
- {t(`dashboard.apiDocs.groups.${group}`)} - -
- {apiDocsEndpoints.filter(endpoint => endpoint.group === group).map(endpoint => ( -
- - {endpoint.path} - {t(`dashboard.apiDocs.endpointNames.${endpoint.name}`)} - - {t('dashboard.apiDocs.docsLink')}
- ))} -
-
-
)} + {apiDocsGroups.map(group => { + const endpoints = apiDocsEndpoints.filter(endpoint => endpoint.group === group); + return
+ {t(`dashboard.apiDocs.groups.${group}`)} + +
+ {endpoints.map((endpoint, index) => { + const cellClassName = `min-w-0 px-2 py-2 ${index === endpoints.length - 1 ? '' : 'border-b border-fui-subtle'}`; + return +
+ {endpoint.path} + {t(`dashboard.apiDocs.endpointNames.${endpoint.name}`)} +
+ + {t('dashboard.apiDocs.docsLink')}
+
; + })} +
+
+
; + })} @@ -110,8 +117,11 @@ export function ApiDocsContent() { } function MethodBadge({ method }: { method: 'GET' | 'POST' }) { - return {method}; + return {method}; } diff --git a/apps/web/src/components/requests/request-detail.tsx b/apps/web/src/components/requests/request-detail.tsx index b28180e2c..ed80cd3a6 100644 --- a/apps/web/src/components/requests/request-detail.tsx +++ b/apps/web/src/components/requests/request-detail.tsx @@ -44,7 +44,7 @@ const useStyles = makeStyles({ code: { backgroundColor: 'var(--colorNeutralBackground1)', color: 'var(--colorNeutralForeground1)', - fontFamily: 'monospace', + fontFamily: 'var(--fontFamilyMonospace)', fontSize: '12px', lineHeight: 1.55, margin: 0, @@ -53,7 +53,7 @@ const useStyles = makeStyles({ tabSize: 2, whiteSpace: 'pre', }, - headers: { borderCollapse: 'collapse', fontFamily: 'monospace', fontSize: '12px', width: '100%' }, + headers: { borderCollapse: 'collapse', fontFamily: 'var(--fontFamilyMonospace)', fontSize: '12px', width: '100%' }, headerRow: { borderBottom: '1px solid var(--colorNeutralStroke3)' }, headerName: { color: 'var(--colorNeutralForeground3)', padding: '7px 14px 7px 16px', textAlign: 'left', verticalAlign: 'top', whiteSpace: 'nowrap', width: '190px' }, headerValue: { color: 'var(--colorNeutralForeground1)', padding: '7px 16px 7px 0', verticalAlign: 'top', whiteSpace: 'nowrap' }, diff --git a/apps/web/src/components/ui/code-block.tsx b/apps/web/src/components/ui/code-block.tsx index 6dcda8871..4ed0a555a 100644 --- a/apps/web/src/components/ui/code-block.tsx +++ b/apps/web/src/components/ui/code-block.tsx @@ -30,11 +30,15 @@ const useStyles = makeStyles({ }, lang: { color: 'var(--colorNeutralForeground2)', - fontFamily: 'monospace', + fontFamily: 'var(--fontFamilyMonospace)', fontSize: '12px', }, + // The Prism themes set the code font through `pre[class*="language-"]`, + // which outranks a single-class rule, so the theme's monospace family has to + // be overridden outright. + // https://github.com/PrismJS/prism-themes/blob/v1.9.0/themes/prism-vs.css#L5-L10 pre: { - fontFamily: 'monospace', + fontFamily: 'var(--fontFamilyMonospace) !important', fontSize: '12px', lineHeight: '1.55', margin: 0, @@ -47,7 +51,7 @@ const useStyles = makeStyles({ '& .token.table': { display: 'inline', }, - fontFamily: 'monospace', + fontFamily: 'var(--fontFamilyMonospace) !important', whiteSpace: 'pre', }, }); diff --git a/apps/web/src/components/upstream-editor/model-detail.tsx b/apps/web/src/components/upstream-editor/model-detail.tsx index 0c5b371ec..7df40bd3d 100644 --- a/apps/web/src/components/upstream-editor/model-detail.tsx +++ b/apps/web/src/components/upstream-editor/model-detail.tsx @@ -32,7 +32,7 @@ const { const useStyles = makeStyles({ endpointLabel: { - fontFamily: 'monospace !important', + fontFamily: 'var(--fontFamilyMonospace) !important', }, }); diff --git a/apps/web/src/components/upstream-editor/provider-config.tsx b/apps/web/src/components/upstream-editor/provider-config.tsx index 902814f11..13ef20332 100644 --- a/apps/web/src/components/upstream-editor/provider-config.tsx +++ b/apps/web/src/components/upstream-editor/provider-config.tsx @@ -51,10 +51,10 @@ const useStyles = makeStyles({ marginLeft: '-8px', }, endpointCheckbox: { - fontFamily: 'monospace !important', + fontFamily: 'var(--fontFamilyMonospace) !important', }, pathOverrideLabel: { - fontFamily: 'monospace !important', + fontFamily: 'var(--fontFamilyMonospace) !important', fontSize: 'var(--fontSizeBase200) !important', }, }); diff --git a/apps/web/src/root.tsx b/apps/web/src/root.tsx index 0afa55e22..a51d21bd2 100644 --- a/apps/web/src/root.tsx +++ b/apps/web/src/root.tsx @@ -24,7 +24,8 @@ import { scrollAreaCss } from './components/ui/scroll-area'; import { fluentComponents } from './fluent'; import { flowayDarkTheme, flowayLightTheme } from './theme'; import './i18n'; -import 'virtual:uno.css'; +import '@fontsource-variable/cascadia-code'; +import './uno.css'; const { FluentProvider } = fluentComponents; diff --git a/apps/web/src/theme.ts b/apps/web/src/theme.ts index f93bda2d7..eda8bd1a2 100644 --- a/apps/web/src/theme.ts +++ b/apps/web/src/theme.ts @@ -23,10 +23,17 @@ export const australianBrand: BrandVariants = { 160: '#f7faff', }; +// Cascadia Code is Microsoft's own coding typeface and the one Fluent surfaces +// pair with; `monospace` alone resolves to Courier on macOS, which reads +// nothing like the rest of the dashboard. The bundled variable face is named +// with the Fontsource `Variable` suffix, so locally installed Cascadia builds +// come next, then the platform coding faces. +// https://github.com/microsoft/cascadia-code/blob/2404.23/README.md#cascadia-code +// https://github.com/fontsource/font-files/blob/main/fonts/variable/cascadia-code/README.md#cascadia-code +const monospaceStack = "'Cascadia Code Variable', 'Cascadia Code', 'Cascadia Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"; + const fontOverrides = { - fontFamilyBase: 'sans-serif', - fontFamilyMonospace: 'monospace', - fontFamilyNumeric: 'sans-serif', + fontFamilyMonospace: monospaceStack, fontSizeBase100: '10px', fontSizeBase200: '12px', fontSizeBase300: '14px', diff --git a/apps/web/src/uno.css b/apps/web/src/uno.css new file mode 100644 index 000000000..4612250aa --- /dev/null +++ b/apps/web/src/uno.css @@ -0,0 +1 @@ +@unocss; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index bc27f89cf..2c703c5ca 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -18,6 +18,7 @@ "react-router.config.ts", "vite.config.ts", "vitest.config.ts", + "postcss.config.ts", "uno.config.ts" ] } diff --git a/apps/web/uno.config.ts b/apps/web/uno.config.ts index 7686e3fbf..8c76f4bd5 100644 --- a/apps/web/uno.config.ts +++ b/apps/web/uno.config.ts @@ -1,10 +1,13 @@ -import { defineConfig, presetWind3, transformerDirectives, transformerVariantGroup } from 'unocss'; +import { defineConfig, presetWind3 } from 'unocss'; export default defineConfig({ presets: [ presetWind3(), ], theme: { + fontFamily: { + mono: 'var(--fontFamilyMonospace)', + }, fontSize: { 'fui-base200': 'var(--fontSizeBase200)', 'fui-base300': 'var(--fontSizeBase300)', @@ -32,7 +35,10 @@ export default defineConfig({ ['font-fui-medium', { 'font-weight': 'var(--fontWeightMedium)' }], ['font-fui-semibold', { 'font-weight': 'var(--fontWeightSemibold)' }], ], - transformers: [transformerDirectives(), transformerVariantGroup()], + // The PostCSS integration reads these globs itself and never sees the module + // graph, so a class only ships if a file here spells it out. It also applies + // no source-level transformers: utilities must appear verbatim in the source, + // not as variant groups. content: { filesystem: ['src/**/*.{ts,tsx}'], }, diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 55651a9b5..62d47d674 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -1,5 +1,4 @@ import { reactRouter } from '@react-router/dev/vite'; -import UnoCSS from 'unocss/vite'; import { defineConfig, type Plugin } from 'vite'; // Prism ships its language components as scripts that mutate a global `Prism` @@ -87,7 +86,7 @@ export default defineConfig({ 'zustand', ], }, - plugins: [prismComponentsEsm(), UnoCSS({ mode: 'per-module' }), reactRouter()], + plugins: [prismComponentsEsm(), reactRouter()], server: { port: 5174, proxy: Object.fromEntries(wranglerProxiedPaths.map(p => [p, { target: wranglerOrigin, changeOrigin: true }])), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e7eb59eb..594fed014 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -153,6 +153,9 @@ importers: '@fluentui/react-icons': specifier: ^2.0.334 version: 2.0.334(react@19.2.8) + '@fontsource-variable/cascadia-code': + specifier: ^5.3.0 + version: 5.3.0 '@hookform/resolvers': specifier: ^5.5.7 version: 5.5.7(@standard-schema/spec@1.1.0)(ajv@8.20.0)(react-hook-form@7.83.0(react@19.2.8))(valibot@1.4.2(typescript@5.9.3))(zod@4.4.3) @@ -235,12 +238,15 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.17) + '@unocss/postcss': + specifier: ^66.7.5 + version: 66.7.5(postcss@8.5.23) happy-dom: specifier: ^20.11.1 version: 20.11.1 unocss: specifier: ^66.7.5 - version: 66.7.5(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0)) + version: 66.7.5(@unocss/postcss@66.7.5(postcss@8.5.23))(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0)) vite: specifier: ^8.1.5 version: 8.1.5(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0) @@ -1609,6 +1615,9 @@ packages: '@fluentui/tokens@1.0.0-alpha.23': resolution: {integrity: sha512-uxrzF9Z+J10naP0pGS7zPmzSkspSS+3OJDmYIK3o1nkntQrgBXq3dBob4xSlTDm5aOQ0kw6EvB9wQgtlyy4eKQ==} + '@fontsource-variable/cascadia-code@5.3.0': + resolution: {integrity: sha512-6I6M029Lum62MGmP0o+rXRrIBiIiptNDC4slPKLPUJiSB7NHZTLb+Ihk9G7jLvUfWziGXHJLjJPJLBzlEPjS1g==} + '@griffel/core@1.21.3': resolution: {integrity: sha512-FMnlwhtmCRWvXEg2j/6W90wzvW+PFqdrsWFslfmxwS6l9X73gO0dnndKphht9ZOsJODvmLdqlnU1Lh8igg2mKw==} @@ -2612,6 +2621,11 @@ packages: '@unocss/inspector@66.7.5': resolution: {integrity: sha512-WmTJMnj8bmRxvw/wGeShmL2eEHr2/L0BdGTXSxhfzwcO8y5XZEbBmVciLcM7pqaTXQ6JY4+KnITrDUf1urjZxQ==} + '@unocss/postcss@66.7.5': + resolution: {integrity: sha512-j9rWTZwXHioPQgZ7YhX5DC3lG6FyvBluf20LI7VgsDk3bsqG5Yz0KrHHwXcvIJqcs6R+IsHTZxRiCgOn9fvZZg==} + peerDependencies: + postcss: ^8.4.21 + '@unocss/preset-attributify@66.7.5': resolution: {integrity: sha512-1Oi1Cp81pqYJwG3h4+OlP5A0FKyaa07MFBXaXc4Yr3faiTbsI8SKj2TqnZCb+NQvBsEqslEd+jKXGZ3lKzCVOQ==} @@ -6647,6 +6661,8 @@ snapshots: dependencies: '@swc/helpers': 0.5.21 + '@fontsource-variable/cascadia-code@5.3.0': {} + '@griffel/core@1.21.3': dependencies: '@emotion/hash': 0.9.2 @@ -7470,7 +7486,7 @@ snapshots: debug: 4.4.3 minimatch: 9.0.9 semver: 7.7.4 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -7533,6 +7549,15 @@ snapshots: gzip-size: 6.0.0 sirv: 3.0.2 + '@unocss/postcss@66.7.5(postcss@8.5.23)': + dependencies: + '@unocss/config': 66.7.5 + '@unocss/core': 66.7.5 + '@unocss/rule-utils': 66.7.5 + css-tree: 3.2.1 + postcss: 8.5.23 + tinyglobby: 0.2.17 + '@unocss/preset-attributify@66.7.5': dependencies: '@unocss/core': 66.7.5 @@ -10157,7 +10182,7 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - unocss@66.7.5(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0)): + unocss@66.7.5(@unocss/postcss@66.7.5(postcss@8.5.23))(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0)): dependencies: '@unocss/cli': 66.7.5 '@unocss/core': 66.7.5 @@ -10176,6 +10201,8 @@ snapshots: '@unocss/transformer-directives': 66.7.5 '@unocss/transformer-variant-group': 66.7.5 '@unocss/vite': 66.7.5(vite@8.1.5(@types/node@22.19.19)(esbuild@0.28.0)(jiti@2.6.1)(tsx@4.22.4)(yaml@2.9.0)) + optionalDependencies: + '@unocss/postcss': 66.7.5(postcss@8.5.23) transitivePeerDependencies: - vite