Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions pages/color-context/simple.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import Box, { BoxProps } from '~components/box';
import ButtonDropdown from '~components/button-dropdown';
import CopyToClipboard from '~components/copy-to-clipboard';
import Icon from '~components/icon';
import {
ColorContextProvider,
ColorContextVariant,
useColorContext,
} from '~components/internal/components/color-context';
import KeyValuePairs from '~components/key-value-pairs';
import Link from '~components/link';
import List from '~components/list';
import ProgressBar from '~components/progress-bar';
import SpaceBetween from '~components/space-between';
import StatusIndicator from '~components/status-indicator';

// ─── Demo: consuming the context programmatically ──────────────────────────────

function ActiveVariantBadge() {
const ctx = useColorContext();
if (!ctx) {
return null;
}
return (
<Box variant="p" color="inherit">
Active variant: <strong>{ctx.variant}</strong> ({ctx.colorScheme})
</Box>
);
}

// ─── Data ──────────────────────────────────────────────────────────────────────

const VARIANTS: ColorContextVariant[] = [
'red',
'yellow',
'indigo',
'green',
'orange',
'purple',
'mint',
'lime',
'grey',
];

const BOX_VARIANTS: { variant: BoxProps['variant']; label: string; content: string }[] = [
{ variant: 'h3', label: 'h3', content: 'Heading 3' },
{ variant: 'h4', label: 'h4', content: 'Heading 4' },
{ variant: 'p', label: 'p', content: 'Body paragraph text' },
];

const LIST_ITEMS: { id: string; content: string; icon: string; variant: ColorContextVariant }[] = [
{ id: 'health', content: 'Health overview', icon: 'face-happy', variant: 'green' },
{ id: 'functions', content: 'Functions', icon: 'script', variant: 'indigo' },
{ id: 'network', content: 'Network configuration', icon: 'globe', variant: 'grey' },
{ id: 'multi-session', content: 'Multi-session data', icon: 'multiscreen', variant: 'purple' },
{ id: 'alert', content: 'Alert center', icon: 'security', variant: 'red' },
{ id: 'communication', content: 'Communication', icon: 'contact', variant: 'mint' },
];

// ─── Page ──────────────────────────────────────────────────────────────────────

