Ver Fonte

Neo: header band colour scheme on web (layout.header.band_scheme) (#5868)

* Neo: header band colour scheme on web (layout.header.band_scheme)

The web header's buttons now follow `layout.header.band_scheme` like iOS
(#5863): while they rest on the band they draw with that scheme's colour
tokens, and with the app's once the header floats over content.

- Web colour-scheme scopes: `.light` / `.dark` on any element declares
  that scheme's semantic tokens for its subtree, and the `light` / `dark`
  variants let a scope win over the page theme on <html>. The semantic
  token blocks move to palette-schemes.css, which palette.css imports
  inside `:root` (unchanged output, native included) and global.css
  imports again for the scopes.
- NeoButton on web makes its root such a scope when
  NeoButtonColorSchemeContext is set.
- The web header reads the band's computed fill (an external store,
  refreshed on resize and theme changes, not per scroll frame) and
  provides the scheme to the flow header and, at the top of the page,
  to the fixed layer that covers the band.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Neo: scheme tokens as @utility instead of nested @import

SonarCloud failed the PR's reliability rating on CSS rules that don't
know Tailwind:
- css:S8778 "Invalid position for @import rule" on the nested imports
  of palette-schemes.css. The semantic tokens are now the utilities
  `scheme-tokens-light` / `scheme-tokens-dark`, applied with `@apply`
  inside `:root` (palette.css) and on the web scopes (global.css). The
  compiled palette is still identical to master's.
- css:S8776 "Missing scoping root" on the four new `&` rules of the
  light / dark @custom-variant blocks: `&` is the variant's target in
  Tailwind's syntax (master's unchanged variant lines carry the same
  finding). Marked NOSONAR with the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko há 6 dias atrás
pai
commit
93e3a29dbf

+ 6 - 4
neo/packages/app/design/controls/neo-button/color-scheme-context.ts

@@ -2,10 +2,12 @@ import { createContext, useContext } from 'react';
 import { processColor } from 'react-native';
 
 /**
- * Colour scheme for native NeoButtons on a surface whose brightness differs from the app's
- * theme, e.g. the header resting on a coloured band (`layout.header.band_scheme`). iOS passes it
- * to the Expo UI Host, so SwiftUI glass draws its chrome and label for that scheme. Free-standing
- * glass doesn't pick it from the backdrop the way system bars do. null = the app's own scheme.
+ * Colour scheme for NeoButtons on a surface whose brightness differs from the app's theme, e.g.
+ * the header resting on a coloured band (`layout.header.band_scheme`). iOS passes it to the Expo
+ * UI Host, so SwiftUI glass draws its chrome and label for that scheme: free-standing glass
+ * doesn't pick it from the backdrop the way system bars do. On web the button's root becomes a
+ * `.light` / `.dark` scope with that scheme's colour tokens (design/styles/global.css).
+ * null = the app's own scheme.
  */
 export type NeoButtonColorScheme = 'light' | 'dark' | null;
 

+ 8 - 2
neo/packages/app/design/controls/neo-button/neo-button.tsx

@@ -70,6 +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 type { NeoButtonAddon, NeoButtonClassNames, NeoButtonProps } from 'app/design/controls/neo-button/neo-button.types';
 import { cueAttributes, usePressCueOverride, type PressCue } from 'app/lib/sound';
 
