فهرست منبع

Neo: neutral fills are translucent ink that stacks (#5923)

Comment bubbles were `bg-muted/50` and bordered buttons an opaque
`--button`, so a button on a bubble blended in (0-2 lightness steps), and
in dark mode the bubble sat lighter than the button.

- `bg-bubble`, `bg-button`, `bg-button-hover` are now the scheme's `--ink`
  (`--neutral-950` light, `--neutral-50` dark) at 4.5%, 7% and 10%. Every
  layer is one even step above what it sits on (page, card, bubble), and
  text keeps AA on every stack (worst: secondary text on a hovered button
  on a bubble on a dark card, 4.56:1).
- The alphas are `color-mix()` in theme.css. Uniwind compiles
  `rgb(var(--x) / a)` and a variable holding `r g b / a` into NaN on
  native, so only color-mix keeps the alpha there.
- Native buttons: iOS bordered (SwiftUI tint) and Android tonal take the
  translucent fill and match the JS buttons pixel for pixel. Android's
  elevated button (`glass`) shows its shadow through a translucent
  container, so it uses the new opaque `bg-button-solid` (the legacy
  `--button`, kept as before).
- The comment bubble uses `bg-bubble`.
- Forks: the ink follows their `--neutral-*` scale. AGENTS.md documents
  how to re-tint it (`--ink`) or change the steps (`@theme inline`).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko 2 روز پیش
والد
کامیت
367b064835

+ 4 - 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. |
 | `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. |
 | `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>/`. |
 | `<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). 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/styles/global.css`, `global.web.css` | CSS variable and `@theme` overrides (both platforms; the second is web only). Neutral fills follow your `--neutral-*` scale through `--ink`; set `--ink` in your `@variant light` / `@variant dark` blocks to tint them differently, or redefine `--color-bubble` / `--color-button` / `--color-button-hover` in an `@theme inline` block (as `color-mix()`, see Styling) to change the steps. `--button` / `--button-hover` keep their old opaque values; only `bg-button-solid` still reads `--button`. 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. |
 | `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/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. |
 | `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. |
@@ -148,6 +148,9 @@ Render icons with `<Icon icon="House" />` from `app/ui/atoms/icon`, using Lucide
 
 
 - Design tokens (colours, radii, shadows) are the Tailwind `@theme inline` map in `packages/app/design/styles/theme.css`, with light and dark values in `palette.css`. `apps/next/tailwind.config.js` and `design/tailwind/theme.js` are not loaded by any build; a token added there does nothing.
 - Design tokens (colours, radii, shadows) are the Tailwind `@theme inline` map in `packages/app/design/styles/theme.css`, with light and dark values in `palette.css`. `apps/next/tailwind.config.js` and `design/tailwind/theme.js` are not loaded by any build; a token added there does nothing.
 - Settings hold class presets per component (`settings/theme/*.js`), read with `appSetting`. Use tokens and existing presets, not raw colours or arbitrary values.
 - Settings hold class presets per component (`settings/theme/*.js`), read with `appSetting`. Use tokens and existing presets, not raw colours or arbitrary values.
+- Neutral fills are translucent "ink", so they stack instead of blending: `bg-bubble` (comment bubbles), `bg-button` and `bg-button-hover` are the scheme's `--ink` at 4.5%, 7% and 10%. A button on a bubble on a card is one even step darker than each layer below, and text keeps AA on every stack. `--ink` is `--neutral-950` in light and `--neutral-50` in dark (`palette-schemes.css`); the alphas are `color-mix()` in `theme.css`, one value for both schemes.
+- Write an alpha only as `color-mix()` or an opacity modifier (`bg-button/50`). Uniwind compiles `rgb(var(--x) / 0.07)` and a variable holding `r g b / a` into `NaN` on native, so the colour silently disappears there.
+- Native buttons read the same tokens through `theme.expo_ui.button` (`settings/theme/expo_ui.js`): iOS bordered (`tint`) and Android tonal take the translucent `bg-button`. Android's elevated button (the `glass` style) draws its shadow under the container, so it uses the opaque `bg-button-solid` (the legacy `--button`) instead.
 - Edit the style partials in `design/styles/` (`theme.css`, `palette.css`, `utilities.css`, `global.css`), not `apps/expo/global.combined.css`, which only imports them for Uniwind.
 - Edit the style partials in `design/styles/` (`theme.css`, `palette.css`, `utilities.css`, `global.css`), not `apps/expo/global.combined.css`, which only imports them for Uniwind.
 - Variants: `web:`, `native:`, `ios:` and `android:` are defined in `design/styles/global.css`, as is `dark:`. Native switches themes with `Uniwind.setTheme`. For a JS value per theme, use `useThemeValue(light, dark)` from `app/design/theme`.
 - Variants: `web:`, `native:`, `ios:` and `android:` are defined in `design/styles/global.css`, as is `dark:`. Native switches themes with `Uniwind.setTheme`. For a JS value per theme, use `useThemeValue(light, dark)` from `app/design/theme`.
 - Breakpoint tiers are mobile (no prefix), tablet (`sm:`) and desktop (`lg:`), as in `lib/responsive-classes.ts`; there is also `3xl:`. Lists size by container queries (`@container/list`, `@list-sm/list:`).
 - Breakpoint tiers are mobile (no prefix), tablet (`sm:`) and desktop (`lg:`), as in `lib/responsive-classes.ts`; there is also `3xl:`. Lists size by container queries (`@container/list`, `@list-sm/list:`).

+ 1 - 1
neo/packages/app/components/units/comments.js

@@ -238,7 +238,7 @@ function UnitCommentsDefault(props) {
                     )}
                     )}
                 </View>
                 </View>
                 <View className=" flex-col mt-2 flex-1">
                 <View className=" flex-col mt-2 flex-1">
-                    <View className="bg-muted/50 rounded-xl py-2 px-3 gap-0.5 me-auto max-w-full">
+                    <View className="bg-bubble rounded-xl py-2 px-3 gap-0.5 me-auto max-w-full">
                         <View className="flex-row items-center gap-3 justify-between ">
                         <View className="flex-row items-center gap-3 justify-between ">
                             <Row className="gap-3 items-center pr-8">
                             <Row className="gap-3 items-center pr-8">
                                 <Profile
                                 <Profile

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

@@ -26,6 +26,14 @@
     --destructive-foreground: var(--white);
     --destructive-foreground: var(--white);
     --border: var(--neutral-300);
     --border: var(--neutral-300);
     --input: var(--white);
     --input: var(--white);
+    /*
+     * The ink of the neutral fills (`bg-bubble`, `bg-button`, `bg-button-hover`
+     * in theme.css): this colour at an alpha, so fills stack instead of
+     * blending (a button on a comment bubble on a card is a step darker than
+     * each layer below). A fork's palette tints it through --neutral-950.
+     */
+    --ink: var(--neutral-950);
+    /* Opaque fill for shadowed native buttons (`bg-button-solid`); bg-button is the ink. */
     --button: var(--neutral-200);
     --button: var(--neutral-200);
     --button-foreground: var(--neutral-800);
     --button-foreground: var(--neutral-800);
     --button-hover: var(--neutral-300);
     --button-hover: var(--neutral-300);
