├── .gitattributes ├── .prettierignore ├── __mocks__ └── nanoid.js ├── docs ├── assets │ └── componentry.png ├── TODO.md ├── Tailwind.md ├── Theming.md ├── CONTRIBUTING.md ├── publishing.stories.mdx └── adr │ ├── 03-utilities.md │ ├── template.md │ └── 02-classes.md ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── ci-cd.yml ├── .renovaterc ├── .storybook ├── preview-head.html ├── preview.js ├── manager.js ├── preview-body.html ├── main.js └── storybook-styles.css ├── src ├── plugin-postcss │ └── index.ts ├── plugin-babel │ ├── index.ts │ └── __fixtures__ │ │ ├── data-precompiled │ │ ├── code.js │ │ └── output.js │ │ ├── ignores-components │ │ ├── code.js │ │ └── output.js │ │ ├── refs │ │ ├── code.js │ │ └── output.js │ │ ├── as-prop │ │ ├── code.js │ │ └── output.js │ │ ├── exclude_option │ │ ├── code.js │ │ └── output.js │ │ ├── transforms-components │ │ ├── code.js │ │ └── output.js │ │ ├── passthrough-props │ │ ├── code.js │ │ └── output.js │ │ ├── checks-import-paths │ │ ├── code.js │ │ └── output.js │ │ ├── styles-props │ │ ├── code.js │ │ └── output.js │ │ ├── component-props │ │ ├── code.js │ │ └── output.js │ │ ├── classname-props │ │ ├── code.js │ │ └── output.js │ │ └── prop-types │ │ ├── code.js │ │ └── output.js ├── components │ ├── Drawer │ │ ├── _drawer.scss │ │ ├── __snapshots__ │ │ │ └── Drawer.spec.jsx.snap │ │ ├── Drawer.spec.jsx │ │ ├── Drawer.stories.tsx │ │ └── Drawer.ts │ ├── Table │ │ ├── Table.spec.jsx │ │ ├── Table.stories.tsx │ │ ├── Table.styles.ts │ │ └── Table.ts │ ├── FormGroup │ │ ├── __snapshots__ │ │ │ └── FormGroup.spec.jsx.snap │ │ ├── FormGroup.styles.ts │ │ ├── FormGroup.ts │ │ ├── FormGroup.spec.jsx │ │ └── FormGroup.stories.tsx │ ├── Text │ │ ├── __snapshots__ │ │ │ └── Text.spec.jsx.snap │ │ ├── Text.spec.jsx │ │ ├── Text.styles.ts │ │ ├── Text.ts │ │ └── Text.stories.tsx │ ├── Block │ │ ├── __snapshots__ │ │ │ └── Block.spec.jsx.snap │ │ ├── Block.spec.jsx │ │ ├── Block.stories.tsx │ │ └── Block.ts │ ├── Flex │ │ ├── __snapshots__ │ │ │ └── Flex.spec.jsx.snap │ │ ├── Flex.stories.tsx │ │ ├── Flex.spec.jsx │ │ └── Flex.ts │ ├── Grid │ │ ├── __snapshots__ │ │ │ └── Grid.spec.jsx.snap │ │ ├── Grid.stories.tsx │ │ ├── Grid.spec.jsx │ │ └── Grid.ts │ ├── Link │ │ ├── __snapshots__ │ │ │ └── Link.spec.jsx.snap │ │ ├── Link.spec.jsx │ │ ├── Link.stories.tsx │ │ ├── Link.styles.ts │ │ └── Link.ts │ ├── Paper │ │ ├── __snapshots__ │ │ │ └── Paper.spec.jsx.snap │ │ ├── Paper.stories.tsx │ │ ├── Paper.spec.jsx │ │ ├── Paper.styles.ts │ │ └── Paper.ts │ ├── Badge │ │ ├── __snapshots__ │ │ │ └── Badge.spec.jsx.snap │ │ ├── Badge.stories.tsx │ │ ├── Badge.spec.jsx │ │ ├── Badge.styles.ts │ │ └── Badge.ts │ ├── Button │ │ ├── __snapshots__ │ │ │ └── Button.spec.jsx.snap │ │ └── Button.stories.tsx │ ├── Icon │ │ ├── Icon.stories.tsx │ │ ├── __snapshots__ │ │ │ └── Icon.spec.jsx.snap │ │ ├── Icon.spec.jsx │ │ ├── Icon.styles.ts │ │ └── Icon.tsx │ ├── Close │ │ ├── Close.stories.tsx │ │ ├── __snapshots__ │ │ │ └── Close.spec.jsx.snap │ │ ├── Close.spec.jsx │ │ ├── Close.tsx │ │ └── Close.styles.ts │ ├── IconButton │ │ ├── __snapshots__ │ │ │ └── IconButton.spec.jsx.snap │ │ ├── IconButton.tsx │ │ └── IconButton.spec.jsx │ ├── Active │ │ ├── __snapshots__ │ │ │ └── Active.spec.jsx.snap │ │ ├── Active.stories.tsx │ │ ├── Active.spec.jsx │ │ ├── Active.ts │ │ └── active-types.ts │ ├── Card │ │ ├── __snapshots__ │ │ │ └── Card.spec.jsx.snap │ │ ├── Card.spec.jsx │ │ ├── Card.stories.tsx │ │ └── Card.styles.ts │ ├── Input │ │ ├── __snapshots__ │ │ │ └── Input.spec.jsx.snap │ │ ├── Input.stories.tsx │ │ └── Input.spec.jsx │ ├── Tooltip │ │ ├── Tooltip.stories.tsx │ │ ├── __snapshots__ │ │ │ └── Tooltip.spec.jsx.snap │ │ ├── Tooltip.spec.jsx │ │ ├── Tooltip.tsx │ │ └── Tooltip.styles.ts │ ├── Alert │ │ ├── __snapshots__ │ │ │ └── Alert.spec.jsx.snap │ │ ├── Alert.stories.tsx │ │ ├── Alert.spec.jsx │ │ └── Alert.styles.ts │ ├── Media │ │ ├── Media.stories.tsx │ │ └── Media.tsx │ ├── Dropdown │ │ ├── __snapshots__ │ │ │ └── Dropdown.spec.jsx.snap │ │ ├── Dropdown.stories.tsx │ │ ├── Dropdown.spec.jsx │ │ └── Dropdown.ts │ ├── Popover │ │ ├── Popover.stories.tsx │ │ ├── __snapshots__ │ │ │ └── Popover.spec.jsx.snap │ │ └── Popover.spec.jsx │ ├── Provider │ │ ├── Provider.stories.tsx │ │ ├── Provider.spec.jsx │ │ └── Provider.tsx │ ├── Tabs │ │ ├── Tabs.stories.tsx │ │ ├── Tabs.spec.jsx │ │ └── __snapshots__ │ │ │ └── Tabs.spec.jsx.snap │ └── Modal │ │ ├── Modal.spec.jsx │ │ ├── Modal.stories.tsx │ │ └── __snapshots__ │ │ └── Modal.spec.jsx.snap ├── test │ └── vitest.setup.mjs ├── utils │ ├── types.ts │ ├── deep-merge.ts │ ├── create-static-component.tsx │ ├── tailwind-plugins.ts │ ├── create-active-content-component.tsx │ ├── create-element.tsx │ ├── tailwind-safelist.ts │ ├── dom.ts │ └── create-active-action-component.tsx ├── api-types.ts ├── theme │ ├── create-theme.spec.js │ └── theme.ts ├── styles │ └── states.styles.ts ├── index.ts └── config │ └── load-config.ts ├── .prettierrc ├── .codeclimate.yml ├── @types └── postcss-js.d.ts ├── postcss.config.js ├── .gitignore ├── tailwind.config.js ├── vitest.config.mjs ├── .vscode └── launch.json ├── .npmignore ├── LICENSE ├── README.md ├── .babelrc.js ├── tsconfig.json └── eslint.config.mjs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.json linguist-language=JSON-with-Comments -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Skip formatting files generated or managed by other tools 2 | 3 | CHANGELOG.md 4 | -------------------------------------------------------------------------------- /__mocks__/nanoid.js: -------------------------------------------------------------------------------- 1 | // Use stable id in testing for snapshots 2 | export const nanoid = () => 'test-guid' 3 | -------------------------------------------------------------------------------- /docs/assets/componentry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crystal-ball/componentry/HEAD/docs/assets/componentry.png -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code Owners 2 | 3 | # Package code owners will be requested for review for pull requests 4 | * @dhedgecock -------------------------------------------------------------------------------- /.renovaterc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>crystal-ball/renovate-base:library" 4 | ], 5 | "packageRules": [] 6 | } 7 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/plugin-postcss/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Componentry PostCSS plugin entry 3 | */ 4 | export { plugin as default } from './plugin' 5 | -------------------------------------------------------------------------------- /src/plugin-babel/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Componentry components pre-compile Babel plugin entry 3 | */ 4 | export { default } from './plugin' 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxSingleQuote": true, 3 | "printWidth": 90, 4 | "proseWrap": "always", 5 | "semi": false, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Drawer/_drawer.scss: -------------------------------------------------------------------------------- 1 | /* componentry/src/Drawer/drawer */ 2 | 3 | .drawer-action-primary { 4 | @include active-action-element; 5 | display: block; 6 | } 7 | -------------------------------------------------------------------------------- /src/test/vitest.setup.mjs: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | 3 | // Setup custom jest matchers to test the state of the DOM 4 | // https://github.com/gnapse/jest-dom 5 | import '@testing-library/jest-dom' 6 | 7 | vi.mock('nanoid') 8 | -------------------------------------------------------------------------------- /src/components/Table/Table.spec.jsx: -------------------------------------------------------------------------------- 1 | import { describe } from 'vitest' 2 | import { elementTests } from '../../test/element-tests' 3 | import { Table } from './Table' 4 | 5 | describe('', () => { 6 | elementTests(Table) 7 | }) 8 | -------------------------------------------------------------------------------- /src/components/FormGroup/__snapshots__/FormGroup.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
8 | `; 9 | -------------------------------------------------------------------------------- /src/components/Text/__snapshots__/Text.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
7 | Componentry 8 |
9 | `; 10 | -------------------------------------------------------------------------------- /src/components/Block/__snapshots__/Block.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
8 | Block content 9 |
10 | `; 11 | -------------------------------------------------------------------------------- /src/components/Flex/__snapshots__/Flex.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
8 | Flex content 9 |
10 | `; 11 | -------------------------------------------------------------------------------- /src/components/Grid/__snapshots__/Grid.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
8 | Block content 9 |
10 | `; 11 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/data-precompiled/code.js: -------------------------------------------------------------------------------- 1 | import { Text } from 'componentry' 2 | 3 | export default function Test() { 4 | // Test that: 5 | // 1. Plugin option `dataFlag` results in a `data-precompiled` flag included 6 | return Precompiled for speed 7 | } 8 | -------------------------------------------------------------------------------- /src/components/Link/__snapshots__/Link.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 | 9 | Link 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /src/components/Paper/__snapshots__/Paper.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
8 | Paper content 9 |
10 | `; 11 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | # Ref: https://docs.codeclimate.com/docs/advanced-configuration#section-default-check-configurations 3 | checks: 4 | # Allow 75 line component functions 5 | method-lines: 6 | config: 7 | threshold: 75 8 | exclude_patterns: 9 | - '**/*.spec.js' 10 | - 'test/' 11 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // --- BASE STYLES --- 2 | 3 | import './storybook-styles.css' 4 | 5 | // --- STORYBOOK DEFAULTS --- 6 | export const parameters = { 7 | layout: 'centered', 8 | viewMode: 'docs', 9 | options: { 10 | storySort: { 11 | order: ['Componentry', 'Components'], 12 | }, 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Badge/__snapshots__/Badge.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
8 | Badge 9 |
10 | `; 11 | -------------------------------------------------------------------------------- /src/components/Button/__snapshots__/Button.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` 10 | `; 11 | -------------------------------------------------------------------------------- /@types/postcss-js.d.ts: -------------------------------------------------------------------------------- 1 | // postcss-js has no TS definitions, this satisfies types in src/ and is 2 | // probably correct ¯\_(ツ)_/¯ 3 | declare module 'postcss-js' { 4 | import { Parser } from 'postcss' 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-empty-function 7 | const parser: Parser = () => {} 8 | export default parser 9 | } 10 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | plugins: [ 5 | // internal development entry for Componentry plugin, when published this is: 6 | // require('componentry/postcss') 7 | require('./dist/commonjs/plugin-postcss/index').default, 8 | require('tailwindcss'), 9 | require('autoprefixer'), 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Icon } from './Icon' 4 | 5 | const meta: Meta = { 6 | component: Icon, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | id: 'coffee', 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Icon/__snapshots__/Icon.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 | 10 | 13 | 14 | `; 15 | -------------------------------------------------------------------------------- /src/components/Close/Close.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Close } from './Close' 4 | 5 | const meta: Meta = { 6 | component: Close, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | onClick: console.log, 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/ignores-components/code.js: -------------------------------------------------------------------------------- 1 | import { Button, Flex, Input } from 'componentry' 2 | 3 | export default function Test() { 4 | // Test that: 5 | // 1. Components that aren't precompile components are ignored 6 | return ( 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from '@storybook/addons' 2 | import { create } from '@storybook/theming/create' 3 | 4 | addons.setConfig({ 5 | // --- Customize theme of Storybook to be hecka rad 6 | theme: create({ 7 | base: 'dark', 8 | brandTitle: 'Componentry', 9 | brandImage: undefined, 10 | 11 | fontCode: "'Fira Code', monospace", 12 | }), 13 | }) 14 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/refs/code.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | import { Flex, Text } from 'componentry' 3 | 4 | export default function Test() { 5 | const ref = useRef(null) 6 | 7 | // Test that: 8 | // 1. Refs are passed through 9 | return ( 10 | 11 | Precompiled for speed 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Paper/Paper.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Paper } from './Paper' 4 | 5 | const meta: Meta = { 6 | component: Paper, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | p: 2, 15 | children: 'Hello World', 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/as-prop/code.js: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from 'componentry' 2 | import FancyText from './fancy-text' 3 | 4 | export default function Test() { 5 | // Test that: 6 | // 1. as="string" works 7 | // 2. as={Identifier} works 8 | return ( 9 | 10 | 11 | Precompiled for speed 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/data-precompiled/output.js: -------------------------------------------------------------------------------- 1 | import { Text } from 'componentry' 2 | import { jsx as _jsx } from 'react/jsx-runtime' 3 | export default function Test() { 4 | // Test that: 5 | // 1. Plugin option `dataFlag` results in a `data-precompiled` flag included 6 | return /*#__PURE__*/ _jsx('div', { 7 | 'data-component': 'Text', 8 | className: 'C9Y-Text-base C9Y-Text-body', 9 | children: 'Precompiled for speed', 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Close/__snapshots__/Close.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 | 19 | `; 20 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/exclude_option/code.js: -------------------------------------------------------------------------------- 1 | import { Badge, Block, Flex, Grid, Paper, Text } from 'componentry' 2 | 3 | export default function Test() { 4 | // Test that: 5 | // 1. exclude config skips pre-compiling 6 | return ( 7 | 8 | 9 | 10 | Precompiled for 11 | 12 | SPEED 13 | Delightful 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/transforms-components/code.js: -------------------------------------------------------------------------------- 1 | import { Badge, Block, Flex, Grid, Paper, Text } from 'componentry' 2 | 3 | export default function Test() { 4 | // Test that: 5 | // 1. Basic component transform works 6 | return ( 7 | 8 | 9 | 10 | Precompiled for 11 | 12 | SPEED 13 | Delightful 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/passthrough-props/code.js: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from 'componentry' 2 | 3 | export default function Test() { 4 | // Test that: 5 | // 1. Props that aren't library props are passed through 6 | // 2. Props that are arrow expressions are passed through 7 | return ( 8 | 9 | Passthrough props 10 | console.log('mouse_enter')}>Passthrough props 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/components/IconButton/__snapshots__/IconButton.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` Snapshots > renders defaults correctly 1`] = ` 4 | 18 | `; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Don't Git Me! 2 | # ------------------------------------- 3 | 4 | # Workspace 5 | # ------------------------------------- 6 | .vscode/settings.json 7 | 8 | # Vendor Libraries 9 | # ------------------------------------- 10 | node_modules 11 | 12 | # Logs 13 | # ------------------------------------- 14 | *.log 15 | 16 | # Published 17 | # ------------------------------------- 18 | dist 19 | types 20 | 21 | # Testing 22 | # ------------------------------------- 23 | coverage 24 | storybook-static 25 | -------------------------------------------------------------------------------- /src/components/FormGroup/FormGroup.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | import { Theme } from '../../theme/theme' 3 | 4 | // styles 5 | // -------------------------------------------------------- 6 | 7 | export const formGroupStyles = (theme: Theme): FormGroupStyles => ({ 8 | '.C9Y-FormGroup': { 9 | marginBottom: theme.spacing[4], 10 | }, 11 | }) 12 | 13 | export interface FormGroupStyles { 14 | '.C9Y-FormGroup': CSSProperties 15 | } 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/refs/output.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react' 2 | import { Flex, Text } from 'componentry' 3 | import { jsx as _jsx } from 'react/jsx-runtime' 4 | export default function Test() { 5 | const ref = useRef(null) 6 | 7 | // Test that: 8 | // 1. Refs are passed through 9 | return /*#__PURE__*/ _jsx('div', { 10 | className: 'flex p-2', 11 | ref: ref, 12 | children: /*#__PURE__*/ _jsx('h3', { 13 | className: 'C9Y-Text-base C9Y-Text-h3', 14 | children: 'Precompiled for speed', 15 | }), 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/as-prop/output.js: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from 'componentry' 2 | import FancyText from './fancy-text' 3 | import { jsx as _jsx } from 'react/jsx-runtime' 4 | export default function Test() { 5 | // Test that: 6 | // 1. as="string" works 7 | // 2. as={Identifier} works 8 | return /*#__PURE__*/ _jsx('main', { 9 | className: 'flex', 10 | children: /*#__PURE__*/ _jsx(FancyText, { 11 | className: 'C9Y-Text-base C9Y-Text-body mt-3', 12 | fancy: true, 13 | children: 'Precompiled for speed', 14 | }), 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/FormGroup/FormGroup.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createStaticComponent } from '../../utils/create-static-component' 3 | import { UtilityProps } from '../../utils/utility-props' 4 | 5 | export interface FormGroupProps 6 | extends UtilityProps, 7 | React.ComponentPropsWithoutRef<'div'> {} 8 | 9 | /** 10 | * Form groups provide a control point for standardizing spacing within forms. 11 | * @experimental 12 | * @see [FormGroup component 📝](https://componentry.design/components/form-group) 13 | */ 14 | export const FormGroup = createStaticComponent('FormGroup') 15 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/ignores-components/output.js: -------------------------------------------------------------------------------- 1 | import { Button, Flex, Input } from 'componentry' 2 | import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime' 3 | export default function Test() { 4 | // Test that: 5 | // 1. Components that aren't precompile components are ignored 6 | return /*#__PURE__*/ _jsxs('div', { 7 | className: 'flex', 8 | children: [ 9 | /*#__PURE__*/ _jsx(Input, { 10 | value: 'not compiled', 11 | }), 12 | /*#__PURE__*/ _jsx(Button, { 13 | children: 'Not compiled', 14 | }), 15 | ], 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Active/__snapshots__/Active.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
9 | 16 | 23 |
24 | `; 25 | -------------------------------------------------------------------------------- /src/components/Card/__snapshots__/Card.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
8 |
11 | Header 12 |
13 |
16 |

19 | Title 20 |

21 | Body 22 |
23 |
26 | Footer 27 |
28 |
29 | `; 30 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | 3 | 'use strict' 4 | 5 | const plugin = require('tailwindcss/plugin') 6 | const { borderPlugin, createTheme, tailwindSafelist } = require('./dist/commonjs') 7 | 8 | const theme = createTheme() 9 | delete theme.height 10 | delete theme.width 11 | delete theme.gridTemplateColumns 12 | delete theme.gridTemplateRows 13 | 14 | module.exports = { 15 | content: ['./src/**/*.ts', './src/**/*.tsx', './src/**/*.mdx'], 16 | corePlugins: { 17 | preflight: false, 18 | }, 19 | theme, 20 | plugins: [plugin(borderPlugin)], 21 | safelist: tailwindSafelist, 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Drawer/__snapshots__/Drawer.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
9 | 17 | 24 |
25 | `; 26 | -------------------------------------------------------------------------------- /src/components/Input/__snapshots__/Input.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
7 | 13 | 18 |
21 | Really important input! 22 |
23 |
26 | Oh no! 27 |
28 |
29 | `; 30 | -------------------------------------------------------------------------------- /src/components/Tooltip/Tooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Tooltip } from './Tooltip' 4 | 5 | const meta: Meta = { 6 | component: Tooltip, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | render: () => ( 14 | 15 | Fun fact 16 | 17 | Only 8% of the world's currency is physical money, the rest only exists on 18 | computers. 19 | 20 | 21 | ), 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Block/Block.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Block } from './Block' 6 | 7 | describe('', () => { 8 | elementTests(Block) 9 | }) 10 | 11 | // Snapshots 12 | // --------------------------------------------------------------------------- 13 | describe(' snapshots', () => { 14 | it('renders correctly', () => { 15 | render(Block content) 16 | 17 | expect(screen.getByTestId('block')).toMatchSnapshot() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/components/Paper/Paper.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Paper } from './Paper' 6 | 7 | describe('', () => { 8 | elementTests(Paper) 9 | }) 10 | 11 | // Snapshots 12 | // --------------------------------------------------------------------------- 13 | describe(' snapshots', () => { 14 | it('renders correctly', () => { 15 | render(Paper content) 16 | 17 | expect(screen.getByTestId('paper')).toMatchSnapshot() 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/components/Badge/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Badge } from './Badge' 4 | 5 | const meta: Meta = { 6 | component: Badge, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | variant: 'filled', 15 | color: 'primary', 16 | children: 'Badge', 17 | }, 18 | } 19 | 20 | export const Sizes: Story = { 21 | render: () => ( 22 | <> 23 | Badge 24 | Badge 25 | Badge 26 | 27 | ), 28 | } 29 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/checks-import-paths/code.js: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from 'componentry' 2 | import { Grid } from './custom-grid' 3 | import { Paper } from '@/custom/componentry_path' 4 | 5 | export default function Test() { 6 | // Test that: 7 | // 1. Componentry imports (Flex, Text) are compiled 8 | // 2. Non-Componentry imports (Grid) are ignored 9 | // 3. Custom import paths (Paper) can be configured 10 | return ( 11 | 12 | 13 | Precompiled for speed 14 | 15 | 16 | Precompiled for speed 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Flex/Flex.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Flex } from './Flex' 4 | 5 | const meta: Meta = { 6 | component: Flex, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | backgroundColor: 'primary-200', 15 | className: 'rounded', 16 | children: [1, 2, 3].map((num) => ( 17 |
21 | #{num > 9 ? num : `0${num}`} 22 |
23 | )), 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Active/Active.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Active } from './Active' 4 | 5 | const meta: Meta = { 6 | component: Active, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | onActivate: console.log, 15 | onActivated: console.log, 16 | onDeactivate: console.log, 17 | onDeactivated: console.log, 18 | children: [ 19 | Toggle, 20 | Active component content, 21 | ], 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /src/components/Close/Close.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Close } from './Close' 6 | 7 | describe('', () => { 8 | // Basic library element test suite 9 | elementTests(Close) 10 | }) 11 | 12 | // Snapshots 13 | // --------------------------------------------------------------------------- 14 | describe(' snapshots', () => { 15 | it('renders correctly', () => { 16 | render() 17 | 18 | expect(screen.getByTestId('close')).toMatchSnapshot() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/components/Block/Block.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Block } from './Block' 4 | 5 | const meta: Meta = { 6 | component: Block, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | backgroundColor: 'primary-200', 15 | className: 'rounded', 16 | children: [1, 2, 3].map((num) => ( 17 |
21 | #{num > 9 ? num : `0${num}`} 22 |
23 | )), 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Grid/Grid.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Grid } from './Grid' 4 | 5 | const meta: Meta = { 6 | component: Grid, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | backgroundColor: 'primary-200', 15 | className: 'rounded grid-cols-2', 16 | gap: 4, 17 | children: [1, 2, 3].map((num) => ( 18 |
22 | #{num > 9 ? num : `0${num}`} 23 |
24 | )), 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /src/components/FormGroup/FormGroup.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { FormGroup } from './FormGroup' 6 | 7 | describe('', () => { 8 | // Basic library element test suite 9 | elementTests(FormGroup) 10 | }) 11 | 12 | // Snapshots 13 | // --------------------------------------------------------------------------- 14 | describe(' snapshots', () => { 15 | it('renders correctly', () => { 16 | render() 17 | 18 | expect(screen.getByTestId('formgroup')).toMatchSnapshot() 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/styles-props/code.js: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from 'componentry' 2 | 3 | export default function Test() { 4 | return ( 5 |
6 | {/* Props without theme values are compiled to inline styles */} 7 | Inline styles 8 | {/* Inline styles are merged correctly with prop values */} 9 | 10 | Styles prop test 11 | 12 | {/* Styles prop takes precedence over utility props (marginTop duplicated) */} 13 | 14 | Styles prop test 15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Utility types for working with complex TS types. 4 | */ 5 | 6 | /** 7 | * Type display utility 8 | * @see https://effectivetypescript.com/2022/02/25/gentips-4-display/ 9 | */ 10 | // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type 11 | export type Resolve = T extends Function ? T : { [K in keyof T]: T[K] } 12 | 13 | /** 14 | * Utility type used to merge two types with user defined overrides. 15 | * @example 16 | * ```ts 17 | * export interface ExampleProps {} 18 | * interface DefaultExampleProps { radical: boolean } 19 | * type Props = MergeTypes 20 | * ``` 21 | */ 22 | export type MergeTypes = Omit & Overrides 23 | -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Breaking 4 | 5 | ... 6 | 7 | ## Housekeeping 8 | 9 | 1. Update className to cx everywhere 10 | 1. Move factories into /factories 11 | 1. Setup emoji className prefixes for all components 12 | 1. Rename doc block 'components' that are actually subcomponents, eg Table.Cell 13 | 14 | ## Small upgrades 15 | 16 | - Add utility props for `flex-grow`, and `flex-shrink`. 17 | - Add fullWidth prop that sets a `.min-w-full` class for 100% width 18 | - Rename direction props to `placement-` 19 | 20 | ## Testing 21 | 22 | - Can the `to` for Button/Link be overridden to allow things like: 23 | `to={{ path: '/path' params: { status: 'rad' }}}` 24 | 25 | ## Types 26 | 27 | - Can the `defaultProps` in `element` be more defined? 28 | -------------------------------------------------------------------------------- /src/utils/deep-merge.ts: -------------------------------------------------------------------------------- 1 | /** Internal utility fn for deep merging theme+style definitions */ 2 | export function deepMerge(base: any, overrides: any) { 3 | const merged = JSON.parse(JSON.stringify(base)) 4 | 5 | Object.keys(overrides).forEach((key) => { 6 | if (!(key in merged)) { 7 | // If base doesn't have this key we can just assign the entire override 8 | merged[key] = overrides[key] 9 | } else if (typeof overrides[key] !== 'object') { 10 | // Else if it's a value the override wins over base 11 | merged[key] = overrides[key] 12 | } else { 13 | // Else if it's an object then recursively deep-merge it 14 | merged[key] = deepMerge(merged[key], overrides[key]) 15 | } 16 | }) 17 | 18 | return merged 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Paper/Paper.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | import { Theme } from '../../theme/theme' 3 | 4 | // styles 5 | // ------------------------------------------------------- 6 | 7 | export const paperStyles = (theme: Theme): PaperStyles => ({ 8 | // BASE 9 | '.C9Y-Paper-base': { 10 | borderRadius: theme.borderRadius.DEFAULT, 11 | }, 12 | 13 | // VARIANTS 14 | '.C9Y-Paper-flat': { 15 | border: theme.border.DEFAULT, 16 | }, 17 | }) 18 | 19 | export interface PaperStyles { 20 | /** Base class applied to all variants for shared structural styles */ 21 | '.C9Y-Paper-base': CSSProperties 22 | /** Variant class applied when `variant="flat"` */ 23 | '.C9Y-Paper-flat': CSSProperties 24 | } 25 | -------------------------------------------------------------------------------- /vitest.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()], 6 | test: { 7 | globals: true, 8 | environment: 'jsdom', 9 | setupFiles: './src/test/vitest.setup.mjs', 10 | 11 | coverage: { 12 | enabled: true, 13 | reporter: ['text-summary', 'lcov'], 14 | include: 'src/**/*.{ts,tsx}', 15 | exclude: [ 16 | '**/*.stories.js', // Ignore story files 17 | '**/*.styles.ts', // Ignore PostCSS styles 18 | 'src/plugin-babel/__fixtures__/**', // Ignore fixture files 19 | 'src/test/**', // Ignore test files 20 | 'src/{Media,Modal,Theme}/stories/**', // Ignore story files 21 | ], 22 | }, 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /.storybook/preview-body.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/Tooltip/__snapshots__/Tooltip.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
9 | 16 | 32 | `; 33 | -------------------------------------------------------------------------------- /src/components/Alert/__snapshots__/Alert.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 | 34 | `; 35 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | framework: { 5 | name: '@storybook/react-webpack5', 6 | 7 | options: { 8 | fastRefresh: true, 9 | }, 10 | }, 11 | 12 | stories: ['../docs/**/*.mdx', '../src/**/*.mdx', '../src/**/*.stories.tsx'], 13 | 14 | addons: [ 15 | '@storybook/addon-essentials', 16 | '@storybook/addon-links', 17 | '@storybook/addon-mdx-gfm', 18 | { 19 | name: '@storybook/addon-styling-webpack', 20 | options: { 21 | rules: [ 22 | { 23 | test: /\.css$/, 24 | use: ['style-loader', 'css-loader', 'postcss-loader'], 25 | }, 26 | ], 27 | }, 28 | }, 29 | ], 30 | 31 | core: { 32 | disableTelemetry: true, 33 | }, 34 | 35 | docs: { 36 | autodocs: true, 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /docs/Tailwind.md: -------------------------------------------------------------------------------- 1 | # TAILWIND 2 | 3 | Problem: Tailwind really wants to scan files for generated classNames, but 4 | Componentry uses generated classNames, eg `flex-${direction}`. This is a problem 5 | for utility classes and a harder problem for generated color classes, eg theme 6 | color "love" for font `text-love` 7 | 8 | ### SOLUTIONS 9 | 10 | 1. Use safelist in config: Requires user safelist all classes, kind of tedious 11 | 2. Scan .ts files and include all classes in a type, kind of clever, but what 12 | about colors?? eg font-love? 13 | 3. 14 | 15 | Recap: Starting in v3 Tailwind is JIT by default for everything. How to get the 16 | styles we need included? 17 | 18 | Ideally: we include the styles that the library will generate. 19 | 20 | - Future: will require thinking through media queries for layout 21 | flex/grid/padding styles... 22 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/component-props/code.js: -------------------------------------------------------------------------------- 1 | import { Badge, Block, Flex, Grid, Paper, Text } from 'componentry' 2 | 3 | export default function Test() { 4 | // Test that: 5 | // 1. Individual component props are transformed correctly 6 | return ( 7 |
8 | 9 | Test Badge component 10 | 11 | Test Block component 12 | 13 | Test Flex component 14 | 15 | 16 | Test Grid component 17 | 18 | 19 | Test Text component 20 | 21 | Test Paper component 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Media/Media.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Media, useMedia } from './Media' 4 | 5 | const meta: Meta = { 6 | component: Media, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | render: () => ( 14 | 15 | 16 | 17 | ), 18 | } 19 | 20 | function MediaConsumer() { 21 | const media = useMedia() 22 | 23 | return ( 24 |
25 |

Media values:

26 |
Small: {String(media.sm)}
27 |
Medium: {String(media.md)}
28 |
Large: {String(media.lg)}
29 |
30 |         {JSON.stringify(media, null, 2)}
31 |       
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/passthrough-props/output.js: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from 'componentry' 2 | import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime' 3 | export default function Test() { 4 | // Test that: 5 | // 1. Props that aren't library props are passed through 6 | // 2. Props that are arrow expressions are passed through 7 | return /*#__PURE__*/ _jsxs('div', { 8 | className: 'flex', 9 | children: [ 10 | /*#__PURE__*/ _jsx('div', { 11 | className: 'C9Y-Text-base C9Y-Text-body', 12 | 'data-skip': 'passthrough', 13 | children: 'Passthrough props', 14 | }), 15 | /*#__PURE__*/ _jsx('div', { 16 | className: 'C9Y-Text-base C9Y-Text-body', 17 | onMouseEnter: () => console.log('mouse_enter'), 18 | children: 'Passthrough props', 19 | }), 20 | ], 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Dropdown/__snapshots__/Dropdown.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
9 | 18 | 34 |
35 | `; 36 | -------------------------------------------------------------------------------- /src/components/Popover/Popover.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Popover } from './Popover' 4 | 5 | const meta: Meta = { 6 | component: Popover, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | render: () => ( 14 | 15 | Toggle 16 | 17 | Fun Fact! 18 | 19 | The new Texas Instrument calculators have ABC keyboards because if they had 20 | QWERTY keyboards, they would be considered computers and wouldn’t be allowed for 21 | standardized test taking. 22 | 23 | 24 | 25 | ), 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Ref: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "node", 7 | "request": "launch", 8 | "name": "Jest: Single File", 9 | "program": "${workspaceFolder}/node_modules/.bin/jest", 10 | "args": ["${relativeFile}"], 11 | "console": "integratedTerminal", 12 | "internalConsoleOptions": "neverOpen", 13 | "disableOptimisticBPs": true, 14 | "env": { 15 | "NODE_ENV": "test" 16 | } 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Jest: Test Suite", 22 | "program": "${workspaceFolder}/node_modules/.bin/jest", 23 | "console": "integratedTerminal", 24 | "internalConsoleOptions": "neverOpen", 25 | "disableOptimisticBPs": true, 26 | "env": { 27 | "NODE_ENV": "test" 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/components/FormGroup/FormGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Input } from '../Input/Input' 4 | import { FormGroup } from './FormGroup' 5 | 6 | const meta: Meta = { 7 | component: FormGroup, 8 | } 9 | 10 | export default meta 11 | type Story = StoryObj 12 | 13 | export const Primary: Story = { 14 | render: () => ( 15 |
16 | {/* @ts-expect-error -- experimental component */} 17 | 18 | 19 | Name 20 | 21 | 22 | 23 | {/* @ts-expect-error -- experimental component */} 24 | 25 | 26 | Email 27 | 28 | 29 | 30 |
31 | ), 32 | } 33 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/exclude_option/output.js: -------------------------------------------------------------------------------- 1 | import { Badge, Block, Flex, Grid, Paper, Text } from 'componentry' 2 | import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime' 3 | export default function Test() { 4 | // Test that: 5 | // 1. exclude config skips pre-compiling 6 | return /*#__PURE__*/ _jsx('div', { 7 | className: 'C9Y-Paper-base C9Y-Paper-flat', 8 | children: /*#__PURE__*/ _jsxs('div', { 9 | className: 'grid', 10 | children: [ 11 | /*#__PURE__*/ _jsx('div', { 12 | className: 'flex', 13 | children: /*#__PURE__*/ _jsx('div', { 14 | className: 'C9Y-Text-base C9Y-Text-body', 15 | children: 'Precompiled for', 16 | }), 17 | }), 18 | /*#__PURE__*/ _jsx('div', { 19 | children: 'SPEED', 20 | }), 21 | /*#__PURE__*/ _jsx(Badge, { 22 | children: 'Delightful', 23 | }), 24 | ], 25 | }), 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /.storybook/storybook-styles.css: -------------------------------------------------------------------------------- 1 | @componentry foundation; 2 | @tailwind base; 3 | 4 | @media screen and (-webkit-min-device-pixel-ratio: 2), 5 | screen and (min-resolution: 2dppx) { 6 | /* Include font smoothing in screens that support it */ 7 | html { 8 | -moz-osx-font-smoothing: grayscale; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | } 12 | 13 | @componentry components; 14 | @tailwind components; 15 | 16 | /* Normally this would be an override in theme, but we don't have a theme for this 17 | Storybook for simplicity -> Feather icons use stroke, not fill. */ 18 | .C9Y-Icon-font { 19 | /* Storybook uses Feather icons -> which use a 2px stroke instead of a fill */ 20 | fill: none; 21 | stroke: currentColor; 22 | stroke-width: 2px; 23 | } 24 | 25 | .sbdocs-h2 { 26 | /* Storybook is setting this to 0 between stories, give them some space */ 27 | margin-top: 20px !important; 28 | } 29 | 30 | @componentry utilities; 31 | @tailwind utilities; 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Don't Publish Me! 2 | # ------------------------------------- 3 | 4 | # Source 5 | # ------------------------------------- 6 | public 7 | src 8 | 9 | # Output 10 | # ------------------------------------- 11 | build 12 | coverage 13 | monitor 14 | 15 | # Logs and caches 16 | # ------------------------------------- 17 | .esm-cache 18 | false 19 | 20 | # Configs 21 | # ------------------------------------- 22 | .babelrc.js 23 | .codeclimate.yml 24 | .czrc 25 | .github 26 | .gitattributes 27 | .storybook 28 | .vscode 29 | .eslintignore 30 | .eslintrc.js 31 | .prettierignore 32 | .prettierrc.js 33 | .renovaterc 34 | tsconfig.json 35 | tsconfig.types.json 36 | webpack.config.js 37 | tsconfig.json 38 | tsconfig.types.json 39 | 40 | # Testing 41 | # ------------------------------------- 42 | __mocks__ 43 | test 44 | jest.config.js 45 | postcss.config.js 46 | tailwind.config.js 47 | 48 | # Documentation 49 | # ------------------------------------- 50 | docs 51 | guides 52 | documentation.yml 53 | -------------------------------------------------------------------------------- /src/utils/create-static-component.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useThemeProps } from '../components/Provider/Provider' 3 | import { createElement } from './create-element' 4 | 5 | /** 6 | * Convenience function for creating components with only static props 7 | * @param displayName - Component name 8 | * @param defaultProps - Componentry library default prop values 9 | */ 10 | export function createStaticComponent( 11 | displayName: string, 12 | defaultProps?: Props, 13 | ): React.FC { 14 | function Component(props: Props) { 15 | return createElement({ 16 | componentClassName: `C9Y-${displayName}`, 17 | ...defaultProps, 18 | // @ts-expect-error -- TODO: can update displayName type to keyof components, but 19 | // that requires adding _every_ subcomponent to the Components map 20 | ...useThemeProps(displayName), 21 | ...props, 22 | }) 23 | } 24 | 25 | Component.displayName = displayName 26 | 27 | return Component 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Grid/Grid.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Grid } from './Grid' 6 | 7 | describe('', () => { 8 | elementTests(Grid) 9 | 10 | it('renders class grid by default', () => { 11 | render(Content) 12 | 13 | expect(screen.getByText('Content')).toHaveClass('grid') 14 | }) 15 | 16 | it('overrides display class when passed', () => { 17 | render(Content) 18 | 19 | expect(screen.getByText('Content')).toHaveClass('inline-grid') 20 | }) 21 | }) 22 | 23 | // Snapshots 24 | // --------------------------------------------------------------------------- 25 | describe(' snapshots', () => { 26 | it('renders correctly', () => { 27 | render(Block content) 28 | 29 | expect(screen.getByTestId('grid')).toMatchSnapshot() 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/utils/tailwind-plugins.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility plugin for generating border utility classes using Tailwind 3 | * @example 4 | * ```js 5 | * // tailwind.config.js 6 | * const { borderPlugin } = require('componentry') 7 | * const plugin = require('tailwindcss/plugin') 8 | * 9 | * module.exports = { 10 | * plugins: [ 11 | * plugin(borderPlugin), 12 | * ] 13 | * } 14 | * ``` 15 | */ 16 | export function borderPlugin({ matchUtilities, theme }: any) { 17 | matchUtilities( 18 | { 19 | border: (value: string) => ({ 20 | border: value, 21 | }), 22 | 'border-t': (value: string) => ({ 23 | 'border-top': value, 24 | }), 25 | 'border-r': (value: string) => ({ 26 | 'border-right': value, 27 | }), 28 | 'border-b': (value: string) => ({ 29 | 'border-bottom': value, 30 | }), 31 | 'border-l': (value: string) => ({ 32 | 'border-left': value, 33 | }), 34 | }, 35 | { values: theme('border') }, 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Active/Active.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { activationTests } from '../../test/activation-tests' 5 | import { elementTests } from '../../test/element-tests' 6 | import { Active } from './Active' 7 | 8 | describe('', () => { 9 | activationTests(Active, { name: 'Active', testArias: ['controls'] }) 10 | elementTests(Active) 11 | elementTests(Active.Action) 12 | elementTests(Active.Content, { mounted: 'always' }) 13 | }) 14 | 15 | // Snapshots 16 | // --------------------------------------------------------------------------- 17 | describe(' snapshots', () => { 18 | it('renders correctly', () => { 19 | render( 20 | 21 | Action 22 | Content 23 | , 24 | ) 25 | 26 | expect(screen.getByTestId('active')).toMatchSnapshot() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/transforms-components/output.js: -------------------------------------------------------------------------------- 1 | import { Badge, Block, Flex, Grid, Paper, Text } from 'componentry' 2 | import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime' 3 | export default function Test() { 4 | // Test that: 5 | // 1. Basic component transform works 6 | return /*#__PURE__*/ _jsx('div', { 7 | className: 'C9Y-Paper-base C9Y-Paper-flat', 8 | children: /*#__PURE__*/ _jsxs('div', { 9 | className: 'grid', 10 | children: [ 11 | /*#__PURE__*/ _jsx('div', { 12 | className: 'flex', 13 | children: /*#__PURE__*/ _jsx('div', { 14 | className: 'C9Y-Text-base C9Y-Text-body', 15 | children: 'Precompiled for', 16 | }), 17 | }), 18 | /*#__PURE__*/ _jsx('div', { 19 | children: 'SPEED', 20 | }), 21 | /*#__PURE__*/ _jsx('div', { 22 | className: 'C9Y-Badge-base C9Y-Badge-filled', 23 | children: 'Delightful', 24 | }), 25 | ], 26 | }), 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Link/Link.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Link } from './Link' 6 | 7 | describe('', () => { 8 | // Basic library element test suite 9 | elementTests(Link) 10 | 11 | it('should render type classes for anchor', () => { 12 | render( 13 | 14 | Link 15 | , 16 | ) 17 | 18 | expect(screen.getByText('Link')).toHaveClass( 19 | 'C9Y-Link-base C9Y-Link-text text-success', 20 | ) 21 | }) 22 | }) 23 | 24 | // Snapshots 25 | // --------------------------------------------------------------------------- 26 | describe(' snapshots', () => { 27 | it('renders correctly', () => { 28 | render( 29 | 30 | Link 31 | , 32 | ) 33 | 34 | expect(screen.getByTestId('link')).toMatchSnapshot() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/styles-props/output.js: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from 'componentry' 2 | import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime' 3 | export default function Test() { 4 | return /*#__PURE__*/ _jsxs('div', { 5 | children: [ 6 | /*#__PURE__*/ _jsx('div', { 7 | className: 'flex', 8 | style: { 9 | marginTop: 99, 10 | }, 11 | children: 'Inline styles', 12 | }), 13 | /*#__PURE__*/ _jsx('div', { 14 | className: 'C9Y-Text-base C9Y-Text-body', 15 | style: { 16 | color: 'hotpink', 17 | overflow: 'hidden', 18 | marginTop: '20px', 19 | }, 20 | children: 'Styles prop test', 21 | }), 22 | /*#__PURE__*/ _jsx('div', { 23 | className: 'C9Y-Text-base C9Y-Text-body', 24 | style: { 25 | marginTop: 17, 26 | overflow: 'hidden', 27 | marginTop: '20px', 28 | }, 29 | children: 'Styles prop test', 30 | }), 31 | ], 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Drawer/Drawer.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { activationTests } from '../../test/activation-tests' 5 | import { elementTests } from '../../test/element-tests' 6 | import { Drawer } from './Drawer' 7 | 8 | describe('', () => { 9 | activationTests(Drawer, { name: 'Drawer', testArias: ['controls', 'expanded'] }) 10 | 11 | elementTests(Drawer) 12 | elementTests(Drawer.Action) 13 | elementTests(Drawer.Content, { mounted: 'always' }) 14 | }) 15 | 16 | // Snapshots 17 | // --------------------------------------------------------------------------- 18 | describe(' snapshots', () => { 19 | it('renders correctly', () => { 20 | render( 21 | 22 | Action 23 | Content 24 | , 25 | ) 26 | 27 | expect(screen.getByTestId('drawer')).toMatchSnapshot() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /docs/Theming.md: -------------------------------------------------------------------------------- 1 | # Theming 2 | 3 | - Theme shape matches Tailwind for easy integration 4 | - Component styles use theme values for creating default styles 5 | 6 | ## Customizing icons 7 | 8 | The default library icons can be enabled by setting `$include-icons` true in 9 | your theme. The included icons are displayed with`background-image` styles. 10 | 11 | The preferred method for customizing icons is with an inlined `` with your 12 | icon. Componentry includes a `` element for each icon, and will use your 13 | included icon. The id for any icon used is documented in the component. 14 | 15 | ```html 16 | 21 | 22 | 25 | 26 | 27 | ``` 28 | 29 | ## Future 30 | 31 | - Explore using CSS variables for dynamic theming 32 | -------------------------------------------------------------------------------- /src/components/Link/Link.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Text } from '../Text/Text' 4 | import { Link } from './Link' 5 | 6 | const meta: Meta = { 7 | component: Link, 8 | } 9 | 10 | export default meta 11 | type Story = StoryObj 12 | 13 | export const Primary: Story = { 14 | render: () => ( 15 |
16 | Text link 17 | 18 | Link can be used inside headings. 19 | 20 |
21 | ), 22 | } 23 | 24 | export const WithoutAnchor: Story = { 25 | args: { 26 | onclick: console.log, 27 | children: 'Link', 28 | }, 29 | } 30 | 31 | export const DisabledState: Story = { 32 | render: () => ( 33 |
34 | 35 | Text link 36 | 37 | 38 | Button link 39 | 40 |
41 | ), 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Daniel Hedgecock 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/checks-import-paths/output.js: -------------------------------------------------------------------------------- 1 | import { Flex, Text } from 'componentry' 2 | import { Grid } from './custom-grid' 3 | import { Paper } from '@/custom/componentry_path' 4 | import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime' 5 | export default function Test() { 6 | // Test that: 7 | // 1. Componentry imports (Flex, Text) are compiled 8 | // 2. Non-Componentry imports (Grid) are ignored 9 | // 3. Custom import paths (Paper) can be configured 10 | return /*#__PURE__*/ _jsxs('div', { 11 | className: 'flex', 12 | children: [ 13 | /*#__PURE__*/ _jsx(Grid, { 14 | children: /*#__PURE__*/ _jsx('div', { 15 | className: 'C9Y-Text-base C9Y-Text-body', 16 | children: 'Precompiled for speed', 17 | }), 18 | }), 19 | /*#__PURE__*/ _jsx('div', { 20 | className: 'C9Y-Paper-base C9Y-Paper-flat', 21 | children: /*#__PURE__*/ _jsx('div', { 22 | className: 'C9Y-Text-base C9Y-Text-body', 23 | children: 'Precompiled for speed', 24 | }), 25 | }), 26 | ], 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Drawer/Drawer.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Drawer } from './Drawer' 4 | 5 | const meta: Meta = { 6 | component: Drawer, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | children: ( 15 | <> 16 | Toggle drawer 17 | Drawer content 18 | 19 | ), 20 | }, 21 | } 22 | 23 | export const WithMultiple: Story = { 24 | args: { 25 | children: ( 26 | <> 27 | Toggle drawer 28 | Drawer content 29 | Toggle drawer 30 | Drawer content 31 | Toggle drawer 32 | Drawer content 33 | 34 | ), 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Provider/Provider.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Button } from '../Button/Button' 4 | import { ComponentryProvider, useTheme } from './Provider' 5 | 6 | const meta: Meta = { 7 | component: ComponentryProvider, 8 | } 9 | 10 | export default meta 11 | type Story = StoryObj 12 | 13 | export const Primary: Story = { 14 | render: () => ( 15 | 24 |
25 | 26 | 27 |
28 |
29 | ), 30 | } 31 | 32 | function ProviderConsumer() { 33 | const theme = useTheme() 34 | 35 | return ( 36 |
37 |

Theme values:

38 |
39 |         {JSON.stringify(theme, null, 2)}
40 |       
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Input/Input.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Input } from './Input' 4 | 5 | const meta: Meta = { 6 | component: Input, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | children: ( 15 | <> 16 | Componentry input 17 | 18 | Really important input! 19 | 20 | ), 21 | }, 22 | } 23 | 24 | export const ErrorState: Story = { 25 | args: { 26 | children: ( 27 | <> 28 | Componentry input 29 | 30 | Oh no! 31 | 32 | ), 33 | }, 34 | } 35 | 36 | export const WithScreenReaderOnlyLabel: Story = { 37 | args: { 38 | children: ( 39 | <> 40 | Storybook 41 | 42 | 43 | ), 44 | }, 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Tabs/Tabs.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Tabs } from './Tabs' 4 | 5 | const meta: Meta = { 6 | component: Tabs, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | render: () => ( 14 | 15 | 16 | Item 1 17 | Tab with long name 18 | Item 3 19 | 20 | Disabled 21 | 22 | 23 | 24 | Tab 1 25 | Tab 2 26 | Tab 3 27 | This tab has been disabled 28 | 29 | 30 | ), 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | Componentry 4 |
5 | 6 |

7 | A React component library for building highly performant, completely customizable design systems. 8 |

9 | 10 | --- 11 | 12 | ### Features 13 | 14 | - Feather light 12kB footprint 15 | - Zero runtime, 100% customizable component styles 16 | - A++ accessibility built in to every component 17 | - Designed for flexibility and component composition 18 | 19 | ### Documentation 20 | 21 | Full documentation is available at 22 | [componentry.design](https://componentry.design) 23 | 24 | 25 | 26 | 27 | 28 | ### Contributing 29 | 30 | Componentry is an open source project that welcomes and appreciates 31 | contributions from everyone 🙌.
Please read the 32 | [Code of Conduct](./CODE_OF_CONDUCT.md) and 33 | [Contributing](./.github/CONTRIBUTING.md) guidelines to get started. 34 | -------------------------------------------------------------------------------- /src/components/Tooltip/Tooltip.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { activationTests } from '../../test/activation-tests' 5 | import { elementTests } from '../../test/element-tests' 6 | import { Tooltip } from './Tooltip' 7 | 8 | describe('', () => { 9 | // Basic library activation test suite 10 | activationTests(Tooltip, { name: 'Tooltip', testArias: ['describedby'] }) 11 | // Basic library element test suite 12 | elementTests(Tooltip) 13 | elementTests(Tooltip.Action) 14 | elementTests(Tooltip.Content, { mounted: 'always' }) 15 | }) 16 | 17 | // Snapshots 18 | // --------------------------------------------------------------------------- 19 | describe(' snapshots', () => { 20 | it('renders correctly', () => { 21 | render( 22 | 23 | Action 24 | Content 25 | , 26 | ) 27 | 28 | expect(screen.getByTestId('tooltip')).toMatchSnapshot() 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /src/components/Badge/Badge.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Badge } from './Badge' 6 | 7 | describe('', () => { 8 | elementTests(Badge) 9 | 10 | it('renders a color className when color is passed', () => { 11 | render(Badge) 12 | 13 | expect(screen.getByText('Badge')).toHaveClass('C9Y-Badge-primaryColor') 14 | }) 15 | 16 | it('renders a size className when size is passed', () => { 17 | render(Badge) 18 | 19 | expect(screen.getByText('Badge')).toHaveClass('C9Y-Badge-largeSize') 20 | }) 21 | }) 22 | 23 | // Snapshots 24 | // --------------------------------------------------------------------------- 25 | describe(' snapshots', () => { 26 | it('renders correctly', () => { 27 | render( 28 | 29 | Badge 30 | , 31 | ) 32 | 33 | expect(screen.getByTestId('badge')).toMatchSnapshot() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/Close/Close.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createStaticComponent } from '../../utils/create-static-component' 3 | import { MergeTypes, Resolve } from '../../utils/types' 4 | import { UtilityProps } from '../../utils/utility-props' 5 | import { Icon } from '../Icon/Icon' 6 | 7 | // Module augmentation interface for overriding component props' types 8 | export interface ClosePropsOverrides {} 9 | 10 | export interface ClosePropsDefaults {} 11 | 12 | export type CloseProps = Resolve> & 13 | UtilityProps & 14 | React.ComponentPropsWithoutRef<'button'> & { as?: React.ElementType } 15 | 16 | export const closeBase: CloseProps & { componentClassName: string } = { 17 | as: 'button', 18 | type: 'button', 19 | componentClassName: `C9Y-Close-base`, 20 | children: , 21 | } 22 | 23 | /** 24 | * Close provides an accessible close target for the Alert and Modal components. 25 | * @experimental 26 | * @see [Close component 📝](https://componentry.design/components/close) 27 | */ 28 | export const Close = createStaticComponent('Close', closeBase) 29 | -------------------------------------------------------------------------------- /src/components/Popover/__snapshots__/Popover.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
9 | 16 | 39 |
40 | `; 41 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Icon, configureIconElementsMap } from './Icon' 6 | 7 | describe('', () => { 8 | elementTests(Icon) 9 | }) 10 | 11 | // Snapshots 12 | // --------------------------------------------------------------------------- 13 | describe(' snapshots', () => { 14 | it('renders correctly', () => { 15 | render() 16 | 17 | expect(screen.getByTestId('icon')).toMatchSnapshot() 18 | }) 19 | }) 20 | 21 | // -------------------------------------------------------- 22 | // Configuration 23 | 24 | describe('configureIconElementsMap', () => { 25 | it('allows configuring icon render elements', () => { 26 | configureIconElementsMap({ 27 | test: () => ( 28 | 29 | 30 | 31 | ), 32 | }) 33 | 34 | render() 35 | 36 | expect(screen.getByTestId('test-el')).toBeInTheDocument() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/classname-props/code.js: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { Text } from 'componentry' 3 | 4 | export default function Test({ className, success }) { 5 | return ( 6 |
7 | {/* ✓ String literal className is merged with library classes */} 8 | 9 | Static className test 10 | 11 | {/* ✓ Identifier expression is merged with clsx with library classes */} 12 | 13 | Identifier expression className test 14 | 15 | {/* ✓ Call expression is merged with clsx with library classes */} 16 | 17 | Call expression className test 18 | 19 | {/* ✓ Conditional expression is merged with clsx with library classes */} 20 | {/* @remarks - Known bug where a conditional className like this case could result in 'undefined' as a className */} 21 | 22 | Conditional expression className test 23 | 24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/classname-props/output.js: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx' 2 | import { Text } from 'componentry' 3 | import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime' 4 | export default function Test({ className, success }) { 5 | return /*#__PURE__*/ _jsxs('div', { 6 | children: [ 7 | /*#__PURE__*/ _jsx('div', { 8 | className: 'C9Y-Text-base C9Y-Text-body truncate ' + 'string-literal-class', 9 | children: 'Static className test', 10 | }), 11 | /*#__PURE__*/ _jsx('div', { 12 | className: 'C9Y-Text-base C9Y-Text-body truncate ' + className, 13 | children: 'Identifier expression className test', 14 | }), 15 | /*#__PURE__*/ _jsx('div', { 16 | className: 17 | 'C9Y-Text-base C9Y-Text-body truncate ' + 18 | clsx(className, ['call-expression-class']), 19 | children: 'Call expression className test', 20 | }), 21 | /*#__PURE__*/ _jsx('div', { 22 | className: 23 | 'C9Y-Text-base C9Y-Text-body truncate ' + 24 | (success ? 'success-class' : undefined), 25 | children: 'Conditional expression className test', 26 | }), 27 | ], 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/Card/Card.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Card } from './Card' 6 | 7 | describe('', () => { 8 | // Basic library element test suite 9 | elementTests(Card) 10 | elementTests(Card.Header) 11 | elementTests(Card.Body) 12 | elementTests(Card.Title) 13 | elementTests(Card.Footer) 14 | 15 | // ('should render a container div with class card by default', () => { 16 | // render() 17 | // expect(screen.getByTestId('card')).toHaveClass('') 18 | // }) 19 | }) 20 | 21 | // Snapshots 22 | // --------------------------------------------------------------------------- 23 | describe(' snapshots', () => { 24 | it('renders correctly', () => { 25 | render( 26 | 27 | Header 28 | 29 | Title 30 | Body 31 | 32 | Footer 33 | , 34 | ) 35 | 36 | expect(screen.getByTestId('card')).toMatchSnapshot() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/prop-types/code.js: -------------------------------------------------------------------------------- 1 | import { Text } from 'componentry' 2 | 3 | export default function Test({ success, position, ...rest }) { 4 | return ( 5 |
6 | {/* ✓ Implicit boolean props */} 7 | Implicit boolean test 8 | {/* ✓ String props */} 9 | String literal test 10 | {/* ✓ Boolean literal expression props */} 11 | Boolean literal expression test 12 | {/* ✓ String literal expression props */} 13 | String literal expression test 14 | {/* ✓ Numeric literal expression props */} 15 | Numeric literal expression test 16 | 17 | {/* X Identifier expression props */} 18 | Identifier expression test 19 | {/* X Conditional expression props */} 20 | Conditional expression test 21 | {/* X Call expression props */} 22 | Call expression test 23 | {/* X Spread attribute props */} 24 | Spread expression test 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | 3 | // styles 4 | // ------------------------------------------------------- 5 | 6 | export const iconStyles = (): IconStyles => ({ 7 | // BASE 8 | '.C9Y-Icon-base': { 9 | display: 'inline-block', 10 | userSelect: 'none', 11 | }, 12 | 13 | // VARIANTS 14 | '.C9Y-Icon-font': { 15 | // 1em width+height makes icons font-sized by default 🔮 16 | height: '1em', 17 | width: '1em', 18 | // Helpful default to help prevent icon from getting squished 19 | flexShrink: 0, 20 | // Alignment: In flex layouts default to centered 21 | alignSelf: 'center', 22 | // Alignment: Outside flex layouts center to text baseline with negative vertical align 23 | verticalAlign: '-.15em', 24 | // Default to use the parent color as the icon color 25 | fill: 'currentColor', 26 | }, 27 | 28 | // SIZES 29 | // ...coming soon 30 | }) 31 | 32 | export interface IconStyles { 33 | /** Base class applied to all variants for shared structural styles */ 34 | '.C9Y-Icon-base': CSSProperties 35 | /** Variant class applied when `variant="font"` */ 36 | '.C9Y-Icon-font': CSSProperties 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Block/Block.ts: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { createElement } from '../../utils/create-element' 3 | import { MergeTypes, Resolve } from '../../utils/types' 4 | import { ElementTypeProps, UtilityProps } from '../../utils/utility-props' 5 | import { useThemeProps } from '../Provider/Provider' 6 | 7 | /** Module augmentation interface for overriding component props' types */ 8 | export interface BlockPropsOverrides {} 9 | 10 | export interface BlockPropsDefaults {} 11 | 12 | export type BlockProps = Resolve< 13 | MergeTypes & { as?: As } & UtilityProps 14 | > & 15 | ElementTypeProps 16 | 17 | /** 18 | * Block provides block layout elements. 19 | * @example 20 | * ```tsx 21 | * 22 | * ... 23 | * 24 | * ``` 25 | * @see [📝 Block](https://componentry.design/docs/components/block) 26 | */ 27 | export interface Block { 28 | (props: BlockProps): React.ReactElement 29 | displayName?: string 30 | } 31 | 32 | export const Block = forwardRef((props, ref) => { 33 | return createElement({ 34 | ref, 35 | ...useThemeProps('Block'), 36 | ...props, 37 | }) 38 | }) as Block 39 | Block.displayName = 'Block' 40 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/component-props/output.js: -------------------------------------------------------------------------------- 1 | import { Badge, Block, Flex, Grid, Paper, Text } from 'componentry' 2 | import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime' 3 | export default function Test() { 4 | // Test that: 5 | // 1. Individual component props are transformed correctly 6 | return /*#__PURE__*/ _jsxs('div', { 7 | children: [ 8 | /*#__PURE__*/ _jsx('div', { 9 | className: 10 | 'C9Y-Badge-base C9Y-Badge-outlined C9Y-Badge-primaryColor C9Y-Badge-smallSize', 11 | children: 'Test Badge component', 12 | }), 13 | /*#__PURE__*/ _jsx('div', { 14 | children: 'Test Block component', 15 | }), 16 | /*#__PURE__*/ _jsx('div', { 17 | className: 'flex items-center flex-col flex-wrap justify-center', 18 | children: 'Test Flex component', 19 | }), 20 | /*#__PURE__*/ _jsx('div', { 21 | className: 'grid items-center justify-items-center', 22 | children: 'Test Grid component', 23 | }), 24 | /*#__PURE__*/ _jsx('h1', { 25 | className: 'C9Y-Text-base C9Y-Text-h1 truncate', 26 | children: 'Test Text component', 27 | }), 28 | /*#__PURE__*/ _jsx('div', { 29 | className: 'C9Y-Paper-base C9Y-Paper-modal', 30 | children: 'Test Paper component', 31 | }), 32 | ], 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Text/Text.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { ComponentryProvider } from '../Provider/Provider' 6 | import { Text } from './Text' 7 | 8 | describe('', () => { 9 | // Basic library element test suite 10 | elementTests(Text) 11 | 12 | it('allows configuring variant render elements with ComponentryProvider', () => { 13 | render( 14 | 25 | Componentry 26 | , 27 | ) 28 | 29 | expect(screen.getByText('Componentry')).toContainHTML( 30 | '
Componentry
', 31 | ) 32 | }) 33 | }) 34 | 35 | // Snapshots 36 | // --------------------------------------------------------------------------- 37 | describe(' snapshots', () => { 38 | it('renders correctly', () => { 39 | render(Componentry) 40 | 41 | expect(screen.getByText('Componentry')).toMatchSnapshot() 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/components/Input/Input.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Input } from './Input' 6 | 7 | function LabelTest(testProps) { 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | LabelTest.displayName = Input.Label.displayName 15 | 16 | describe('', () => { 17 | // Basic library element test suite 18 | // elementTests(Input) - no wrapper element to test... 19 | elementTests(LabelTest) 20 | // elementTests(Input.Field) - TODO: cannot test Input because it cannot have children 21 | elementTests(Input.Description) 22 | elementTests(Input.Error) 23 | }) 24 | 25 | // Snapshots 26 | // --------------------------------------------------------------------------- 27 | describe(' snapshots', () => { 28 | it('renders correctly', () => { 29 | render( 30 |
31 | 32 | Storybook 33 | 34 | Really important input! 35 | Oh no! 36 | 37 |
, 38 | ) 39 | 40 | expect(screen.getByTestId('input')).toMatchSnapshot() 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Modal } from './Modal' 6 | 7 | describe('', () => { 8 | // Basic library element test suite 9 | // elementTests(Modal) 10 | // elementTests(Modal.Header) 11 | // elementTests(Modal.Title) 12 | // elementTests(Modal.Body) 13 | elementTests(Modal.Footer) 14 | }) 15 | 16 | // Snapshots 17 | // --------------------------------------------------------------------------- 18 | describe(' snapshots', () => { 19 | it('renders correctly', () => { 20 | const { container } = render( 21 | 22 | 23 | Demo uncontrolled modal 24 | 25 | 26 |

27 | This is an uncontrolled modal that will automatically manage its active state 28 | using the parent Active component. 29 |

30 |
31 | Modal Footer 32 |
, 33 | ) 34 | 35 | // eslint-disable-next-line testing-library/no-node-access -- Modal returns overlay and dialog 36 | expect(container.children).toMatchSnapshot() 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/components/Flex/Flex.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Flex } from './Flex' 6 | 7 | describe('', () => { 8 | elementTests(Flex) 9 | 10 | it('renders class flex by default', () => { 11 | render(Content) 12 | 13 | expect(screen.getByText('Content')).toHaveClass('flex') 14 | }) 15 | 16 | it('when display is passed, then class flex is overridden', () => { 17 | render(Content) 18 | 19 | expect(screen.getByText('Content')).toHaveClass('inline-flex') 20 | }) 21 | 22 | it('when modifier props are passed, then the expanded classNames are rendered', () => { 23 | render( 24 | 25 | Content 26 | , 27 | ) 28 | 29 | expect(screen.getByText('Content')).toHaveClass( 30 | 'flex flex-col items-start flex-wrap justify-start', 31 | ) 32 | }) 33 | }) 34 | 35 | // Snapshots 36 | // --------------------------------------------------------------------------- 37 | describe(' snapshots', () => { 38 | it('renders correctly', () => { 39 | render(Flex content) 40 | 41 | expect(screen.getByTestId('flex')).toMatchSnapshot() 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide for 🔮 Projects 2 | 3 | _👋 Welcome and thank you for contributing, you are awesome 🎉_ 4 | 5 | ## Code of Conduct 6 | 7 | Please read the repository [Code of Conduct][], we take it seriously and 8 | contributors are required to adhere to the guidelines. 9 | 10 | ## Code authoring 11 | 12 | This project uses ESLint and Prettier to make writing consistent code easy. 13 | Formatting and linting can be run with npm commands: 14 | 15 | ```sh 16 | # Format the project with Prettier 17 | npm run format 18 | 19 | # Lint the project with ESLint 20 | npm run test:lint 21 | ``` 22 | 23 | ## Roadmap 24 | 25 | _ℹ️ Roadmap items track long term project goals, see the [ZenHub board][] for 26 | ready to work issues._ 27 | 28 | - Add experimental dark mode support to ``. There is basically no 29 | support for this media query yet... but these queries can be checked for 30 | truthy values to determine which mode to use 31 | - darkScheme: `window.matchMedia('(prefers-color-scheme: dark)')` 32 | - lightScheme: `window.matchMedia('(prefers-color-scheme: light)')` 33 | - Add support for theming with an `$enable-themes` flag. Ideally theme colors 34 | would be css variables, but that would also mean the color calculations need 35 | to be replaced (which is probably fine) 36 | 37 | 38 | 39 | [Code of Conduct]:../CODE_OF_CONDUCT.md 40 | 41 | -------------------------------------------------------------------------------- /src/components/Link/Link.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | import { Theme } from '../../theme/theme' 3 | 4 | // styles 5 | // ------------------------------------------------------- 6 | 7 | export const linkStyles = (theme: Theme): LinkStyles => ({ 8 | // BASE 9 | '.C9Y-Link-base': { 10 | // Reset browser defaults for when Link renders a button element 11 | border: 'none', 12 | 13 | '&:disabled, &.C9Y-disabled': { 14 | cursor: 'not-allowed', 15 | }, 16 | }, 17 | 18 | // VARIANTS 19 | '.C9Y-Link-text': { 20 | fontSize: 'inherit', 21 | color: theme.colors.primary[500], 22 | textDecoration: 'underline', 23 | '&:hover, .C9Y-hover': { 24 | color: theme.colors.primary[700], 25 | }, 26 | '&:active, &.C9Y-active': { 27 | color: theme.colors.primary[900], 28 | }, 29 | '&:disabled, &.C9Y-disabled': { 30 | color: theme.colors.primary[300], 31 | }, 32 | }, 33 | }) 34 | 35 | export interface LinkStyles { 36 | /** Base class applied to all variants for shared structural styles */ 37 | '.C9Y-Link-base': { '&:disabled, &.C9Y-disabled': CSSProperties } & CSSProperties 38 | 39 | /** Variant class applied when `variant="text"` */ 40 | '.C9Y-Link-text': { 41 | '&:hover, .C9Y-hover': CSSProperties 42 | '&:active, &.C9Y-active': CSSProperties 43 | '&:disabled, &.C9Y-disabled': CSSProperties 44 | } & CSSProperties 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Provider/Provider.spec.jsx: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | import { ComponentryProvider, useThemeProps } from './Provider' 4 | 5 | describe('useThemeProps()', () => { 6 | it('should return undefined when no Provider is present', () => { 7 | const { result } = renderHook(() => useThemeProps('Button')) 8 | 9 | expect(result.current).toBeUndefined() 10 | }) 11 | 12 | it('should return an empty object when no value is configured for component', () => { 13 | const { result } = renderHook(() => useThemeProps('Flex'), { 14 | wrapper: ({ children }) => { 15 | return ( 16 | 19 | {children} 20 | 21 | ) 22 | }, 23 | }) 24 | 25 | expect(result.current).toBeUndefined() 26 | }) 27 | 28 | it('should return the prop values if present for component', () => { 29 | const { result } = renderHook(() => useThemeProps('Button'), { 30 | wrapper: ({ children }) => { 31 | return ( 32 | 35 | {children} 36 | 37 | ) 38 | }, 39 | }) 40 | 41 | expect(result.current).toStrictEqual({ variant: 'filled' }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /src/components/Paper/Paper.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | import { createElement } from '../../utils/create-element' 3 | import { MergeTypes, Resolve } from '../../utils/types' 4 | import { ElementTypeProps, UtilityProps } from '../../utils/utility-props' 5 | import { useThemeProps } from '../Provider/Provider' 6 | 7 | /** Module augmentation interface for overriding component props' types */ 8 | export interface PaperPropsOverrides {} 9 | 10 | export interface PaperPropsDefaults { 11 | /** Display variant */ 12 | variant?: 'flat' 13 | } 14 | 15 | export type PaperProps = Resolve< 16 | MergeTypes & { as?: As } & UtilityProps 17 | > & 18 | ElementTypeProps 19 | 20 | /** 21 | * Paper provides containers for custom elements. 22 | * @example 23 | * ```tsx 24 | * 25 | * ... 26 | * 27 | * ``` 28 | * @see [📝 Paper](https://componentry.design/docs/components/paper) 29 | */ 30 | export interface Paper { 31 | (props: PaperProps): React.ReactElement 32 | displayName?: string 33 | } 34 | 35 | export const Paper = forwardRef((props, ref) => { 36 | const { variant = 'flat', ...rest } = { 37 | ...useThemeProps('Paper'), 38 | ...props, 39 | } 40 | 41 | return createElement({ 42 | ref, 43 | componentClassName: ['C9Y-Paper-base', `C9Y-Paper-${variant}`], 44 | ...rest, 45 | }) 46 | }) as Paper 47 | Paper.displayName = 'Paper' 48 | -------------------------------------------------------------------------------- /docs/publishing.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs' 2 | 3 | 4 | 5 | # Publishing notes 6 | 7 | ### package.json fields 8 | 9 | 1. `exports` provides the 'modern' way to explicitly define exports, and has the advantage 10 | of being able to define a set of mapped paths for cleaner imports by users. It is the 11 | preferred field and is supported by webpack. 12 | 2. `main` and `module` are included for tools that don't support `exports` yet, which 13 | includes `eslint-plugin-import`. 14 | 3. `types` defines the type definitions path. 15 | 16 | ### Babel compilation 17 | 18 | Source code is compiled using `preset-env` to ensure that any modern syntax is compiled to 19 | browserslist `"defaults"` option, this should ensure the library can be included in any 20 | application without having to be compiled. 21 | 22 | #### Polyfills 23 | 24 | No language features are polyfilled, this seems to match the setup of most other packages. 25 | On the plus side this means no unnecessary polyfills are included in the final 26 | distribution. On the cons side it means users must ensure their applications polyfills for 27 | browser (instead of polyfills for just application use). 28 | 29 | ### Types 30 | 31 | Type declaration files are output in `/types` instead of `/dist` so that module 32 | augmentation paths are more intuitive, eg: 33 | 34 | `declare module 'componentry/types/components/Text/Text'` 35 | 36 | vs => 37 | 38 | `declare module 'componentry/dist/browser/components/Text/Text'` 39 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Active } from '../Active/Active' 4 | import { Button } from '../Button/Button' 5 | import { Text } from '../Text/Text' 6 | import { Tooltip } from '../Tooltip/Tooltip' 7 | import { Modal } from './Modal' 8 | 9 | const meta: Meta = { 10 | component: Modal, 11 | } 12 | 13 | export default meta 14 | type Story = StoryObj 15 | 16 | export const Primary: Story = { 17 | render: () => ( 18 | 19 | Open 20 | 21 | 22 | Demo modal 23 | 24 | 25 | 26 | This is an uncontrolled modal that will automatically manage its active state 27 | using the parent Active component. 28 | 29 | 30 | Z-Index and Modal stacking 31 | 32 | You can use tooltips and dropdowns in Modals and they will be placed in the 33 | modal's stacking context, overlaying elements as you'd expect. 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ), 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Badge/Badge.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | import { Theme } from '../../theme/theme' 3 | 4 | // styles 5 | // -------------------------------------------------------- 6 | 7 | export const badgeStyles = (theme: Theme): BadgeStyles => ({ 8 | // BASE 9 | '.C9Y-Badge-base': { 10 | display: 'inline-flex', 11 | alignItems: 'center', 12 | whiteSpace: 'nowrap', 13 | }, 14 | 15 | // VARIANTS 16 | '.C9Y-Badge-filled': { 17 | padding: '4px 8px', 18 | color: theme.colors.inverse, 19 | backgroundColor: theme.colors.gray[700], 20 | fontWeight: theme.fontWeight.bold, 21 | borderRadius: theme.borderRadius.DEFAULT, 22 | fontSize: theme.fontSize.sm, 23 | lineHeight: 1, 24 | // 💡 Use em with font-size and padding to auto-scale with text 25 | }, 26 | 27 | // SIZES 28 | '.C9Y-Badge-smallSize': { 29 | fontSize: theme.fontSize.sm, 30 | padding: '2px 6px', 31 | }, 32 | '.C9Y-Badge-largeSize': { 33 | fontSize: theme.fontSize.body, 34 | padding: '6px 12px', 35 | }, 36 | }) 37 | 38 | export interface BadgeStyles { 39 | /** Base class applied to all variants for shared structural styles */ 40 | '.C9Y-Badge-base': CSSProperties 41 | /** Variant class applied when `variant="filled"` */ 42 | '.C9Y-Badge-filled': CSSProperties 43 | /** Sizing class applied when `size="small"` */ 44 | '.C9Y-Badge-smallSize': CSSProperties 45 | /** Sizing class applied when `size="large"` */ 46 | '.C9Y-Badge-largeSize': CSSProperties 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Modal/__snapshots__/Modal.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 | HTMLCollection [ 5 |
, 8 | , 57 | ] 58 | `; 59 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # Reference: https://github.com/github/codeql-action 2 | name: 'CodeQL Code Scanning' 3 | 4 | on: 5 | push: 6 | branches: [main] 7 | pull_request: 8 | # The branches below must be a subset of the branches above 9 | branches: [main] 10 | schedule: 11 | # Scan every Saturday at 1am 12 | - cron: '0 1 * * 0' 13 | 14 | jobs: 15 | analyse: 16 | name: CodeQL Analyse 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | # Initializes the CodeQL tools for scanning. 24 | - name: Initialize CodeQL 25 | uses: github/codeql-action/init@v2 26 | # Override language selection by uncommenting this and choosing your languages 27 | # with: 28 | # languages: go, javascript, csharp, python, cpp, java 29 | 30 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 31 | # If this step fails, then you should remove it and run the build manually (see below) 32 | - name: Autobuild 33 | uses: github/codeql-action/autobuild@v2 34 | 35 | # ℹ️ Command-line programs to run using the OS shell. 36 | # 📚 https://git.io/JvXDl 37 | 38 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 39 | # and modify them (or add more) to build your code if your project 40 | # uses a compiled language 41 | 42 | #- run: | 43 | # make bootstrap 44 | # make release 45 | 46 | - name: Perform CodeQL Analysis 47 | uses: github/codeql-action/analyze@v2 48 | -------------------------------------------------------------------------------- /src/api-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Export for rolling up component props+styles API definitions, primarily for 4 | * creating documentation. 5 | */ 6 | 7 | export { 8 | type BadgePropsDefaults, 9 | type BadgePropsOverrides, 10 | } from './components/Badge/Badge' 11 | export { type BadgeStyles } from './components/Badge/Badge.styles' 12 | export { 13 | type ButtonPropsDefaults, 14 | type ButtonPropsOverrides, 15 | } from './components/Button/Button' 16 | export { type ButtonStyles } from './components/Button/Button.styles' 17 | export { type FlexPropsDefaults } from './components/Flex/Flex' 18 | export { type GridPropsDefaults } from './components/Grid/Grid' 19 | export { type IconPropsDefaults, type IconPropsOverrides } from './components/Icon/Icon' 20 | export { type IconStyles } from './components/Icon/Icon.styles' 21 | export { 22 | type IconButtonPropsDefaults, 23 | type IconButtonPropsOverrides, 24 | } from './components/IconButton/IconButton' 25 | export { type IconButtonStyles } from './components/IconButton/IconButton.styles' 26 | export { type LinkPropsDefaults, type LinkPropsOverrides } from './components/Link/Link' 27 | export { type LinkStyles } from './components/Link/Link.styles' 28 | export { 29 | type PaperPropsDefaults, 30 | type PaperPropsOverrides, 31 | } from './components/Paper/Paper' 32 | export { type PaperStyles } from './components/Paper/Paper.styles' 33 | export { type TextPropsDefaults, type TextPropsOverrides } from './components/Text/Text' 34 | export { type TextStyles } from './components/Text/Text.styles' 35 | 36 | export { type UtilityPropsBase, type UtilityPropsOverrides } from './utils/utility-props' 37 | -------------------------------------------------------------------------------- /src/components/Text/Text.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | import { Theme } from '../../theme/theme' 3 | 4 | // styles 5 | // ------------------------------------------------------- 6 | 7 | export const textStyles = (theme: Theme): TextStyles => ({ 8 | // BASE 9 | '.C9Y-Text-base': {}, 10 | 11 | // VARIANTS 12 | '.C9Y-Text-h1': { 13 | fontSize: theme.fontSize.h1, 14 | color: theme.colors.gray[900], 15 | }, 16 | '.C9Y-Text-h2': { 17 | fontSize: theme.fontSize.h2, 18 | color: theme.colors.gray[900], 19 | }, 20 | '.C9Y-Text-h3': { 21 | fontSize: theme.fontSize.h3, 22 | color: theme.colors.gray[900], 23 | }, 24 | '.C9Y-Text-body': { 25 | fontSize: theme.fontSize.body, 26 | color: theme.colors.gray[800], 27 | 28 | // Set spacing between multiple paragraphs using sibling selector and 29 | // margin-top. 30 | '& + &': { 31 | marginTop: theme.spacing[4], 32 | }, 33 | }, 34 | }) 35 | 36 | export interface TextStyles { 37 | /** Base class applied to all variants for shared structural styles */ 38 | '.C9Y-Text-base': CSSProperties 39 | /** Variant class applied when `variant="h1"` */ 40 | '.C9Y-Text-h1': CSSProperties 41 | /** Variant class applied when `variant="h2"` */ 42 | '.C9Y-Text-h2': CSSProperties 43 | /** Variant class applied when `variant="h3"` */ 44 | '.C9Y-Text-h3': CSSProperties 45 | /** Variant class applied when `variant="body"` */ 46 | '.C9Y-Text-body': { 47 | /** Sibling selector for auto-spacing multiple body elements */ 48 | '& + &': CSSProperties 49 | } & CSSProperties 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Alert/Alert.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Active } from '../Active/Active' 4 | import { Link } from '../Link/Link' 5 | import { Text } from '../Text/Text' 6 | import { Alert } from './Alert' 7 | 8 | const meta: Meta = { 9 | component: Alert, 10 | parameters: { 11 | docs: { 12 | description: { 13 | component: 14 | 'Alerts can contain any HTML elements, and can be dismissible or static. A dismissible Alert requires Componentry Active `active` and `deactivate` props, which can be passed directly or the Alert can be nested inside of an Active component to automatically create an uncontrolled Alert.', 15 | }, 16 | }, 17 | }, 18 | } 19 | 20 | export default meta 21 | type Story = StoryObj 22 | 23 | export const Primary: Story = { 24 | args: { 25 | color: 'primary', 26 | children: ( 27 | <> 28 | 29 | Well done! 30 | 31 | You successfully read this important alert message. 32 |
33 | 34 | Go home 35 | 36 | 37 | ), 38 | }, 39 | } 40 | 41 | export const Dismissible: Story = { 42 | render: () => ( 43 | 44 | 45 | 46 | Success! 47 | 48 | You created a dismissible alert. 49 | 50 | 51 | ), 52 | } 53 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | /** 4 | * @file Compilation configuration 5 | * 6 | * Library is compiled to two targets before publishing, a CommonJS version and 7 | * an ESModules version. Both are compiled to work across the maximum number of 8 | * active browsers using Browserslist "defaults" target 9 | * (https://github.com/browserslist/browserslist#best-practices). 10 | * 11 | * Polyfills are _not_ included in the compilation: There is no clear guidance 12 | * on whether they should be included and most libraries do not include them. 13 | * This is probably because most current frameworks, like Next.js and Create 14 | * React App, include their own set of sensible polyfills. 15 | */ 16 | 17 | const { BABEL_ENV } = process.env 18 | 19 | const targets = BABEL_ENV === 'test' ? 'node 16' : 'defaults' // Testing runs in Node 20 | const useESM = BABEL_ENV === 'browser' 21 | 22 | module.exports = { 23 | // Base configs are used by ESLint babel parser 24 | presets: [ 25 | [ 26 | '@babel/preset-env', 27 | { 28 | bugfixes: true, 29 | modules: useESM ? false : 'commonjs', 30 | targets, 31 | exclude: [ 32 | 'transform-typeof-symbol', // https://github.com/facebook/create-react-app/issues/5277 33 | ], 34 | }, 35 | ], 36 | ['@babel/preset-react', { runtime: 'automatic' }], 37 | '@babel/preset-typescript', 38 | ], 39 | 40 | plugins: [ 41 | [ 42 | '@babel/plugin-transform-runtime', 43 | { 44 | useESModules: useESM, 45 | version: '^7.17.0', // Default is 7.0, include current version for smaller bundle improvements 46 | }, 47 | ], 48 | ], 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Grid/Grid.ts: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { createElement } from '../../utils/create-element' 3 | import { MergeTypes, Resolve } from '../../utils/types' 4 | import { ElementTypeProps, UtilityProps } from '../../utils/utility-props' 5 | import { useThemeProps } from '../Provider/Provider' 6 | 7 | /** Module augmentation interface for overriding component props' types */ 8 | export interface GridPropsOverrides {} 9 | 10 | export interface GridPropsDefaults { 11 | /** Sets an `align-items` style */ 12 | align?: 'start' | 'end' | 'center' | 'baseline' | 'stretch' 13 | /** Sets a `justify-items` style */ 14 | justify?: 'start' | 'end' | 'center' | 'stretch' 15 | } 16 | 17 | export type GridProps = Resolve< 18 | MergeTypes & { as?: As } & UtilityProps 19 | > & 20 | ElementTypeProps 21 | 22 | /** 23 | * Grid provides CSS grid layout elements 24 | * @example 25 | * ```tsx 26 | * 27 | * ... 28 | * 29 | * ``` 30 | * @see [📝 Grid](https://componentry.design/docs/components/grid) 31 | */ 32 | export interface Grid { 33 | (props: GridProps): React.ReactElement 34 | displayName?: string 35 | } 36 | 37 | export const Grid = forwardRef((props, ref) => { 38 | const { align, justify, ...rest } = { 39 | ...useThemeProps('Grid'), 40 | ...props, 41 | } 42 | 43 | return createElement({ 44 | ref, 45 | display: 'grid', 46 | alignItems: align, 47 | justifyItems: justify, 48 | ...rest, 49 | }) 50 | }) as Grid 51 | Grid.displayName = 'Grid' 52 | -------------------------------------------------------------------------------- /src/plugin-babel/__fixtures__/prop-types/output.js: -------------------------------------------------------------------------------- 1 | import { Text } from 'componentry' 2 | import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime' 3 | export default function Test({ success, position, ...rest }) { 4 | return /*#__PURE__*/ _jsxs('div', { 5 | children: [ 6 | /*#__PURE__*/ _jsx('div', { 7 | className: 'C9Y-Text-base C9Y-Text-body font-bold', 8 | children: 'Implicit boolean test', 9 | }), 10 | /*#__PURE__*/ _jsx('div', { 11 | className: 'C9Y-Text-base C9Y-Text-body flex', 12 | children: 'String literal test', 13 | }), 14 | /*#__PURE__*/ _jsx('div', { 15 | className: 'C9Y-Text-base C9Y-Text-body font-bold', 16 | children: 'Boolean literal expression test', 17 | }), 18 | /*#__PURE__*/ _jsx('div', { 19 | className: 'C9Y-Text-base C9Y-Text-body uppercase', 20 | children: 'String literal expression test', 21 | }), 22 | /*#__PURE__*/ _jsx('div', { 23 | className: 'C9Y-Text-base C9Y-Text-body pt-20', 24 | children: 'Numeric literal expression test', 25 | }), 26 | /*#__PURE__*/ _jsx(Text, { 27 | position: position, 28 | children: 'Identifier expression test', 29 | }), 30 | /*#__PURE__*/ _jsx(Text, { 31 | color: success ? 'success' : 'error', 32 | children: 'Conditional expression test', 33 | }), 34 | /*#__PURE__*/ _jsx(Text, { 35 | mt: computeMargin(position), 36 | children: 'Call expression test', 37 | }), 38 | /*#__PURE__*/ _jsx(Text, { 39 | ...rest, 40 | children: 'Spread expression test', 41 | }), 42 | ], 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Close/Close.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | 3 | // styles 4 | // -------------------------------------------------------- 5 | 6 | // The C9Y-Close-base class allows for targeted styles for close buttons. The 7 | // .icon-close can also be styled for SVG customizations. 8 | // 9 | // 📝 Close originally included a `color` and `fontSize` style, but due to 10 | // ordering these styles would override Alert close customizations so they were 11 | // removed to keep this element as flexible as possible. -> The intent is that 12 | // this building block will be used in other design system elements, and those 13 | // elements can customize size/color as needed 14 | // 15 | // ℹ️ The background image styles for close icons is located in the Icon styles 16 | export const closeStyles = (): CloseStyles => ({ 17 | '.C9Y-Close-base': { 18 | // Layout 19 | alignItems: 'center', 20 | display: 'inline-flex', 21 | justifyContent: 'center', 22 | lineHeight: 1, // ensures icon is center aligned within flex layout 23 | 24 | // Button resets 25 | appearance: 'none', // Remove Chrome native button styling 26 | padding: 0, 27 | backgroundColor: 'transparent', 28 | border: 'none', 29 | borderRadius: 0, 30 | userSelect: 'none', 31 | 32 | // Animate close icon opacity on hover 33 | opacity: 0.6, 34 | transition: 'opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1)', 35 | 36 | '&:hover,&:focus': { 37 | opacity: 1, 38 | }, 39 | }, 40 | }) 41 | 42 | export interface CloseStyles { 43 | '.C9Y-Close-base': { 44 | '&:hover,&:focus': CSSProperties 45 | } & CSSProperties 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Table/Table.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Table } from './Table' 4 | 5 | const meta: Meta = { 6 | component: Table, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | render: () => ( 14 |
15 | 16 | 17 | Library 18 | Package 19 | Version 20 | Size 21 | 22 | 23 | 24 | 25 | Material UI 26 | 27 | @material-ui/core 28 | 29 | 4.4.2 30 | 84.7kb 31 | 32 | 33 | Ant Design 34 | 35 | antd 36 | 37 | 3.23.3 38 | 583.8kb 39 | 40 | 41 | Semantic UI 42 | 43 | semantic-ui-react 44 | 45 | 0.88.1 46 | 83kb 47 | 48 | 49 |
50 | ), 51 | } 52 | -------------------------------------------------------------------------------- /src/theme/create-theme.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { createTheme } from './theme' 3 | import { themeDefaults } from './theme-defaults' 4 | 5 | describe('merge()', () => { 6 | it('merges theme values', () => { 7 | const theme = createTheme({ 8 | colors: { 9 | gray: { 10 | 200: '#333', 11 | }, 12 | }, 13 | fontWeight: { 14 | bold: 600, 15 | }, 16 | }) 17 | 18 | expect(theme.colors).toStrictEqual({ 19 | gray: { 20 | 200: '#333', 21 | }, 22 | }) 23 | 24 | expect(theme.fontWeight).toStrictEqual({ 25 | bold: 600, 26 | }) 27 | }) 28 | 29 | it('extends theme values', () => { 30 | const theme = createTheme({ 31 | extend: { 32 | colors: { 33 | gray: { 34 | 200: '#333', 35 | }, 36 | }, 37 | fontWeight: { 38 | veryBold: 600, 39 | }, 40 | }, 41 | }) 42 | 43 | // Spot check that theme defaults are carried through 44 | expect(theme.border).toStrictEqual(themeDefaults.border) 45 | expect(theme.colors.primary).toStrictEqual(themeDefaults.colors.primary) 46 | 47 | // Check that defaults are extended 48 | expect(theme.fontWeight).toStrictEqual({ 49 | light: 300, 50 | normal: 400, 51 | bold: 700, 52 | veryBold: 600, 53 | }) 54 | expect(theme.colors.gray).toStrictEqual({ 55 | 100: '#eff2f3', 56 | 200: '#333', 57 | 300: '#bfcbd1', 58 | 400: '#90a4ae', 59 | 500: '#607d8b', 60 | 600: '#56717d', 61 | 700: '#3a4b53', 62 | 800: '#2b383f', 63 | 900: '#1d262a', 64 | }) 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/theme/theme.ts: -------------------------------------------------------------------------------- 1 | import { deepMerge } from '../utils/deep-merge' 2 | import { MergeTypes, Resolve } from '../utils/types' 3 | import { themeDefaults } from './theme-defaults' 4 | 5 | /** Module augmentation interface for overriding default theme values */ 6 | export interface ThemeOverrides {} 7 | 8 | /** Application theme values */ 9 | export type Theme = Resolve> 10 | 11 | /** 12 | * createTheme merges the passed custom theme values with the Componentry default values 13 | * to create a new theme. You can extend the default theme values using the `extend` 14 | * field. All other fields will overwrite the defaults entirely. 15 | * @example 16 | * ```ts 17 | * createTheme({ 18 | * // Using the extend field to add an additional gray to the defaults 19 | * extend: { 20 | * gray: { 21 | * 950: '#100' 22 | * } 23 | * }, 24 | * // Overwriting the fontWeight options to a single custom value 25 | * fontWeight: { 26 | * superBold: 900 27 | * } 28 | * }) 29 | * ``` 30 | */ 31 | export function createTheme(themeCustomizations?: any): Theme { 32 | // Extend 33 | let theme: typeof themeDefaults = JSON.parse(JSON.stringify(themeDefaults)) 34 | 35 | if (themeCustomizations) { 36 | if (themeCustomizations.extend) { 37 | theme = deepMerge(theme, themeCustomizations.extend) 38 | } 39 | 40 | // Overrides 41 | Object.entries(themeCustomizations).forEach(([key, value]) => { 42 | if (key !== 'extend') { 43 | // @ts-expect-error -- Need to type the theme properly and include index access definition 44 | theme[key] = value 45 | } 46 | }) 47 | } 48 | 49 | return theme 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Table/Table.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | import { Theme } from '../../theme/theme' 3 | 4 | // styles 5 | // -------------------------------------------------------- 6 | 7 | export const tableStyles = (theme: Theme): TableStyles => ({ 8 | '.C9Y-Table-base': { 9 | display: 'block', 10 | width: '100%', 11 | }, 12 | 13 | '.C9Y-TableHead': { 14 | // 📝 Notes 15 | // - Borders: Table head has a border bottom, and then sibling rows have border top. This 16 | // keeps a border below the head for tables with body scrolling. 17 | borderBottom: theme.border.DEFAULT, 18 | }, 19 | 20 | '.C9Y-TableHeader': { 21 | fontWeight: theme.fontWeight.bold, 22 | padding: theme.spacing[2], 23 | }, 24 | 25 | '.C9Y-TableBody': {}, 26 | 27 | '.C9Y-TableRow': { 28 | display: 'grid', 29 | // grid-template-columns: repeat(auto-fit, minmax(1px, 1fr)); 30 | // Default table row grid will be columns of even width 31 | gridAutoColumns: '1fr', 32 | gridAutoFlow: 'column', 33 | // Add a border top to every row after the first one (not on first or there would 34 | // be a double border at the top of the table) 35 | '& + &': { 36 | borderTop: theme.border.DEFAULT, 37 | }, 38 | }, 39 | 40 | '.C9Y-TableCell': { 41 | padding: theme.spacing[2], 42 | }, 43 | }) 44 | 45 | export interface TableStyles { 46 | '.C9Y-Table-base': CSSProperties 47 | '.C9Y-TableHead': CSSProperties 48 | '.C9Y-TableHeader': CSSProperties 49 | '.C9Y-TableBody': CSSProperties 50 | '.C9Y-TableRow': { '& + &': CSSProperties } & CSSProperties 51 | '.C9Y-TableCell': CSSProperties 52 | } 53 | -------------------------------------------------------------------------------- /src/styles/states.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | 3 | /** 4 | * Global states styles 5 | * 6 | * Individual components may use these styles to represent their current state. 7 | * The global styles are provided here to document what styles _should_ be used. 8 | */ 9 | export const statesStyles = (): StatesStyles => ({ 10 | // Actions states 11 | '.C9Y-disabled': {}, 12 | '.C9Y-focused': {}, 13 | '.C9Y-hovered': {}, 14 | '.C9Y-pressed': {}, 15 | '.C9Y-selected': {}, 16 | 17 | // Validation states 18 | '.C9Y-valid': {}, 19 | '.C9Y-invalid': {}, 20 | 21 | // Deprecated 22 | '.C9Y-active': {}, 23 | '.C9Y-hover': {}, 24 | '.C9Y-focus': {}, 25 | '.C9Y-checked': {}, 26 | }) 27 | 28 | export interface StatesStyles { 29 | /** Indicates element interactions are disabled */ 30 | '.C9Y-disabled': CSSProperties 31 | /** Indicates an element has focus */ 32 | '.C9Y-focused': CSSProperties 33 | /** Indicates an element is being hovered */ 34 | '.C9Y-hovered': CSSProperties 35 | /** Indicates an element is being clicked or pressed */ 36 | '.C9Y-pressed': CSSProperties 37 | /** Indicates an element an element has been selected */ 38 | '.C9Y-selected': CSSProperties 39 | 40 | /** Indicates an element's contents failed to validate */ 41 | '.C9Y-invalid': CSSProperties 42 | /** Indicates an element's contents validated successfully */ 43 | '.C9Y-valid': CSSProperties 44 | 45 | /** @deprecated use C9Y-pressed */ 46 | '.C9Y-active': CSSProperties 47 | /** @deprecated use C9Y-hovered */ 48 | '.C9Y-hover': CSSProperties 49 | /** @deprecated use C9Y-focused */ 50 | '.C9Y-focus': CSSProperties 51 | /** @deprecated use C9Y-selected */ 52 | '.C9Y-checked': CSSProperties 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Dropdown } from './Dropdown' 4 | 5 | const meta: Meta = { 6 | component: Dropdown, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | args: { 14 | direction: 'bottom', 15 | children: ( 16 | <> 17 | Action 18 | 19 |

Available actions

20 | Dropdown Item 21 | {/* @ts-expect-error -- experimental component */} 22 | 23 | Link item 24 | 25 | Button item 26 | 27 | Disabled button 28 | 29 |
30 |
Dropdown item text is not interactive
31 | 32 | 33 | ), 34 | }, 35 | } 36 | 37 | export const WithMultiple: Story = { 38 | args: { 39 | className: 'd-block w-50', 40 | children: ( 41 | <> 42 | {/* @ts-expect-error -- experimental component */} 43 | 44 | 45 | Interactive Item 1 46 | Interactive Item 2 47 | 48 | 49 | ), 50 | }, 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Tabs/Tabs.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Tabs } from './Tabs' 6 | 7 | describe('', () => { 8 | elementTests(Tabs) 9 | elementTests(Tabs.ActionsContainer) 10 | elementTests(Tabs.Action) 11 | elementTests(Tabs.ContentContainer) 12 | elementTests(Tabs.Content, { mounted: 'always' }) 13 | }) 14 | 15 | // Snapshots 16 | // --------------------------------------------------------------------------- 17 | describe(' snapshots', () => { 18 | it('renders correctly', () => { 19 | render( 20 | 21 | 22 | Item 1 23 | Tab with long name 24 | Item 3 25 | 26 | Disabled 27 | 28 | 29 | 30 | 31 | Tab 1 32 | 33 | 34 | Tab 2 35 | 36 | 37 | Tab 3 38 | 39 | 40 | This tab has been disabled 41 | 42 | 43 | , 44 | ) 45 | 46 | expect(screen.getByTestId('tabs')).toMatchSnapshot() 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /src/components/Active/Active.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createActiveAction } from '../../utils/create-active-action-component' 3 | import { createActiveContainer } from '../../utils/create-active-container-component' 4 | import { createActiveContent } from '../../utils/create-active-content-component' 5 | import { UtilityProps } from '../../utils/utility-props' 6 | import { Link } from '../Link/Link' 7 | import { 8 | ActiveActionBaseProps, 9 | ActiveContainerBaseProps, 10 | ActiveContentBaseProps, 11 | } from './active-types' 12 | 13 | export interface ActiveProps 14 | extends ActiveContainerBaseProps, 15 | UtilityProps, 16 | React.ComponentPropsWithoutRef<'div'> {} 17 | 18 | export interface ActiveActionProps 19 | extends ActiveActionBaseProps, 20 | UtilityProps, 21 | React.ComponentPropsWithoutRef<'button'> {} 22 | 23 | export interface ActiveContentProps 24 | extends ActiveContentBaseProps, 25 | UtilityProps, 26 | React.ComponentPropsWithoutRef<'div'> {} 27 | 28 | export interface Active { 29 | (props: ActiveProps): React.ReactElement 30 | /** 31 | * [Active action component 📝](https://componentry.design/components/active) 32 | */ 33 | Action: React.FC 34 | /** 35 | * [Active content component 📝](https://componentry.design/components/active) 36 | */ 37 | Content: React.FC 38 | } 39 | 40 | /** 41 | * [Active component 📝](https://componentry.design/components/active) 42 | * @experimental 43 | */ 44 | export const Active = createActiveContainer('Active', { 45 | escEvents: true, 46 | }) as Active 47 | 48 | Active.Action = createActiveAction('ActiveAction', { 49 | aria: { controls: true }, 50 | defaultAs: Link, 51 | }) 52 | 53 | Active.Content = createActiveContent('ActiveContent', { 54 | aria: { id: true, hidden: true }, 55 | }) 56 | -------------------------------------------------------------------------------- /docs/adr/03-utilities.md: -------------------------------------------------------------------------------- 1 | # Library Utilities 2 | 3 | - Date: 1/17/22 4 | 5 | ## Context and Problem Statement 6 | 7 | Componentry should provide a helpful set of utilities for making small style 8 | adjustments. 9 | 10 | ## Decision Drivers 11 | 12 | - Utilities can add significant maintenance overhead to the repo, we want to 13 | focus on high ROI. 14 | - Tailwind utility classes are configurable, so we don't know all classes 15 | upfront. 16 | - Although all utility values are technically strings, it's desireable to use 17 | numbers for values like spacing/sizing classes. 18 | 19 | ## Decision Outcome 20 | 21 | Beginning in V4 Componentry will narrow utilities support to only create 22 | classNames from utility prop values. This is intended to "lean in" to 23 | Componentry helping drive alignment by encouraging use of a predefined set of 24 | utility classes based on the design system theme. Removing ad-hoc styles support 25 | encourages class usage, and helps highlight where custom values are being used. 26 | 27 | ## Considered Options 28 | 29 | ### Ad-hoc styles support 30 | 31 | Option: Consider supporting ad-hoc styles for utility prop values that don't 32 | match the user's theme values. 33 | 34 | This option was rejected because it's convenient, but it also "encourages" using 35 | any value and makes it difficult to track where one-off values have been used. 36 | Going forward Componentry is focused on design system alignment across teams. 37 | 38 | ### Positive Consequences 39 | 40 | - Sets a good direction on Componentry integrating closely with Tailwind, and 41 | providing the "guardrails" that will help teams stay consistent to a 42 | predefined theme. 43 | 44 | ### Negative Consequences 45 | 46 | - Removes convenience feature of ad-hoc styles. Values not matching a theme 47 | value now have to be defined separately in a class. 48 | 49 | ## Links 50 | 51 | _none_ 52 | -------------------------------------------------------------------------------- /src/utils/create-active-content-component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { ActiveContentBaseProps } from '../components/Active/active-types' 3 | import { useThemeProps } from '../components/Provider/Provider' 4 | import { ComponentName } from '../config/config' 5 | import { useVisible } from '../hooks' 6 | import { ARIAControls, computeARIA } from './aria' 7 | import { ActiveCtx } from './create-active-container-component' 8 | import { createElement } from './create-element' 9 | 10 | interface ActiveContentDefaults { 11 | /** Map of aria attributes to render with component */ 12 | aria?: ARIAControls 13 | defaultAs?: React.ElementType 14 | defaultRenderArrow?: boolean 15 | } 16 | 17 | /** 18 | * Factory returns custom `` components defined by the options. 19 | */ 20 | export function createActiveContent< 21 | Name extends ComponentName, 22 | Props extends ActiveContentBaseProps, 23 | >( 24 | displayName: Name, 25 | { aria = {}, defaultAs }: ActiveContentDefaults = {}, 26 | ): React.FC { 27 | function ActiveContent(props: Props) { 28 | const { guid, ...activeCtx } = useContext(ActiveCtx) 29 | const { 30 | active: _active, 31 | activeId, 32 | mounted = 'visible', 33 | ...rest 34 | } = { 35 | ...useThemeProps(displayName), 36 | ...activeCtx, 37 | ...props, 38 | } 39 | 40 | const { active, visible } = useVisible(_active) 41 | 42 | if (!active && mounted === 'visible') return null 43 | 44 | return createElement({ 45 | as: defaultAs, 46 | active: visible, 47 | componentClassName: `C9Y-${displayName}`, 48 | ...computeARIA({ 49 | active, 50 | activeId, 51 | aria, 52 | guid, 53 | type: 'content', 54 | }), 55 | ...rest, 56 | }) 57 | } 58 | ActiveContent.displayName = displayName 59 | return ActiveContent 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Badge/Badge.ts: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { createElement } from '../../utils/create-element' 3 | import { MergeTypes, Resolve } from '../../utils/types' 4 | import { ElementTypeProps, UtilityProps } from '../../utils/utility-props' 5 | import { useThemeProps } from '../Provider/Provider' 6 | 7 | /** Module augmentation interface for overriding component props' types */ 8 | export interface BadgePropsOverrides {} 9 | 10 | export interface BadgePropsDefaults { 11 | /** Display style */ 12 | variant?: 'filled' 13 | /** Theme color for display variant */ 14 | color?: 'primary' 15 | /** Display size */ 16 | size?: 'small' | 'large' 17 | } 18 | 19 | export type BadgeProps = Resolve< 20 | MergeTypes & { as?: As } & Omit< 21 | UtilityProps, 22 | 'color' 23 | > 24 | > & 25 | ElementTypeProps 26 | 27 | /** 28 | * Badge provides a short label for describing elements. 29 | * @example 30 | * ```tsx 31 | * 32 | * Delightful 33 | * 34 | * ``` 35 | * @see [📝 Badge docs](https://componentry.design/docs/components/badge) 36 | */ 37 | export interface Badge { 38 | (props: BadgeProps): React.ReactElement 39 | displayName?: string 40 | } 41 | 42 | export const Badge = forwardRef((props, ref) => { 43 | const { 44 | color, 45 | size, 46 | variant = 'filled', 47 | ...rest 48 | } = { 49 | ...useThemeProps('Badge'), 50 | ...props, 51 | } 52 | 53 | return createElement({ 54 | ref, 55 | componentClassName: [ 56 | `C9Y-Badge-base C9Y-Badge-${variant}`, 57 | { 58 | [`C9Y-Badge-${color}Color`]: color, 59 | [`C9Y-Badge-${size}Size`]: size, 60 | }, 61 | ], 62 | ...rest, 63 | }) 64 | }) as Badge 65 | Badge.displayName = 'Badge' 66 | -------------------------------------------------------------------------------- /src/utils/create-element.tsx: -------------------------------------------------------------------------------- 1 | import clsx, { ClassValue } from 'clsx' 2 | import React, { createElement as createReactElement } from 'react' 3 | import { UtilityProps, createUtilityProps } from './utility-props' 4 | 5 | /** Shared base props across all library components. */ 6 | type ElementBaseProps = { 7 | as?: React.ElementType 8 | className?: ClassValue 9 | componentClassName?: ClassValue 10 | style?: React.CSSProperties 11 | themeClassName?: ClassValue 12 | } & UtilityProps 13 | 14 | /** 15 | * `createElement` provides a convenience function for creating elements that support the 16 | * library core shared props. 17 | * 18 | * - Implements the `as` props 19 | * - Creates and merges utility props with component props 20 | * 21 | * @remarks 22 | * This adds another layer of abstraction to component creation but in return 23 | * eliminates a lot of library boilerplate calling React.createElement with the 24 | * correct values. 25 | * 26 | * For precompile components this additional overhead is computed at compile 27 | * time, eliminating execution cost. 28 | */ 29 | export function createElement({ 30 | as = 'div', 31 | className, 32 | componentClassName, 33 | style, 34 | themeClassName, 35 | ...props 36 | }: Props): React.ReactElement { 37 | const { filteredProps, utilityClassName, utilityStyle } = createUtilityProps(props) 38 | 39 | return createReactElement(as, { 40 | className: clsx( 41 | themeClassName, // User defined default classes from theme context 42 | componentClassName, // Library defined component specific classes, eg 'C9Y-Text-base' 43 | className, // User supplied className 44 | utilityClassName, // Utility classes, eg 'mt-xl' 45 | ), 46 | // Perf: Only create a merged styles object if there's styles included 47 | style: utilityStyle || style ? { ...utilityStyle, ...style } : undefined, 48 | ...filteredProps, 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Link/Link.ts: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { createElement } from '../../utils/create-element' 3 | import { MergeTypes, Resolve } from '../../utils/types' 4 | import { ElementTypeProps, UtilityProps } from '../../utils/utility-props' 5 | import { useThemeProps } from '../Provider/Provider' 6 | 7 | /** Module augmentation interface for overriding component props' types */ 8 | export interface LinkPropsOverrides {} 9 | 10 | export interface LinkPropsDefaults { 11 | /** Display variant */ 12 | variant?: 'text' 13 | /** Disables the element, preventing mouse and keyboard events */ 14 | disabled?: boolean 15 | /** HTML element href */ 16 | href?: string 17 | } 18 | 19 | export type LinkProps = Resolve< 20 | MergeTypes & { as?: As } & UtilityProps 21 | > & 22 | ElementTypeProps 23 | 24 | /** 25 | * Link provides action elements styled as links. 26 | * @example 27 | * ```tsx 28 | * 29 | * Componentry 30 | * 31 | * ``` 32 | * @see [📝 Link component](https://componentry.design/docs/components/link) 33 | */ 34 | export interface Link { 35 | (props: LinkProps): React.ReactElement 36 | displayName?: string 37 | } 38 | 39 | export const Link = forwardRef((props, ref) => { 40 | const { 41 | disabled, 42 | variant = 'text', 43 | ...merged 44 | } = { 45 | ...useThemeProps('Link'), 46 | ...props, 47 | } 48 | 49 | return createElement({ 50 | ref, 51 | disabled, 52 | as: merged.href ? 'a' : 'button', 53 | // @ts-expect-error - Ensure button works for router library usage even though to isn't in props 54 | type: merged.href || merged.to ? undefined : 'button', 55 | componentClassName: `C9Y-Link-base C9Y-Link-${variant}`, 56 | ...merged, 57 | }) 58 | }) as Link 59 | Link.displayName = 'Link' 60 | -------------------------------------------------------------------------------- /src/components/Drawer/Drawer.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createActiveAction } from '../../utils/create-active-action-component' 3 | import { createActiveContainer } from '../../utils/create-active-container-component' 4 | import { createActiveContent } from '../../utils/create-active-content-component' 5 | import { UtilityProps } from '../../utils/utility-props' 6 | import { 7 | ActiveActionBaseProps, 8 | ActiveContainerBaseProps, 9 | ActiveContentBaseProps, 10 | } from '../Active/active-types' 11 | import { Link } from '../Link/Link' 12 | 13 | export interface DrawerProps 14 | extends ActiveContainerBaseProps, 15 | UtilityProps, 16 | React.ComponentPropsWithoutRef<'div'> {} 17 | 18 | export interface DrawerActionProps 19 | extends ActiveActionBaseProps, 20 | UtilityProps, 21 | React.ComponentPropsWithoutRef<'button'> { 22 | /** Display variant */ 23 | variant?: 'primary' 24 | } 25 | 26 | export interface DrawerContentProps 27 | extends ActiveContentBaseProps, 28 | UtilityProps, 29 | React.ComponentPropsWithoutRef<'div'> { 30 | /** Display variant */ 31 | variant?: 'primary' 32 | } 33 | 34 | export interface Drawer { 35 | (props: DrawerProps): React.ReactElement 36 | /** 37 | * [Drawer action component 📝](https://componentry.design/components/drawer) 38 | */ 39 | Action: React.FC 40 | /** 41 | * [Drawer content component 📝](https://componentry.design/components/drawer) 42 | */ 43 | Content: React.FC 44 | } 45 | 46 | /** 47 | * [Drawer component 📝](https://componentry.design/components/drawer) 48 | * @experimental 49 | */ 50 | export const Drawer = createActiveContainer('Drawer') as Drawer 51 | 52 | Drawer.Action = createActiveAction('DrawerAction', { 53 | aria: { controls: true, expanded: true }, 54 | defaultAs: Link, 55 | }) 56 | 57 | Drawer.Content = createActiveContent('DrawerContent', { 58 | aria: { id: true, hidden: true }, 59 | }) 60 | -------------------------------------------------------------------------------- /src/components/Popover/Popover.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { activationTests } from '../../test/activation-tests' 5 | import { elementTests } from '../../test/element-tests' 6 | import { Popover } from './Popover' 7 | 8 | describe('', () => { 9 | // Basic library activation test suite 10 | activationTests(Popover, { name: 'Popover', testArias: ['describedby'] }) 11 | // Basic library element test suite 12 | elementTests(Popover) 13 | elementTests(Popover.Action) 14 | elementTests(Popover.Content, { mounted: 'always' }) 15 | elementTests(Popover.Heading) 16 | elementTests(Popover.Body) 17 | 18 | it('renders the correct directional classes using direction', () => { 19 | render( 20 | 21 | Action 22 | Content 23 | , 24 | ) 25 | 26 | expect(screen.getByTestId('popover')).toHaveClass('C9Y-Popover-left') 27 | }) 28 | }) 29 | 30 | // Snapshots 31 | // --------------------------------------------------------------------------- 32 | describe(' snapshots', () => { 33 | it('renders correctly', () => { 34 | render( 35 | 36 | Toggle 37 | 38 | Fun Fact! 39 | 40 | 41 | The new Texas Instrument calculators have ABC keyboards because if they had 42 | QWERTY keyboards, they would be considered computers and wouldn't be allowed 43 | for standardized test taking. 44 | 45 | 46 | 47 | , 48 | ) 49 | 50 | expect(screen.getByTestId('popover')).toMatchSnapshot() 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/components/Flex/Flex.ts: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { createElement } from '../../utils/create-element' 3 | import { MergeTypes, Resolve } from '../../utils/types' 4 | import { ElementTypeProps, UtilityProps } from '../../utils/utility-props' 5 | import { useThemeProps } from '../Provider/Provider' 6 | 7 | /** Module augmentation interface for overriding component props' types */ 8 | export interface FlexPropsOverrides {} 9 | 10 | export interface FlexPropsDefaults { 11 | /** Sets an `align-items` style */ 12 | align?: 'start' | 'end' | 'center' | 'baseline' | 'stretch' 13 | /** Sets a `flex-direction` flex style */ 14 | direction?: 'column' | 'column-reverse' | 'row-reverse' | 'row' 15 | /** Sets a `justify-content` style */ 16 | justify?: 'start' | 'end' | 'center' | 'space-between' | 'space-around' | 'space-evenly' 17 | /** Sets a `flex-wrap` flex style */ 18 | wrap?: 'wrap' | 'nowrap' | 'wrap-reverse' 19 | } 20 | 21 | export type FlexProps = Resolve< 22 | MergeTypes & { as?: As } & UtilityProps 23 | > & 24 | ElementTypeProps 25 | 26 | /** 27 | * Flex provides flexbox layout elements. 28 | * @example 29 | * ```tsx 30 | * 31 | * ... 32 | * 33 | * ``` 34 | * @see [📝 Flex](https://componentry.design/docs/components/flex) 35 | */ 36 | export interface Flex { 37 | (props: FlexProps): React.ReactElement 38 | displayName?: string 39 | } 40 | 41 | export const Flex = forwardRef((props, ref) => { 42 | const { align, direction, justify, wrap, ...rest } = { 43 | ...useThemeProps('Flex'), 44 | ...props, 45 | } 46 | 47 | return createElement({ 48 | ref, 49 | display: 'flex', 50 | alignItems: align, 51 | flexDirection: direction, 52 | flexWrap: wrap, 53 | justifyContent: justify, 54 | ...rest, 55 | }) 56 | }) as Flex 57 | Flex.displayName = 'Flex' 58 | -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { activationTests } from '../../test/activation-tests' 5 | import { elementTests } from '../../test/element-tests' 6 | import { Dropdown } from './Dropdown' 7 | 8 | describe('', () => { 9 | // Basic library activation test suite 10 | activationTests(Dropdown, { 11 | name: 'Dropdown', 12 | testArias: ['labelledby', 'expanded'], 13 | }) 14 | 15 | // Basic library element test suite 16 | elementTests(Dropdown) 17 | elementTests(Dropdown.Action) 18 | elementTests(Dropdown.Content, { mounted: 'always' }) 19 | elementTests(Dropdown.Item) 20 | 21 | it('renders the correct directional classes using direction', () => { 22 | const { rerender } = render( 23 | 24 | Toggle 25 | Testing 26 | , 27 | ) 28 | 29 | expect(screen.getByTestId('dropdown')).toHaveClass('C9Y-Dropdown-bottom') // default value 30 | 31 | rerender( 32 | 33 | Toggle 34 | Testing 35 | , 36 | ) 37 | 38 | expect(screen.getByTestId('dropdown')).toHaveClass('C9Y-Dropdown-top') // default value 39 | }) 40 | }) 41 | 42 | // Snapshots 43 | // --------------------------------------------------------------------------- 44 | describe(' snapshots', () => { 45 | it('renders correctly', () => { 46 | render( 47 | 48 | Action 49 | 50 | Item 1 51 | Item 2 52 | 53 | , 54 | ) 55 | 56 | expect(screen.getByTestId('dropdown')).toMatchSnapshot() 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/utils/tailwind-safelist.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Pre-configured Tailwind safelist configs for including all Componentry 3 | * classes. 4 | */ 5 | export const tailwindSafelist = [ 6 | // SCREEN READERS 7 | 'sr-only', 8 | 9 | // LAYOUT 10 | // display 11 | 'block', 12 | 'contents', 13 | 'flex', 14 | 'flow-root', 15 | 'grid', 16 | 'hidden', 17 | 'inline', 18 | 'inline-block', 19 | 'inline-flex', 20 | 'inline-grid', 21 | 'list-item', 22 | // position 23 | 'absolute', 24 | 'fixed', 25 | 'relative', 26 | 'static', 27 | 'sticky', 28 | // visibility 29 | 'invisible', 30 | 'visible', 31 | 32 | // FLEXBOX+GRID 33 | { pattern: /^content.*/ }, // alignContent 34 | { pattern: /^items.*/ }, // alignItems 35 | { pattern: /^self.*/ }, // alignSelf 36 | { pattern: /^flex.*/ }, // flex 37 | { pattern: /^grow.*/ }, // flex-grow 38 | { pattern: /^shrink.*/ }, // flex-shrink 39 | { pattern: /^justify.*/ }, // justify: 40 | 41 | // SPACING 42 | { pattern: /^gap-.*/ }, 43 | { pattern: /^m[trblxy]?-.*/ }, 44 | { pattern: /^p[trblxy]?-.*/ }, 45 | 46 | // SIZING 47 | { pattern: /^h-.*/ }, // height 48 | { pattern: /^max-h-.*/ }, // maxHeight 49 | { pattern: /^min-h-.*/ }, // minHeight 50 | { pattern: /^w-.*/ }, // width 51 | { pattern: /^max-w-.*/ }, // maxWidth 52 | { pattern: /^min-w-.*/ }, // minWidth 53 | 54 | // TYPOGRAPHY 55 | { pattern: /^text.*/ }, // color / fontSize / textAlign 56 | 57 | { pattern: /^font.*/ }, // fontFamily / fontWeight: 58 | { pattern: /^tracking.*/ }, // letterSpacing 59 | { pattern: /^leading.*/ }, // lineHeight 60 | // fontStyle 61 | 'italic', 62 | 'not-italic', 63 | // textTransform 64 | 'uppercase', 65 | 'lowercase', 66 | 'capitalize', 67 | 'normal-case', 68 | // overflow 69 | 'truncate', 70 | 71 | // BACKGROUNDS 72 | { pattern: /^bg.*/ }, // backgroundColor 73 | 74 | // BORDERS 75 | 76 | { pattern: /^border.*/ }, // border / borderColor / borderStyle / borderWidth 77 | { pattern: /^rounded.*/ }, // borderRadius 78 | 79 | // EFFECTS 80 | { pattern: /^shadow.*/ }, // boxShadow 81 | ] 82 | -------------------------------------------------------------------------------- /src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * DOM manipulation and inspection utility fns. 4 | */ 5 | 6 | // -------------------------------------------------------- 7 | // Closest handler 8 | 9 | /** 10 | * Find the closest DOM parent with the a `data-id` matching `guid`. If a matching 11 | * ancestor is not found returns `null` 12 | */ 13 | export function closest(target: EventTarget | null, guid: string): EventTarget | null { 14 | if (!target || !(target instanceof HTMLElement)) return null 15 | 16 | if (target.dataset.id === guid) return target 17 | if (target.parentNode && target.parentNode instanceof HTMLElement) { 18 | return closest(target.parentNode, guid) 19 | } 20 | 21 | // Default null when no matches are found 22 | return null 23 | } 24 | 25 | // -------------------------------------------------------- 26 | // Click/Scroll outline handlers 27 | 28 | const mouseMoveSuppressOutline = () => { 29 | document.body.classList.add('suppress-outline') 30 | document.removeEventListener('mousemove', mouseMoveSuppressOutline) 31 | document.addEventListener('keydown', onKeyTab) 32 | } 33 | const touchMoveSuppressOutline = () => { 34 | document.body.classList.add('suppress-outline') 35 | document.removeEventListener('touchmove', touchMoveSuppressOutline) 36 | document.addEventListener('keydown', onKeyTab) 37 | } 38 | 39 | function onKeyTab(e: KeyboardEvent) { 40 | if (e.key === 'tab') { 41 | document.body.classList.remove('suppress-outline') 42 | document.removeEventListener('keydown', onKeyTab) 43 | 44 | document.addEventListener('mousemove', mouseMoveSuppressOutline) 45 | document.addEventListener('touchmove', touchMoveSuppressOutline) 46 | } 47 | } 48 | 49 | /** 50 | * Setup the mouse and touch listeners on app start, suppress outlines only if a 51 | * user shows us they're not a keyboard user. Re-enable focus outlines if they 52 | * start navigating w/ keyboard. 53 | */ 54 | export function setupOutlineHandlers(): void { 55 | document.addEventListener('mousemove', mouseMoveSuppressOutline) 56 | document.addEventListener('touchmove', touchMoveSuppressOutline) 57 | } 58 | -------------------------------------------------------------------------------- /src/components/Alert/Alert.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { userEvent } from '@testing-library/user-event' 3 | import { describe, it, expect } from 'vitest' 4 | 5 | import { elementTests } from '../../test/element-tests' 6 | import { Alert } from './Alert' 7 | 8 | describe('', () => { 9 | // Basic library element test suite 10 | elementTests(Alert) 11 | elementTests(Alert.Close) 12 | 13 | it('should render an alert without a close button by default', () => { 14 | render(Warning!) 15 | 16 | expect(screen.getByRole('alert')).toHaveClass( 17 | 'C9Y-Alert-base C9Y-Alert-filled C9Y-Alert-successColor', 18 | ) 19 | expect(screen.getByRole('alert')).toHaveAttribute('role', 'alert') 20 | }) 21 | 22 | it('should not render a close button if not dismissible', () => { 23 | render(Warning!) 24 | 25 | expect(screen.queryByLabelText('close')).not.toBeInTheDocument() 26 | }) 27 | 28 | it('should bind passed deactivate to close button', async () => { 29 | const deactivate = vi.fn() 30 | render( 31 | 32 | Warning! 33 | , 34 | ) 35 | 36 | expect(screen.getByText('Warning!')).toBeInTheDocument() 37 | 38 | await userEvent.click(screen.getByLabelText('close')) 39 | 40 | // eslint-disable-next-line jest/prefer-called-with 41 | expect(deactivate).toHaveBeenCalled() 42 | 43 | // Alert visibility state change handler has been overridden, other than calling 44 | // passed deactivate Alert should still be visible & unchanged 45 | expect(screen.getByRole('alert')).toHaveAttribute('aria-hidden', 'false') 46 | }) 47 | 48 | // TODO: test active and deactivate handling 49 | }) 50 | 51 | describe(' snapshots', () => { 52 | it('renders correctly', () => { 53 | render( 54 | 55 | Warning! 56 | , 57 | ) 58 | 59 | expect(screen.getByRole('alert')).toMatchSnapshot() 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // --- Code generation 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "outDir": "./types", 7 | "importHelpers": false, // False on the assumption that this doesn't affect the size of the compiled type definitions 8 | "lib": ["DOM", "ESNext"], 9 | 10 | // --- Type checking 11 | "allowJs": true, // Allow JS files to be compiled 12 | "checkJs": false, // Opt in to type checking for JS files 13 | "strict": true, 14 | "noUncheckedIndexedAccess": false, // Every property access with an index could be undefined - todo 15 | "skipLibCheck": true, // Skip type checking .d.ts declaration files (recommended by TS) 16 | "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file 17 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 18 | // "strictNullChecks": true, /* Enable strict null checks. */ 19 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 20 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 21 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 22 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 23 | // "alwaysStrict": true, 24 | 25 | // --- Module resolution and file parsing 26 | "moduleResolution": "node", // Use Node.js algorithm to resolve modules 27 | "esModuleInterop": true, // Interop commonJS and ESM, fixes some issues importing commonJS (recommended by TS) 28 | "isolatedModules": true, // Disallow features that require cross-file information for compilation (Babel safety check) 29 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 30 | "jsx": "react-jsx" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 31 | }, 32 | 33 | "include": ["@types/**/*", "src/**/*"] 34 | } 35 | -------------------------------------------------------------------------------- /docs/adr/template.md: -------------------------------------------------------------------------------- 1 | # [short title of solved problem and solution] 2 | 3 | - Date: [YYYY-MM-DD when the decision was last updated] 4 | 5 | ## Context and Problem Statement 6 | 7 | [Describe the context and problem statement, e.g., in free form using two to 8 | three sentences. You may want to articulate the problem in form of a question.] 9 | 10 | ## Decision Drivers 11 | 12 | - [driver 1, e.g., a force, facing concern, …] 13 | - [driver 2, e.g., a force, facing concern, …] 14 | - … 15 | 16 | ## Considered Options 17 | 18 | - [option 1] 19 | - [option 2] 20 | - [option 3] 21 | - … 22 | 23 | ## Decision Outcome 24 | 25 | Chosen option: "[option 1]", because [justification. e.g., only option, which 26 | meets k.o. criterion decision driver | which resolves force force | … | comes 27 | out best (see below)]. 28 | 29 | ### Positive Consequences 30 | 31 | - [e.g., improvement of quality attribute satisfaction, follow-up decisions 32 | required, …] 33 | - … 34 | 35 | ### Negative Consequences 36 | 37 | - [e.g., compromising quality attribute, follow-up decisions required, …] 38 | - … 39 | 40 | ## Pros and Cons of the Options 41 | 42 | ### [option 1] 43 | 44 | [example | description | pointer to more information | …] 45 | 46 | - Good, because [argument a] 47 | - Good, because [argument b] 48 | - Bad, because [argument c] 49 | - … 50 | 51 | ### [option 2] 52 | 53 | [example | description | pointer to more information | …] 54 | 55 | - Good, because [argument a] 56 | - Good, because [argument b] 57 | - Bad, because [argument c] 58 | - … 59 | 60 | ### [option 3] 61 | 62 | [example | description | pointer to more information | …] 63 | 64 | - Good, because [argument a] 65 | - Good, because [argument b] 66 | - Bad, because [argument c] 67 | - … 68 | 69 | ## Links 70 | 71 | - [Link type] [Link to ADR] 72 | 73 | - … 74 | -------------------------------------------------------------------------------- /src/components/Table/Table.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createStaticComponent } from '../../utils/create-static-component' 3 | import { UtilityProps } from '../../utils/utility-props' 4 | 5 | export interface TableProps extends UtilityProps, React.ComponentPropsWithoutRef<'div'> {} 6 | export interface TableBodyProps 7 | extends UtilityProps, 8 | React.ComponentPropsWithoutRef<'div'> {} 9 | export interface TableCellProps 10 | extends UtilityProps, 11 | React.ComponentPropsWithoutRef<'div'> {} 12 | export interface TableHeadProps 13 | extends UtilityProps, 14 | React.ComponentPropsWithoutRef<'div'> {} 15 | export interface TableHeaderProps 16 | extends UtilityProps, 17 | React.ComponentPropsWithoutRef<'div'> {} 18 | export interface TableRowProps 19 | extends UtilityProps, 20 | React.ComponentPropsWithoutRef<'div'> {} 21 | 22 | export interface Table { 23 | (props: TableProps): React.ReactElement 24 | displayName: 'Table' 25 | /** 26 | * [Table body component 📝](https://componentry.design/components/table) 27 | */ 28 | Body: React.FC 29 | /** 30 | * [Table cell component 📝](https://componentry.design/components/table) 31 | */ 32 | Cell: React.FC 33 | /** 34 | * [Table head component 📝](https://componentry.design/components/table) 35 | */ 36 | Head: React.FC 37 | /** 38 | * [Table header component 📝](https://componentry.design/components/table) 39 | */ 40 | Header: React.FC 41 | /** 42 | * [Table row component 📝](https://componentry.design/components/table) 43 | */ 44 | Row: React.FC 45 | } 46 | 47 | /** 48 | * [Table component 📝](https://componentry.design/components/table) 49 | * @experimental 50 | */ 51 | export const Table = createStaticComponent('Table', { 52 | role: 'table', 53 | }) as Table 54 | 55 | Table.Body = createStaticComponent('TableBody', { role: 'rowgroup' }) 56 | Table.Head = createStaticComponent('TableHead', { role: 'rowgroup' }) 57 | Table.Row = createStaticComponent('TableRow', { role: 'row' }) 58 | Table.Header = createStaticComponent('TableHeader', { 59 | role: 'columnheader', 60 | }) 61 | Table.Cell = createStaticComponent('TableCell', { role: 'cell' }) 62 | -------------------------------------------------------------------------------- /src/components/Card/Card.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Flex } from '../Flex/Flex' 4 | import { Link } from '../Link/Link' 5 | import { Text } from '../Text/Text' 6 | import { Card } from './Card' 7 | 8 | const meta: Meta = { 9 | component: Card, 10 | } 11 | 12 | export default meta 13 | type Story = StoryObj 14 | 15 | export const Primary: Story = { 16 | args: { 17 | children: ( 18 | <> 19 | 20 | Card Header 21 | 22 | 23 | Card title 24 | Card subtitle 25 | 26 | God help us, we're in the hands of engineers. So you two dig up, dig up 27 | dinosaurs? Checkmate... Do you have any idea how long it takes those cups to 28 | decompose. God help us, we're in the hands of engineers. You really think 29 | you can fly that thing? They're using our own satellites against us. And 30 | the clock is ticking. 31 | 32 | 33 | Card link 34 | Another link 35 | 36 | 37 | 38 | 2 days ago 39 | 40 | 41 | ), 42 | }, 43 | } 44 | 45 | export const WithImageCap: Story = { 46 | args: { 47 | children: ( 48 | <> 49 | 59 | Placeholder 60 | 61 | 62 | Image cap 63 | 64 | 65 | 66 | Card title 67 | Card subtitle 68 | 69 | 70 | ), 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import ts from 'typescript-eslint' 3 | 4 | import reactPlugin from 'eslint-plugin-react' 5 | import reactHooksPlugin from 'eslint-plugin-react-hooks' 6 | import jsxA11yPlugin from 'eslint-plugin-jsx-a11y' 7 | 8 | export default ts.config( 9 | { 10 | ignores: [ 11 | // Babel plugin fixtures aren't fully valid 12 | 'src/plugin-babel/__fixtures__', 13 | 14 | // Deps 15 | 'node_modules', 16 | 17 | // Build/Compile output 18 | 'storybook-static', 19 | 'dist', 20 | 'types', 21 | ], 22 | }, 23 | js.configs.recommended, 24 | ts.configs.recommended, 25 | ts.configs.recommendedTypeChecked, 26 | 27 | /** Required for type checking */ 28 | { 29 | languageOptions: { 30 | parserOptions: { 31 | projectService: true, 32 | tsconfigRootDir: import.meta.dirname, 33 | }, 34 | }, 35 | }, 36 | // ts.configs.strictTypeChecked, 37 | // ts.configs.stylisticTypeChecked, 38 | 39 | /** Disable TS rules outside TS context */ 40 | { 41 | files: ['**/*.js', '**/*.mjs'], 42 | extends: [ts.configs.disableTypeChecked], 43 | }, 44 | 45 | /** Disable rules related to using any until types don't use any */ 46 | { 47 | rules: { 48 | '@typescript-eslint/no-explicit-any': 'off', 49 | '@typescript-eslint/no-unsafe-assignment': 'off', 50 | '@typescript-eslint/no-unsafe-argument': 'off', 51 | '@typescript-eslint/no-unsafe-member-access': 'off', 52 | '@typescript-eslint/no-unsafe-return': 'off', 53 | '@typescript-eslint/no-unsafe-call': 'off', 54 | }, 55 | }, 56 | { 57 | rules: { 58 | // Componentry uses empty interfaces frequently for module augmentation 59 | '@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'always' }], 60 | }, 61 | }, 62 | 63 | reactPlugin.configs.flat.recommended, // This is not a plugin object, but a shareable config object 64 | reactPlugin.configs.flat['jsx-runtime'], // Add this if you are using React 17+ 65 | { 66 | settings: { 67 | react: { 68 | version: 'detect', 69 | }, 70 | }, 71 | }, 72 | 73 | { 74 | plugins: { 75 | 'react-hooks': reactHooksPlugin, 76 | }, 77 | rules: reactHooksPlugin.configs.recommended.rules, 78 | }, 79 | 80 | jsxA11yPlugin.flatConfigs.recommended, 81 | ) 82 | -------------------------------------------------------------------------------- /src/components/Text/Text.ts: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { TextElementMap } from '../../theme/theme-defaults' 3 | import { createElement } from '../../utils/create-element' 4 | import { MergeTypes, Resolve } from '../../utils/types' 5 | import { ElementTypeProps, UtilityProps } from '../../utils/utility-props' 6 | import { useThemeProps } from '../Provider/Provider' 7 | 8 | // -------------------------------------------------------- 9 | // TEXT ELEMENTS MAP 10 | 11 | /** Default element map */ 12 | const defaulTextElementMap: TextElementMap = { 13 | h1: 'h1', 14 | h2: 'h2', 15 | h3: 'h3', 16 | body: 'div', 17 | code: 'code', 18 | small: 'small', 19 | } 20 | 21 | // -------------------------------------------------------- 22 | // TEXT COMPONENT 23 | 24 | /** Module augmentation interface for overriding component props' types */ 25 | export interface TextPropsOverrides {} 26 | 27 | export interface TextPropsDefaults { 28 | /** Display variant */ 29 | variant?: 'h1' | 'h2' | 'h3' | 'body' | 'code' | 'small' 30 | /** Truncates overflowing text with an ellipses */ 31 | truncate?: boolean 32 | /** Mapping of Text variants to rendered elements */ 33 | textElementMap?: TextElementMap 34 | } 35 | 36 | export type TextProps = Resolve< 37 | MergeTypes & { as?: As } & UtilityProps 38 | > & 39 | ElementTypeProps 40 | 41 | /** 42 | * Text provides consistently themed typography elements. 43 | * @example 44 | * ```tsx 45 | * 46 | * Build something delightful! 47 | * 48 | * ``` 49 | * @see [📝 Text docs](https://componentry.design/docs/components/text) 50 | */ 51 | export interface Text { 52 | (props: TextProps): React.ReactElement 53 | displayName?: string 54 | } 55 | 56 | export const Text = forwardRef((props, ref) => { 57 | const { 58 | textElementMap = defaulTextElementMap, 59 | truncate = false, 60 | variant = 'body', 61 | ...rest 62 | } = { 63 | ...useThemeProps('Text'), 64 | ...props, 65 | } 66 | 67 | return createElement({ 68 | ref, 69 | as: textElementMap[variant], 70 | componentClassName: [`C9Y-Text-base C9Y-Text-${variant}`, { truncate }], 71 | ...rest, 72 | }) 73 | }) as Text 74 | Text.displayName = 'Text' 75 | -------------------------------------------------------------------------------- /src/components/Text/Text.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Block } from '../Block/Block' 4 | import { Text } from './Text' 5 | 6 | const meta: Meta = { component: Text } 7 | 8 | export default meta 9 | type Story = StoryObj 10 | 11 | export const Primary: Story = { 12 | render: () => ( 13 | <> 14 | Heading 1 15 | 16 | Heading 2 17 | Heading 3 18 | 19 | Life finds a way. Life finds a way. God help us, we're in the hands of 20 | engineers. Life finds a way. Must go faster. Hey, you know how I'm, like, 21 | always trying to save the planet? Here's my chance. Eventually, you do plan 22 | to have dinosaurs on your dinosaur tour, right? 23 | 24 | Small copy 25 | 26 | ), 27 | } 28 | 29 | export const BodySpacing: Story = { 30 | render: () => ( 31 | <> 32 | 33 | By default Componentry includes margin-top between sibling body elements, this can 34 | be configured using the `& + &` stle in the body variant styles. 35 | 36 | 37 | By default Componentry includes margin-top between sibling body elements, this can 38 | be configured using the `& + &` stle in the body variant styles. 39 | 40 | 41 | ), 42 | } 43 | 44 | export const Truncation: Story = { 45 | render: () => ( 46 | // @ts-expect-error -- storybook types 47 | 48 | 49 | Eventually, you do plan to have dinosaurs on your dinosaur tour, right? 50 | 51 | 52 | ), 53 | } 54 | 55 | export const FontSize: Story = { 56 | render: () => ( 57 | <> 58 | 59 | Small size body. 60 | 61 | 62 | Base size body. 63 | 64 | 65 | Large size body. 66 | 67 | 68 | ), 69 | } 70 | 71 | export const BoldItalic: Story = { 72 | render: () => ( 73 | <> 74 | Italic text 75 | Bold text 76 | 77 | ), 78 | } 79 | -------------------------------------------------------------------------------- /src/components/Button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { Button } from './Button' 4 | 5 | const meta: Meta = { 6 | component: Button, 7 | } 8 | 9 | export default meta 10 | type Story = StoryObj 11 | 12 | export const Primary: Story = { 13 | decorators: [ 14 | (Story) => ( 15 |
16 |
base
17 |
hover
18 |
active
19 |
disabled
20 | 21 |
22 | ), 23 | ], 24 | render: () => ( 25 | <> 26 | 27 | 30 | 33 | 36 | 37 | 40 | 43 | 46 | 47 | ), 48 | } 49 | 50 | export const WithIcon: Story = { 51 | render: () => ( 52 |
53 | 56 | 59 | 62 |
63 | ), 64 | } 65 | 66 | export const Sizes: Story = { 67 | render: () => ( 68 |
69 | 70 | 71 | 72 |
73 | ), 74 | } 75 | 76 | export const WithAnchor: Story = { 77 | args: { 78 | href: '#test', 79 | children: 'Button', 80 | }, 81 | parameters: { 82 | docs: { 83 | description: 'Buttons with an `href` will be rendered as anchors', 84 | }, 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /src/components/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createActiveAction } from '../../utils/create-active-action-component' 3 | import { createActiveContainer } from '../../utils/create-active-container-component' 4 | import { createActiveContent } from '../../utils/create-active-content-component' 5 | import { UtilityProps } from '../../utils/utility-props' 6 | import { 7 | ActiveActionBaseProps, 8 | ActiveContainerBaseProps, 9 | ActiveContentBaseProps, 10 | } from '../Active/active-types' 11 | import { Link } from '../Link/Link' 12 | 13 | export interface TooltipProps 14 | extends ActiveContainerBaseProps, 15 | UtilityProps, 16 | React.ComponentPropsWithoutRef<'div'> {} 17 | 18 | export interface TooltipActionProps 19 | extends ActiveActionBaseProps, 20 | UtilityProps, 21 | React.ComponentPropsWithoutRef<'button'> { 22 | /** Display variant */ 23 | variant?: 'primary' 24 | } 25 | 26 | export interface TooltipContentProps 27 | extends ActiveContentBaseProps, 28 | UtilityProps, 29 | React.ComponentPropsWithoutRef<'div'> { 30 | /** Display variant */ 31 | variant?: 'primary' 32 | } 33 | 34 | export interface Tooltip { 35 | (props: TooltipProps): React.ReactElement 36 | /** 37 | * [Tooltip action component 📝](https://componentry.design/components/tooltip) 38 | */ 39 | Action: React.FC 40 | /** 41 | * [Tooltip content component 📝](https://componentry.design/components/tooltip) 42 | */ 43 | Content: React.FC 44 | } 45 | 46 | /** 47 | * [Tooltip component 📝](https://componentry.design/components/tooltip) 48 | * @experimental 49 | */ 50 | export const Tooltip = createActiveContainer('Tooltip', { 51 | escEvents: true, 52 | mouseEvents: true, 53 | }) as Tooltip 54 | 55 | Tooltip.Action = createActiveAction('TooltipAction', { 56 | aria: { describedby: true }, 57 | defaultAs: Link, 58 | }) 59 | 60 | Tooltip.Content = createActiveContent('TooltipContent', { 61 | aria: { id: true, role: 'tooltip', hidden: true }, 62 | defaultAs: TooltipContentElement, 63 | }) 64 | 65 | function TooltipContentElement({ 66 | children, 67 | renderArrow = true, 68 | ...rest 69 | }: { 70 | children: React.ReactNode 71 | renderArrow: boolean 72 | }) { 73 | return ( 74 |
75 | {renderArrow &&
} 76 |
{children}
77 |
78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // --- Config/Theme 2 | export { type Config } from './config/config' 3 | export { type TextElementMap } from './theme/theme-defaults' 4 | export { type Theme, createTheme } from './theme/theme' 5 | 6 | // --- Providers 7 | export { ComponentryProvider, useTheme } from './components/Provider/Provider' 8 | export { Media, useMedia } from './components/Media/Media' 9 | 10 | // --- Components 11 | export { Active } from './components/Active/Active' 12 | export { Alert } from './components/Alert/Alert' 13 | export { Badge, type BadgeProps } from './components/Badge/Badge' 14 | export { Block, type BlockProps } from './components/Block/Block' 15 | export { Button, type ButtonProps } from './components/Button/Button' 16 | export { Card } from './components/Card/Card' 17 | export { Close } from './components/Close/Close' 18 | export { Drawer } from './components/Drawer/Drawer' 19 | export { Dropdown } from './components/Dropdown/Dropdown' 20 | export { Flex, type FlexProps } from './components/Flex/Flex' 21 | export { FormGroup } from './components/FormGroup/FormGroup' 22 | export { Grid, type GridProps } from './components/Grid/Grid' 23 | export { 24 | Icon, 25 | configureIconElementsMap, 26 | type IconElementsMap, 27 | type IconProps, 28 | } from './components/Icon/Icon' 29 | export { IconButton, type IconButtonProps } from './components/IconButton/IconButton' 30 | export { Input } from './components/Input/Input' 31 | export { Link, type LinkProps } from './components/Link/Link' 32 | export { Modal } from './components/Modal/Modal' 33 | export { Paper, type PaperProps } from './components/Paper/Paper' 34 | export { Popover } from './components/Popover/Popover' 35 | export { Table } from './components/Table/Table' 36 | export { Tabs } from './components/Tabs/Tabs' 37 | export { Text, type TextProps } from './components/Text/Text' 38 | export { Tooltip } from './components/Tooltip/Tooltip' 39 | 40 | // --- Utilities 41 | export { useActive, useActiveScrollReset, useNoScroll, useVisible } from './hooks' 42 | export { createElement } from './utils/create-element' 43 | export { setupOutlineHandlers } from './utils/dom' 44 | export { type MergeTypes } from './utils/types' 45 | export { 46 | createUtilityProps, 47 | initializeUtilityPropsTheme, 48 | type ElementTypeProps, 49 | type UtilityProps, 50 | } from './utils/utility-props' 51 | 52 | // --- Tailwind 53 | export { borderPlugin } from './utils/tailwind-plugins' 54 | export { tailwindSafelist } from './utils/tailwind-safelist' 55 | -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createActiveAction } from '../../utils/create-active-action-component' 3 | import { createActiveContainer } from '../../utils/create-active-container-component' 4 | import { createActiveContent } from '../../utils/create-active-content-component' 5 | import { UtilityProps } from '../../utils/utility-props' 6 | import { 7 | ActiveActionBaseProps, 8 | ActiveContainerBaseProps, 9 | ActiveContentBaseProps, 10 | } from '../Active/active-types' 11 | import { Button } from '../Button/Button' 12 | 13 | export interface DropdownProps 14 | extends ActiveContainerBaseProps, 15 | UtilityProps, 16 | React.ComponentPropsWithoutRef<'div'> {} 17 | 18 | export interface DropdownActionProps 19 | extends ActiveActionBaseProps, 20 | UtilityProps, 21 | React.ComponentPropsWithoutRef<'button'> { 22 | /** Display variant */ 23 | variant?: 'primary' 24 | } 25 | 26 | export interface DropdownContentProps 27 | extends ActiveContentBaseProps, 28 | UtilityProps, 29 | React.ComponentPropsWithoutRef<'div'> { 30 | /** Display variant */ 31 | variant?: 'primary' 32 | } 33 | 34 | export interface DropdownItemProps 35 | extends ActiveActionBaseProps, 36 | UtilityProps, 37 | React.ComponentPropsWithoutRef<'button'> { 38 | /** Display variant */ 39 | variant?: 'unstyled' 40 | } 41 | 42 | export interface Dropdown { 43 | (props: DropdownProps): React.ReactElement 44 | /** 45 | * [Dropdown action component 📝](https://componentry.design/components/dropdown) 46 | */ 47 | Action: React.FC 48 | /** 49 | * [Dropdown content component 📝](https://componentry.design/components/dropdown) 50 | */ 51 | Content: React.FC 52 | /** 53 | * [Dropdown item component 📝](https://componentry.design/components/dropdown) 54 | */ 55 | Item: React.FC 56 | } 57 | 58 | /** 59 | * [Dropdown component 📝](https://componentry.design/components/dropdown) 60 | * @experimental 61 | */ 62 | export const Dropdown = createActiveContainer('Dropdown', { 63 | clickEvents: true, 64 | direction: 'bottom', 65 | escEvents: true, 66 | }) as Dropdown 67 | 68 | Dropdown.Action = createActiveAction('DropdownAction', { 69 | aria: { expanded: true, haspopup: true, id: true }, 70 | defaultAs: Button, 71 | }) 72 | 73 | Dropdown.Content = createActiveContent('DropdownContent', { 74 | aria: { labelledby: true, hidden: true }, 75 | }) 76 | 77 | Dropdown.Item = createActiveAction('DropdownItem') 78 | -------------------------------------------------------------------------------- /src/components/Tooltip/Tooltip.styles.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react' 2 | import { Theme } from '../../theme/theme' 3 | 4 | // styles 5 | // -------------------------------------------------------- 6 | 7 | const tooltipArrowWidth = 10 // in pixels 8 | 9 | export const tooltipStyles = (theme: Theme): TooltipStyles => ({ 10 | '.C9Y-Tooltip-base': { 11 | display: 'inline-block', 12 | position: 'relative', 13 | }, 14 | 15 | // --- ACTION 16 | '.C9Y-TooltipAction': {}, 17 | 18 | // --- CONTENT POSITIONER 19 | '.C9Y-TooltipContent': { 20 | // Content container overrides the width constraints for parent element 21 | width: '300px', 22 | position: 'absolute', 23 | opacity: 0, 24 | transition: 'opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1)', 25 | 26 | '&.C9Y-active': { 27 | opacity: 1, 28 | }, 29 | }, 30 | 31 | // --- CONTENT 32 | '.C9Y-TooltipContentContents': { 33 | backgroundColor: theme.colors.gray[800], 34 | borderRadius: theme.borderRadius.DEFAULT, 35 | color: theme.colors.inverse, 36 | fontSize: theme.fontSize.sm, 37 | marginTop: `${tooltipArrowWidth}px`, 38 | maxWidth: '300px', 39 | padding: '0.25rem 0.5rem', 40 | position: 'relative', 41 | display: 'inline-block', 42 | textAlign: 'left', 43 | wordWrap: 'break-word', // Allow breaking very long words so they don't overflow the tooltip's bounds 44 | }, 45 | 46 | // --- ARROW 47 | '.C9Y-TooltipContentArrow': { 48 | height: `${tooltipArrowWidth * 2}px`, 49 | left: '0.5rem', 50 | overflow: 'hidden', 51 | pointerEvents: 'none', // Prevents mouseenter of tip that slightly overlaps action 52 | position: 'absolute', 53 | top: `${tooltipArrowWidth * -1}px`, 54 | width: `${tooltipArrowWidth * 2}px`, 55 | 56 | '&:after': { 57 | background: theme.colors.gray[800], 58 | content: `''`, 59 | height: tooltipArrowWidth, 60 | left: `${tooltipArrowWidth / 2}px`, 61 | position: 'absolute', 62 | top: `${tooltipArrowWidth * 1.5}px`, 63 | transform: 'rotate(45deg)', 64 | width: tooltipArrowWidth, 65 | }, 66 | }, 67 | }) 68 | 69 | export interface TooltipStyles { 70 | '.C9Y-Tooltip-base': CSSProperties 71 | '.C9Y-TooltipAction': CSSProperties 72 | '.C9Y-TooltipContent': { 73 | '&.C9Y-active': CSSProperties 74 | } & CSSProperties 75 | '.C9Y-TooltipContentContents': CSSProperties 76 | '.C9Y-TooltipContentArrow': { 77 | '&:after': CSSProperties 78 | } & CSSProperties 79 | } 80 | -------------------------------------------------------------------------------- /src/components/Card/Card.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | import { Theme } from '../../theme/theme' 3 | 4 | // styles 5 | // -------------------------------------------------------- 6 | 7 | export const cardStyles = (theme: Theme): CardStyles => ({ 8 | '.C9Y-Card-base': { 9 | position: 'relative', 10 | display: 'flex', 11 | flexDirection: 'column', 12 | minWidth: 0, 13 | wordWrap: 'break-word', 14 | backgroundClip: 'border-box', 15 | }, 16 | 17 | '.C9Y-Card-outlined': { 18 | backgroundColor: theme.colors.background, 19 | border: theme.border.DEFAULT, 20 | borderRadius: theme.borderRadius.DEFAULT, 21 | }, 22 | 23 | // --- BODY SUB-COMPONENT 24 | '.C9Y-CardBody': { 25 | // Enable `flex-grow: 1` for decks and groups so that card blocks take up 26 | // as much space as possible, ensuring footers are aligned to the bottom. 27 | flex: '1 1 auto', 28 | padding: theme.spacing[4], 29 | }, 30 | 31 | // --- HEADER SUB-COMPONENT 32 | '.C9Y-CardHeader': { 33 | padding: theme.spacing[4], 34 | // margin-bottom: 0; // Removes the default margin-bottom of 35 | backgroundColor: theme.colors.background, 36 | borderBottom: theme.border.DEFAULT, 37 | 38 | '&:first-child': { 39 | borderRadius: `3px 3px 0 0`, // 🤔 To properly handle layout this value needs to be: card borderRadius - card borderWidth 40 | }, 41 | }, 42 | 43 | // --- FOOTER SUB-COMPONENT 44 | '.C9Y-CardFooter': { 45 | padding: theme.spacing[4], 46 | backgroundColor: theme.colors.background, 47 | borderTop: theme.border.DEFAULT, 48 | 49 | '&:last-child': { 50 | borderRadius: `0 0 3px 3px`, // 🤔 To properly handle layout this value needs to be: card borderRadius - card borderWidth 51 | }, 52 | }, 53 | 54 | // --- TITLE SUB-COMPONENT 55 | '.C9Y-CardTitle': { 56 | fontSize: theme.fontSize.h3, 57 | color: theme.colors.gray[900], // Matches default header color 58 | }, 59 | '.C9Y-CardSubtitle': { 60 | fontSize: theme.fontSize.sm, 61 | color: theme.colors.gray[600], 62 | }, 63 | }) 64 | 65 | export interface CardStyles { 66 | '.C9Y-Card-base': CSSProperties 67 | '.C9Y-Card-outlined': CSSProperties 68 | '.C9Y-CardBody': CSSProperties 69 | '.C9Y-CardHeader': { 70 | '&:first-child': CSSProperties 71 | } & CSSProperties 72 | '.C9Y-CardFooter': { 73 | '&:last-child': CSSProperties 74 | } & CSSProperties 75 | '.C9Y-CardTitle': CSSProperties 76 | '.C9Y-CardSubtitle': CSSProperties 77 | } 78 | -------------------------------------------------------------------------------- /src/utils/create-active-action-component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react' 2 | import { ActiveActionBaseProps } from '../components/Active/active-types' 3 | import { useThemeProps } from '../components/Provider/Provider' 4 | import { ComponentName } from '../config/config' 5 | import { ARIAControls, computeARIA } from './aria' 6 | import { ActiveCtx } from './create-active-container-component' 7 | import { createElement } from './create-element' 8 | 9 | interface ActiveActionDefaults { 10 | /** Overrides component onClick to specified activate/deactivate action */ 11 | action?: 'activate' | 'deactivate' 12 | /** Map of aria attributes to render with component */ 13 | aria?: ARIAControls 14 | defaultAs?: React.ElementType 15 | } 16 | 17 | /** 18 | * Factory returns custom `` components defined by the fn options. 19 | * Componentry sets up actions to be buttons styled as links by default, this 20 | * can be overridden by passing an as and type props for an anchor. 21 | */ 22 | export function createActiveAction< 23 | Name extends ComponentName, 24 | Props extends ActiveActionBaseProps, 25 | >( 26 | displayName: Name, 27 | { action, aria = {}, defaultAs }: ActiveActionDefaults = {}, 28 | ): React.FC { 29 | function ActiveAction(props: Props) { 30 | const { guid, ...activeCtx } = useContext(ActiveCtx) 31 | const { activeId, active, activate, deactivate, ...rest } = { 32 | ...useThemeProps(displayName), 33 | ...activeCtx, 34 | ...props, 35 | } 36 | 37 | // Handle determining whether to call activate or deactivate on click 38 | // 1. If an action type was passed, call that action handler always 39 | // 2. else if in a compound-active context check if this activeId is active 40 | // 3. else use opposite of active status 41 | let onClick 42 | if (action) { 43 | onClick = action === 'activate' ? activate : deactivate 44 | } else if (activeId) { 45 | onClick = activeId === active ? deactivate : activate 46 | } else { 47 | onClick = active ? deactivate : activate 48 | } 49 | 50 | return createElement({ 51 | as: defaultAs, 52 | componentClassName: `C9Y-${displayName}`, 53 | ...computeARIA({ 54 | active, 55 | activeId, 56 | guid, 57 | type: 'action', 58 | aria, 59 | }), 60 | onClick, 61 | // For compound-active contexts, the value attr is to expose the activeId 62 | 'data-active-id': activeId, 63 | ...rest, 64 | }) 65 | } 66 | ActiveAction.displayName = displayName 67 | return ActiveAction 68 | } 69 | -------------------------------------------------------------------------------- /src/config/load-config.ts: -------------------------------------------------------------------------------- 1 | import { lilconfigSync } from 'lilconfig' 2 | 3 | import { alertStyles } from '../components/Alert/Alert.styles' 4 | import { badgeStyles } from '../components/Badge/Badge.styles' 5 | import { buttonStyles } from '../components/Button/Button.styles' 6 | import { cardStyles } from '../components/Card/Card.styles' 7 | import { closeStyles } from '../components/Close/Close.styles' 8 | import { formGroupStyles } from '../components/FormGroup/FormGroup.styles' 9 | import { iconStyles } from '../components/Icon/Icon.styles' 10 | import { iconButtonStyles } from '../components/IconButton/IconButton.styles' 11 | import { inputStyles } from '../components/Input/Input.styles' 12 | import { linkStyles } from '../components/Link/Link.styles' 13 | import { modalStyles } from '../components/Modal/Modal.styles' 14 | import { paperStyles } from '../components/Paper/Paper.styles' 15 | import { popoverStyles } from '../components/Popover/Popover.styles' 16 | import { tableStyles } from '../components/Table/Table.styles' 17 | import { textStyles } from '../components/Text/Text.styles' 18 | import { tooltipStyles } from '../components/Tooltip/Tooltip.styles' 19 | import { foundationStyles } from '../styles/foundation.styles' 20 | import { statesStyles } from '../styles/states.styles' 21 | import { Theme, createTheme } from '../theme/theme' 22 | import { deepMerge } from '../utils/deep-merge' 23 | 24 | import { type ComponentProps, type ComponentStyles } from './config' 25 | 26 | export const loadConfig = (): { 27 | theme: Theme 28 | styles: ComponentStyles 29 | defaultProps: ComponentProps 30 | } => { 31 | const userConfig = lilconfigSync('componentry').search()?.config ?? {} 32 | const theme = createTheme(userConfig.theme) 33 | 34 | return { 35 | theme, 36 | styles: deepMerge( 37 | { 38 | // FOUNDATION 39 | foundation: foundationStyles(theme), 40 | // COMPONENTS 41 | Alert: alertStyles(theme), 42 | Badge: badgeStyles(theme), 43 | Button: buttonStyles(theme), 44 | Card: cardStyles(theme), 45 | Close: closeStyles(), 46 | Icon: iconStyles(), 47 | FormGroup: formGroupStyles(theme), 48 | IconButton: iconButtonStyles(theme), 49 | Input: inputStyles(theme), 50 | Link: linkStyles(theme), 51 | Modal: modalStyles(theme), 52 | Paper: paperStyles(theme), 53 | Popover: popoverStyles(theme), 54 | Table: tableStyles(theme), 55 | Text: textStyles(theme), 56 | Tooltip: tooltipStyles(theme), 57 | // UTILITIES 58 | states: statesStyles(), 59 | }, 60 | userConfig.styles ?? {}, 61 | ), 62 | defaultProps: userConfig.defaultProps ?? {}, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | on: [push] 3 | 4 | jobs: 5 | # --- Package testing ✅ 6 | test: 7 | name: Continuous Integration 8 | if: contains(github.event.head_commit.message, 'skip ci') == false 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - uses: pnpm/action-setup@v4 15 | with: 16 | version: 9 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '22.12' 21 | cache: 'pnpm' 22 | - name: Code Climate - Setup 23 | run: | 24 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 25 | chmod +x ./cc-test-reporter 26 | ./cc-test-reporter before-build 27 | - name: Install 28 | run: pnpm install 29 | - name: Test 30 | run: pnpm run test 31 | - name: Code Climate - Report 32 | if: success() 33 | run: | 34 | export GIT_BRANCH="${GIT_BRANCH:-${GITHUB_REF:11}}" 35 | ./cc-test-reporter after-build 36 | env: 37 | # Add required git env values for code cov reporting, ref: 38 | # https://docs.codeclimate.com/docs/github-actions-test-coverage 39 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 40 | GIT_BRANCH: ${{ github.head_ref }} 41 | - name: Codecov report 42 | if: success() 43 | uses: codecov/codecov-action@v3 44 | # --- Chromatic publishing 📚 45 | chromatic-deployment: 46 | runs-on: ubuntu-latest 47 | if: contains(github.event.head_commit.message, 'skip ci') == false 48 | steps: 49 | - name: Checkout repository 50 | uses: actions/checkout@v4 51 | with: 52 | fetch-depth: 0 # Git history required for comparing snapshots 53 | - uses: pnpm/action-setup@v4 54 | with: 55 | version: 9 56 | - name: Setup Node.js 57 | uses: actions/setup-node@v3 58 | with: 59 | node-version: '22.12' 60 | cache: 'pnpm' 61 | - name: Install dependencies 62 | run: pnpm install 63 | - name: Compile PostCSS plugin 64 | run: pnpm run compile:commonjs 65 | - name: Publish to Chromatic 66 | uses: chromaui/action@v1 67 | with: 68 | # Project is linked to GH and Chromatic checks will update after upload so we 69 | # don't need to wait for build results in this action 70 | exitOnceUploaded: true 71 | skip: '@(renovate/**|dependabot/**)' 72 | token: ${{ secrets.GITHUB_TOKEN }} 73 | projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} 74 | -------------------------------------------------------------------------------- /src/components/Active/active-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 3 | * Base types used for component prop type definitions. 4 | */ 5 | 6 | import React from 'react' 7 | 8 | // -------------------------------------------------------- 9 | // Active components 10 | 11 | export interface ActiveContainerBaseProps { 12 | /** Container children */ 13 | children?: React.ReactNode 14 | 15 | /** Sets a container content placement direction className */ 16 | direction?: 'top' | 'left' | 'right' | 'bottom' 17 | /** Sets a container size className */ 18 | size?: 'sm' | 'lg' 19 | 20 | /** Controlled active state */ 21 | active?: boolean | string 22 | /** Starting active state */ 23 | defaultActive?: boolean | string 24 | /** Called to handle activate event */ 25 | activate?: (event: KeyboardEvent | MouseEvent | TouchEvent | React.MouseEvent) => void 26 | /** Called to handle deactivate event */ 27 | deactivate?: (event: KeyboardEvent | MouseEvent | TouchEvent | React.MouseEvent) => void 28 | /** Called before activate event */ 29 | onActivate?: (event: KeyboardEvent | MouseEvent | TouchEvent | React.MouseEvent) => void 30 | /** Called after activate event */ 31 | onActivated?: ( 32 | event: KeyboardEvent | MouseEvent | TouchEvent | React.MouseEvent, 33 | ) => void 34 | /** Called before deactivate event */ 35 | onDeactivate?: ( 36 | event: KeyboardEvent | MouseEvent | TouchEvent | React.MouseEvent, 37 | ) => void 38 | /** Called after deactivate event */ 39 | onDeactivated?: ( 40 | event: KeyboardEvent | MouseEvent | TouchEvent | React.MouseEvent, 41 | ) => void 42 | } 43 | 44 | export interface ActiveActionBaseProps { 45 | /** Controlled active state */ 46 | active?: boolean | string 47 | /** Action/Content pairing id for compound active components */ 48 | activeId?: string 49 | /** Component children */ 50 | children?: React.ReactNode 51 | /** Called to handle activate event */ 52 | activate?: (event: KeyboardEvent | MouseEvent | TouchEvent | React.MouseEvent) => void 53 | /** Called to handle deactivate event */ 54 | deactivate?: (event: KeyboardEvent | MouseEvent | TouchEvent | React.MouseEvent) => void 55 | } 56 | 57 | export interface ActiveContentBaseProps { 58 | /** Action/Content pairing id for compound active components */ 59 | activeId?: string 60 | /** Component children */ 61 | children?: React.ReactNode 62 | /** 63 | * Controls when the component content is mounted where: 64 | * - `'always'` - The content will be mounted when the element is both visible 65 | * and not visible 66 | * - `'visible'` - The content will only be mounted when visible, when not 67 | * visible the contents are not rendered for performance. 68 | */ 69 | mounted?: 'always' | 'visible' 70 | } 71 | -------------------------------------------------------------------------------- /src/components/IconButton/IconButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { createElement } from '../../utils/create-element' 3 | import { MergeTypes, Resolve } from '../../utils/types' 4 | import { ElementTypeProps, UtilityProps } from '../../utils/utility-props' 5 | import { Icon } from '../Icon/Icon' 6 | import { useThemeProps } from '../Provider/Provider' 7 | 8 | /** Module augmentation interface for overriding component props' types */ 9 | export interface IconButtonPropsOverrides {} 10 | 11 | export interface IconButtonPropsDefaults { 12 | /** Display variant */ 13 | variant?: 'filled' | 'outlined' 14 | /** Display variant color */ 15 | color?: 'primary' 16 | /** Disables the element, preventing mouse and keyboard events */ 17 | disabled?: boolean 18 | /** Button content */ 19 | icon: string | React.ReactElement 20 | /** Toggles full width element layout */ 21 | fullWidth?: boolean 22 | /** HTML element href */ 23 | href?: string 24 | /** Sets the display size */ 25 | size?: 'small' | 'large' 26 | } 27 | 28 | export type IconButtonProps = Resolve< 29 | MergeTypes & { as?: As } & Omit< 30 | UtilityProps, 31 | 'color' 32 | > 33 | > & 34 | ElementTypeProps 35 | 36 | /** 37 | * IconButton provides action elements using icons. 38 | * @example 39 | * ```tsx 40 | * buildSomethingDelightful()} /> 41 | * ``` 42 | * @see [📝 IconButton](https://componentry.design/docs/components/iconbutton) 43 | */ 44 | export interface IconButton { 45 | ( 46 | props: IconButtonProps, 47 | ): React.ReactElement 48 | displayName?: string 49 | } 50 | 51 | export const IconButton = forwardRef((props, ref) => { 52 | const { 53 | variant = 'filled', 54 | color, 55 | disabled, 56 | icon, 57 | size, 58 | ...merged 59 | } = { 60 | ...useThemeProps('IconButton'), 61 | ...props, 62 | } 63 | 64 | return createElement({ 65 | ref, 66 | disabled, 67 | // If an href is passed, this instance should render an anchor tag 68 | as: merged.href ? 'a' : 'button', 69 | // @ts-expect-error - Ensure button works for router library usage even though to isn't in props 70 | type: merged.href || merged.to ? undefined : 'button', 71 | componentClassName: [ 72 | `C9Y-IconButton-base C9Y-IconButton-${variant}`, 73 | { 74 | [`C9Y-IconButton-${color}Color`]: color, 75 | [`C9Y-IconButton-${size}Size`]: size, 76 | }, 77 | ], 78 | children: typeof icon === 'string' ? : icon, 79 | ...merged, 80 | }) 81 | }) as IconButton 82 | IconButton.displayName = 'IconButton' 83 | -------------------------------------------------------------------------------- /src/components/Provider/Provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react' 2 | 3 | import { ComponentName, ComponentProps, Config } from '../../config/config' 4 | import { Theme } from '../../theme/theme' 5 | import { themeDefaults } from '../../theme/theme-defaults' 6 | import { initializeUtilityPropsTheme } from '../../utils/utility-props' 7 | 8 | /** Componentry Context */ 9 | const ComponentryCtx = createContext(undefined) 10 | 11 | export interface ComponentryProviderProps { 12 | children: React.ReactElement 13 | /** Component default props */ 14 | config: Config 15 | } 16 | 17 | /** 18 | * Provider for the application theme and default component props. 19 | * @example 20 | * ```tsx 21 | * const appTheme = {} 22 | * const defaultProps = {} 23 | * 24 | * 25 | * 26 | * ``` 27 | * @see [📝 ComponentryProvider](https://componentry.design/components/provider) 28 | */ 29 | export const ComponentryProvider = ({ children, config }: ComponentryProviderProps) => { 30 | // Internal convenience helper: if user has provided a theme value initialize 31 | // the utility classes module with it for them 32 | if (config.theme) { 33 | initializeUtilityPropsTheme(config.theme) 34 | } 35 | 36 | return {children} 37 | } 38 | ComponentryProvider.displayName = 'ComponentryProvider' 39 | 40 | // -------------------------------------------------------- 41 | // THEME 42 | 43 | /** 44 | * [Theme hook 📝](https://componentry.design/components/theme) 45 | */ 46 | export function useTheme(): Theme { 47 | const ctx = useContext(ComponentryCtx) 48 | return ctx?.theme ?? themeDefaults 49 | } 50 | 51 | // -------------------------------------------------------- 52 | // PROPS 53 | 54 | let preCompileMode = false 55 | let preCompileContext: Config | undefined 56 | 57 | export function __initializePreCompileMode(config: Config): void { 58 | preCompileMode = true 59 | preCompileContext = config 60 | 61 | if (config.theme) { 62 | initializeUtilityPropsTheme(config.theme) 63 | } 64 | } 65 | 66 | /** 67 | * Internal function for accessing component default props through context. 68 | */ 69 | export function useThemeProps( 70 | componentName: Name, 71 | ): ComponentProps[Name] | undefined { 72 | if (preCompileMode) { 73 | return preCompileContext?.defaultProps?.[componentName] 74 | } 75 | 76 | // During Babel pre-compiling `preCompileMode` will always be true, during 77 | // runtime execution it will always be false 78 | // eslint-disable-next-line react-hooks/rules-of-hooks 79 | const ctx = useContext(ComponentryCtx) 80 | return ctx?.defaultProps?.[componentName] 81 | } 82 | -------------------------------------------------------------------------------- /src/components/Alert/Alert.styles.ts: -------------------------------------------------------------------------------- 1 | import { type CSSProperties } from 'react' 2 | import { Theme } from '../../theme/theme' 3 | 4 | // Componentry styles 5 | // ----------------------------------------------------------------- 6 | 7 | export const alertStyles = (theme: Theme): AlertStyles => ({ 8 | // BASE 9 | '.C9Y-Alert-base': { 10 | // Make the alert container a flex container by default with space-between 11 | // to align close button to right side 12 | display: 'flex', 13 | alignItems: 'flex-start', 14 | justifyContent: 'space-between', 15 | }, 16 | 17 | '.C9Y-Alert-dismissible': { 18 | opacity: 1, 19 | transition: 'opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1)', 20 | }, 21 | '.C9Y-Alert-dismissed': { 22 | opacity: 0, 23 | }, 24 | 25 | // VARIANTS 26 | '.C9Y-Alert-filled': { 27 | padding: theme.spacing[4], 28 | background: theme.colors.primary[100], 29 | color: theme.colors.primary[500], 30 | border: '1px solid transparent', 31 | borderRadius: theme.borderRadius.DEFAULT, 32 | borderColor: theme.colors.primary[300], 33 | 34 | '& .C9Y-AlertLink': { 35 | fontWeight: theme.fontWeight.bold, 36 | }, 37 | 38 | // TODO: Recreate this with a Divider component 39 | // '& hr': { borderTopColor: theme.colors.primary[300] }, 40 | }, 41 | 42 | // ELEMENTS 43 | '.C9Y-AlertContent': { 44 | flexGrow: 1, 45 | flexShrink: 1, 46 | }, 47 | 48 | '.C9Y-AlertHeading': { 49 | color: 'inherit', 50 | marginBottom: theme.spacing[4], 51 | }, 52 | 53 | '.C9Y-AlertClose': { 54 | flexShrink: 0, 55 | marginLeft: theme.spacing[2], 56 | color: 'inherit', 57 | }, 58 | }) 59 | 60 | // -------------------------------------------------------- 61 | // COLORS 62 | 63 | // Example of adding theme colors to an alert variant 64 | // const themeColors = ['info', 'success', 'warning', 'error'] as const 65 | // themeColors.forEach((color) => { 66 | // Alert[`.C9Y-Alert-filled.C9Y-Alert-${color}Color`] = { 67 | // 'background': theme.colors[color][100], 68 | // 'borderColor': theme.colors[color][300], 69 | // 'color': theme.colors[color][500], 70 | 71 | // '& .C9Y-Alert-link': { 72 | // color: theme.colors[color][500], 73 | // fontWeight: theme.fontWeight.bold, 74 | // }, 75 | 76 | // '& hr': { 77 | // borderTopColor: theme.colors[color][300], 78 | // }, 79 | // } 80 | // }) 81 | 82 | export interface AlertStyles { 83 | '.C9Y-Alert-base': CSSProperties 84 | '.C9Y-Alert-dismissible': CSSProperties 85 | '.C9Y-Alert-dismissed': CSSProperties 86 | '.C9Y-Alert-filled': { 87 | '& .C9Y-AlertLink': CSSProperties 88 | } & CSSProperties 89 | '.C9Y-AlertContent': CSSProperties 90 | '.C9Y-AlertHeading': CSSProperties 91 | '.C9Y-AlertClose': CSSProperties 92 | } 93 | -------------------------------------------------------------------------------- /src/components/Tabs/__snapshots__/Tabs.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[` snapshots > renders correctly 1`] = ` 4 |
9 |
13 | 24 | 35 | 46 | 58 |
59 |
62 |
69 | Tab 1 70 |
71 | 80 | 89 | 98 |
99 |
100 | `; 101 | -------------------------------------------------------------------------------- /src/components/Media/Media.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from 'react' 2 | 3 | export interface ApplicationMedia { 4 | sm: boolean 5 | md: boolean 6 | lg: boolean 7 | } 8 | 9 | /** Media Context */ 10 | const MediaCtx = createContext(null) 11 | 12 | // Default breakpoint values 13 | const sm = 0 14 | const md = 768 15 | const lg = 1250 16 | 17 | /** 18 | * Calculates the state for each breakpoint based on current window width 19 | * @param breakpoints - Set of application breakpoint values 20 | */ 21 | function calcBreakpoints(breakpoints: number[]): ApplicationMedia { 22 | const w = window.innerWidth 23 | 24 | return { 25 | sm: w < (breakpoints[1] ?? md), 26 | md: w >= (breakpoints[1] ?? md) && w < (breakpoints[2] ?? lg), 27 | lg: w >= (breakpoints[2] ?? lg), 28 | } 29 | } 30 | 31 | /** 32 | * Uses `window.matchMedia` to listen for changes to window media queries and 33 | * updates breakpoint status on every change. 34 | * @param breakpoints - Set of application breakpoints 35 | * @param updateBps - Hook state update function updater 36 | */ 37 | function mountListeners( 38 | breakpoints: number[], 39 | updateBps: (state: ApplicationMedia) => void, 40 | ) { 41 | function setBreakpoints() { 42 | updateBps(calcBreakpoints(breakpoints)) 43 | } 44 | 45 | breakpoints.forEach((bp) => { 46 | // https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Testing_media_queries#Receiving_query_notifications 47 | const mq = window.matchMedia(`(min-width: ${bp}px)`) 48 | mq.addListener(setBreakpoints) 49 | }) 50 | } 51 | 52 | export interface MediaProps { 53 | children: React.ReactNode 54 | breakpoints: number[] 55 | } 56 | /** 57 | * [Media component 📝](https://componentry.design/components/media) 58 | * @experimental 59 | */ 60 | export function Media({ children, breakpoints = [sm, md, lg] }: MediaProps): JSX.Element { 61 | const [bps, updateBps] = useState(calcBreakpoints(breakpoints)) 62 | 63 | // ℹ️ Call to mount media query listeners is wrapped in useEffect to prevent 64 | // mounting listeners multiple times. Currently we don't remove and remount 65 | // listeners if the breakpoints change, but that's a really edge case to 66 | // support... 67 | /* eslint-disable react-hooks/exhaustive-deps */ 68 | useEffect(() => { 69 | mountListeners(breakpoints, updateBps) 70 | }, []) 71 | /* eslint-enable react-hooks/exhaustive-deps */ 72 | 73 | return {children} 74 | } 75 | Media.displayName = 'Media' 76 | 77 | /** 78 | * [Media hook 📝](https://componentry.design/components/media) 79 | */ 80 | export const useMedia = (): ApplicationMedia => { 81 | const media = useContext(MediaCtx) 82 | if (!media) throw new Error('useMedia used outside of a provider') 83 | 84 | return media 85 | } 86 | -------------------------------------------------------------------------------- /src/components/IconButton/IconButton.spec.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react' 2 | import { describe, it, expect } from 'vitest' 3 | 4 | import { elementTests } from '../../test/element-tests' 5 | import { Icon } from '../Icon/Icon' 6 | import { IconButton } from './IconButton' 7 | 8 | describe('', () => { 9 | // Basic library element test suite 10 | elementTests(IconButton) 11 | 12 | // --- RENDER 13 | it('renders React element values', () => { 14 | render( 15 | } 17 | />, 18 | ) 19 | 20 | expect(screen.getByTestId('custom-element').dataset.special).toBe('very') 21 | }) 22 | 23 | it('renders Icon components for string values', () => { 24 | render() 25 | 26 | expect(screen.getByLabelText('code')).toHaveClass( 27 | 'C9Y-Icon-base C9Y-Icon-font icon-code', 28 | ) 29 | }) 30 | 31 | // --- BUTTON ATTRS 32 | it('when no props are passed, then defaults should be rendered', () => { 33 | render() 34 | 35 | // By default the button should have type button for a11y 36 | expect(screen.getByRole('button')).toHaveAttribute('type', 'button') 37 | // By default the variant filled 38 | expect(screen.getByRole('button')).toHaveClass( 39 | 'C9Y-IconButton-base C9Y-IconButton-filled', 40 | ) 41 | }) 42 | 43 | it('when `type` is passed, then it overrides the default', () => { 44 | render() 45 | 46 | expect(screen.getByRole('button')).toHaveAttribute('type', 'submit') 47 | }) 48 | 49 | it('when `variant` is passed, then it should be used as base className value', () => { 50 | render() 51 | 52 | expect(screen.getByRole('button')).toHaveClass( 53 | 'C9Y-IconButton-base C9Y-IconButton-outlined', 54 | ) 55 | }) 56 | 57 | it('when `color` is passed, then the color className should render', () => { 58 | render() 59 | 60 | expect(screen.getByRole('button')).toHaveClass( 61 | 'C9Y-IconButton-base C9Y-IconButton-filled C9Y-IconButton-primaryColor', 62 | ) 63 | }) 64 | 65 | it('when `size` is passed, then the size className should render', () => { 66 | render() 67 | 68 | expect(screen.getByRole('button')).toHaveClass( 69 | 'C9Y-IconButton-base C9Y-IconButton-smallSize', 70 | ) 71 | }) 72 | }) 73 | 74 | // Snapshots 75 | // --------------------------------------------------------------------------- 76 | describe(' Snapshots', () => { 77 | it('renders defaults correctly', () => { 78 | render() 79 | 80 | expect(screen.getByRole('button')).toMatchSnapshot() 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react' 2 | import { createElement } from '../../utils/create-element' 3 | import { MergeTypes, Resolve } from '../../utils/types' 4 | import { ElementTypeProps, UtilityProps } from '../../utils/utility-props' 5 | import { useThemeProps } from '../Provider/Provider' 6 | 7 | // -------------------------------------------------------- 8 | // ICON ELEMENTS MAP 9 | 10 | /** Mapping of icon IDs to components rendered by Icon */ 11 | export type IconElementsMap = { [ID: string]: React.ComponentType } 12 | 13 | let iconElementsMap: IconElementsMap = {} 14 | 15 | /** 16 | * Configuration method for defining the elements to render for each Icon ID. 17 | * @remarks 18 | * Configuring an icon elements map isn't necessary if you've setup an SVG 19 | * symbol sprite. 20 | * @example 21 | * ```ts 22 | * import Info from './info.svg' 23 | * import Coffee from './coffee.svg' 24 | * 25 | * configureIconElementsMap({ 26 | * info: Info, 27 | * coffee: Coffee 28 | * }) 29 | * ``` 30 | */ 31 | export function configureIconElementsMap(elementsMap: IconElementsMap): void { 32 | iconElementsMap = elementsMap 33 | } 34 | 35 | // -------------------------------------------------------- 36 | // ICON COMPONENT 37 | 38 | /** Module augmentation interface for overriding component props' types */ 39 | export interface IconPropsOverrides {} 40 | 41 | export interface IconPropsDefaults { 42 | /** Display variant */ 43 | variant?: 'font' 44 | /** External path to symbol sprite */ 45 | externalURI?: string 46 | /** ID for the `iconElementsMap` or href attribute for symbol sprites */ 47 | id: string 48 | } 49 | 50 | export type IconProps = Resolve< 51 | MergeTypes & { as?: As } & UtilityProps 52 | > & 53 | ElementTypeProps 54 | 55 | /** 56 | * Icon provides consistently themed iconography elements. 57 | * @example 58 | * ```tsx 59 | * 60 | * ``` 61 | * @see [📝 Icon](https://componentry.design/docs/components/icon) 62 | */ 63 | export interface Icon { 64 | (props: IconProps): React.ReactElement 65 | displayName?: string 66 | } 67 | 68 | export const Icon = forwardRef((props, ref) => { 69 | const { 70 | externalURI = '', 71 | id, 72 | variant = 'font', 73 | ...rest 74 | } = { ...useThemeProps('Icon'), ...props } 75 | 76 | const hasMappedElement = id in iconElementsMap 77 | 78 | return createElement({ 79 | ref, 80 | as: hasMappedElement ? iconElementsMap[id] : 'svg', 81 | children: hasMappedElement ? undefined : , 82 | componentClassName: `C9Y-Icon-base C9Y-Icon-${variant} icon-${id}`, 83 | role: 'img', 84 | 'aria-label': id, 85 | ...rest, 86 | }) 87 | }) as Icon 88 | Icon.displayName = 'Icon' 89 | -------------------------------------------------------------------------------- /docs/adr/02-classes.md: -------------------------------------------------------------------------------- 1 | # Component classes 2 | 3 | - Date: 1/11/22 4 | 5 | ## Context and Problem Statement 6 | 7 | The pattern for component classNames is a key interaction point for users, the 8 | library needs to balance brevity with specificity with configurability. 9 | 10 | ## Decision Drivers 11 | 12 | - The end result classNames should be readable and look "good". 13 | - Ideally the library avoids extra string manipulation if possible, eg changing 14 | `size="small"` to `C9Y-Button-sizeSmall` requires upper-casing first letter, 15 | adding code and execution overhead. 16 | - User style configuration is easier with camelCase b/c the styles are defined 17 | in JS. 18 | - The library needs to support users setting specific styles for 19 | variant+color/size combo. eg set color vs background color for filled vs 20 | outlined variant colors. 21 | - Library needs to avoid name collisions with any Tailwind classes. 22 | - Overriding classes should be possible by searching for the style definitions 23 | in Componentry, defining classes with some magic makes customization less 24 | intuitive. 25 | 26 | ## Decision Outcome 27 | 28 | Component classes are prefixed with `C9Y-Component`, variants don't have a 29 | suffix, modifier classes have suffixes, eg for a Button, variant filled, error 30 | color, and small size: 31 | 32 | ```html 33 | 38 | ``` 39 | 40 | Classes for sub-components do not have `-base`, eg for Card, there are classes 41 | `C9Y-CardHeader`, `C9Y-CardFooter`, etc. This pattern is intended to direct 42 | styles and usage towards using a single variant for each component style. 43 | 44 | ## Considered Options 45 | 46 | ### Decision - Variants 47 | 48 | ```html 49 |

Heading 1

50 | ``` 51 | 52 | Including "Variant" with the className was considered, but it seems heavy 53 | handed, and isn't necessary for distinguishing between variant and color 54 | classes. 55 | 56 | ### Decision - lowercase kebab 57 | 58 | ```html 59 | 64 | ``` 65 | 66 | Using lowercase kebab style was considered, but that makes defining override 67 | styles in JS more difficult. 68 | 69 | ### Positive Consequences 70 | 71 | - Meets all the requirements of Decision Drivers. 72 | 73 | ### Negative Consequences 74 | 75 | - Override styles use the library C9Y- emoji which is fun, but isn't easy to 76 | copy/paste 77 | 78 | ## Links 79 | 80 | - Blueprint is a good kebab case example: 81 | https://blueprintjs.com/docs/#core/components/button 82 | - MUI example of difficult to configure styles: 83 | https://github.com/mui-org/material-ui/blob/master/packages/mui-material-next/src/Button/Button.js 84 | --------------------------------------------------------------------------------