@@ -879,6 +880,11 @@ const NeoButtonStyled = (props: NeoButtonProps & { titleHidden?: boolean }) => {
     // cannot paint over its ring (global.web.css). The layout classes go on
     // the outermost box: the Tooltip wrapper when there is one.
     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();
+    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.
     const tooltipWraps = isWeb && !!tooltip && isDesktop && !!appSetting('layout', 'tooltips');
@@ -888,7 +894,7 @@ const NeoButtonStyled = (props: NeoButtonProps & { titleHidden?: boolean }) => {
             : (pinIconOnlySquare ? 'self-start' : 'self-start max-w-full min-w-0'),
         classNames?.root,
     );
-    const wrapperClass = cn('u-neo-btn-root', tooltipWraps ? '' : rootLayoutClass);
+    const wrapperClass = cn('u-neo-btn-root', schemeClass, tooltipWraps ? '' : rootLayoutClass);
 
     // Glass effect: press/hover motion is CSS keyed off `data-neo-glass`,
     // so the JS PressTransition scale is skipped.
@@ -919,7 +925,7 @@ const NeoButtonStyled = (props: NeoButtonProps & { titleHidden?: boolean }) => {
 
     if (tooltipWraps) {
         return (
-            <Tooltip content={tooltip} side={tooltipSide} enabled className={cn('u-neo-btn-root', rootLayoutClass)}>
+            <Tooltip content={tooltip} side={tooltipSide} enabled className={cn('u-neo-btn-root', schemeClass, rootLayoutClass)}>
                 {wrapped}
             </Tooltip>
         );

+ 27 - 2
neo/packages/app/design/styles/global.css

@@ -10,9 +10,16 @@
 
 /*
  * Light variant — counterpart to dark, needed by shared `@variant light` tokens.
+ *
+ * `.light` / `.dark` on an element is a colour-scheme scope for its subtree (see the
+ * scope tokens after the palette import). A scope wins over the page theme on <html>,
+ * so the page-theme branch skips scopes of the other scheme.
  */
 @custom-variant light {
-    &:where(.light, .light *, [theme='light'], [theme='light'] *, [data-theme='light'], [data-theme='light'] *) {
+    &:where(.light, .light *) { /* NOSONAR css:S8776: Tailwind @custom-variant rule, & is the variant's target */
+        @slot;
+    }
+    &:where([theme='light'], [theme='light'] *, [data-theme='light'], [data-theme='light'] *):where(:not(.dark, .dark *)) { /* NOSONAR css:S8776: Tailwind @custom-variant rule, & is the variant's target */
         @slot;
     }
     @media (prefers-color-scheme: light) {
@@ -35,7 +42,10 @@
  * (matches Uniwind's built-in `dark` variant on native — by design).
  */
 @custom-variant dark {
-    &:where(.dark, .dark *, [theme='dark'], [theme='dark'] *, [data-theme='dark'], [data-theme='dark'] *) {
+    &:where(.dark, .dark *) { /* NOSONAR css:S8776: Tailwind @custom-variant rule, & is the variant's target */
+        @slot;
+    }
+    &:where([theme='dark'], [theme='dark'] *, [data-theme='dark'], [data-theme='dark'] *):where(:not(.light, .light *)) { /* NOSONAR css:S8776: Tailwind @custom-variant rule, & is the variant's target */
         @slot;
     }
     @media (prefers-color-scheme: dark) {
@@ -56,6 +66,21 @@
 /* Shared palette + semantic tokens (also imported by apps/expo/global.combined.css). */
 @import './palette.css';
 
+/*
+ * Colour-scheme scopes (web). The palette declares its semantic tokens on <html> only, so
+ * an element with `.light` / `.dark` declares them again for its subtree: NeoButtons under
+ * `NeoButtonColorSchemeContext` (the header on a coloured band, `layout.header.band_scheme`)
+ * and Uniwind's `ScopedTheme`. Native scopes through Uniwind and Expo UI instead.
+ */
+@layer base {
+    .light {
+        @apply scheme-tokens-light;
+    }
+    .dark {
+        @apply scheme-tokens-dark;
+    }
+}
+
 @plugin 'tailwindcss-animate';
 
 /*

+ 105 - 0
neo/packages/app/design/styles/palette-schemes.css

@@ -0,0 +1,105 @@
+/*
+ * Semantic colour tokens per scheme, as Tailwind utilities so one definition serves two
+ * places: palette.css applies them inside `:root` (web and native), and on web global.css
+ * applies them to colour-scheme scopes (`.light` / `.dark` on any element), so a subtree
+ * can take the other scheme's tokens. Keep only these two utilities here; the raw palette
+ * stays in palette.css.
+ */
+/* Semantic — light (explicit light/dark variants for Uniwind parity) */
+@utility scheme-tokens-light {
+    --background: var(--neutral-100);
+    --foreground: var(--neutral-950);
+    --card: var(--white);
+    --card-foreground: var(--neutral-800);
+    --popover: var(--white);
+    --popover-foreground: var(--neutral-900);
+    --primary: var(--accent-600);
+    --primary-hover: var(--accent-700);
+    --primary-foreground: var(--neutral-50);
+    --secondary: var(--neutral-200);
+    --secondary-foreground: var(--neutral-600);
+    --muted: var(--neutral-300);
+    --muted-foreground: var(--neutral-600);
+    --accent: var(--accent-100);
+    --accent-foreground: var(--accent-600);
+    --destructive: var(--red);
+    --destructive-foreground: var(--white);
+    --border: var(--neutral-300);
+    --input: var(--white);
+    --button: var(--neutral-200);
+    --button-foreground: var(--neutral-800);
+    --button-hover: var(--neutral-300);
+    --ring: var(--accent-600);
+    --segment: var(--neutral-200);
+    --segment-foreground: var(--neutral-700);
+    --link-primary: var(--primary);
+    --link-secondary: var(--secondary-foreground);
+    --link-tertiary: var(--muted-foreground);
+    --shadow: var(--neutral-950);
+    --shadow-xs: var(--neutral-950);
+    --shadow-sm: var(--neutral-950);
+    --shadow-md: var(--neutral-950);
+    --shadow-lg: var(--neutral-950);
+    --shadow-xl: var(--neutral-950);
+    --shadow-2xl: var(--neutral-950);
+    /* Chart series: the accent, then cyan, amber, pink, green, violet (3:1 on cards) */
+    --chart-1: var(--accent-600);
+    --chart-2: 8 145 178;
+    --chart-3: 217 119 6;
+    --chart-4: 219 39 119;
+    --chart-5: 22 163 74;
+    --chart-6: 124 58 237;
+    /* Chart series that are good news (3:1 on cards); bad news uses --destructive */
+    --chart-good: 5 150 105;
+    /* Text of a change for the better or the worse (4.5:1 on cards) */
+    --trend-good: 4 120 87;
+    --trend-bad: 190 18 60;
+}
+
+/* Semantic — dark overrides */
+@utility scheme-tokens-dark {
+    --background: var(--neutral-950);
+    --foreground: var(--neutral-50);
+    --card: var(--neutral-900);
+    --card-foreground: var(--neutral-200);
+    --popover: var(--neutral-800);
+    --popover-foreground: var(--neutral-100);
+    --primary: var(--accent-600);
+    --primary-hover: var(--accent-700);
+    --primary-foreground: var(--neutral-50);
+    --secondary: var(--neutral-800);
+    --secondary-foreground: var(--neutral-400);
+    --accent: var(--accent-950);
+    --accent-foreground: var(--accent-500);
+    --muted: var(--neutral-700);
+    --muted-foreground: var(--neutral-400);
+    --destructive: var(--red);
+    --destructive-foreground: var(--neutral-950);
+    --border: var(--neutral-800);
+    --input: var(--neutral-900);
+    --button: var(--neutral-800);
+    --button-foreground: var(--neutral-200);
+    --button-hover: var(--neutral-700);
+    --ring: var(--accent-600);
+    --segment: var(--neutral-800);
+    --segment-foreground: var(--neutral-200);
+    --link-primary: var(--primary);
+    --link-secondary: var(--secondary-foreground);
+    --link-tertiary: var(--muted-foreground);
+    --shadow: var(--black);
+    --shadow-xs: var(--black);
+    --shadow-sm: var(--black);
+    --shadow-md: var(--black);
+    --shadow-lg: var(--black);
+    --shadow-xl: var(--black);
+    --shadow-2xl: var(--black);
+    --chart-1: var(--accent-400);
+    --chart-2: 103 232 249;
+    --chart-3: 252 211 77;
+    --chart-4: 249 168 212;
+    --chart-5: 134 239 172;
+    --chart-6: 196 181 253;
+    --chart-good: 52 211 153;
+    --trend-good: 110 231 183;
+    --trend-bad: 253 164 175;
+}

+ 5 - 94
neo/packages/app/design/styles/palette.css

@@ -12,6 +12,8 @@
  * AND a `@media (prefers-color-scheme: dark)` block (with `:not([data-theme='light'])`
  * guards so an explicit light attribute always wins over system dark).
  */
+@import './palette-schemes.css';
+
 @layer base {
     :root {
         /* Raw palette */
@@ -57,103 +59,12 @@
         --alert-warning: var(--yellow);
         --alert-caution: var(--red);
 
-        /* Semantic — explicit light/dark variants for Uniwind parity */
+        /* Semantic tokens, light and dark (palette-schemes.css; web scopes reuse them). */
         @variant light {
-            --background: var(--neutral-100);
-            --foreground: var(--neutral-950);
-            --card: var(--white);
-            --card-foreground: var(--neutral-800);
-            --popover: var(--white);
-            --popover-foreground: var(--neutral-900);
-            --primary: var(--accent-600);
-            --primary-hover: var(--accent-700);
-            --primary-foreground: var(--neutral-50);
-            --secondary: var(--neutral-200);
-            --secondary-foreground: var(--neutral-600);
-            --muted: var(--neutral-300);
-            --muted-foreground: var(--neutral-600);
-            --accent: var(--accent-100);
-            --accent-foreground: var(--accent-600);
-            --destructive: var(--red);
-            --destructive-foreground: var(--white);
-            --border: var(--neutral-300);
-            --input: var(--white);
-            --button: var(--neutral-200);
-            --button-foreground: var(--neutral-800);
-            --button-hover: var(--neutral-300);
-            --ring: var(--accent-600);
-            --segment: var(--neutral-200);
-            --segment-foreground: var(--neutral-700);
-            --link-primary: var(--primary);
-            --link-secondary: var(--secondary-foreground);
-            --link-tertiary: var(--muted-foreground);
-            --shadow: var(--neutral-950);
-            --shadow-xs: var(--neutral-950);
-            --shadow-sm: var(--neutral-950);
-            --shadow-md: var(--neutral-950);
-            --shadow-lg: var(--neutral-950);
-            --shadow-xl: var(--neutral-950);
-            --shadow-2xl: var(--neutral-950);
-            /* Chart series: the accent, then cyan, amber, pink, green, violet (3:1 on cards) */
-            --chart-1: var(--accent-600);
-            --chart-2: 8 145 178;
-            --chart-3: 217 119 6;
-            --chart-4: 219 39 119;
-            --chart-5: 22 163 74;
-            --chart-6: 124 58 237;
-            /* Chart series that are good news (3:1 on cards); bad news uses --destructive */
-            --chart-good: 5 150 105;
-            /* Text of a change for the better or the worse (4.5:1 on cards) */
-            --trend-good: 4 120 87;
-            --trend-bad: 190 18 60;
+            @apply scheme-tokens-light;
         }
-
-        /* Semantic — dark overrides */
         @variant dark {
-            --background: var(--neutral-950);
-            --foreground: var(--neutral-50);
-            --card: var(--neutral-900);
-            --card-foreground: var(--neutral-200);
-            --popover: var(--neutral-800);
-            --popover-foreground: var(--neutral-100);
-            --primary: var(--accent-600);
-            --primary-hover: var(--accent-700);
-            --primary-foreground: var(--neutral-50);
-            --secondary: var(--neutral-800);
-            --secondary-foreground: var(--neutral-400);
-            --accent: var(--accent-950);
-            --accent-foreground: var(--accent-500);
-            --muted: var(--neutral-700);
-            --muted-foreground: var(--neutral-400);
-            --destructive: var(--red);
-            --destructive-foreground: var(--neutral-950);
-            --border: var(--neutral-800);
-            --input: var(--neutral-900);
-            --button: var(--neutral-800);
-            --button-foreground: var(--neutral-200);
-            --button-hover: var(--neutral-700);
-            --ring: var(--accent-600);
-            --segment: var(--neutral-800);
-            --segment-foreground: var(--neutral-200);
-            --link-primary: var(--primary);
-            --link-secondary: var(--secondary-foreground);
-            --link-tertiary: var(--muted-foreground);
-            --shadow: var(--black);
-            --shadow-xs: var(--black);
-            --shadow-sm: var(--black);
-            --shadow-md: var(--black);
-            --shadow-lg: var(--black);
-            --shadow-xl: var(--black);
-            --shadow-2xl: var(--black);
-            --chart-1: var(--accent-400);
-            --chart-2: 103 232 249;
-            --chart-3: 252 211 77;
-            --chart-4: 249 168 212;
-            --chart-5: 134 239 172;
-            --chart-6: 196 181 253;
-            --chart-good: 52 211 153;
-            --trend-good: 110 231 183;
-            --trend-bad: 253 164 175;
+            @apply scheme-tokens-dark;
         }
     }
 }

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

@@ -123,11 +123,12 @@ export const settingsLayout = {
              */
             band: 'bg-card sm:bg-card/80 lg:bg-transparent',
             /**
-             * Colour scheme of the header's native buttons while they rest on the band (iOS:
-             * SwiftUI glass chrome and labels). '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 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`).
              */
             band_scheme: null,
         },

+ 98 - 11
neo/packages/app/ui/molecules/header/page-header.web.js

@@ -1,5 +1,11 @@
-import { useEffect, useLayoutEffect, useRef, useState } from 'react';
+import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
 import { View } from 'app/design/view';
+import { appSetting } from 'app/lib/util';
+import {
+    NeoButtonColorSchemeContext,
+    colorSchemeForBackground,
+    isVisibleColor,
+} from 'app/design/controls/neo-button/color-scheme-context';
 import {
     useScrollValue,
     useSetScrollValue,
@@ -36,6 +42,80 @@ function clearEnterFrame(frameRef) {
     }
 }
 
+let colorCanvas = null;
+
+/**
+ * Any CSS colour as `rgba()`, or null. Computed styles can report `oklab()` or `color()`
+ * (Tailwind's `/80` opacity modifier is a `color-mix()`), which processColor doesn't parse.
+ */
+function cssColorToRgba(color) {
+    if (!color || !CSS.supports('color', color)) return null;
+    colorCanvas ??= document.createElement('canvas').getContext('2d', { willReadFrequently: true });
+    if (!colorCanvas) return null;
+    colorCanvas.clearRect(0, 0, 1, 1);
+    colorCanvas.fillStyle = color;
+    colorCanvas.fillRect(0, 0, 1, 1);
+    const [r, g, b, a] = colorCanvas.getImageData(0, 0, 1, 1).data;
+    return `rgba(${r}, ${g}, ${b}, ${a / 255})`;
+}
+
+/** `layout.header.band_scheme` for the band's current fill (see settings/layout.js). */
+function readBandScheme(band, setting) {
+    const fill = cssColorToRgba(getComputedStyle(band).backgroundColor);
+    if (!isVisibleColor(fill)) return null;
+    if (setting === 'light' || setting === 'dark') return setting;
+    if (setting !== 'auto') return null;
+    const page = getComputedStyle(document.documentElement).getPropertyValue('--background').trim();
+    return colorSchemeForBackground(fill, page ? cssColorToRgba(`rgb(${page})`) : undefined);
+}
+
+/**
+ * The band's scheme as an external store: read from its computed fill, so `sm:` / `lg:` /
+ * `dark:` classes count, and read again only after a resize or a theme change on <html>
+ * (the header re-renders on every scroll frame).
+ */
+function createBandSchemeStore(band, setting) {
+    let value = null;
+    let stale = true;
+    return {
+        getSnapshot() {
+            if (stale) {
+                value = band && setting ? readBandScheme(band, setting) : null;
+                stale = false;
+            }
+            return value;
+        },
+        subscribe(onChange) {
+            if (!band || !setting) return () => {};
+            const update = () => {
+                stale = true;
+                onChange();
+            };
+            window.addEventListener('resize', update);
+            const themeObserver = new MutationObserver(update);
+            themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['theme', 'data-theme', 'class'] });
+            return () => {
+                window.removeEventListener('resize', update);
+                themeObserver.disconnect();
+            };
+        },
+    };
+}
+
+const getServerBandScheme = () => null;
+
+/**
+ * Colour scheme for the header's buttons while they rest on the band (collapsible layout
+ * only; the band isn't rendered otherwise). The flow header scrolls away together with the
+ * band, so it always has it; the fixed layer has it at the top of the page, where it covers
+ * the band, and keeps the app's scheme once it floats over content.
+ */
+function useBandScheme(band, enabled) {
+    const setting = enabled ? appSetting('layout', 'header', 'band_scheme') : null;
+    const store = useMemo(() => createBandSchemeStore(band, setting), [band, setting]);
+    return useSyncExternalStore(store.subscribe, store.getSnapshot, getServerBandScheme);
+}
+
 function HeaderFade({ headerHeight, isScrolled }) {
     const fadeClass = getHeaderFadeClass();
     const fadeExtend = getHeaderFadeExtend();
@@ -69,6 +149,8 @@ export const PageHeader = ({ pageData }) => {
     const scrollY = useScrollValue();
     const setScrollValue = useSetScrollValue();
     const usesFixedOverlayHeader = isCollapsibleHeader;
+    const [band, setBand] = useState(null);
+    const bandScheme = useBandScheme(band, usesFixedOverlayHeader);
     const pastRevealThreshold =
         usesFixedOverlayHeader && headerHeight > 0 && scrollY >= headerHeight;
 
@@ -180,6 +262,7 @@ export const PageHeader = ({ pageData }) => {
                 layer that shows at the top keeps its gradient chrome. */}
             {usesFixedOverlayHeader && headerHeight > 0 ? (
                 <div
+                    ref={setBand}
                     aria-hidden
                     className={`pointer-events-none absolute left-0 right-0 top-0 ${getPageHeaderBandClass()}`}
                     style={{ height: headerHeight }}
@@ -194,11 +277,13 @@ export const PageHeader = ({ pageData }) => {
             >
                 <HeaderFade headerHeight={headerHeight} isScrolled={isScrolled} />
                 <View className="relative z-10">
-                    <PageHeaderBody
-                        {...headerState}
-                        contentClassName={flowHeaderContentClassName}
-                        pageData={pageData}
-                    />
+                    <NeoButtonColorSchemeContext.Provider value={bandScheme}>
+                        <PageHeaderBody
+                            {...headerState}
+                            contentClassName={flowHeaderContentClassName}
+                            pageData={pageData}
+                        />
+                    </NeoButtonColorSchemeContext.Provider>
                 </View>
             </View>
             {usesFixedOverlayHeader && shouldRenderFixedLayer && (
@@ -209,11 +294,13 @@ export const PageHeader = ({ pageData }) => {
                 >
                     <HeaderFade headerHeight={headerHeight} isScrolled={isScrolled} />
                     <View className="relative z-10">
-                        <PageHeaderBody
-                            {...headerState}
-                            contentClassName={fixedOverlayContentClassName}
-                            pageData={pageData}
-                        />
+                        <NeoButtonColorSchemeContext.Provider value={isScrolled ? null : bandScheme}>
+                            <PageHeaderBody
+                                {...headerState}
+                                contentClassName={fixedOverlayContentClassName}
+                                pageData={pageData}
+                            />
+                        </NeoButtonColorSchemeContext.Provider>
                     </View>
                 </View>
             )}