From 6b06f624e87735c5e5aa5cc9aae6be230e4f38dd Mon Sep 17 00:00:00 2001 From: Menci Date: Tue, 28 Jul 2026 22:30:33 +0800 Subject: [PATCH 1/3] feat(web): publish Cascadia Code as the dashboard's code face MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every code surface asked for the generic `monospace` family, which macOS resolves to Courier — a face that shares neither metrics nor mood with the rest of the dashboard. Bundle Cascadia Code through Fontsource, publish it as the theme's monospace family, and route UnoCSS's `font-mono` plus the remaining hardcoded declarations through that one token. The Prism themes address the code block through `pre[class*="language-"]`, which outranks a single-class rule, so those two declarations override the theme outright. Drop the base and numeric family overrides at the same time: they pinned the bare `sans-serif`, which resolves to Helvetica on macOS, while Fluent's own stacks lead with Segoe UI and fall back to the platform UI face. --- apps/web/package.json | 1 + apps/web/src/components/requests/request-detail.tsx | 4 ++-- apps/web/src/components/ui/code-block.tsx | 10 +++++++--- .../src/components/upstream-editor/model-detail.tsx | 2 +- .../components/upstream-editor/provider-config.tsx | 4 ++-- apps/web/src/root.tsx | 1 + apps/web/src/theme.ts | 13 ++++++++++--- apps/web/uno.config.ts | 3 +++ pnpm-lock.yaml | 8 ++++++++ 9 files changed, 35 insertions(+), 11 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index f699ef717..49078f3e2 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", 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 067807989..ae6d6e114 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 d9927fa4e..f72394d49 100644 --- a/apps/web/src/root.tsx +++ b/apps/web/src/root.tsx @@ -28,6 +28,7 @@ import { scrollAreaCss } from './components/ui/scroll-area'; import { fluentComponents } from './fluent'; import { flowayDarkTheme, flowayLightTheme } from './theme'; import './i18n'; +import '@fontsource-variable/cascadia-code'; import 'virtual: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/uno.config.ts b/apps/web/uno.config.ts index 7686e3fbf..a3e034703 100644 --- a/apps/web/uno.config.ts +++ b/apps/web/uno.config.ts @@ -5,6 +5,9 @@ export default defineConfig({ presetWind3(), ], theme: { + fontFamily: { + mono: 'var(--fontFamilyMonospace)', + }, fontSize: { 'fui-base200': 'var(--fontSizeBase200)', 'fui-base300': 'var(--fontSizeBase300)', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e7eb59eb..90679876c 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) @@ -1609,6 +1612,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==} @@ -6647,6 +6653,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 From 26a8ed9c93382901583b3313dbc48a2419c94589 Mon Sep 17 00:00:00 2001 From: Menci Date: Tue, 28 Jul 2026 23:30:17 +0800 Subject: [PATCH 2/3] fix(web): generate the dashboard stylesheet in one pass The Vite integration's global mode emits nothing here: it keys its vite:css-post handle by the top-level build.outDir, while React Router sets outDir only per environment and opts into builder.sharedConfigBuild, so the lookup misses and the layer placeholder ships as the entire stylesheet (unocss/unocss#4990). Per-module mode sidestepped that by generating one sheet per module, but concatenating those in module-graph order put breakpoint variants ahead of the base utilities they override. A media query adds no specificity, so the base utility won at every width: 42 responsive rules across 25 files were inert, among them the sidebar collapse and the mobile navigation trigger. Generate through PostCSS instead. One pass over the content set is a single generate() call, which is where the rule ordering guarantee lives -- base utilities, then lt-*, then ascending min-width breakpoints. Pin the plugin's cwd so config discovery and the content globs resolve against this package rather than the build process's working directory; without it a build launched from the workspace root emits an empty sheet. The source-level transformers go with the Vite plugin, which was the only thing applying them. Neither was reachable anyway: no variant groups appear in the source, and there are no CSS files for the directive transformer to process. Checked the emitted sheet against every responsive class in the source (42 overridden, now 0) and confirmed the computed layout at 500, 700, 900, and 1400px in a headless browser. --- apps/web/package.json | 1 + apps/web/postcss.config.ts | 25 +++++++++++++++++++++++++ apps/web/src/root.tsx | 2 +- apps/web/src/uno.css | 1 + apps/web/tsconfig.json | 1 + apps/web/uno.config.ts | 7 +++++-- apps/web/vite.config.ts | 3 +-- pnpm-lock.yaml | 25 ++++++++++++++++++++++--- 8 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 apps/web/postcss.config.ts create mode 100644 apps/web/src/uno.css diff --git a/apps/web/package.json b/apps/web/package.json index 49078f3e2..ea99fa20a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -47,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/root.tsx b/apps/web/src/root.tsx index f72394d49..ada833c26 100644 --- a/apps/web/src/root.tsx +++ b/apps/web/src/root.tsx @@ -29,7 +29,7 @@ import { fluentComponents } from './fluent'; import { flowayDarkTheme, flowayLightTheme } from './theme'; import './i18n'; import '@fontsource-variable/cascadia-code'; -import 'virtual:uno.css'; +import './uno.css'; const { FluentProvider } = fluentComponents; 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 a3e034703..8c76f4bd5 100644 --- a/apps/web/uno.config.ts +++ b/apps/web/uno.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, presetWind3, transformerDirectives, transformerVariantGroup } from 'unocss'; +import { defineConfig, presetWind3 } from 'unocss'; export default defineConfig({ presets: [ @@ -35,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 0deb81036..c58ed6072 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` @@ -53,7 +52,7 @@ const wranglerProxiedPaths = [ ]; export default defineConfig({ - 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 90679876c..594fed014 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -238,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) @@ -2618,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==} @@ -7478,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: @@ -7541,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 @@ -10165,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 @@ -10184,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 From d43340ce66c057233f48a6955c1323961e256ea4 Mon Sep 17 00:00:00 2001 From: Menci Date: Wed, 29 Jul 2026 00:49:37 +0800 Subject: [PATCH 3/3] fix(web): align the Fluent endpoint grid --- .../components/api-docs/api-docs-content.tsx | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) 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}; }