Răsfoiți Sursa

Neo: header title and active tab follow the band's colour scheme (#5880)

With layout.header.band_scheme set, the page title now takes the band's scheme like the
header's buttons (Uniwind ScopedTheme, web and native): white on a dark band, the app's own
scheme once the header floats over content. On web a selected button on the band takes the
opposite scheme, so the active tab stands out as a light pill instead of a dark accent wash.
AGENTS.md notes that forks overriding semantic tokens on :root must set them on the .light /
.dark scopes too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko 6 zile în urmă
părinte
comite
c1b036dcb1

+ 1 - 1
neo/AGENTS.md

@@ -51,7 +51,7 @@ Rules for forks:
 | `functions.js` | Re-exports `app/default/functions`. Declare a local export with the same name to replace one function. |
 | `loading.js`, `sounds.js`, `covers/`, `splash-mockups/`, `nav/footer.web.js` | Replace the export. |
 | `<type>/_map.js` for elements, form-fields, forms, molecules, page-layout, menu-items, units, units/content-list, units/profile-list, skeletons, workers | Import `componentsMapDefault` from the core `_map.js`, add or replace keys, re-export it. Put new components under `customization/<type>/`. |
-| `design/styles/global.css`, `global.web.css` | CSS variable and `@theme` overrides (both platforms; the second is web only). |
+| `design/styles/global.css`, `global.web.css` | CSS variable and `@theme` overrides (both platforms; the second is web only). On web, `.light` / `.dark` colour-scheme scopes (the header on a band, `layout.header.band_scheme`) re-declare core's semantic tokens, so set a semantic token you override on `:root` (`--accent-foreground`, ...) on `.light` / `.dark` in `global.web.css` too. |
 | `design/fonts/fonts-web.js`, `design/fonts/fonts.js` | Web `next/font` variables and the native `useFonts` map. |
 | `config/fonts.js` | Font family per role and platform; see [Fonts](#fonts). |
 | `config/next.config.js` | Deep-merged into `apps/next/next.config.js`. Arrays are concatenated. Functions (`webpack`, `rewrites`, `headers`) replace upstream's, so define one only if you reimplement upstream's. This is the fork hook despite the "don't edit" comment at its top. |

+ 11 - 0
neo/packages/app/design/controls/neo-button/color-scheme-context.ts

@@ -15,6 +15,17 @@ export const NeoButtonColorSchemeContext = createContext<NeoButtonColorScheme>(n
 
 export const useNeoButtonColorScheme = (): NeoButtonColorScheme => useContext(NeoButtonColorSchemeContext);
 
+/**
+ * The scheme a web button draws with on that surface. A selected button takes the opposite one:
+ * in the surface's own scheme its toggled fill sinks into the band (a dark accent wash on a
+ * bright band), while the inverse stands out from its neighbours, as a selected segment does.
+ * iOS needs no flip: selected glass takes its fill and label from the app's own tokens.
+ */
+export function selectedColorScheme(scheme: NeoButtonColorScheme, selected: boolean): NeoButtonColorScheme {
+    if (!scheme || !selected) return scheme;
+    return scheme === 'dark' ? 'light' : 'dark';
+}
+
 /** Any RN colour as unsigned 0xAARRGGBB; null when it can't be parsed. */
 function toArgb(color: unknown): number | null {
     const value = processColor(color as Parameters<typeof processColor>[0]);

+ 4 - 3
neo/packages/app/design/controls/neo-button/neo-button.tsx

@@ -70,7 +70,7 @@ import {
     resolveScoped,
 } from 'app/design/controls/neo-button/neo-button-resolver';
 import { NeoButtonExpoUI, getNativeIcon, hostsCustomContent } from 'app/design/controls/neo-button/neo-button-expoui';
-import { useNeoButtonColorScheme } from 'app/design/controls/neo-button/color-scheme-context';
+import { selectedColorScheme, useNeoButtonColorScheme } from 'app/design/controls/neo-button/color-scheme-context';
 import type { NeoButtonAddon, NeoButtonClassNames, NeoButtonProps } from 'app/design/controls/neo-button/neo-button.types';
 import { cueAttributes, usePressCueOverride, type PressCue } from 'app/lib/sound';
 
@@ -882,8 +882,9 @@ const NeoButtonStyled = (props: NeoButtonProps & { titleHidden?: boolean }) => {
     const { isDesktop } = useNeoEnv();
     // A surface with its own brightness (header on a coloured band): on web the
     // root becomes a `.light` / `.dark` colour-scheme scope, so the button draws
-    // with that scheme's tokens (global.css). iOS: see color-scheme-context.
-    const colorScheme = useNeoButtonColorScheme();
+    // with that scheme's tokens (global.css), the opposite one when selected.
+    // iOS: see color-scheme-context.
+    const colorScheme = selectedColorScheme(useNeoButtonColorScheme(), !!selected);
     const schemeClass = isWeb && colorScheme ? colorScheme : '';
     // Web only: native Tooltip renders no wrapper and drops `className`, so a
     // wide native screen (isDesktop) would lose the root layout classes.

+ 7 - 6
neo/packages/app/settings/layout.js

@@ -123,12 +123,13 @@ export const settingsLayout = {
              */
             band: 'bg-card sm:bg-card/80 lg:bg-transparent',
             /**
-             * Colour scheme of the header's buttons while they rest on the band (iOS: SwiftUI
-             * glass chrome and labels; web: the buttons' colour tokens). 'auto' picks by the
-             * band colour's contrast (white or dark labels, whichever reads better); 'light' |
-             * 'dark' set it; null keeps the app's scheme. The app's scheme also applies once the
-             * header floats over content, and at widths where `band` resolves to no fill
-             * (`lg:bg-transparent`).
+             * Colour scheme of the header's buttons and page title while they rest on the band
+             * (iOS: SwiftUI glass chrome and labels; web: the buttons' colour tokens; the title
+             * through Uniwind's ScopedTheme). On web a selected button (the active tab) takes the
+             * opposite scheme, so it stands out from the band. 'auto' picks by the band colour's
+             * contrast (white or dark labels, whichever reads better); 'light' | 'dark' set it;
+             * null keeps the app's scheme. The app's scheme also applies once the header floats
+             * over content, and at widths where `band` resolves to no fill (`lg:bg-transparent`).
              */
             band_scheme: null,
         },

+ 8 - 1
neo/packages/app/ui/molecules/header/page-header-parts.js

@@ -1,6 +1,7 @@
 import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { useAnimatedValue } from 'app/lib/hooks/use-animated-value';
 import { Animated, Platform, useWindowDimensions } from 'react-native';
+import { ScopedTheme } from 'uniwind';
 import { nativeDriver } from 'app/lib/platform/animation';
 import { View, Row } from 'app/design/view';
 import { Text } from 'app/design/typography';
@@ -12,6 +13,7 @@ import { usePathname, useRouter } from 'app/lib/hooks/router';
 import { components } from 'app/components/registry';
 import { useIsDesktop } from 'app/context/measure';
 import { NeoButton, NeoButtonLink } from 'app/design/controls';
+import { useNeoButtonColorScheme } from 'app/design/controls/neo-button/color-scheme-context';
 import Link from 'app/ui/atoms/link';
 import { getTabKeyFromPathname } from 'app/lib/navigation/tab-history';
 import { useIsPushedScreen } from 'app/context/tab-chrome';
@@ -202,11 +204,16 @@ export function resolveHeaderContainerClassName(isScrolled) {
 }
 
 export const TextHeader = memo(({ text }) => {
-    return (
+    // On a coloured band the title takes the band's scheme, as the header's buttons do
+    // (`layout.header.band_scheme`): white on a dark band, back to the app's own once the
+    // header floats over content.
+    const colorScheme = useNeoButtonColorScheme();
+    const title = (
         <Text className="font-bold truncate leading-12 text-card-foreground text-2xl tracking-tight">
             {text}
         </Text>
     );
+    return colorScheme ? <ScopedTheme theme={colorScheme}>{title}</ScopedTheme> : title;
 });
 
 export const PageHeaderSmall = ({ pageData }) => {