diff --git a/build-tools/tasks/docs.js b/build-tools/tasks/docs.js index f777a43af0..1b9129573b 100644 --- a/build-tools/tasks/docs.js +++ b/build-tools/tasks/docs.js @@ -1,20 +1,65 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +const fs = require('fs'); const path = require('path'); const { writeComponentsDocumentation, writeTestUtilsDocumentation } = require('@cloudscape-design/documenter'); const workspace = require('../utils/workspace'); +// Dash-case names of the versioned beta components — the dirs matching the beta documenter glob +// (src/beta/-//index.tsx). Used to flag their generated definitions. +function getBetaComponentNames(srcDir = 'src') { + const betaRoot = path.join(srcDir, 'beta'); + if (!fs.existsSync(betaRoot)) { + return []; + } + const names = []; + for (const versionDir of fs.readdirSync(betaRoot)) { + const versionPath = path.join(betaRoot, versionDir); + if (!fs.statSync(versionPath).isDirectory()) { + continue; + } + for (const component of fs.readdirSync(versionPath)) { + if (fs.existsSync(path.join(versionPath, component, 'index.tsx'))) { + names.push(component); + } + } + } + return names; +} + module.exports = function docs() { + const componentsOutDir = path.join(workspace.apiDocsPath, 'components'); + + // Document the stable components AND the versioned beta components into the SINGLE `components` + // output (one combined glob → one index barrel; the documenter rewrites the index from its glob, + // so a second same-outDir pass would clobber it). Beta components are NOT shipped as a separate + // barrel; they live in `components` and are distinguished by the `releaseStatus: 'beta'` flag + // stamped below. writeComponentsDocumentation({ - outDir: path.join(workspace.apiDocsPath, 'components'), + outDir: componentsOutDir, tsconfigPath: require.resolve('../../tsconfig.json'), - publicFilesGlob: 'src/*/index.tsx', + publicFilesGlob: 'src/{*/index.tsx,beta/*/*/index.tsx}', extraExports: { FileDropzone: ['useFilesDragging'], IconProvider: ['defineIcons', 'IconRegistry', 'IconMap'], TagEditor: ['getTagsDiff'], }, }); + + // The documenter hard-codes `releaseStatus: 'stable'` with no tag override, so stamp the beta + // components' generated definitions as `beta`. The website derives `isBeta` from this (beta page + // header alert + nav badge), keeping the single components barrel with per-component flagging. + for (const name of getBetaComponentNames('src')) { + const definitionFile = path.join(componentsOutDir, `${name}.js`); + if (!fs.existsSync(definitionFile)) { + continue; + } + + const definition = require(path.resolve(definitionFile)); + definition.releaseStatus = 'beta'; + fs.writeFileSync(definitionFile, `module.exports = ${JSON.stringify(definition, null, 2)};`); + } + writeTestUtilsDocumentation({ outDir: path.join(workspace.apiDocsPath, 'test-utils-doc'), tsconfigPath: require.resolve('../../src/test-utils/tsconfig.json'), diff --git a/build-tools/tasks/package-json.js b/build-tools/tasks/package-json.js index 09e7a74fec..1f46796d4e 100644 --- a/build-tools/tasks/package-json.js +++ b/build-tools/tasks/package-json.js @@ -3,7 +3,7 @@ const { parallel } = require('gulp'); const path = require('path'); const fs = require('fs'); -const { writeFile, listPublicItems } = require('../utils/files'); +const { writeFile, listPublicItems, listBetaItems } = require('../utils/files'); const themes = require('../utils/themes'); const { task, copyTask } = require('../utils/gulp-utils'); const workspace = require('../utils/workspace'); @@ -51,6 +51,13 @@ function getComponentsExports() { result[`./${component}`] = `./${component}/index.js`; } + // Versioned beta components, published only at their explicit subpath (e.g. + // `@cloudscape-design/components/beta/basic-table-0.1`) — the versioning escape-hatch. Not added + // to the top-level barrel. + for (const betaItem of listBetaItems('src')) { + result[`./${betaItem}`] = `./${betaItem}/index.js`; + } + // Per-component test-utils wrappers, both DOM and selectors // (e.g. `.../test-utils/dom/button` and `.../test-utils/selectors/button`). for (const component of listPublicItems('src/test-utils/dom')) { diff --git a/build-tools/utils/files.js b/build-tools/utils/files.js index 2bf5d0e366..39621a25da 100644 --- a/build-tools/utils/files.js +++ b/build-tools/utils/files.js @@ -24,8 +24,25 @@ function listPublicItems(baseDir) { elem !== 'i18n' && elem !== 'theming' && elem !== 'plugins' && - elem !== 'contexts' + elem !== 'contexts' && + // `beta` is not a component: it is a container for versioned, opt-in beta components + // (e.g. `beta/basic-table-0.1`) enumerated separately via `listBetaItems`. + elem !== 'beta' ); } -module.exports = { writeFile, listPublicItems }; +// Lists the versioned beta components as `beta/` (e.g. `beta/basic-table-0.1`). Beta components +// are opt-in and published only at their versioned export subpath — they are intentionally excluded +// from the top-level barrel and treated as their own kind of public item elsewhere in the build. +function listBetaItems(srcDir = 'src') { + const betaDir = path.join(srcDir, 'beta'); + if (!fs.existsSync(betaDir)) { + return []; + } + return fs + .readdirSync(betaDir) + .filter(elem => !elem.startsWith('__') && !elem.startsWith('.') && fs.statSync(path.join(betaDir, elem)).isDirectory()) + .map(elem => `beta/${elem}`); +} + +module.exports = { writeFile, listPublicItems, listBetaItems }; diff --git a/build-tools/utils/pluralize.js b/build-tools/utils/pluralize.js index 0a6439d9c0..ce0c758267 100644 --- a/build-tools/utils/pluralize.js +++ b/build-tools/utils/pluralize.js @@ -11,6 +11,7 @@ const pluralizationMap = { Autosuggest: 'Autosuggests', Badge: 'Badges', BarChart: 'BarCharts', + BasicTable: 'BasicTables', Box: 'Boxes', BreadcrumbGroup: 'BreadcrumbGroups', Button: 'Buttons', diff --git a/pages/basic-table/cell-permutations.page.tsx b/pages/basic-table/cell-permutations.page.tsx new file mode 100644 index 0000000000..08b6a93a86 --- /dev/null +++ b/pages/basic-table/cell-permutations.page.tsx @@ -0,0 +1,68 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import BasicTable, { + BasicTableBody, + BasicTableCell, + BasicTableHeader, + BasicTableHeaderCell, + BasicTableRow, +} from '~components/beta/basic-table-0.1'; +import Box from '~components/box'; +import SpaceBetween from '~components/space-between'; + +// Cell content permutations. `wrapText` is a CELL-scoped prop (per cell / per header cell), so wrap +// vs. truncate is chosen where the long content lives — not a table-global `wrapLines`. The +// `verticalAlign` axis from Table's cell-permutations is NOT built in BasicTable (Bucket A), so it +// is omitted here. +const LONG = + 'A deliberately long cell value that exceeds the column width so truncation vs wrapping is visible'; + +const columns = [{ width: 160 }, { width: 200 }]; + +export default function BasicTableCellPermutationsPage() { + return ( + + + BasicTable — cell permutations (wrapText) + + + Default (truncate) + + + Name + Description + + + {[0, 1].map(index => ( + + Resource {index} + {LONG} + + ))} + + + + + + wrapText on the description cells + header + + + Name + Description that itself wraps onto lines + + + {[0, 1].map(index => ( + + Resource {index} + {LONG} + + ))} + + + + + + ); +} diff --git a/pages/basic-table/common.tsx b/pages/basic-table/common.tsx new file mode 100644 index 0000000000..96400ec6d7 --- /dev/null +++ b/pages/basic-table/common.tsx @@ -0,0 +1,58 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import { + BasicTableBody, + BasicTableCell, + BasicTableHeader, + BasicTableHeaderCell, + BasicTableRow, +} from '~components/beta/basic-table-0.1'; + +export interface Item { + id: string; + name: string; + type: string; + size: string; + status: string; +} + +export const makeItems = (n: number): Item[] => + Array.from({ length: n }, (_, index) => ({ + id: `resource-${index}`, + name: `Resource ${index}`, + type: index % 3 === 0 ? 'Compute' : index % 3 === 1 ? 'Storage' : 'Network', + size: `${(index % 8) + 1} GiB`, + status: index % 2 === 0 ? 'Available' : 'Pending', + })); + +// A 4-column data layout (no control column): Name fixed, Type/Size flexible-with-min, Status flexible. +export const DATA_COLUMNS = [{ width: 220 }, { minWidth: 140 }, { minWidth: 120 }, {}]; + +// Renders the standard header row + body for the shared 4-column item shape. +export function DataHeader() { + return ( + + Name + Type + Size + Status + + ); +} + +export function DataBody({ items }: { items: Item[] }) { + return ( + + {items.map((item, index) => ( + + {item.name} + {item.type} + {item.size} + {item.status} + + ))} + + ); +} diff --git a/pages/basic-table/compact-mode.page.tsx b/pages/basic-table/compact-mode.page.tsx new file mode 100644 index 0000000000..e4d9601c2d --- /dev/null +++ b/pages/basic-table/compact-mode.page.tsx @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import BasicTable from '~components/beta/basic-table-0.1'; +import Box from '~components/box'; +import Header from '~components/header'; +import SpaceBetween from '~components/space-between'; + +import { DataBody, DataHeader, DATA_COLUMNS, makeItems } from './common'; + +// Compact density is a table-global visual concern (shared Cloudscape compact-table context), so it +// is the root `contentDensity` prop — not composable per row. Rendered alongside a comfortable table +// so the reduced cell padding / row height is directly comparable. +export default function BasicTableCompactModePage() { + const items = makeItems(6); + return ( + + + BasicTable — content density + + +
Comfortable (default)
+ + + + +
+ + +
Compact
+ + + + +
+
+
+ ); +} diff --git a/pages/basic-table/expandable-rows.page.tsx b/pages/basic-table/expandable-rows.page.tsx new file mode 100644 index 0000000000..7541d2da5e --- /dev/null +++ b/pages/basic-table/expandable-rows.page.tsx @@ -0,0 +1,100 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import BasicTable, { + BasicTableBody, + BasicTableCell, + BasicTableExpandedContent, + BasicTableHeader, + BasicTableHeaderCell, + BasicTableRow, +} from '~components/beta/basic-table-0.1'; +import Box from '~components/box'; +import Header from '~components/header'; +import Icon from '~components/icon'; +import KeyValuePairs from '~components/key-value-pairs'; +import SpaceBetween from '~components/space-between'; + +import { makeItems } from './common'; + +// Row expansion via ExpandedContent. The disclosure toggle lives in a `variant="disclosure"` leading +// cell with id `${row.id}-toggle`, so pressing Escape inside the expanded region returns focus to it. +// Expansion is consumer-controlled per row. Non-sticky faithful port of table/expandable-rows; sticky +// parity is owned by sticky-header.page.tsx. +export default function BasicTableExpandableRowsPage() { + const items = makeItems(20); + const [expanded, setExpanded] = useState>(new Set([items[0].id])); + + const toggle = (id: string) => + setExpanded(prev => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + + return ( + + + BasicTable — expandable rows +
Expandable resources
+ + + + Name + Type + Status + + + {items.map((item, index) => { + const isExpanded = expanded.has(item.id); + return ( + toggle(item.id)} + > + + + + {item.name} + {item.type} + {item.status} + + + + + ); + })} + + +
+
+ ); +} diff --git a/pages/basic-table/in-app-layout.page.tsx b/pages/basic-table/in-app-layout.page.tsx new file mode 100644 index 0000000000..f3c48a3749 --- /dev/null +++ b/pages/basic-table/in-app-layout.page.tsx @@ -0,0 +1,77 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import AppLayout from '~components/app-layout'; +import BasicTable, { + BasicTableBody, + BasicTableCell, + BasicTableHeader, + BasicTableHeaderCell, + BasicTableRow, +} from '~components/beta/basic-table-0.1'; +import Button from '~components/button'; +import Header from '~components/header'; + +import { DATA_COLUMNS, makeItems } from './common'; +import ScreenshotArea from '../utils/screenshot-area'; +import { Breadcrumbs, Footer, Navigation, Notifications, Tools } from '../app-layout/utils/content-blocks'; +import labels from '../app-layout/utils/labels'; +import * as toolsContent from '../app-layout/utils/tools-content'; + +const items = makeItems(50); + +// BasicTable in the primary console context (AppLayout content), mirroring Table's full-page +// variant. BasicTable uses the body-scroll model (no `height`/`maxHeight`): the AppLayout content +// area is the scroll runway, and the `stickyHeader` slot pins its title band + column-header row via +// the P4 sticky overlay. The overlay's top offset folds in AppLayout's chrome height through the +// `--awsui-sticky-vertical-top-offset` variable, so the pinned header seats directly below the app +// header / notifications rather than overlapping them. +export default function WithBasicTablePage() { + const [toolsOpen, setToolsOpen] = useState(false); + + return ( + + } + navigation={} + contentType="table" + tools={{toolsContent.long}} + toolsOpen={toolsOpen} + onToolsChange={({ detail }) => setToolsOpen(detail.open)} + notifications={} + content={ + Create resource}> + Resources + + } + > + + Name + Type + Size + Status + + + {items.map((item, index) => ( + + {item.name} + {item.type} + {item.size} + {item.status} + + ))} + + + } + /> +