export default function ColorContextPage() {
return (
<article>
<Box variant="h1" padding={{ bottom: 'l' }}>
ColorContextProvider — React Context + CSS custom property injection
</Box>

<Box variant="p" color="text-body-secondary" padding={{ bottom: 'xl' }}>
The provider injects CSS custom properties as inline styles. No class names are applied. Descendant components
re-style automatically because they already read those tokens. The active variant is also readable via{' '}
<code>useColorContext()</code>.
</Box>

{/* ── Box variants × color variants ─────────────────────────────── */}
<Box variant="h2" padding={{ top: 'l', bottom: 'm' }}>
Box variants
</Box>

{BOX_VARIANTS.map(({ variant, label, content }) => (
<section key={label}>
<Box variant="h3" padding={{ top: 'l', bottom: 's' }}>
Box variant=&quot;{label}&quot;
</Box>
<SpaceBetween size="m" direction="horizontal">
{VARIANTS.map(v => (
<ColorContextProvider key={v} variant={v} style={{ borderRadius: 2 }}>
<Box variant={variant}>{content}</Box>
</ColorContextProvider>
))}
</SpaceBetween>
</section>
))}

{/* ── useColorContext() demo ─────────────────────────────────────── */}
<Box variant="h2" padding={{ top: 'xxxl', bottom: 'm' }}>
useColorContext() — programmatic access
</Box>
<ColorContextProvider variant="indigo" style={{ borderRadius: 8 }}>
<SpaceBetween size="xs">
<ActiveVariantBadge />
<Box variant="p">
Any descendant can call <code>useColorContext()</code> to read the active variant and color scheme without
touching the DOM or CSS.
</Box>
</SpaceBetween>
</ColorContextProvider>

{/* ── Application in components ──────────────────────────────────── */}
<Box variant="h2" padding={{ top: 'xxxl', bottom: 'm' }}>
Application in components
</Box>

{/* KeyValuePairs — context wraps only the value */}
<Box variant="h3" padding={{ bottom: 's' }}>
KeyValuePairs
</Box>
<KeyValuePairs
columns={3}
items={[
{
label: 'Distribution ID',
value: (
<ColorContextProvider variant="indigo" style={{ borderRadius: 2 }}>
<Box variant="p">E1WG1ZNPRXT0D4</Box>
</ColorContextProvider>
),
info: (
<Link variant="info" href="#">
Info
</Link>
),
},
{
label: 'ARN',
value: (
<CopyToClipboard
copyButtonAriaLabel="Copy ARN"
copyErrorText="ARN failed to copy"
copySuccessText="ARN copied"
textToCopy="arn:service23G24::111122223333:distribution/23E1WG1ZNPR"
variant="inline"
/>
),
},
{
label: 'Status',
value: <StatusIndicator>Available</StatusIndicator>,
},
{
label: 'SSL Certificate',
id: 'ssl-certificate-id',
value: (
<ProgressBar
value={30}
additionalInfo="Additional information"
description="Progress bar description"
ariaLabelledby="ssl-certificate-id"
/>
),
},
{
label: 'Price class',
value: (
<ColorContextProvider variant="green" style={{ borderRadius: 2 }}>
<Box variant="p">Use only US, Canada, Europe</Box>
</ColorContextProvider>
),
},
{
label: 'CNAMEs',
value: (
<Link external={true} href="#">
abc.service23G24.xyz
</Link>
),
},
]}
/>

{/* List — context circle wraps only the icon */}
<Box variant="h3" padding={{ top: 'xl', bottom: 's' }}>
List
</Box>
<List
ariaLabel="List with circle icon badges"
items={LIST_ITEMS}
renderItem={item => ({
id: item.id,
content: item.content,
icon: (
<ColorContextProvider
variant={item.variant}
as="span"
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
width: 32,
height: 32,
flexShrink: 0,
paddingInline: 0,
paddingBlock: 0,
}}
>
<Icon name={item.icon as any} size="normal" variant="subtle" />
</ColorContextProvider>
),
actions: (
<ButtonDropdown
items={[
{ id: '1', text: 'Action one' },
{ id: '2', text: 'Action two' },
{ id: '3', text: 'Action three' },
]}
variant="icon"
ariaLabel={`Actions for ${item.content}`}
/>
),
})}
/>
</article>
);
}
154 changes: 154 additions & 0 deletions src/internal/components/color-context/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

/**
* @experimental This component and hook are unstable and subject to change without
* a major version bump. Available to selected builders only.
*/

import React, { createContext, CSSProperties, useContext, useRef } from 'react';

import { useCurrentMode } from '@cloudscape-design/component-toolkit/internal';

// ─── Variant type ─────────────────────────────────────────────────────────────

export type ColorContextVariant =
| 'red'
| 'yellow'
| 'indigo'
| 'green'
| 'orange'
| 'purple'
| 'mint'
| 'lime'
| 'grey';

// ─── Palette ──────────────────────────────────────────────────────────────────
// Source: style-dictionary/core/color-palette.ts
// Each entry: [light-bg (50), dark-bg (950 @ 80%), light-content (600), dark-content (200)]

const PALETTE: Record<
ColorContextVariant,
{ bgLight: string; bgDark: string; contentLight: string; contentDark: string }
> = {
red: { bgLight: '#fff5f5', bgDark: 'rgba(82,0,0,0.6)', contentLight: '#db0000', contentDark: '#ffc2c2' },
yellow: { bgLight: '#fffef0', bgDark: 'rgba(87,58,0,0.6)', contentLight: '#855900', contentDark: '#fef571' },
indigo: { bgLight: '#f5f7ff', bgDark: 'rgba(0,20,117,0.6)', contentLight: '#295eff', contentDark: '#c2d1ff' },
green: { bgLight: '#effff1', bgDark: 'rgba(0,51,17,0.6)', contentLight: '#00802f', contentDark: '#aeffa8' },
orange: { bgLight: '#fff7f5', bgDark: 'rgba(71,17,0,0.6)', contentLight: '#A82700', contentDark: '#ffc0ad' },
purple: { bgLight: '#faf5ff', bgDark: 'rgba(48,0,97,0.6)', contentLight: '#962eff', contentDark: '#e8d1ff' },
mint: { bgLight: '#ebfff6', bgDark: 'rgba(0,51,34,0.6)', contentLight: '#006b48', contentDark: '#8fffce' },
lime: { bgLight: '#f7ffeb', bgDark: 'rgba(0,46,0,0.6)', contentLight: '#007000', contentDark: '#d1ff8a' },
grey: { bgLight: '#fcfcfc', bgDark: '#2d2d2db7', contentLight: '#1A1A1A', contentDark: '#f5f5f5' },
};

