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/__tests__/Card.test.tsx b/packages/ui-card/src/Card/__tests__/Card.test.tsx new file mode 100644 index 0000000000..70a11d0a3e --- /dev/null +++ b/packages/ui-card/src/Card/__tests__/Card.test.tsx @@ -0,0 +1,169 @@ +/* + * 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) + }) + + // `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 + // 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/packages/ui-card/src/Card/v1/README.md b/packages/ui-card/src/Card/v1/README.md new file mode 100644 index 0000000000..e87479b6c5 --- /dev/null +++ b/packages/ui-card/src/Card/v1/README.md @@ -0,0 +1,177 @@ +--- +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 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 +--- +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. 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 +--- +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 +--- + +
+ + + Lorem ipsum + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do + eiusmod tempor incididunt ut labore. + + + + + Dolor sit amet + + + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris + nisi ut aliquip ex ea commodo. + + + + + Consectetur adipiscing + + + Duis aute irure dolor in reprehenderit in voluptate velit esse + cillum dolore eu fugiat nulla. + + +
+
+``` + +### 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 + + + Use a wrapping flex or grid layout when placing several{' '} + nested Cards side by side, so they can reflow to fit the + available width + +
+
+ + 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 + +
+
+``` 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..d49d1d6958 --- /dev/null +++ b/packages/ui-card/src/Card/v1/props.ts @@ -0,0 +1,64 @@ +/* + * 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' + /** + * 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' +} + +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..df0d262c2a --- /dev/null +++ b/packages/ui-card/src/Card/v1/styles.ts @@ -0,0 +1,97 @@ +/* + * 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) + + // 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, + ...widthVariants.sm + }, + md: { + padding: componentTheme.padding[variant].md, + ...widthVariants.md + }, + lg: { + padding: componentTheme.padding[variant].lg, + ...widthVariants.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..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.5.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/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..2300c2b9f4 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)(@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: '@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.7.0 + version: https://codeload.github.com/instructure/instructure-design-tokens/tar.gz/1200a1bdbcf1b05ad9ebe0fd466fe645e6b2156c 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/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': @@ -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/1200a1bdbcf1b05ad9ebe0fd466fe645e6b2156c': 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 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',