From 0331922672e835b8533ed274e26dc85f1ca288ca Mon Sep 17 00:00:00 2001 From: adamlobler Date: Mon, 27 Jul 2026 15:16:13 +0200 Subject: [PATCH 1/6] feat(ui-card,ui,ui-scripts): add Card component with base and nested variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Card is a wrapper for grouping related content. The variant prop (base | nested) and size prop (sm | md | lg) are its only two options. The base variant renders a background, border colour and elevation shadow; the nested variant is meant to sit inside a base Card and omits all three, using a smaller border radius. Each size drives its own padding, border radius and min/max width breakpoints. Built on the v11.7 token system (useStyleNew plus generated Card theme tokens) rather than a legacy per-component theme.ts, so it resolves tokens across all four themes. This requires instructure-design-tokens v1.6.0, the release that adds the Card token set. Exported from the @instructure/ui v11_7 entrypoint. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 --- packages/__docs__/package.json | 1 + packages/__docs__/tsconfig.build.json | 3 + packages/ui-card/.gitignore | 4 ++ packages/ui-card/.npmignore | 5 ++ packages/ui-card/CHANGELOG.md | 4 ++ packages/ui-card/README.md | 24 ++++++++ packages/ui-card/babel.config.js | 36 +++++++++++ packages/ui-card/package.json | 74 +++++++++++++++++++++++ packages/ui-card/src/Card/v1/index.tsx | 58 ++++++++++++++++++ packages/ui-card/src/Card/v1/props.ts | 62 +++++++++++++++++++ packages/ui-card/src/Card/v1/styles.ts | 83 ++++++++++++++++++++++++++ packages/ui-card/src/exports/a.ts | 25 ++++++++ packages/ui-card/tsconfig.build.json | 29 +++++++++ packages/ui-card/tsconfig.json | 4 ++ packages/ui-scripts/package.json | 2 +- packages/ui/package.json | 1 + packages/ui/src/v11_7.ts | 2 + packages/ui/tsconfig.build.json | 3 + pnpm-lock.yaml | 60 ++++++++++++++++--- 19 files changed, 470 insertions(+), 10 deletions(-) create mode 100644 packages/ui-card/.gitignore create mode 100644 packages/ui-card/.npmignore create mode 100644 packages/ui-card/CHANGELOG.md create mode 100644 packages/ui-card/README.md create mode 100644 packages/ui-card/babel.config.js create mode 100644 packages/ui-card/package.json create mode 100644 packages/ui-card/src/Card/v1/index.tsx create mode 100644 packages/ui-card/src/Card/v1/props.ts create mode 100644 packages/ui-card/src/Card/v1/styles.ts create mode 100644 packages/ui-card/src/exports/a.ts create mode 100644 packages/ui-card/tsconfig.build.json create mode 100644 packages/ui-card/tsconfig.json diff --git a/packages/__docs__/package.json b/packages/__docs__/package.json index 48ba039960..dca2722b20 100644 --- a/packages/__docs__/package.json +++ b/packages/__docs__/package.json @@ -46,6 +46,7 @@ "@instructure/ui-buttons": "workspace:*", "@instructure/ui-byline": "workspace:*", "@instructure/ui-calendar": "workspace:*", + "@instructure/ui-card": "workspace:*", "@instructure/ui-checkbox": "workspace:*", "@instructure/ui-color-picker": "workspace:*", "@instructure/ui-color-utils": "workspace:*", diff --git a/packages/__docs__/tsconfig.build.json b/packages/__docs__/tsconfig.build.json index 267f8c1550..517f057000 100644 --- a/packages/__docs__/tsconfig.build.json +++ b/packages/__docs__/tsconfig.build.json @@ -68,6 +68,9 @@ { "path": "../ui-calendar/tsconfig.build.json" }, + { + "path": "../ui-card/tsconfig.build.json" + }, { "path": "../ui-checkbox/tsconfig.build.json" }, diff --git a/packages/ui-card/.gitignore b/packages/ui-card/.gitignore new file mode 100644 index 0000000000..02125f1a5b --- /dev/null +++ b/packages/ui-card/.gitignore @@ -0,0 +1,4 @@ +lib/ +.babel-cache/ +es/ +types/ diff --git a/packages/ui-card/.npmignore b/packages/ui-card/.npmignore new file mode 100644 index 0000000000..9b51f84223 --- /dev/null +++ b/packages/ui-card/.npmignore @@ -0,0 +1,5 @@ +**/.* +**/__tests__ +**/__testfixtures__ +*.config.js +*.conf.js diff --git a/packages/ui-card/CHANGELOG.md b/packages/ui-card/CHANGELOG.md new file mode 100644 index 0000000000..e4d87c4d45 --- /dev/null +++ b/packages/ui-card/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. diff --git a/packages/ui-card/README.md b/packages/ui-card/README.md new file mode 100644 index 0000000000..08dde1e2a5 --- /dev/null +++ b/packages/ui-card/README.md @@ -0,0 +1,24 @@ +## ui-card + +[![npm][npm]][npm-url] +[![MIT License][license-badge]][license] +[![Code of Conduct][coc-badge]][coc] + +### Components + +The `ui-card` package contains the following: + +- Card + +### Installation + +```sh +npm install @instructure/ui-card +``` + +[npm]: https://img.shields.io/npm/v/@instructure/ui-card.svg +[npm-url]: https://npmjs.com/package/@instructure/ui-card +[license-badge]: https://img.shields.io/npm/l/instructure-ui.svg?style=flat-square +[license]: https://github.com/instructure/instructure-ui/blob/master/LICENSE.md +[coc-badge]: https://img.shields.io/badge/code%20of-conduct-ff69b4.svg?style=flat-square +[coc]: https://github.com/instructure/instructure-ui/blob/master/CODE_OF_CONDUCT.md diff --git a/packages/ui-card/babel.config.js b/packages/ui-card/babel.config.js new file mode 100644 index 0000000000..647821c390 --- /dev/null +++ b/packages/ui-card/babel.config.js @@ -0,0 +1,36 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +module.exports = { + presets: [ + [ + require('@instructure/ui-babel-preset'), + { + esModules: Boolean(process.env.ES_MODULES), + removeConsole: process.env.NODE_ENV === 'production', + transformImports: Boolean(process.env.TRANSFORM_IMPORTS) + } + ] + ] +} diff --git a/packages/ui-card/package.json b/packages/ui-card/package.json new file mode 100644 index 0000000000..3bf7f57ebb --- /dev/null +++ b/packages/ui-card/package.json @@ -0,0 +1,74 @@ +{ + "name": "@instructure/ui-card", + "version": "11.7.4", + "description": "A basic wrapper component for grouping content with padding, border radius, shadow, and background.", + "author": "Instructure, Inc. Engineering and Product Design", + "module": "./es/index.js", + "main": "./lib/index.js", + "types": "./types/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/instructure/instructure-ui.git" + }, + "homepage": "https://instructure.github.io/instructure-ui/", + "bugs": "https://github.com/instructure/instructure-ui/issues", + "scripts": { + "lint": "ui-scripts lint", + "lint:fix": "ui-scripts lint --fix", + "clean": "ui-scripts clean", + "build": "ui-scripts build --modules es,cjs", + "build:watch": "pnpm run ts:check -- --watch & ui-scripts build --watch", + "build:types": "tsc -p tsconfig.build.json", + "ts:check": "tsc -p tsconfig.build.json --noEmit --emitDeclarationOnly false" + }, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@instructure/emotion": "workspace:*", + "@instructure/shared-types": "workspace:*", + "@instructure/ui-react-utils": "workspace:*", + "@instructure/ui-themes": "workspace:*" + }, + "devDependencies": { + "@instructure/ui-axe-check": "workspace:*", + "@instructure/ui-babel-preset": "workspace:*", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "16.3.2", + "vitest": "^4.1.9" + }, + "peerDependencies": { + "react": ">=18 <=19" + }, + "publishConfig": { + "access": "public" + }, + "sideEffects": false, + "exports": { + "./lib/*": "./lib/*", + "./es/*": "./es/*", + "./types/*": "./types/*", + "./package.json": "./package.json", + "./src/*": "./src/*", + ".": { + "src": "./src/exports/a.ts", + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./v11_7": { + "src": "./src/exports/a.ts", + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + }, + "./latest": { + "src": "./src/exports/a.ts", + "types": "./types/exports/a.d.ts", + "import": "./es/exports/a.js", + "require": "./lib/exports/a.js", + "default": "./es/exports/a.js" + } + } +} diff --git a/packages/ui-card/src/Card/v1/index.tsx b/packages/ui-card/src/Card/v1/index.tsx new file mode 100644 index 0000000000..b496bd8911 --- /dev/null +++ b/packages/ui-card/src/Card/v1/index.tsx @@ -0,0 +1,58 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { useStyleNew } from '@instructure/emotion' +import { passthroughProps } from '@instructure/ui-react-utils' + +import type { CardProps } from './props.js' +import generateStyle from './styles.js' + +/** +--- +category: components +--- +**/ +const Card = ({ + children, + variant = 'base', + size = 'md', + themeOverride, + ...rest +}: CardProps) => { + const styles = useStyleNew({ + generateStyle, + themeOverride, + params: { variant, size }, + componentId: 'Card', + displayName: 'Card' + }) + + return ( +
+ {children} +
+ ) +} + +export default Card +export { Card } diff --git a/packages/ui-card/src/Card/v1/props.ts b/packages/ui-card/src/Card/v1/props.ts new file mode 100644 index 0000000000..d3f831005e --- /dev/null +++ b/packages/ui-card/src/Card/v1/props.ts @@ -0,0 +1,62 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import type { ReactNode } from 'react' +import type { ComponentStyle, NewThemeOverrideProp } from '@instructure/emotion' +import type { OtherHTMLAttributes } from '@instructure/shared-types' +import type { NewComponentTypes } from '@instructure/ui-themes' + +type CardOwnProps = { + /** + * The content to be rendered inside the Card + */ + children?: ReactNode + /** + * `base` renders a background, border color, and shadow. `nested` is + * meant to be placed inside a `base` Card and omits the background, + * border color, and shadow. + */ + variant?: 'base' | 'nested' + /** + * `sm` applies a max-width, `md` applies a min- and max-width, and `lg` + * applies a min-width, each based on breakpoint tokens. Padding and + * border radius also scale per size. + */ + size?: 'sm' | 'md' | 'lg' +} + +type PropKeys = keyof CardOwnProps + +type AllowedPropKeys = Readonly> + +type CardProps = CardOwnProps & + NewThemeOverrideProp> & + OtherHTMLAttributes + +type CardStyle = ComponentStyle<'card'> + +const allowedProps: AllowedPropKeys = ['children', 'variant', 'size'] + +export type { CardProps, CardStyle } +export { allowedProps } diff --git a/packages/ui-card/src/Card/v1/styles.ts b/packages/ui-card/src/Card/v1/styles.ts new file mode 100644 index 0000000000..59c2023fd9 --- /dev/null +++ b/packages/ui-card/src/Card/v1/styles.ts @@ -0,0 +1,83 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { boxShadowObjectsToCSSString } from '@instructure/ui-themes' +import type { NewComponentTypes } from '@instructure/ui-themes' +import type { CardProps, CardStyle } from './props' + +/** + * --- + * private: true + * --- + * Generates the style object from the theme and provided additional information + * @param componentTheme The theme variable object. + * @param props the props of the component, the style is applied to + * @return The final style object, which will be used in the component + */ +const generateStyle = ( + componentTheme: ReturnType, + props: CardProps +): CardStyle => { + const { variant = 'base', size = 'md' } = props + + const boxShadow = boxShadowObjectsToCSSString(componentTheme.boxShadow) + + const sizeVariants = { + sm: { + padding: componentTheme.padding[variant].sm, + maxWidth: componentTheme.breakpoint.md + }, + md: { + padding: componentTheme.padding[variant].md, + minWidth: componentTheme.breakpoint.md, + maxWidth: componentTheme.breakpoint.lg + }, + lg: { + padding: componentTheme.padding[variant].lg, + minWidth: componentTheme.breakpoint.lg + } + } + + const variantVariants = { + base: { + background: componentTheme.backgroundColor, + boxShadow + }, + nested: { + border: `1px solid ${componentTheme.nestedBorderColor}` + } + } + + return { + card: { + label: 'card', + boxSizing: 'border-box', + borderRadius: componentTheme.borderRadius[variant][size], + ...sizeVariants[size], + ...variantVariants[variant] + } + } +} + +export default generateStyle diff --git a/packages/ui-card/src/exports/a.ts b/packages/ui-card/src/exports/a.ts new file mode 100644 index 0000000000..d64ba77528 --- /dev/null +++ b/packages/ui-card/src/exports/a.ts @@ -0,0 +1,25 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +export { Card } from '../Card/v1/index.js' +export type { CardProps } from '../Card/v1/props' diff --git a/packages/ui-card/tsconfig.build.json b/packages/ui-card/tsconfig.build.json new file mode 100644 index 0000000000..9684ded0d6 --- /dev/null +++ b/packages/ui-card/tsconfig.build.json @@ -0,0 +1,29 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./types", + "composite": true, + "rootDir": "./src" + }, + "include": ["src"], + "references": [ + { + "path": "../emotion/tsconfig.build.json" + }, + { + "path": "../shared-types/tsconfig.build.json" + }, + { + "path": "../ui-axe-check/tsconfig.build.json" + }, + { + "path": "../ui-babel-preset/tsconfig.build.json" + }, + { + "path": "../ui-react-utils/tsconfig.build.json" + }, + { + "path": "../ui-themes/tsconfig.build.json" + } + ] +} diff --git a/packages/ui-card/tsconfig.json b/packages/ui-card/tsconfig.json new file mode 100644 index 0000000000..b29a7b46c4 --- /dev/null +++ b/packages/ui-card/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": {} +} diff --git a/packages/ui-scripts/package.json b/packages/ui-scripts/package.json index 773d94eb00..00b879d511 100644 --- a/packages/ui-scripts/package.json +++ b/packages/ui-scripts/package.json @@ -23,7 +23,7 @@ "dependencies": { "@babel/cli": "^7.27.2", "@instructure/command-utils": "workspace:*", - "@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.5.0", + "@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.6.0", "dprint": "^0.55.1", "http-server": "^14.1.1", "inquirer": "^14.0.2", diff --git a/packages/ui/package.json b/packages/ui/package.json index 21f888c9e1..778dd7cd47 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -34,6 +34,7 @@ "@instructure/ui-buttons": "workspace:*", "@instructure/ui-byline": "workspace:*", "@instructure/ui-calendar": "workspace:*", + "@instructure/ui-card": "workspace:*", "@instructure/ui-checkbox": "workspace:*", "@instructure/ui-color-picker": "workspace:*", "@instructure/ui-date-input": "workspace:*", diff --git a/packages/ui/src/v11_7.ts b/packages/ui/src/v11_7.ts index a0b7911323..7b8045bd6c 100644 --- a/packages/ui/src/v11_7.ts +++ b/packages/ui/src/v11_7.ts @@ -78,6 +78,8 @@ export type { CalendarDayProps, CalendarProps } from '@instructure/ui-calendar/v11_7' +export { Card } from '@instructure/ui-card/v11_7' +export type { CardProps } from '@instructure/ui-card/v11_7' export { Checkbox, CheckboxGroup, diff --git a/packages/ui/tsconfig.build.json b/packages/ui/tsconfig.build.json index 85f3206f78..f57dd0435c 100644 --- a/packages/ui/tsconfig.build.json +++ b/packages/ui/tsconfig.build.json @@ -37,6 +37,9 @@ { "path": "../ui-calendar/tsconfig.build.json" }, + { + "path": "../ui-card/tsconfig.build.json" + }, { "path": "../ui-checkbox/tsconfig.build.json" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4f0078d86..01c82f763c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -249,6 +249,9 @@ importers: '@instructure/ui-calendar': specifier: workspace:* version: link:../ui-calendar + '@instructure/ui-card': + specifier: workspace:* + version: link:../ui-card '@instructure/ui-checkbox': specifier: workspace:* version: link:../ui-checkbox @@ -707,6 +710,9 @@ importers: '@instructure/ui-calendar': specifier: workspace:* version: link:../ui-calendar + '@instructure/ui-card': + specifier: workspace:* + version: link:../ui-card '@instructure/ui-checkbox': specifier: workspace:* version: link:../ui-checkbox @@ -1497,6 +1503,43 @@ importers: specifier: ^4.1.9 version: 4.1.9(@types/node@22.19.15)(@vitest/browser-playwright@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1)(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0)) + packages/ui-card: + dependencies: + '@babel/runtime': + specifier: ^7.29.7 + version: 7.29.7 + '@instructure/emotion': + specifier: workspace:* + version: link:../emotion + '@instructure/shared-types': + specifier: workspace:* + version: link:../shared-types + '@instructure/ui-react-utils': + specifier: workspace:* + version: link:../ui-react-utils + '@instructure/ui-themes': + specifier: workspace:* + version: link:../ui-themes + react: + specifier: 18.3.1 + version: 18.3.1 + devDependencies: + '@instructure/ui-axe-check': + specifier: workspace:* + version: link:../ui-axe-check + '@instructure/ui-babel-preset': + specifier: workspace:* + version: link:../ui-babel-preset + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: 16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + vitest: + specifier: ^4.1.9 + version: 4.1.9(@types/node@24.1.0)(jsdom@29.1.1)(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0)) + packages/ui-checkbox: dependencies: '@babel/runtime': @@ -3930,8 +3973,8 @@ importers: specifier: workspace:* version: link:../command-utils '@instructure/instructure-design-tokens': - specifier: github:instructure/instructure-design-tokens#v1.5.0 - version: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/ae8f600e8ad4cbadddbaad857f0b6477c4a1e1d6 + specifier: github:instructure/instructure-design-tokens#v1.6.0 + version: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/4828298799329c972e8515ed959a789e8757906b dprint: specifier: ^0.55.1 version: 0.55.1 @@ -7099,8 +7142,8 @@ packages: '@types/node': optional: true - '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/ae8f600e8ad4cbadddbaad857f0b6477c4a1e1d6': - resolution: {gitHosted: true, tarball: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/ae8f600e8ad4cbadddbaad857f0b6477c4a1e1d6} + '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/4828298799329c972e8515ed959a789e8757906b': + resolution: {gitHosted: true, integrity: sha512-hpkiuQ127m9VWCN0z9Ap+dlWTZzL99QK2yFkPWUieDXRHsLGIUC/9wsdVgXcZ/8XupOXQ7C2gWDelsCkXKyXMg==, tarball: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/4828298799329c972e8515ed959a789e8757906b} version: 1.0.0 '@isaacs/cliui@8.0.2': @@ -15255,7 +15298,7 @@ snapshots: optionalDependencies: '@types/node': 22.19.15 - '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/ae8f600e8ad4cbadddbaad857f0b6477c4a1e1d6': + '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/4828298799329c972e8515ed959a789e8757906b': dependencies: glob: 13.0.6 @@ -18064,8 +18107,7 @@ snapshots: es-module-lexer@2.1.0: {} - es-module-lexer@2.2.0: - optional: true + es-module-lexer@2.2.0: {} es-object-atoms@1.1.1: dependencies: @@ -22269,7 +22311,7 @@ snapshots: '@vitest/snapshot': 4.1.9 '@vitest/spy': 4.1.9 '@vitest/utils': 4.1.9 - es-module-lexer: 2.1.0 + es-module-lexer: 2.2.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.3 @@ -22299,7 +22341,7 @@ snapshots: '@vitest/snapshot': 4.1.9 '@vitest/spy': 4.1.9 '@vitest/utils': 4.1.9 - es-module-lexer: 2.1.0 + es-module-lexer: 2.2.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.3 From cd51009692ec0303c8edd412120145d1beb3739f Mon Sep 17 00:00:00 2001 From: adamlobler Date: Mon, 27 Jul 2026 15:16:45 +0200 Subject: [PATCH 2/6] docs(ui-card): document Card with nested examples and usage guidelines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds examples for each size, nested cards at every size, mixed sizes inside one base Card, and a base Card holding several nested Cards with heading and body copy. Adds a do/don't guidelines section for the two composition rules: a base Card must not contain another base Card, and a nested Card needs a surface behind it (a Card, Modal, or utility panel such as Tray) because it has no background colour of its own. Examples use Heading and Text rather than bare strings so example copy picks up token-driven colours and stays legible in dark mode. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 --- packages/ui-card/src/Card/v1/README.md | 172 +++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 packages/ui-card/src/Card/v1/README.md diff --git a/packages/ui-card/src/Card/v1/README.md b/packages/ui-card/src/Card/v1/README.md new file mode 100644 index 0000000000..021f8e0b4e --- /dev/null +++ b/packages/ui-card/src/Card/v1/README.md @@ -0,0 +1,172 @@ +--- +describes: Card +--- + +Use `` as a basic wrapper for grouping related content with padding, +border radius, shadow, and background color. + +```js +--- +type: example +--- + + + Base card + + Medium size, the default. + +``` + +### Sizes + +`size` controls padding, border radius, and the card's min-/max-width +breakpoints: `sm` applies a max-width, `md` applies a min- and max-width, and +`lg` applies a min-width. + +```js +--- +type: example +--- +
+ + Small card + + + Medium card + + + Large card + +
+``` + +### Nested cards + +The `nested` variant is meant to be placed inside a `base` Card. It omits the +background color and shadow, and uses a smaller border radius. A nested +Card's `size` is independent of its parent's — choose it based on the nested +content's own needs, not to match the parent. + +```js +--- +type: example +--- +
+ + + Nested small card + + + + + Nested medium card + + + + + Nested large card + + +
+``` + +### Mixed sizes + +Nested Cards don't need to match their parent's size, or each other's — you +can place several different-sized `nested` Cards side by side inside one +larger `base` Card. + +```js +--- +type: example +--- + +
+ + Small nested card + + + Medium nested card + +
+
+``` + +### Multiple nested cards + +A `base` Card can hold several `nested` Cards, for example to lay out a group +of related sub-sections inside a single container. + +```js +--- +type: example +--- + +
+ + + Course settings + + + Control who can see this course and when it becomes available to + students. + + + + + Grading + + + Choose the grading scheme and decide how final grades are calculated. + + + + + Notifications + + + Pick which course events should send an alert to enrolled users. + + +
+
+``` + +### Guidelines + +```js +--- +type: embed +--- + +
+ + Place a nested Card inside a base Card + + + Place a nested Card on top of another surface, such as a + Card, Modal, or utility panel (e.g. Tray, DrawerLayout) + + + Size each nested Card independently, based on its own + content — it doesn't need to match its parent{' '} + base Card's size + + + Give a row of md/lg nested Cards + enough width to fit their minimum widths, or let them wrap, when + placing several side by side + +
+
+ + Place a base Card inside another base Card + + + Place a nested Card directly on the page background — it + relies on a surface behind it for contrast, since it has no background + color of its own + +
+
+``` From fff8ec85d5e0e6807c8dacfa2ae12a70ea2714be Mon Sep 17 00:00:00 2001 From: adamlobler Date: Mon, 27 Jul 2026 15:17:22 +0200 Subject: [PATCH 3/6] test(ui-card,regression-test): add Card unit tests and visual regression page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unit tests assert concrete token values (the 20rem and 40rem width breakpoints, and padding and radius relationships per variant) instead of only asserting that some value is set, so a swapped size or variant token fails the suite rather than passing silently. Adds the /card regression page and its axe check to the Cypress spec. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5 --- .../ui-card/src/Card/__tests__/Card.test.tsx | 157 ++++++++++++++++++ regression-test/cypress/e2e/spec.cy.ts | 1 + regression-test/src/app/card/page.tsx | 68 ++++++++ regression-test/src/app/page.tsx | 1 + 4 files changed, 227 insertions(+) create mode 100644 packages/ui-card/src/Card/__tests__/Card.test.tsx create mode 100644 regression-test/src/app/card/page.tsx diff --git a/packages/ui-card/src/Card/__tests__/Card.test.tsx b/packages/ui-card/src/Card/__tests__/Card.test.tsx new file mode 100644 index 0000000000..e603f12956 --- /dev/null +++ b/packages/ui-card/src/Card/__tests__/Card.test.tsx @@ -0,0 +1,157 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import { render, screen } from '@testing-library/react' +import { runAxeCheck } from '@instructure/ui-axe-check' + +import '@testing-library/jest-dom' +import { Card } from '@instructure/ui-card/latest' + +// Values come from the generated Card theme tokens, see +// packages/ui-themes/src/themes/newThemeTokens//components/card.ts +const BREAKPOINT_MD = '20rem' +const BREAKPOINT_LG = '40rem' + +const renderCard = (props = {}) => { + const { container } = render(content) + const card = container.querySelector('div')! + return { card, style: getComputedStyle(card) } +} + +describe('', () => { + describe('for a11y', () => { + it('should be accessible', async () => { + const { container } = render(content) + const axeCheck = await runAxeCheck(container) + expect(axeCheck).toBe(true) + }) + }) + + it('should render children', () => { + render(Hello world) + expect(screen.getByText('Hello world')).toBeInTheDocument() + }) + + it('should render a div', () => { + const { card } = renderCard() + expect(card.tagName).toBe('DIV') + }) + + it('should pass through arbitrary HTML attributes', () => { + render(content) + expect(document.getElementById('my-card')).toBeInTheDocument() + }) + + it('should not leak component props onto the DOM node', () => { + const { card } = renderCard({ variant: 'nested', size: 'lg' }) + expect(card).not.toHaveAttribute('variant') + expect(card).not.toHaveAttribute('size') + }) + + describe('variant', () => { + it('base should render a background and a box-shadow', () => { + const { style } = renderCard({ variant: 'base' }) + // an opaque surface colour, not the transparent jsdom default + expect(style.backgroundColor).not.toBe('') + expect(style.backgroundColor).not.toBe('rgba(0, 0, 0, 0)') + expect(style.boxShadow).toContain('rgba') + }) + + it('nested should render a border instead of a background and shadow', () => { + const { style } = renderCard({ variant: 'nested' }) + // positively assert the border the nested variant *does* apply + expect(style.borderStyle).toBe('solid') + expect(style.borderWidth).toBe('1px') + expect(style.borderColor).not.toBe('') + // and that it opts out of the base surface treatment + expect(style.backgroundColor).toBe('rgba(0, 0, 0, 0)') + expect(style.boxShadow).toBe('') + }) + + it('should apply a smaller padding for nested than for base at the same size', () => { + const base = renderCard({ variant: 'base', size: 'md' }) + const nested = renderCard({ variant: 'nested', size: 'md' }) + expect(base.style.padding).not.toBe(nested.style.padding) + expect(parseFloat(nested.style.padding)).toBeLessThan( + parseFloat(base.style.padding) + ) + }) + }) + + describe('size', () => { + it('sm should cap width at the md breakpoint with no min-width', () => { + const { style } = renderCard({ size: 'sm' }) + expect(style.maxWidth).toBe(BREAKPOINT_MD) + expect(style.minWidth).toBe('auto') + }) + + it('md should span the md and lg breakpoints', () => { + const { style } = renderCard({ size: 'md' }) + expect(style.minWidth).toBe(BREAKPOINT_MD) + expect(style.maxWidth).toBe(BREAKPOINT_LG) + }) + + it('lg should start at the lg breakpoint with no max-width', () => { + const { style } = renderCard({ size: 'lg' }) + expect(style.minWidth).toBe(BREAKPOINT_LG) + expect(style.maxWidth).toBe('none') + }) + + it('should grow padding from sm through lg', () => { + const sm = parseFloat(renderCard({ size: 'sm' }).style.padding) + const md = parseFloat(renderCard({ size: 'md' }).style.padding) + const lg = parseFloat(renderCard({ size: 'lg' }).style.padding) + expect(sm).toBeLessThan(md) + expect(md).toBeLessThan(lg) + }) + + // The documented contract is that `nested` uses a smaller radius than + // `base`. Absolute radii are deliberately not asserted: the legacy Canvas + // themes flatten every radius token to the same value, so only the + // relationship between the two variants holds across all four themes. + it.each(['sm', 'md', 'lg'] as const)( + 'nested should not have a larger border-radius than base at size %s', + (size) => { + const base = parseFloat( + renderCard({ variant: 'base', size }).style.borderRadius + ) + const nested = parseFloat( + renderCard({ variant: 'nested', size }).style.borderRadius + ) + expect(nested).toBeLessThanOrEqual(base) + } + ) + }) + + describe('defaults', () => { + it('should default to the base variant at the md size', () => { + const bare = renderCard() + const explicit = renderCard({ variant: 'base', size: 'md' }) + expect(bare.style.padding).toBe(explicit.style.padding) + expect(bare.style.minWidth).toBe(BREAKPOINT_MD) + expect(bare.style.maxWidth).toBe(BREAKPOINT_LG) + expect(bare.style.boxShadow).toContain('rgba') + }) + }) +}) diff --git a/regression-test/cypress/e2e/spec.cy.ts b/regression-test/cypress/e2e/spec.cy.ts index 62424e9fda..f26ae0e30b 100644 --- a/regression-test/cypress/e2e/spec.cy.ts +++ b/regression-test/cypress/e2e/spec.cy.ts @@ -124,6 +124,7 @@ const PAGES: PageSpec[] = [ { slug: 'button', title: 'Button and derivatives', wait: 100 }, { slug: 'byline', title: 'Byline' }, { slug: 'calendar', title: 'Calendar' }, + { slug: 'card', title: 'Card' }, { slug: 'checkbox', title: 'Checkbox', wait: 100 }, { slug: 'checkboxgroup', title: 'Checkboxgroup', wait: 300 }, { diff --git a/regression-test/src/app/card/page.tsx b/regression-test/src/app/card/page.tsx new file mode 100644 index 0000000000..0450e2bae9 --- /dev/null +++ b/regression-test/src/app/card/page.tsx @@ -0,0 +1,68 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2015 - present Instructure, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +'use client' +import React from 'react' +import { Card as c, Text as t } from '@instructure/ui/latest' + +// alias to avoid TS/SSR friction like other pages +const Card = c as any +const Text = t as any + +export default function CardPage() { + return ( +
+ {/* Sizes */} +
+ + Small base card + + + Medium base card + + + Large base card + +
+ + {/* Nested cards */} +
+ + + Nested small card + + + + + Nested medium card + + + + + Nested large card + + +
+
+ ) +} diff --git a/regression-test/src/app/page.tsx b/regression-test/src/app/page.tsx index 0a5a4e7a6c..1ae7355d83 100644 --- a/regression-test/src/app/page.tsx +++ b/regression-test/src/app/page.tsx @@ -37,6 +37,7 @@ const components = [ 'tooltip', 'byline', 'calendar', + 'card', 'checkbox', 'checkboxgroup', 'colorpicker', From 1005540ee599abb4de1c48232ce9a8fec9fcca8b Mon Sep 17 00:00:00 2001 From: adamlobler Date: Mon, 27 Jul 2026 17:15:53 +0200 Subject: [PATCH 4/6] docs(ui-card): use lorem ipsum instead of course-specific copy in examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The multiple nested cards example used Canvas-specific text (course settings, grading, notifications), which reads as domain-specific product copy rather than a generic component example. Swapped to lorem ipsum so the example stays neutral. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5 --- packages/ui-card/src/Card/v1/README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/ui-card/src/Card/v1/README.md b/packages/ui-card/src/Card/v1/README.md index 021f8e0b4e..e4025acb6f 100644 --- a/packages/ui-card/src/Card/v1/README.md +++ b/packages/ui-card/src/Card/v1/README.md @@ -105,27 +105,29 @@ type: example
- Course settings + Lorem ipsum - Control who can see this course and when it becomes available to - students. + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do + eiusmod tempor incididunt ut labore. - Grading + Dolor sit amet - Choose the grading scheme and decide how final grades are calculated. + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo. - Notifications + Consectetur adipiscing - Pick which course events should send an alert to enrolled users. + Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla.
From 6080286e199f395ba494bc8f6e008abc68edead8 Mon Sep 17 00:00:00 2001 From: adamlobler Date: Wed, 29 Jul 2026 12:54:10 +0200 Subject: [PATCH 5/6] build(ui-scripts): bump instructure-design-tokens to v1.7.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/ui-scripts/package.json | 2 +- pnpm-lock.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/ui-scripts/package.json b/packages/ui-scripts/package.json index 00b879d511..6511be8c25 100644 --- a/packages/ui-scripts/package.json +++ b/packages/ui-scripts/package.json @@ -23,7 +23,7 @@ "dependencies": { "@babel/cli": "^7.27.2", "@instructure/command-utils": "workspace:*", - "@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.6.0", + "@instructure/instructure-design-tokens": "github:instructure/instructure-design-tokens#v1.7.0", "dprint": "^0.55.1", "http-server": "^14.1.1", "inquirer": "^14.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01c82f763c..2300c2b9f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1538,7 +1538,7 @@ importers: version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.1)(@types/react@18.3.26)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) vitest: specifier: ^4.1.9 - version: 4.1.9(@types/node@24.1.0)(jsdom@29.1.1)(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0)) + version: 4.1.9(@types/node@24.1.0)(@vitest/browser-playwright@4.1.9)(@vitest/ui@4.1.9)(jsdom@29.1.1)(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(terser@5.48.0)(yaml@2.9.0)) packages/ui-checkbox: dependencies: @@ -3973,8 +3973,8 @@ importers: specifier: workspace:* version: link:../command-utils '@instructure/instructure-design-tokens': - specifier: github:instructure/instructure-design-tokens#v1.6.0 - version: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/4828298799329c972e8515ed959a789e8757906b + specifier: github:instructure/instructure-design-tokens#v1.7.0 + version: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/1200a1bdbcf1b05ad9ebe0fd466fe645e6b2156c dprint: specifier: ^0.55.1 version: 0.55.1 @@ -7142,8 +7142,8 @@ packages: '@types/node': optional: true - '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/4828298799329c972e8515ed959a789e8757906b': - resolution: {gitHosted: true, integrity: sha512-hpkiuQ127m9VWCN0z9Ap+dlWTZzL99QK2yFkPWUieDXRHsLGIUC/9wsdVgXcZ/8XupOXQ7C2gWDelsCkXKyXMg==, tarball: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/4828298799329c972e8515ed959a789e8757906b} + '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/1200a1bdbcf1b05ad9ebe0fd466fe645e6b2156c': + resolution: {gitHosted: true, integrity: sha512-cL6sntorkv8WvSOd9JkjgKvrro8mXNIUmki+/0WuDgcMy4cmPA9wPPd0bRqCt1y21z61HCg5kkvfwnonOhh25w==, tarball: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/1200a1bdbcf1b05ad9ebe0fd466fe645e6b2156c} version: 1.0.0 '@isaacs/cliui@8.0.2': @@ -15298,7 +15298,7 @@ snapshots: optionalDependencies: '@types/node': 22.19.15 - '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/4828298799329c972e8515ed959a789e8757906b': + '@instructure/instructure-design-tokens@https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/1200a1bdbcf1b05ad9ebe0fd466fe645e6b2156c': dependencies: glob: 13.0.6 From fb2cb9c0a6618f75db6ca1888d02a558240f2433 Mon Sep 17 00:00:00 2001 From: adamlobler Date: Wed, 29 Jul 2026 13:52:11 +0200 Subject: [PATCH 6/6] fix(ui-card): remove width breakpoints from nested Card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nested variant shared the same min-/max-width breakpoint tokens as base, but it renders inside base's padded content box. A nested card sized the same as (or larger than) its parent could structurally fail to fit once the parent was near its own min-width. nested no longer enforces a width at any size; size still scales its padding and border radius. Width now comes from the parent and the surrounding layout, matching the existing guidance to let nested cards wrap. Card hasn't shipped yet, so this isn't a breaking change for consumers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../ui-card/src/Card/__tests__/Card.test.tsx | 12 ++++++++++ packages/ui-card/src/Card/v1/README.md | 21 ++++++++++-------- packages/ui-card/src/Card/v1/props.ts | 8 ++++--- packages/ui-card/src/Card/v1/styles.ts | 22 +++++++++++++++---- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/packages/ui-card/src/Card/__tests__/Card.test.tsx b/packages/ui-card/src/Card/__tests__/Card.test.tsx index e603f12956..70a11d0a3e 100644 --- a/packages/ui-card/src/Card/__tests__/Card.test.tsx +++ b/packages/ui-card/src/Card/__tests__/Card.test.tsx @@ -126,6 +126,18 @@ describe('', () => { expect(md).toBeLessThan(lg) }) + // `nested` must never enforce a width: it lives inside a `base` Card's + // padded content box, so its available width is already smaller than + // the breakpoint tokens it would otherwise share with `base`. + it.each(['sm', 'md', 'lg'] as const)( + 'nested should apply no width constraint at size %s', + (size) => { + const { style } = renderCard({ variant: 'nested', size }) + expect(style.minWidth).toBe('auto') + expect(style.maxWidth).toBe('none') + } + ) + // The documented contract is that `nested` uses a smaller radius than // `base`. Absolute radii are deliberately not asserted: the legacy Canvas // themes flatten every radius token to the same value, so only the diff --git a/packages/ui-card/src/Card/v1/README.md b/packages/ui-card/src/Card/v1/README.md index e4025acb6f..e87479b6c5 100644 --- a/packages/ui-card/src/Card/v1/README.md +++ b/packages/ui-card/src/Card/v1/README.md @@ -19,9 +19,9 @@ type: example ### Sizes -`size` controls padding, border radius, and the card's min-/max-width -breakpoints: `sm` applies a max-width, `md` applies a min- and max-width, and -`lg` applies a min-width. +`size` controls padding and border radius. For `variant="base"`, it also +controls the card's min-/max-width breakpoints: `sm` applies a max-width, +`md` applies a min- and max-width, and `lg` applies a min-width. ```js --- @@ -43,9 +43,12 @@ type: example ### Nested cards The `nested` variant is meant to be placed inside a `base` Card. It omits the -background color and shadow, and uses a smaller border radius. A nested -Card's `size` is independent of its parent's — choose it based on the nested -content's own needs, not to match the parent. +background color and shadow, and uses a smaller border radius. Unlike +`base`, `nested` doesn't apply min-/max-width breakpoints — its width comes +from its parent and the surrounding layout, since it's already constrained +by the `base` Card's padded content area. A nested Card's `size` still +scales its own padding and border radius independently of its parent's — +choose it based on the nested content's own needs, not to match the parent. ```js --- @@ -155,9 +158,9 @@ type: embed base Card's size - Give a row of md/lg nested Cards - enough width to fit their minimum widths, or let them wrap, when - placing several side by side + Use a wrapping flex or grid layout when placing several{' '} + nested Cards side by side, so they can reflow to fit the + available width
diff --git a/packages/ui-card/src/Card/v1/props.ts b/packages/ui-card/src/Card/v1/props.ts index d3f831005e..d49d1d6958 100644 --- a/packages/ui-card/src/Card/v1/props.ts +++ b/packages/ui-card/src/Card/v1/props.ts @@ -39,9 +39,11 @@ type CardOwnProps = { */ variant?: 'base' | 'nested' /** - * `sm` applies a max-width, `md` applies a min- and max-width, and `lg` - * applies a min-width, each based on breakpoint tokens. Padding and - * border radius also scale per size. + * Scales padding and border radius. For `variant="base"` only, `size` + * also applies min-/max-width breakpoints: `sm` applies a max-width, `md` + * applies a min- and max-width, and `lg` applies a min-width. `nested` + * doesn't enforce a width — its available space is already constrained + * by its parent `base` Card. */ size?: 'sm' | 'md' | 'lg' } diff --git a/packages/ui-card/src/Card/v1/styles.ts b/packages/ui-card/src/Card/v1/styles.ts index 59c2023fd9..df0d262c2a 100644 --- a/packages/ui-card/src/Card/v1/styles.ts +++ b/packages/ui-card/src/Card/v1/styles.ts @@ -43,19 +43,33 @@ const generateStyle = ( const boxShadow = boxShadowObjectsToCSSString(componentTheme.boxShadow) + // Width breakpoints only apply to `base`: a `nested` Card's available width + // is already constrained by its parent's padded content box, so enforcing + // the same breakpoints on `nested` could make it not fit inside `base`. + const widthVariants = + variant === 'base' + ? { + sm: { maxWidth: componentTheme.breakpoint.md }, + md: { + minWidth: componentTheme.breakpoint.md, + maxWidth: componentTheme.breakpoint.lg + }, + lg: { minWidth: componentTheme.breakpoint.lg } + } + : { sm: {}, md: {}, lg: {} } + const sizeVariants = { sm: { padding: componentTheme.padding[variant].sm, - maxWidth: componentTheme.breakpoint.md + ...widthVariants.sm }, md: { padding: componentTheme.padding[variant].md, - minWidth: componentTheme.breakpoint.md, - maxWidth: componentTheme.breakpoint.lg + ...widthVariants.md }, lg: { padding: componentTheme.padding[variant].lg, - minWidth: componentTheme.breakpoint.lg + ...widthVariants.lg } }