@@ -77,6 +85,9 @@
     --destructive-foreground: var(--neutral-950);
     --destructive-foreground: var(--neutral-950);
     --border: var(--neutral-800);
     --border: var(--neutral-800);
     --input: var(--neutral-900);
     --input: var(--neutral-900);
+    /* Near-white ink in dark mode (see the light scheme). */
+    --ink: var(--neutral-50);
+    /* Opaque fill for shadowed native buttons (`bg-button-solid`); bg-button is the ink. */
     --button: var(--neutral-800);
     --button: var(--neutral-800);
     --button-foreground: var(--neutral-200);
     --button-foreground: var(--neutral-200);
     --button-hover: var(--neutral-700);
     --button-hover: var(--neutral-700);

+ 16 - 2
neo/packages/app/design/styles/theme.css

@@ -20,9 +20,23 @@
     --container-list-xl: 80rem;
     --container-list-xl: 80rem;
     --color-border: rgb(var(--border));
     --color-border: rgb(var(--border));
     --color-input: rgb(var(--input));
     --color-input: rgb(var(--input));
-    --color-button: rgb(var(--button));
+    /*
+     * Neutral fills: the scheme's ink (palette-schemes.css) at an alpha, so they
+     * stack. color-mix, not `rgb(var(--ink) / a)`: Uniwind (native) only keeps
+     * the alpha of a color-mix. One alpha for both schemes; every step stays
+     * even on the page background, a card and a bubble, and text keeps AA.
+     */
+    --color-button: color-mix(in oklab, rgb(var(--ink)) 7%, transparent);
     --color-button-foreground: rgb(var(--button-foreground));
     --color-button-foreground: rgb(var(--button-foreground));
-    --color-button-hover: rgb(var(--button-hover));
+    --color-button-hover: color-mix(in oklab, rgb(var(--ink)) 10%, transparent);
+    /*
+     * Opaque neutral fill (the legacy --button) for native surfaces that cast a
+     * shadow: Android's elevated button draws its shadow under the container,
+     * and a translucent fill lets it show through.
+     */
+    --color-button-solid: rgb(var(--button));
+    /* Comment bubbles and other content grouped on a card. */
+    --color-bubble: color-mix(in oklab, rgb(var(--ink)) 4.5%, transparent);
     --color-ring: rgb(var(--ring));
     --color-ring: rgb(var(--ring));
     --color-background: rgb(var(--background));
     --color-background: rgb(var(--background));
     --color-foreground: rgb(var(--foreground));
     --color-foreground: rgb(var(--foreground));

+ 3 - 1
neo/packages/app/settings/theme/expo_ui.js

@@ -52,8 +52,10 @@ export const settingsExpoUi = {
                     tintedContainerColor: 'bg-accent',
                     tintedContainerColor: 'bg-accent',
                     tintedContentColor: 'text-accent-foreground',
                     tintedContentColor: 'text-accent-foreground',
                 },
                 },
+                // Opaque: the elevated button's shadow sits under the container
+                // and would show through the translucent `bg-button` ink.
                 elevated: {
                 elevated: {
-                    containerColor: 'bg-button',
+                    containerColor: 'bg-button-solid',
                     contentColor: 'text-button-foreground',
                     contentColor: 'text-button-foreground',
                     tintedContainerColor: 'bg-accent',
                     tintedContainerColor: 'bg-accent',
                     tintedContentColor: 'text-accent-foreground',
                     tintedContentColor: 'text-accent-foreground',