From 0edcb325c76482a96d4ed1276e624404202b0fa9 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Mon, 22 Dec 2025 22:53:50 +0100 Subject: [PATCH] First pass at adding the new surfaces Signed-off-by: Charles de Dreuille --- packages/ui/src/components/Box/Box.module.css | 16 ++++ .../ui/src/components/Box/Box.stories.tsx | 48 ++++++++++++ packages/ui/src/components/Box/Box.tsx | 16 ++-- packages/ui/src/components/Box/definition.ts | 3 + packages/ui/src/components/Box/types.ts | 3 +- packages/ui/src/css/tokens.css | 74 +++++++++++++++---- packages/ui/src/hooks/useSurface.tsx | 60 +++++++++++++++ packages/ui/src/types.ts | 6 ++ 8 files changed, 203 insertions(+), 23 deletions(-) create mode 100644 packages/ui/src/hooks/useSurface.tsx diff --git a/packages/ui/src/components/Box/Box.module.css b/packages/ui/src/components/Box/Box.module.css index 688358fda0..77200b297f 100644 --- a/packages/ui/src/components/Box/Box.module.css +++ b/packages/ui/src/components/Box/Box.module.css @@ -22,4 +22,20 @@ font-weight: var(--bui-font-weight-regular); color: var(--bui-fg-primary); } + + .bui-Box[data-surface='0'] { + background-color: var(--bui-bg-surface-0); + } + + .bui-Box[data-surface='1'] { + background-color: var(--bui-bg-surface-1); + } + + .bui-Box[data-surface='2'] { + background-color: var(--bui-bg-surface-2); + } + + .bui-Box[data-surface='3'] { + background-color: var(--bui-bg-surface-3); + } } diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx index 766a6668c7..7033b75e79 100644 --- a/packages/ui/src/components/Box/Box.stories.tsx +++ b/packages/ui/src/components/Box/Box.stories.tsx @@ -18,6 +18,7 @@ import preview from '../../../../../.storybook/preview'; import { ReactNode } from 'react'; import { Box } from './Box'; import { Flex } from '../Flex'; +import { Button } from '../Button'; const meta = preview.meta({ title: 'Backstage UI/Box', @@ -344,3 +345,50 @@ export const Display = meta.story({ ), }); + +export const Surfaces = meta.story({ + args: { px: '6', py: '4' }, + render: args => ( + + Default + + Surface 0 + + + Surface 1 + + + Surface 2 + + + Surface 3 + + + Responsive Surface + + + ), +}); + +export const SurfacesNested = meta.story({ + args: { px: '6', py: '4' }, + render: args => ( + + + In this test, we are nesting boxes and buttons on different surfaces to + ensure that the correct surface is applied to each element. If a Button + is placed on a surface that doesn't have the surface prop set, it will + inherit the surface from the parent. + + + + + + + + + + + + ), +}); diff --git a/packages/ui/src/components/Box/Box.tsx b/packages/ui/src/components/Box/Box.tsx index 34c337824b..4864a27a0e 100644 --- a/packages/ui/src/components/Box/Box.tsx +++ b/packages/ui/src/components/Box/Box.tsx @@ -20,15 +20,14 @@ import clsx from 'clsx'; import { useStyles } from '../../hooks/useStyles'; import styles from './Box.module.css'; import { BoxDefinition } from './definition'; +import { SurfaceProvider } from '../../hooks/useSurface'; /** @public */ export const Box = forwardRef((props, ref) => { - const { classNames, utilityClasses, style, cleanedProps } = useStyles( - BoxDefinition, - props, - ); + const { classNames, dataAttributes, utilityClasses, style, cleanedProps } = + useStyles(BoxDefinition, props); - const { as = 'div', children, className, ...rest } = cleanedProps; + const { as = 'div', children, className, surface, ...rest } = cleanedProps; return createElement( as, @@ -41,9 +40,14 @@ export const Box = forwardRef((props, ref) => { className, ), style, + ...dataAttributes, ...rest, }, - children, + surface ? ( + {children} + ) : ( + children + ), ); }); diff --git a/packages/ui/src/components/Box/definition.ts b/packages/ui/src/components/Box/definition.ts index f5b6fc6553..1bd1db97e6 100644 --- a/packages/ui/src/components/Box/definition.ts +++ b/packages/ui/src/components/Box/definition.ts @@ -48,4 +48,7 @@ export const BoxDefinition = { 'minHeight', 'maxHeight', ], + dataAttributes: { + surface: ['0', '1', '2', '3'] as const, + }, } as const satisfies ComponentDefinition; diff --git a/packages/ui/src/components/Box/types.ts b/packages/ui/src/components/Box/types.ts index c0f4a71035..dd72c4b820 100644 --- a/packages/ui/src/components/Box/types.ts +++ b/packages/ui/src/components/Box/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { SpaceProps, Responsive } from '../../types'; +import type { SpaceProps, Responsive, Surface } from '../../types'; /** @public */ export interface BoxProps extends SpaceProps { @@ -32,4 +32,5 @@ export interface BoxProps extends SpaceProps { children?: React.ReactNode; className?: string; style?: React.CSSProperties; + surface?: Responsive; } diff --git a/packages/ui/src/css/tokens.css b/packages/ui/src/css/tokens.css index a60107f9d2..83fb3cd0b0 100644 --- a/packages/ui/src/css/tokens.css +++ b/packages/ui/src/css/tokens.css @@ -80,24 +80,45 @@ --bui-gray-7: #757575; --bui-gray-8: #595959; - /* Background Colors */ + /* Surfaces colors */ --bui-bg-surface-0: var(--bui-gray-1); --bui-bg-surface-1: var(--bui-white); --bui-bg-surface-2: var(--bui-gray-1); --bui-bg-surface-3: var(--bui-gray-2); + + /* Solid background colors */ --bui-bg-solid: #1f5493; --bui-bg-solid-hover: #163a66; --bui-bg-solid-pressed: #0f2b4e; --bui-bg-solid-disabled: #ebebeb; - --bui-bg-tint: transparent; - --bui-bg-tint-hover: rgba(31, 84, 147, 0.4); - --bui-bg-tint-pressed: rgba(31, 84, 147, 0.6); - --bui-bg-tint-disabled: #ebebeb; + + /* Neutral background colors */ + --bui-bg-neutral-on-surface-0: oklch(0% 0 0 / 6%); + --bui-bg-neutral-on-surface-0-hover: oklch(0% 0 0 / 12%); + --bui-bg-neutral-on-surface-0-pressed: oklch(0% 0 0 / 16%); + --bui-bg-neutral-on-surface-0-disabled: oklch(0% 0 0 / 6%); + + --bui-bg-neutral-on-surface-1: oklch(0% 0 0 / 6%); + --bui-bg-neutral-on-surface-1-hover: oklch(0% 0 0 / 12%); + --bui-bg-neutral-on-surface-1-pressed: oklch(0% 0 0 / 16%); + --bui-bg-neutral-on-surface-1-disabled: oklch(0% 0 0 / 6%); + + --bui-bg-neutral-on-surface-2: oklch(0% 0 0 / 6%); + --bui-bg-neutral-on-surface-2-hover: oklch(0% 0 0 / 12%); + --bui-bg-neutral-on-surface-2-pressed: oklch(0% 0 0 / 16%); + --bui-bg-neutral-on-surface-2-disabled: oklch(0% 0 0 / 6%); + + --bui-bg-neutral-on-surface-3: oklch(0% 0 0 / 6%); + --bui-bg-neutral-on-surface-3-hover: oklch(0% 0 0 / 12%); + --bui-bg-neutral-on-surface-3-pressed: oklch(0% 0 0 / 16%); + --bui-bg-neutral-on-surface-3-disabled: oklch(0% 0 0 / 6%); + + /* Status background colors */ --bui-bg-danger: #feebe7; --bui-bg-warning: #fff2b2; --bui-bg-success: #e6f6eb; - /* Foreground Colors */ + /* Foreground colors */ --bui-fg-primary: var(--bui-black); --bui-fg-secondary: var(--bui-gray-7); --bui-fg-link: #1f5493; @@ -111,7 +132,7 @@ --bui-fg-warning: #e36d05; --bui-fg-success: #1db954; - /* Border Colors */ + /* Border colors */ --bui-border: rgba(0, 0, 0, 0.1); --bui-border-hover: rgba(0, 0, 0, 0.2); --bui-border-pressed: rgba(0, 0, 0, 0.4); @@ -120,7 +141,7 @@ --bui-border-warning: #e36d05; --bui-border-success: #53db83; - /* Special Colors */ + /* Special colors */ --bui-ring: #1f5493; --bui-scrollbar: #a0a0a03b; --bui-scrollbar-thumb: #a0a0a0; @@ -140,24 +161,45 @@ --bui-gray-7: #9e9e9e; --bui-gray-8: #b4b4b4; - /* Background Colors */ + /* Surfaces colors */ --bui-bg-surface-0: var(--bui-black); --bui-bg-surface-1: var(--bui-gray-1); --bui-bg-surface-2: var(--bui-gray-2); --bui-bg-surface-3: var(--bui-gray-3); + + /* Solid background colors */ --bui-bg-solid: #9cc9ff; --bui-bg-solid-hover: #83b9fd; --bui-bg-solid-pressed: #83b9fd; --bui-bg-solid-disabled: #222222; - --bui-bg-tint: transparent; - --bui-bg-tint-hover: rgba(156, 201, 255, 0.12); - --bui-bg-tint-pressed: rgba(156, 201, 255, 0.16); - --bui-bg-tint-disabled: transparent; + + /* Neutral background colors */ + --bui-bg-neutral-on-surface-0: oklch(100% 0 0 / 10%); + --bui-bg-neutral-on-surface-0-hover: oklch(100% 0 0 / 14%); + --bui-bg-neutral-on-surface-0-pressed: oklch(100% 0 0 / 20%); + --bui-bg-neutral-on-surface-0-disabled: oklch(100% 0 0 / 10%); + + --bui-bg-neutral-on-surface-1: oklch(100% 0 0 / 6%); + --bui-bg-neutral-on-surface-1-hover: oklch(100% 0 0 / 10%); + --bui-bg-neutral-on-surface-1-pressed: oklch(100% 0 0 / 16%); + --bui-bg-neutral-on-surface-1-disabled: oklch(100% 0 0 / 6%); + + --bui-bg-neutral-on-surface-2: oklch(100% 0 0 / 8%); + --bui-bg-neutral-on-surface-2-hover: oklch(100% 0 0 / 12%); + --bui-bg-neutral-on-surface-2-pressed: oklch(100% 0 0 / 20%); + --bui-bg-neutral-on-surface-2-disabled: oklch(100% 0 0 / 8%); + + --bui-bg-neutral-on-surface-3: oklch(100% 0 0 / 8%); + --bui-bg-neutral-on-surface-3-hover: oklch(100% 0 0 / 12%); + --bui-bg-neutral-on-surface-3-pressed: oklch(100% 0 0 / 20%); + --bui-bg-neutral-on-surface-3-disabled: oklch(100% 0 0 / 8%); + + /* Status background colors */ --bui-bg-danger: #3b1219; --bui-bg-warning: #302008; --bui-bg-success: #132d21; - /* Foreground Colors */ + /* Foreground colors */ --bui-fg-primary: var(--bui-white); --bui-fg-secondary: var(--bui-gray-7); --bui-fg-link: #9cc9ff; @@ -171,7 +213,7 @@ --bui-fg-warning: #e36d05; --bui-fg-success: #1db954; - /* Border Colors */ + /* Border colors */ --bui-border: rgba(255, 255, 255, 0.12); --bui-border-hover: rgba(255, 255, 255, 0.4); --bui-border-pressed: rgba(255, 255, 255, 0.5); @@ -180,7 +222,7 @@ --bui-border-warning: #e36d05; --bui-border-success: #53db83; - /* Special Colors */ + /* Special colors */ --bui-ring: #1f5493; --bui-scrollbar: #3636363a; --bui-scrollbar-thumb: #575757; diff --git a/packages/ui/src/hooks/useSurface.tsx b/packages/ui/src/hooks/useSurface.tsx new file mode 100644 index 0000000000..50047bf721 --- /dev/null +++ b/packages/ui/src/hooks/useSurface.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createContext, useContext, ReactNode } from 'react'; +import { Surface, Responsive } from '../types'; + +/** @public */ +export interface SurfaceContextValue { + surface: Responsive | undefined; +} + +/** @public */ +export interface SurfaceProviderProps { + surface: Responsive; + children: ReactNode; +} + +const SurfaceContext = createContext({ + surface: undefined, +}); + +/** + * Provider component that establishes the surface context for child components. + * This allows components to adapt their styling based on their background surface. + * + * @public + */ +export const SurfaceProvider = ({ + surface, + children, +}: SurfaceProviderProps) => { + return ( + + {children} + + ); +}; + +/** + * Hook to access the current surface context. + * Returns the current surface level, or undefined if no provider is present. + * + * @public + */ +export const useSurface = (): SurfaceContextValue => { + return useContext(SurfaceContext); +}; diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 7166194091..8b2ea94873 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -173,3 +173,9 @@ export interface ComponentDefinition { dataAttributes?: DataAttributesMap; utilityProps?: string[]; } + +/** + * Surface type + * @public + */ +export type Surface = '0' | '1' | '2' | '3';