diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..23d67e7d15 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +React component library for [Cloudscape Design System](https://cloudscape.design/) — an open source design system for building accessible, inclusive web experiences at scale. + +To install dependencies use `npm install` +Build with `npm run quick-build` for dev, or `npm run build` for a full production build. + +For component structure, props, events, and refs, see docs/COMPONENT_CONVENTIONS.md. +For design tokens, CSS rules, and RTL support, see docs/STYLING.md. +For test commands, see docs/TESTING.md. +For writing tests and test utils, see docs/WRITING_TESTS.md. +For prettier, stylelint, and eslint (`npm run lint`), see docs/CODE_STYLE.md. +For dev/test pages, see docs/DEV_PAGES.md. +For API documentation comments, see docs/API_DOCS.md. +For internal shared utilities, see docs/INTERNALS.md. \ No newline at end of file diff --git a/build-tools/utils/pluralize.js b/build-tools/utils/pluralize.js index 5cacfb96f9..ec5eac61e8 100644 --- a/build-tools/utils/pluralize.js +++ b/build-tools/utils/pluralize.js @@ -56,6 +56,7 @@ const pluralizationMap = { Pagination: 'Paginations', AppLayoutToolbar: 'AppLayoutToolbars', PanelLayout: 'PanelLayouts', + NavigationBar: 'NavigationBars', PieChart: 'PieCharts', Popover: 'Popovers', ProgressBar: 'ProgressBars', diff --git a/docs/API_DOCS.md b/docs/API_DOCS.md new file mode 100644 index 0000000000..e5a2c65592 --- /dev/null +++ b/docs/API_DOCS.md @@ -0,0 +1,10 @@ +# API Documentation Comments + +This project uses `@cloudscape-design/documenter` to generate API docs from JSDoc comments in component interface files (`src//interfaces.ts`). + +## Special tags + +- `@i18n` — marks internationalization properties +- `@analytics` — marks analytics metadata properties +- `@deprecated` — marks deprecated properties (include replacement info) +- `@displayname` — overrides the display name (e.g. `children` → `text`) diff --git a/docs/CODE_STYLE.md b/docs/CODE_STYLE.md new file mode 100644 index 0000000000..cce901bf1f --- /dev/null +++ b/docs/CODE_STYLE.md @@ -0,0 +1,18 @@ +# Code Style + +This project enforces formatting and linting via prettier, stylelint, and eslint. Run `npm run lint` to check everything. + +Before writing or modifying code, read the relevant config files: + +- Prettier: `.prettierrc` +- Stylelint: `.stylelintrc` +- ESLint: `eslint.config.mjs` + +Follow whatever rules are defined there. Do not guess — read the configs. + +All source files must include the license header: + +``` +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +``` diff --git a/docs/COMPONENT_CONVENTIONS.md b/docs/COMPONENT_CONVENTIONS.md new file mode 100644 index 0000000000..79cb490235 --- /dev/null +++ b/docs/COMPONENT_CONVENTIONS.md @@ -0,0 +1,65 @@ +# Component Conventions + +## Ground Rules + +- Always read existing code for the component you're working on before writing new code. For new components, pick a similar one as your template. +- Inner HTML structure and class names are not public API — don't rely on them. + +## Component Structure + +Each public component lives in `src//index.tsx` and must: + +- Assign default prop values in the destructuring signature (not `defaultProps`), then pass them explicitly to the internal component +- Call `useBaseComponent` for telemetry — pass `props` (primitive/enum values with defaults applied) and optionally `metadata` (derived counters or booleans). No state props, no PII, no user input strings. +- Use `getExternalProps` or `getBaseProps` to strip internal `__`-prefixed props +- Call `applyDisplayName` at the bottom of the file +- Export the props interface as `${ComponentName}Props` + +Each public component has a private counterpart at `src//internal.tsx` for composition. Internal props are prefixed with `__`. The public component must not add behavior beyond what the internal component provides. + +Shared hooks, components, contexts, and helpers live in `src/internal/`. Always check there before building new shared code. + +## Props & Interfaces + +- Props interface: `${ComponentName}Props`, namespace sub-types under it (e.g. `${ComponentName}Props.Variant`) +- Union types must be type aliases (no inline unions) +- Array types must use `ReadonlyArray` +- Cast string props to string at runtime if rendered in JSX — React accepts JSX content there +- Component return type must be exactly `JSX.Element` — `null` or arrays break the doc generator +- Components exported as default from `index.tsx` are public; everything else is private + +## Events + +- Use `CancelableEventHandler` or `NonCancelableEventHandler` from `../internal/events` +- All `on*` props must use these interfaces (build fails otherwise) + +## Refs + +- Expose via `${ComponentName}Props.Ref` interface +- Use `useForwardFocus(ref, elementRef)` for simple focus delegation +- For `React.forwardRef` generics, create a `${ComponentName}ForwardRefType` interface + +## Controllable Components + +- Controlled: requires `value` + `onChange` (e.g. Input) +- Uncontrolled: internal state only (e.g. dropdown open) +- Controllable: uncontrolled by default, controlled when `value` is set + +Use `useControllable` — read existing components for the pattern. + +## Element Queries + +Use `useContainerBreakpoints` instead of CSS media queries. Handle `null` on first render. + +## I18n + +Three string categories: +- Static → `i18nStrings` property +- Dynamic → functions in `i18nStrings` returning strings +- Context-dependent → top-level props (out of scope for i18n provider) + +Use `useInternalI18n('')` to consume. Test with `TestI18nProvider` from `src/i18n/testing`. + +## Dependencies + +Before adding any dependency: must support React 16.8+ and latest 3 major Chrome/Firefox/Edge, no global state, ESM preferred, no external resources (CSP). diff --git a/docs/DEV_PAGES.md b/docs/DEV_PAGES.md new file mode 100644 index 0000000000..0393712295 --- /dev/null +++ b/docs/DEV_PAGES.md @@ -0,0 +1,11 @@ +# Dev Pages + +Dev and test pages live in `pages//`. They are used for local development, integration tests, and visual regression tests. + +## Rules + +- Export a default function component +- Import components via `~components/` (not relative paths to `src/`) +- For visual regression content, either use `SimplePage` from `pages/app/templates` (handles heading, screenshot area, i18n, and layout) or wrap content in `ScreenshotArea` from `pages/utils/screenshot-area` with a manual `

` +- Use `createPermutations` and `PermutationsView` from `pages/utils/` for permutation pages +- If multiple pages share data (like `i18nStrings`), put it in a `common.tsx` in the same directory diff --git a/docs/INTERNALS.md b/docs/INTERNALS.md new file mode 100644 index 0000000000..390a4526f7 --- /dev/null +++ b/docs/INTERNALS.md @@ -0,0 +1,16 @@ +# Internal Utilities + +Shared infrastructure lives in `src/internal/`. + +## What's in there + +- `hooks/` — shared React hooks (component telemetry, controllable state, focus management, container/element queries, visual mode detection, intersection observers, debounce/throttle, etc.) +- `components/` — shared internal UI building blocks (dropdowns, options, focus locks, tooltips, chart primitives, transitions, screen-reader utilities, drag handles, etc.) +- `context/` — React contexts for cross-component communication (form fields, modals, split panels, container headers, etc.) +- `utils/` — pure helper functions (DOM helpers, key handling, date/time, locale, display names, prop stripping, etc.) +- `events/` — event handler type definitions used by all component `on*` props +- `base-component/` — base prop extraction utilities +- `styles/` — shared SCSS mixins +- `analytics/` — telemetry and funnel metrics + +Always check `src/internal/` before introducing new utilities, hooks, or shared components. diff --git a/docs/STYLING.md b/docs/STYLING.md new file mode 100644 index 0000000000..2100b931d0 --- /dev/null +++ b/docs/STYLING.md @@ -0,0 +1,26 @@ +# Styling + +Never hardcode CSS spacing, colors, borders, shadows, typography, or motion values. This project uses design tokens and custom CSS properties. + +## Design Tokens + +Tokens are defined in `style-dictionary/` and consumed in SCSS via the `awsui` namespace. + +## Custom CSS Properties + +Custom CSS properties are defined in `build-tools/utils/custom-css-properties.js`. Read this file to see what's available. + +## Rules + +- Apply `styles-reset` mixin on every root element — prevents parent styles from leaking in +- Root elements must not have outer margins — spacing is managed by parent components +- No descendant combinators (`.a .b` with a space) — breaks CSS scoping +- Wrap all animations in the `with-motion` mixin — ensures motion can be toggled +- Use logical properties only — no `left`/`right`/`top`/`bottom`/`width`/`height` in CSS. Use `inline-start`/`inline-end`/`block-start`/`block-end`/`inline-size`/`block-size` instead. This is required for RTL support. + +## RTL Support + +For bidirectional layout support, use the internal RTL utilities in `src/internal/`: + +- SCSS: `with-direction` mixin for direction-aware styles +- JS: `getIsRtl` for detecting direction, `handleKey` for direction-aware key handling, `getLogicalBoundingClientRect` / `getOffsetInlineStart` / `getScrollInlineStart` for direction-aware measurements diff --git a/docs/TESTING.md b/docs/TESTING.md new file mode 100644 index 0000000000..5197b2d716 --- /dev/null +++ b/docs/TESTING.md @@ -0,0 +1,37 @@ +# Testing + +## Quick Reference + +``` +npm test # all tests (unit + integ + a11y) +npm run test:unit # unit + build-tool tests +npm run test:integ # integration tests (starts dev server automatically) +npm run test:motion # motion tests (starts dev server automatically) +npm run test:a11y # accessibility tests +``` + +The npm scripts use gulp tasks that handle env vars (`TZ=UTC`, `NODE_OPTIONS=--experimental-vm-modules`) and dev server lifecycle automatically. + +## Targeting Specific Files + +For running a specific test file or folder, call jest directly with the appropriate config: + +``` +# Unit +TZ=UTC node_modules/.bin/jest -c jest.unit.config.js src/button/__tests__/button.test.tsx + +# Integration (requires dev server running via `npm start`) +NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest -c jest.integ.config.js src/input/__integ__/ + +# Motion (requires dev server running via `npm start`) +NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest -c jest.motion.config.js src/flashbar/__motion__/ + +# Build-tool / stylelint +NODE_OPTIONS=--experimental-vm-modules node_modules/.bin/jest -c jest.build-tools.config.js build-tools/stylelint +``` + +## Updating Snapshots + +``` +TZ=UTC node_modules/.bin/jest -u -c jest.unit.config.js src/__tests__/snapshot-tests/ +``` diff --git a/docs/WRITING_TESTS.md b/docs/WRITING_TESTS.md new file mode 100644 index 0000000000..803e977687 --- /dev/null +++ b/docs/WRITING_TESTS.md @@ -0,0 +1,31 @@ +# Writing Tests + +## Test Utils + +Use the project's test utils in `src/test-utils/` — don't query the DOM directly. + +- `src/test-utils/dom/` — unit test wrappers (JSDOM) +- `src/test-utils/selectors/` — integration test wrappers (real browser) + +Read the wrapper for the component you're testing to see available `find*` methods. + +### Authoring Test Utils + +Each wrapper extends `ComponentWrapper`, has a static `rootSelector`, and explicit return types on all methods. Test-util CSS classes go in `src//test-classes/styles.scss`. Read existing wrappers for the pattern. + +## Unit Tests + +Use `react-testing-library` to render, combined with test-utils. Prefer test-utils for public interactions, react-testing-library for internal corner cases. Read an existing test file for the setup pattern. + +## Integration Tests + +Use `useBrowser` from `@amzn/awsui-browser-test-tools/use-browser`. Use a `setupTest` wrapper with `waitForVisible`. Don't wait on tag name selectors — they're visible before JS loads. Multiple assertions per test are fine (e2e tests are slow). Use page object pattern for files with many tests. + +## I18n Testing + +Test `useInternalI18n` with `TestI18nProvider` from `src/i18n/testing`. Read existing i18n tests for the pattern. + +## Gotchas + +- `findAll`/`findAllByClassName` with `.get()` uses `:nth-child()` — only works if items share the same parent node +- All dev pages are axe-checked automatically. A11y violations fail the build. Checks run in dark mode only. diff --git a/pages/navigation-bar/simple.page.tsx b/pages/navigation-bar/simple.page.tsx new file mode 100644 index 0000000000..df3d6cee95 --- /dev/null +++ b/pages/navigation-bar/simple.page.tsx @@ -0,0 +1,74 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import ButtonGroup from '~components/button-group'; +import NavigationBar from '~components/navigation-bar'; + +import ScreenshotArea from '../utils/screenshot-area'; + +export default function NavigationBarPage() { + return ( +
+

Navigation Bar

+ +

Primary — horizontal (default)

+ + My Product +
+ +
+
+ +

Secondary — horizontal

+ + Breadcrumbs / Page title +
+ +
+
+ +

Primary — vertical

+
+ + + +
Main content
+
+ +

Secondary — vertical

+
+ + + +
Main content
+
+
+
+ ); +} diff --git a/src/navigation-bar/index.tsx b/src/navigation-bar/index.tsx new file mode 100644 index 0000000000..74f0c4ad32 --- /dev/null +++ b/src/navigation-bar/index.tsx @@ -0,0 +1,34 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +'use client'; +import React from 'react'; + +import useBaseComponent from '../internal/hooks/use-base-component'; +import { applyDisplayName } from '../internal/utils/apply-display-name'; +import { getExternalProps } from '../internal/utils/external-props'; +import { NavigationBarProps } from './interfaces'; +import InternalNavigationBar from './internal'; + +export { NavigationBarProps }; + +const NavigationBar = React.forwardRef(function NavigationBar( + { variant = 'primary', placement = 'horizontal', ...props }, + ref +) { + const baseComponentProps = useBaseComponent('NavigationBar', { + props: { variant, placement, disableBorder: props.disableBorder }, + }); + return ( + + ); +}); + +export default NavigationBar; + +applyDisplayName(NavigationBar, 'NavigationBar'); diff --git a/src/navigation-bar/interfaces.ts b/src/navigation-bar/interfaces.ts new file mode 100644 index 0000000000..ee04ad373a --- /dev/null +++ b/src/navigation-bar/interfaces.ts @@ -0,0 +1,66 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import { BaseComponentProps } from '../internal/base-component'; + +export interface NavigationBarProps extends BaseComponentProps { + /** + * Visual style and semantic role of the navigation bar. + * + * - `"primary"`: Top-navigation style. Renders as a `