// ─── Base styles applied to every provider instance ─────────────────────────
// Builders can override any of these via the `style` prop.

const BASE_STYLES: CSSProperties = {
display: 'inline-flex',
paddingInline: '2px',
paddingBlock: '0px',
};
// These hash suffixes are derived from token names and stable across builds.

const CSS_PROPS_LIGHT = (color: string): CSSProperties =>
({
'--color-text-body-default-a7br70': color,
'--color-text-heading-default-5p4ugs': color,
'--color-text-body-secondary-6zl7e0': color,
'--color-text-heading-secondary-c1zwy4': color,
'--color-text-icon-subtle-xnb03v': color,
'--color-text-small-ldm4or': color,
}) as CSSProperties;

// ─── React Context ─────────────────────────────────────────────────────────────

interface ColorContextValue {
variant: ColorContextVariant;
colorScheme: 'light' | 'dark';
}

const ColorContext = createContext<ColorContextValue | null>(null);

/**
* Returns the active ColorContext value, or null if no provider is in scope.
*
* @experimental
*/
export function useColorContext(): ColorContextValue | null {
return useContext(ColorContext);
}

// ─── Provider props ───────────────────────────────────────────────────────────

export interface ColorContextProviderProps {
/**
* The color variant to activate. Each variant provides a background color
* and re-scopes text/icon CSS custom properties to a matching hue:
* light mode → 600-level | dark mode → 400-level
*
* Dark mode is detected automatically from the nearest Cloudscape dark-mode
* ancestor — no `colorScheme` prop needed.
*
* @experimental
*/
variant: ColorContextVariant;

/**
* The HTML element rendered as the context boundary. Defaults to `"div"`.
* Use a semantic element (e.g. `"section"`, `"aside"`) when appropriate.
*/
as?: keyof JSX.IntrinsicElements;

/** Additional class names to merge onto the wrapper element. */
className?: string;

/** Additional inline styles. These are merged with the injected CSS custom properties. */
style?: CSSProperties;

children: React.ReactNode;
}

/**
* ColorContextProvider activates a color variant for its subtree by injecting
* CSS custom properties as inline styles on a wrapper element. Dark mode is
* detected automatically — when a Cloudscape dark-mode class is present on an
* ancestor, the 950-level backgrounds and 400-level content colors are applied.
*
* The active variant is also available via `useColorContext()`.
*
* @experimental
*/
export function ColorContextProvider({
variant,
as: Tag = 'div',
className,
style,
children,
}: ColorContextProviderProps) {
const ref = useRef<HTMLElement>(null);
const colorMode = useCurrentMode(ref);
const isDark = colorMode === 'dark';

const { bgLight, bgDark, contentLight, contentDark } = PALETTE[variant];
const backgroundColor = isDark ? bgDark : bgLight;
const contentColor = isDark ? contentDark : contentLight;

const injectedStyles: CSSProperties = {
...BASE_STYLES,
backgroundColor,
...CSS_PROPS_LIGHT(contentColor),
...style,
};

return (
<ColorContext.Provider value={{ variant, colorScheme: colorMode ?? 'light' }}>
<Tag className={className} style={injectedStyles}>
{/* Hidden span to anchor the color-mode probe ref */}
<span ref={ref} style={{ display: 'none' }} aria-hidden="true" />
{children}
</Tag>
</ColorContext.Provider>
);
}
Loading