diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..efd3ef7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,54 @@ +# CLAUDE.md + +Guidance for Claude Code when working in this repository. + +## Project overview + +`@bigbluebutton/bbb-ui-components-react` is a library of reusable React components extracted from BigBlueButton (BBB), published to npm. Each component lives in its own folder under `src/components//` and is built/exported independently (tree-shakeable per-component entry points). + +`CHANGELOG.md` and the package version are generated automatically on release by `TriPSs/conventional-changelog-action` (see `.github/workflows/tag_and_release.yml`), using the `conventionalcommits` preset. This is why **correct commit type and scope matter**: they directly drive the changelog content and the version bump (see "Commit conventions" below). + +## Adding a new component + +Component folder/file names are **raw** PascalCase (`Button`, `Input`, `Navigation`...), but the **exported** component name is prefixed: `BBB` (e.g. `Toggle` folder → `BBBToggle` export). The one known exception is `Button` → `BBButton` (only one extra `B`, since `BBBButton` reads badly) — check `src/components/index.ts` for how similarly-named components were prefixed before deciding, but default to the full `BBB` form. + +### Files to create in `src/components//` + +| File | Required? | Purpose | +|---|---|---| +| `component.tsx` | always | Component implementation. | +| `styles.ts` | always | styled-components, using `$`-prefixed transient props (internal, not part of the public API). | +| `index.ts` | always | `export { default as BBB } from './component';` | +| `component.stories.tsx` | always | Storybook stories: `title: 'BBB'`, `tags: ['autodocs']`, an `argTypes` entry with a `description` per public prop, and a one-line JSDoc comment above every named story export describing what that story demonstrates. | +| `README.md` | always | Usage examples + a Props table (`Property \| Type \| Default \| Description`). | +| `assets/example.png` | conventional | Demo screenshot referenced by the README as `![Demo](assets/example.png)`. Most components have one; add it when you can. | +| `types.ts` | if the component takes props | The public `Props` type/interface. Document every field with a single-line JSDoc comment (see "Prop JSDoc conventions" below). Internal styled-components prop interfaces (`Styled*Props`, `$`-prefixed fields) don't need JSDoc — they aren't public API. | +| `constants.ts` | if the component has variant/size/layout-style enums or shared defaults | Exported `*_VALUES` arrays and `DEFAULT_*` constants, reused by `types.ts`, `component.tsx`, and `component.stories.tsx`. | + +### References to update elsewhere + +- `src/components/index.ts` — add `export { BBB } from './';` (keep alphabetical by folder name). +- Root `README.md` — add `- [BBB](./src/components//README.md)` to the "Available Components" list (keep alphabetical). +- `webpack.config.babel.js` — add `: './src/components//index.ts',` to the `entry` map. +- `package.json` `exports` — add a `"./"` block, mirroring an existing one (e.g. copy the `"./Toggle"` block and replace `Toggle` with `` throughout). This is what makes `import { BBB } from '@bigbluebutton/bbb-ui-components-react/'` tree-shakeable. +- `src/index.ts` needs **no** change — it already does `export * from './components'`, which picks up new components automatically. + +## Adding a new prop to an existing component + +1. `types.ts`/`type.ts` — add the field to the public props type, with a single-line JSDoc comment directly above it (blank line before the comment; add an `@default` tag if the prop has a default value). This is enforced by the `jsdoc/multiline-blocks` and `jsdoc/lines-before-block` rules in `eslint.config.mjs`, scoped to `types.ts`/`type.ts`. +2. `component.tsx` — destructure the prop (with its default value, if any) and wire up the logic/rendering. +3. `styles.ts` — if the prop affects styling, thread it through as a `$`-prefixed transient prop on the relevant styled-component; it doesn't need JSDoc. +4. `component.stories.tsx` — add an `argTypes.` entry with a `description` (keep the wording in sync with the `types.ts` JSDoc), and add a new named story if the prop introduces a visually distinct state worth its own example, with a one-line JSDoc comment describing what it demonstrates. +5. `README.md` (component-level) — add a row to the Props table. + +A new prop does **not** require changes to the root `README.md`, `package.json` `exports`, or `webpack.config.babel.js` — those only change when adding a whole new component. + +## Commit conventions + +Commits follow Conventional Commits: `type(scope): subject`. + +- **Type**: `feat` (new component or new prop), `fix` (bug fix), `docs` (README/Storybook/JSDoc-only changes), `chore` (tooling/config), or `refactor`/`style`/`test`/`build`/`ci` as appropriate. `feat`/`fix` drive the automatic version bump, so getting the type right matters. +- **Scope**: the component's **exported** name — e.g. `BBButton`, `BBBInput`, `BBBModal`, `BBBTypography` — not the raw folder name. Some older commits in this repo used the raw lowercase name instead (e.g. `fix(toggle)`, `fix(spinner)`); that's the inconsistency to avoid, not a pattern to follow. +- Changes spanning multiple components (tooling, lint config, cross-cutting docs) can omit the scope, e.g. `chore(ci): ...`, `docs: ...`. + +Examples: `feat(BBBToggle): add indeterminate state`, `fix(BBButton): ...`, `docs(BBBTypography): add stories for every typography variant`. diff --git a/README.md b/README.md index 8f6d7b0..e9068ae 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Below is a list of the components available in this library. Each component has - [BBBCheckbox](./src/components/Checkbox/README.md) - [BBBDivider](./src/components/Divider/README.md) - [BBBHint](./src/components/Hint/README.md) +- [BBBInput](./src/components/Input/README.md) - [BBBModal](./src/components/Modal//README.md) - [BBBNavigation](./src/components/Navigation/README.md) - [BBBSearch](./src/components/Search/README.md) @@ -71,28 +72,28 @@ The following table lists the supported CSS variables for color overriding, extr | `--color-light-gray` | No | #F4F6FA | | `--color-gray` | No | #4E5A66 | | `--color-dark-gray` | No | #393C48 | -| `--color-brand-1` | No | #1D65D4 | -| `--color-brand-2` | No | #1D65D4 | -| `--color-brand-3` | No | #1D65D4 | +| `--color-brand-1` | No | #0F70D7 | +| `--color-brand-2` | No | #0F70D7 | +| `--color-brand-3` | No | #0F70D7 | | `--color-brand-light` | Yes (`--color-blue-aux`) | #E5EFFB | | `--color-brand-aux` | Yes (`--color-brand-aux`) | #E5EFFB | | `--color-success` | No | #2DD36F | | `--color-warning` | No | #F59240 | -| `--color-error` | Yes (`--color-danger`) | #D6232D | -| `--color-error-dark` | Yes (`--color-danger-dark`)| #B91C25 | +| `--color-error` | Yes (`--color-danger`) | #DF2721 | +| `--color-error-dark` | Yes (`--color-danger-dark`)| #AE1010 | | `--color-background-white` | No | #FFFFFF | | `--color-background-light` | No | #F4F6FA | | `--color-background-blue` | No | #E5EFFB | | `--color-border-default` | No | #B0BDC9 | -| `--color-border-selected` | No | #1D65D4 | -| `--color-border-error` | No | #D6232D | +| `--color-border-selected` | No | #0F70D7 | +| `--color-border-error` | No | #DF2721 | | `--color-border-default` | Yes (`--default-border`) | #B0BDC9 | | `--color-text-default` | No | #393C48 | | `--color-text-light` | No | #717C91 | | `--color-icon-default` | No | #717C91 | -| `--color-icon-blue` | No | #1D65D4 | +| `--color-icon-blue` | No | #0F70D7 | | `--color-icon-white` | No | #FFFFFF | -| `--color-hover-dark` | No | #1E53AF | +| `--color-hover-dark` | No | #0C57A7 | | `--color-hover-light` | No | #D4E5FA | | `--color-hover-neutral` | No | #DCE4EC | @@ -111,7 +112,7 @@ If you need to override colors for specific components or add new variables, ref You can install the library directly from npm: ```bash -npm install @mconf/bbb-ui-components-react +npm install @bigbluebutton/bbb-ui-components-react ``` ## Local Development diff --git a/eslint.config.mjs b/eslint.config.mjs index 4af6df1..529bff2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -6,6 +6,7 @@ import reactRecommended from 'eslint-plugin-react/configs/recommended.js'; import hooksPlugin from 'eslint-plugin-react-hooks'; import importPlugin from 'eslint-plugin-import'; import jsxA11yPlugin from 'eslint-plugin-jsx-a11y'; +import jsdocPlugin from 'eslint-plugin-jsdoc'; import globals from 'globals'; export default tseslint.config( @@ -71,4 +72,22 @@ export default tseslint.config( }, }, }, + + { + // Prop JSDoc in component `types.ts`/`type.ts` files: single-line blocks only, + // each preceded by a blank line (but not the first prop right after `{`). + files: ['src/components/**/types.ts', 'src/components/**/type.ts'], + plugins: { + jsdoc: jsdocPlugin, + }, + rules: { + 'jsdoc/multiline-blocks': ['error', { + noMultilineBlocks: true, + allowMultipleTags: false, + }], + 'jsdoc/lines-before-block': ['error', { + ignoreSingleLines: false, + }], + }, + }, ); diff --git a/package-lock.json b/package-lock.json index faf1c7e..e1683f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "eslint": "^9.33.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsdoc": "^62.9.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", @@ -2027,6 +2028,43 @@ "license": "MIT", "peer": true }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.86.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.86.0.tgz", + "integrity": "sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@typescript-eslint/types": "^8.58.0", + "comment-parser": "1.4.6", + "esquery": "^1.7.0", + "jsdoc-type-pratt-parser": "~7.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@es-joy/jsdoccomment/node_modules/jsdoc-type-pratt-parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-7.2.0.tgz", + "integrity": "sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@es-joy/resolve.exports": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@es-joy/resolve.exports/-/resolve.exports-1.2.0.tgz", + "integrity": "sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -3327,6 +3365,7 @@ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.5.0.tgz", "integrity": "sha512-VPuPqXqbBPlcVSA0BmnoE4knW4/xG6Thazo8vCLWkOKusko6DtwFV6B665MMWJ9j0KFohTIf3yx2zYtYacvG1g==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.26.0" }, @@ -3610,6 +3649,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/base62": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/base62/-/base62-1.0.0.tgz", + "integrity": "sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@storybook/addon-a11y": { "version": "8.6.18", "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-8.6.18.tgz", @@ -5968,6 +6020,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6960,6 +7022,16 @@ "node": ">= 6" } }, + "node_modules/comment-parser": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.6.tgz", + "integrity": "sha512-ObxuY6vnbWTN6Od72xfwN9DbzC7Y2vv8u1Soi9ahRKL37gb6y1qk6/dgjs+3JWuXJHWvsg3BXIwzd/rkmAwavg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/common-path-prefix": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", @@ -8309,6 +8381,79 @@ "strip-bom": "^3.0.0" } }, + "node_modules/eslint-plugin-jsdoc": { + "version": "62.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-62.9.0.tgz", + "integrity": "sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.86.0", + "@es-joy/resolve.exports": "1.2.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.6", + "debug": "^4.4.3", + "escape-string-regexp": "^4.0.0", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "html-entities": "^2.6.0", + "object-deep-merge": "^2.0.0", + "parse-imports-exports": "^0.2.4", + "semver": "^7.7.4", + "spdx-expression-parse": "^4.0.0", + "to-valid-identifier": "^1.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-jsx-a11y": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", @@ -11530,6 +11675,13 @@ "node": ">=0.10.0" } }, + "node_modules/object-deep-merge": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-deep-merge/-/object-deep-merge-2.0.1.tgz", + "integrity": "sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==", + "dev": true, + "license": "MIT" + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -11850,6 +12002,16 @@ "node": ">=6" } }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -11868,6 +12030,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12828,6 +12997,19 @@ "dev": true, "license": "MIT" }, + "node_modules/reserved-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reserved-identifiers/-/reserved-identifiers-1.2.0.tgz", + "integrity": "sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -13515,6 +13697,31 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -14198,6 +14405,23 @@ "node": ">=8.0" } }, + "node_modules/to-valid-identifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-valid-identifier/-/to-valid-identifier-1.0.0.tgz", + "integrity": "sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/base62": "^1.0.0", + "reserved-identifiers": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", diff --git a/package.json b/package.json index 6d8fb1e..a75aece 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,13 @@ "require": "./dist/components/Hint.js", "default": "./dist/components/Hint.js" }, + "./Input": { + "types": "./dist/types/components/Input/index.d.ts", + "node": "./dist/components/Input.js", + "import": "./dist/esm/components/Input/index.js", + "require": "./dist/components/Input.js", + "default": "./dist/components/Input.js" + }, "./Modal": { "types": "./dist/types/components/Modal/index.d.ts", "node": "./dist/components/Modal.js", @@ -172,6 +179,9 @@ "Hint": [ "dist/types/components/Hint/index.d.ts" ], + "Input": [ + "dist/types/components/Input/index.d.ts" + ], "Modal": [ "dist/types/components/Modal/index.d.ts" ], @@ -230,8 +240,8 @@ "peerDependencies": { "@emotion/react": "^11.13.0", "@emotion/styled": "^11.13.0", - "@mui/material": "^6.1.4 || ^7.0.0", "@mui/icons-material": "^6.1.4 || ^7.0.0", + "@mui/material": "^6.1.4 || ^7.0.0", "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -247,6 +257,8 @@ "@babel/preset-typescript": "^7.24.7", "@babel/register": "^7.24.6", "@eslint/js": "^9.33.0", + "@storybook/addon-a11y": "^8.6.18", + "@storybook/addon-actions": "^8.6.18", "@storybook/addon-docs": "^8.6.18", "@storybook/addon-links": "^8.6.18", "@storybook/addon-webpack5-compiler-babel": "^3.0.5", @@ -262,6 +274,7 @@ "eslint": "^9.33.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsdoc": "^62.9.0", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", diff --git a/src/components/Accordion/component.stories.tsx b/src/components/Accordion/component.stories.tsx index b8a8501..7b54464 100644 --- a/src/components/Accordion/component.stories.tsx +++ b/src/components/Accordion/component.stories.tsx @@ -54,6 +54,7 @@ const meta = { export default meta; type Story = StoryObj; +/** Basic accordion with a title and simple content, expanded/collapsed via the header. */ export const Default: Story = { args: { title: 'Accordion Title', @@ -65,6 +66,7 @@ export const Default: Story = { }, }; +/** Shows the header tooltip enabled via `tooltipLabel` and `tooltipPlacement`. */ export const WithTooltip: Story = { args: { title: 'With Tooltip', diff --git a/src/components/Accordion/styles.ts b/src/components/Accordion/styles.ts index 72ebb61..721cbb7 100644 --- a/src/components/Accordion/styles.ts +++ b/src/components/Accordion/styles.ts @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { colorBackgroundLight, colorBrand1, colorTextDefault, colorWhite } from '../../stylesheets/pallete'; +import { colorBackgroundLight, colorBrand1, colorTextDefault, colorWhite } from '../../stylesheets/palette'; import { fontSizeDefault } from '../../stylesheets/typography'; import { borderRadiusDefault, spacingMedium, spacingSmall } from '../../stylesheets/sizing'; import { StyledAccordionContent, StyledExpandIcon } from './types'; diff --git a/src/components/Accordion/types.ts b/src/components/Accordion/types.ts index 24b2e0c..87b09e4 100644 --- a/src/components/Accordion/types.ts +++ b/src/components/Accordion/types.ts @@ -13,12 +13,27 @@ export interface StyledAccordionContent { type TooltipPlacementType = typeof TOOLTIP_PLACEMENT_VALUES[number]; export interface AccordionProps { + /** The text to be displayed in the accordion header. */ title: string; + + /** Optional label for a tooltip that appears when hovering the header; no tooltip is shown when omitted. */ tooltipLabel?: string; + + /** Placement of the tooltip when `tooltipLabel` is provided. @default 'top' */ tooltipPlacement?: TooltipPlacementType; + + /** Accessible name for the expand button. */ ariaLabel?: string; + + /** ID of the element that labels the expand button. */ ariaLabelledBy?: string; + + /** ID of the element that describes the expand button. */ ariaDescribedBy?: string; + + /** Optional React node rendered inside the button header, alongside the title. @default null */ buttonHeader?: React.ReactNode; + + /** Content shown when the accordion is expanded. */ children?: React.ReactNode; }; diff --git a/src/components/Button/README.md b/src/components/Button/README.md index 9438bcd..9e6914e 100644 --- a/src/components/Button/README.md +++ b/src/components/Button/README.md @@ -1,6 +1,6 @@ # BBButton -The `BBButton` component is a versatile button that supports multiple layouts and styles. It can be rendered as a standard inline button, a circular icon button, or a stacked button with an icon and a label. +The `BBButton` component is a versatile button that supports multiple layouts and styles. It can be rendered as a standard inline button, a circular icon button, a compact squared icon button, or a stacked button with an icon and a label. ![Demo](assets/example.png) @@ -33,6 +33,15 @@ import { MdFavorite } from 'react-icons/md'; } tooltipLabel="Favorite" onClick={() => (console.log('clicked'))}/> ``` +### Squared Button + +```jsx +import { BBButton } from 'bbb-ui-components-react'; +import { MdSend } from 'react-icons/md'; + +} ariaLabel="Send" onClick={() => (console.log('clicked'))}/> +``` + ### Stacked Button ```jsx @@ -62,9 +71,12 @@ import { MdPlayArrow } from 'react-icons/md'; | `layout` | `keyof typeof LAYOUTS` | `'default'` | The layout of the button. | | `disabled` | `boolean` | `false` | If `true`, the button will be disabled. | | `children` | `React.ReactNode` | | The content of the button, typically used for the 'default' layout. | -| `icon` | `React.ReactNode` | | The icon to be displayed. Used for 'circle' and 'stacked' layouts. | +| `icon` | `React.ReactNode` | | The icon to be displayed. Used for 'circle', 'squared' and 'stacked' layouts. | | `iconStart` | `React.ReactNode` | | The icon to be displayed at the start of the button. Used for 'default' layout.| | `iconEnd` | `React.ReactNode` | | The icon to be displayed at the end of the button. Used for 'default' layout. | | `helperIcon` | `React.ReactNode` | | The auxiliary icon to be displayed. Used for 'stacked' layout. | | `hideHelperIcon` | `boolean` | `false` | If `true`, the auxiliary icon will be hidden. Used for 'stacked' layout.| | `helperOnClick` | `(event: React.MouseEvent) => void` | | The function to be called when the auxiliary icon is clicked. | +| `showFeedback` | `boolean` | `false` | If `true`, temporarily shows `feedbackContent` in place of the button's own label/icon for `feedbackDuration` after `onClick` fires. | +| `feedbackContent` | `React.ReactNode` | `` | Content rendered while the click feedback is visible (e.g. a checkmark icon and "Copied"). | +| `feedbackDuration` | `number` | `2000` | How long, in milliseconds, the click feedback stays visible. | diff --git a/src/components/Button/component.stories.tsx b/src/components/Button/component.stories.tsx index 12a4806..8020311 100644 --- a/src/components/Button/component.stories.tsx +++ b/src/components/Button/component.stories.tsx @@ -2,7 +2,9 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; import BBButton from './component'; -import { MdFavorite, MdMoreVert, MdSettings } from 'react-icons/md'; +import { + MdFavorite, MdMoreVert, MdSettings, MdCheckCircle, MdContentCopy, +} from 'react-icons/md'; import { VARIANT_VALUES, SIZE_VALUES, @@ -69,7 +71,7 @@ const meta = { }, icon: { control: false, - description: `Main icon for the button. Commonly used for icon-only buttons (layout 'circle') or in 'stacked' layout. When using a label, prefer \`iconStart\` or \`iconEnd\` in the 'default' layout.`, + description: `Main icon for the button. Commonly used for icon-only buttons (layouts 'circle' and 'squared') or in 'stacked' layout. When using a label, prefer \`iconStart\` or \`iconEnd\` in the 'default' layout.`, table: { defaultValue: { summary: 'null' } }, }, iconStart: { @@ -102,6 +104,21 @@ const meta = { description: `Keyboard keydown event handler. Useful to handle custom keyboard interactions; receives the native keyboard event. Works across all layouts.`, table: { defaultValue: { summary: 'null' } }, }, + showFeedback: { + control: 'boolean', + description: 'When `true`, temporarily shows `feedbackContent` in place of the button\'s own label/icon for `feedbackDuration` after `onClick` fires.', + table: { defaultValue: { summary: 'false' } }, + }, + feedbackContent: { + control: false, + description: 'Content rendered while the click feedback is visible (e.g. a checkmark icon and "Copied"). Only relevant when `showFeedback` is `true`.', + table: { defaultValue: { summary: '' } }, + }, + feedbackDuration: { + control: { type: 'number', min: 0, step: 100 }, + description: 'How long, in milliseconds, the click feedback stays visible. Only relevant when `showFeedback` is `true`.', + table: { defaultValue: { summary: '2000' } }, + }, }, args: { @@ -115,6 +132,7 @@ const meta = { export default meta; type Story = StoryObj; +/** Renders the `primary` variant across all available colors. */ export const Primary: Story = { name: 'Primary', args: { @@ -134,6 +152,7 @@ export const Primary: Story = { ), }; +/** Renders the `secondary` variant across all available colors. */ export const Secondary: Story = { name: 'Secondary', args: { @@ -153,6 +172,7 @@ export const Secondary: Story = { ), }; +/** Renders the `tertiary` variant across all available colors. */ export const Tertiary: Story = { name: 'Tertiary', args: { @@ -172,6 +192,7 @@ export const Tertiary: Story = { ), }; +/** Renders the `subtle` variant across all available colors. */ export const Subtle: Story = { name: 'Subtle', args: { @@ -191,6 +212,7 @@ export const Subtle: Story = { ), }; +/** Renders the `stacked` layout with an icon and label across all available colors. */ export const Stacked: Story = { name: 'Stacked', args: { @@ -212,6 +234,7 @@ export const Stacked: Story = { ), }; +/** Demonstrates the `stacked` layout with a clickable helper icon separate from the main action. */ export const StackedWithHelperClick: Story = { name: 'Stacked with Helper Click', args: { @@ -225,6 +248,7 @@ export const StackedWithHelperClick: Story = { }, }; +/** Renders the icon-only `circle` layout across all available colors, each with a tooltip. */ export const Circle: Story = { name: 'Circle', args: { @@ -247,6 +271,30 @@ export const Circle: Story = { ), }; +/** Renders the icon-only `squared` layout across all available colors, each with a tooltip. */ +export const Squared: Story = { + name: 'Squared', + args: { + tooltipPlacement: 'top', + variant: 'primary', + layout: 'squared', + icon: , + }, + render: (args) => ( +
+ {COLOR_VALUES.map((color) => ( + + ))} +
+ ), +}; + +/** Shows a labeled button with a leading icon via `iconStart`. */ export const WithIcon: Story = { args: { label: 'Like', @@ -255,6 +303,7 @@ export const WithIcon: Story = { }, }; +/** Renders every available `size` value side by side. */ export const AllSizes: Story = { name: 'All Sizes', args: { @@ -274,6 +323,7 @@ export const AllSizes: Story = { ), }; +/** Shows the button in its disabled state. */ export const Disabled: Story = { args: { label: 'Disabled Button', @@ -282,6 +332,7 @@ export const Disabled: Story = { }, }; +/** Renders the same button with a tooltip in each of the available `tooltipPlacement` positions. */ export const WithTooltip: Story = { name: 'With Tooltip (All Placements)', args: { @@ -302,6 +353,7 @@ export const WithTooltip: Story = { ), }; +/** Compares buttons using `iconStart`, `iconEnd`, and both together. */ export const WithStartAndEndIcons: Story = { name: 'With Start and End Icons', args: { @@ -331,3 +383,19 @@ export const WithStartAndEndIcons: Story = { ), }; + +/** Shows a transient "Copied" feedback in place of the label after clicking. */ +export const WithFeedback: Story = { + name: 'With Click Feedback', + args: { + label: 'Copy Link', + variant: 'primary', + iconStart: , + showFeedback: true, + feedbackContent: ( + + Copied + + ), + }, +}; diff --git a/src/components/Button/component.tsx b/src/components/Button/component.tsx index bb96125..f0fd766 100644 --- a/src/components/Button/component.tsx +++ b/src/components/Button/component.tsx @@ -1,5 +1,7 @@ -import React, { JSX, useId } from 'react'; -import { MdSettings } from 'react-icons/md'; +import React, { + JSX, useCallback, useEffect, useId, useRef, useState, +} from 'react'; +import { MdSettings, MdCheckCircle } from 'react-icons/md'; import { ButtonProps } from './type'; import { DEFAULT_VARIANT, @@ -39,8 +41,28 @@ function Button(props: ButtonProps): JSX.Element { layout = DEFAULT_LAYOUT, disabled = false, children, + showFeedback = false, + feedbackContent = , + feedbackDuration = 2000, } = props; + const feedbackTimeoutRef = useRef | null>(null); + const [isFeedbackVisible, setFeedbackVisible] = useState(false); + + useEffect(() => () => { + if (feedbackTimeoutRef.current) clearTimeout(feedbackTimeoutRef.current); + }, []); + + const handleClick: React.MouseEventHandler = useCallback((event) => { + onClick(event); + + if (showFeedback) { + setFeedbackVisible(true); + if (feedbackTimeoutRef.current) clearTimeout(feedbackTimeoutRef.current); + feedbackTimeoutRef.current = setTimeout(() => setFeedbackVisible(false), feedbackDuration); + } + }, [onClick, showFeedback, feedbackDuration]); + const accessibilityProps: { 'aria-label'?: string; 'aria-labelledby'?: string; @@ -48,12 +70,23 @@ function Button(props: ButtonProps): JSX.Element { } = {}; const generatedId = useId(); const defaultLabelId = `${id || generatedId}-label`; + // Only default aria-labelledby to defaultLabelId when an element with that id is + // actually rendered (stacked always renders ButtonText; default when labeled, or + // when the feedback content is taking the label's place). circle never renders a + // label element, so it must rely on aria-label instead — otherwise aria-labelledby + // would point at a non-existent id. + const hasRenderedLabelElement = layout === LAYOUTS.STACKED + || (layout === LAYOUTS.DEFAULT && (Boolean(label) || isFeedbackVisible)); accessibilityProps['aria-label'] = ariaLabel || label; - accessibilityProps['aria-labelledby'] = ariaLabelledBy || defaultLabelId; + if (ariaLabelledBy) { + accessibilityProps['aria-labelledby'] = ariaLabelledBy; + } else if (hasRenderedLabelElement) { + accessibilityProps['aria-labelledby'] = defaultLabelId; + } if (ariaDescribedBy) accessibilityProps['aria-describedby'] = ariaDescribedBy; const buttonElement = (() => { - if (props.layout === LAYOUTS.CIRCLE) { + if (props.layout === LAYOUTS.CIRCLE || props.layout === LAYOUTS.SQUARED) { const { icon } = props; const testId = dataTest || `button-${id || 'default'}`; @@ -61,7 +94,7 @@ function Button(props: ButtonProps): JSX.Element { - {icon} + {isFeedbackVisible ? ( + + {feedbackContent} + + ) : icon} ); } @@ -88,7 +125,7 @@ function Button(props: ButtonProps): JSX.Element { {helperIcon} )} - {icon && {icon}} + {isFeedbackVisible ? ( + + + {feedbackContent} + + + ) : (icon && {icon})} {label} @@ -132,7 +175,7 @@ function Button(props: ButtonProps): JSX.Element { - {iconStart && iconStart} - {label && {label}} - {children} - {iconEnd && iconEnd} + {isFeedbackVisible ? ( + + {feedbackContent} + + ) : ( + <> + {iconStart && iconStart} + {label && {label}} + {children} + {iconEnd && iconEnd} + + )} ); })(); diff --git a/src/components/Button/constants.ts b/src/components/Button/constants.ts index 7deb88f..22517be 100644 --- a/src/components/Button/constants.ts +++ b/src/components/Button/constants.ts @@ -14,7 +14,7 @@ import { colorIconDefault, colorGray, colorBrandAux, -} from '../../stylesheets/pallete'; +} from '../../stylesheets/palette'; import { spacingMedium, spacingSmall, spacingSmallMedium } from '../../stylesheets/sizing'; import { CSSColorPropertiesType, CSSSizePropertiesType, EssentialColorProperties } from './type'; @@ -46,6 +46,7 @@ const LAYOUTS = { DEFAULT: 'default', STACKED: 'stacked', CIRCLE: 'circle', + SQUARED: 'squared', } as const; const LAYOUT_VALUES = Object.values(LAYOUTS); const DEFAULT_LAYOUT = LAYOUT_VALUES[0]; diff --git a/src/components/Button/styles.ts b/src/components/Button/styles.ts index 25256a8..d384e80 100644 --- a/src/components/Button/styles.ts +++ b/src/components/Button/styles.ts @@ -15,9 +15,10 @@ import { } from './type'; import { borderRadiusDefault, + borderRadiusSmall, spacingSmall, } from '../../stylesheets/sizing'; -import { colorBorderDefault, colorTextDefault } from '../../stylesheets/pallete'; +import { colorBorderDefault, colorTextDefault } from '../../stylesheets/palette'; import { fontSizeSmall } from '../../stylesheets/typography'; const commonButtonStyles = css` @@ -129,6 +130,18 @@ const circleLayoutStyles = css` align-items: center; `; +// Icon-only, sized by its own (equal, non-text) padding rather than a fixed +// box — stays compact next to a single line of text instead of forcing a +// fixed height like circle/stacked do. +const squaredLayoutStyles = css` + border-radius: ${borderRadiusSmall}; + aspect-ratio: 1; + padding: ${spacingSmall}; + display: flex; + justify-content: center; + align-items: center; +`; + const defaultLayoutStyles = css` display: inline-flex; justify-content: center; @@ -156,6 +169,8 @@ export const Button = styled.button` return stackedLayoutStyles; case LAYOUTS.CIRCLE: return circleLayoutStyles; + case LAYOUTS.SQUARED: + return squaredLayoutStyles; default: return defaultLayoutStyles; } @@ -205,6 +220,13 @@ export const IconWrapper = styled.div` } `; +export const FeedbackContent = styled.span` + display: inline-flex; + align-items: center; + justify-content: center; + gap: ${spacingSmall}; +`; + export const ButtonText = styled.div` color: ${colorTextDefault}; font-size: ${fontSizeSmall}; diff --git a/src/components/Button/type.ts b/src/components/Button/type.ts index a48b355..30c9281 100644 --- a/src/components/Button/type.ts +++ b/src/components/Button/type.ts @@ -61,40 +61,107 @@ export interface StyledButtonProps { } type BaseButtonProps = { + /** HTML `id` for the root element; auto-generated via `useId` when omitted, and used to derive the label's id and default `data-test` value. @default '' */ id?: string; + + /** Value for the `data-test` attribute, used for test selectors; falls back to an auto-generated id based on `id`/`label` when omitted. */ dataTest?: string; + + /** Text label for the button; rendered in layouts that display a label (e.g. `default`, `stacked`). */ label?: string; + + /** Text shown in a tooltip; the tooltip only renders when this is provided. */ tooltipLabel?: string; + + /** Placement of the tooltip relative to the button; only used when `tooltipLabel` is set. @default 'top' */ tooltipPlacement?: TooltipPlacementType; + + /** Click handler for the button's primary action (required); for the `stacked` layout this is distinct from `helperOnClick`. */ onClick: React.MouseEventHandler; + + /** Keydown handler for the button element, for custom keyboard interactions. */ onKeyDown?: React.KeyboardEventHandler; + + /** Id of the element that labels the button (`aria-labelledby`); when omitted, falls back to the id of the button's own rendered label element, if any. */ ariaLabelledBy?: string; + + /** Id of the element that describes the button, wired to `aria-describedby`, when provided. */ ariaDescribedBy?: string; + + /** Accessible name for the button (`aria-label`); falls back to `label` when not provided. */ ariaLabel?: string; + + /** Color theme for the button (affects background/border/text depending on `variant`). @default 'default' */ color?: ColorType; + + /** Visual variant controlling emphasis and background/border styling. @default 'primary' */ variant?: VariantType; + + /** Button size; affects padding and, for the `circle` layout, the overall diameter. @default 'md' */ size?: SizeType; + + /** Disables interaction and applies disabled styling. @default false */ disabled?: boolean; + + /** Additional content rendered inside the button, after the label (only used in the `default` layout). */ children?: React.ReactNode; + + /** + * When `true`, temporarily shows `feedbackContent` in place of the button's own + * label/icon for `feedbackDuration` after `onClick` fires. + * @default false + */ + showFeedback?: boolean; + + /** Content rendered while the click feedback is visible (e.g. a checkmark icon and "Copied"). @default */ + feedbackContent?: React.ReactNode; + + /** How long, in milliseconds, the click feedback stays visible. @default 2000 */ + feedbackDuration?: number; } type DefaultLayoutProps = BaseButtonProps & { + /** Layout mode for this variant — always `'default'`, the standard inline label/icon button; also the overall default. @default 'default' */ layout?: typeof LAYOUTS.DEFAULT; + + /** Icon rendered before the label (left side); effective when a label is present. */ iconStart?: React.ReactNode; + + /** Icon rendered after the label (right side); effective when a label is present. */ iconEnd?: React.ReactNode; }; type StackedLayoutProps = BaseButtonProps & { + /** Layout mode for this variant — must be `'stacked'`, rendering a main icon with a label underneath plus an optional helper icon. */ layout: typeof LAYOUTS.STACKED; + + /** Main icon for the button, shown above the label. */ icon?: React.ReactNode; + + /** Auxiliary icon for a secondary action, shown next to the main icon; clickable via `helperOnClick`. @default MdSettings icon */ helperIcon?: React.ReactNode; + + /** Hides the helper icon entirely when true. @default false */ hideHelperIcon?: boolean; + + /** Click handler for the helper icon, triggering a secondary action separate from `onClick`. @default null */ helperOnClick?: React.MouseEventHandler; }; type CircleLayoutProps = BaseButtonProps & { + /** Layout mode for this variant — must be `'circle'`, rendering an icon-only circular button. */ layout: typeof LAYOUTS.CIRCLE; + + /** Icon rendered inside the circular button (no label is shown in this layout). */ + icon?: React.ReactNode; +}; + +type SquaredLayoutProps = BaseButtonProps & { + /** Layout mode for this variant — must be `'squared'`, rendering an icon-only square button. */ + layout: typeof LAYOUTS.SQUARED; + + /** Icon rendered inside the square button (no label is shown in this layout). */ icon?: React.ReactNode; }; -export type ButtonProps = DefaultLayoutProps | StackedLayoutProps | CircleLayoutProps; +export type ButtonProps = DefaultLayoutProps | StackedLayoutProps | CircleLayoutProps | SquaredLayoutProps; diff --git a/src/components/Checkbox/component.stories.tsx b/src/components/Checkbox/component.stories.tsx index 65c029f..d5d14ac 100644 --- a/src/components/Checkbox/component.stories.tsx +++ b/src/components/Checkbox/component.stories.tsx @@ -31,12 +31,14 @@ const meta = { export default meta; type Story = StoryObj; +/** Basic checkbox with a label, using the default layout. */ export const Default: Story = { args: { label: 'Checkbox label', }, }; +/** Renders the checkbox as a round radio-style control via the `round` prop. */ export const Round: Story = { args: { label: 'Round Checkbox', diff --git a/src/components/Checkbox/styles.ts b/src/components/Checkbox/styles.ts index 08f1b72..2354210 100644 --- a/src/components/Checkbox/styles.ts +++ b/src/components/Checkbox/styles.ts @@ -1,6 +1,6 @@ import styled from 'styled-components'; import { Checkbox as MuiCheckbox } from '@mui/material'; -import { colorTextDefault, colorBrand1 } from '../../stylesheets/pallete'; +import { colorTextDefault, colorBrand1 } from '../../stylesheets/palette'; import { fontSizeDefault } from '../../stylesheets/typography'; import { spacingSmall } from '../../stylesheets/sizing'; diff --git a/src/components/Checkbox/types.ts b/src/components/Checkbox/types.ts index a4c6032..bd7298c 100644 --- a/src/components/Checkbox/types.ts +++ b/src/components/Checkbox/types.ts @@ -4,8 +4,15 @@ import { LAYOUTS } from './constants'; type Layout = typeof LAYOUTS[keyof typeof LAYOUTS]; export interface CheckboxProps extends MuiCheckboxProps { + /** Text label displayed next to the checkbox. */ label?: string; + + /** Position of the label relative to the checkbox. @default 'right' */ layout?: Layout; + + /** When true, renders the checkbox as a round radio-style control. @default false */ round?: boolean; + + /** Accessible name for the checkbox. */ ariaLabel?: string; } diff --git a/src/components/Divider/component.stories.tsx b/src/components/Divider/component.stories.tsx index fb8d12f..d89e4b5 100644 --- a/src/components/Divider/component.stories.tsx +++ b/src/components/Divider/component.stories.tsx @@ -10,4 +10,5 @@ const meta = { export default meta; type Story = StoryObj; +/** Renders the divider with its default styles and no custom props. */ export const Default: Story = {}; diff --git a/src/components/Divider/styles.ts b/src/components/Divider/styles.ts index a8e4589..a3f1651 100644 --- a/src/components/Divider/styles.ts +++ b/src/components/Divider/styles.ts @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { colorBorderDefault } from '../../stylesheets/pallete'; +import { colorBorderDefault } from '../../stylesheets/palette'; export const Divider = styled.hr` width: 100%; diff --git a/src/components/Hint/component.stories.tsx b/src/components/Hint/component.stories.tsx index 5879041..65a8a14 100644 --- a/src/components/Hint/component.stories.tsx +++ b/src/components/Hint/component.stories.tsx @@ -32,6 +32,7 @@ const meta = { export default meta; type Story = StoryObj; +/** Basic hint rendering with only a label and the default info icon. */ export const Default: Story = { args: { label: 'Helpful hint', diff --git a/src/components/Hint/styles.ts b/src/components/Hint/styles.ts index e6af464..e9605cd 100644 --- a/src/components/Hint/styles.ts +++ b/src/components/Hint/styles.ts @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { colorTextDefault, colorBrand1, colorBackgroundLight, colorIconDefault, colorHoverLight } from '../../stylesheets/pallete'; +import { colorTextDefault, colorBrand1, colorBackgroundLight, colorIconDefault, colorHoverLight } from '../../stylesheets/palette'; import { fontSizeDefault } from '../../stylesheets/typography'; import { borderRadiusSmall, spacingMedium, spacingSmall, spacingSmallMedium } from '../../stylesheets/sizing'; diff --git a/src/components/Hint/types.ts b/src/components/Hint/types.ts index 347e096..eddf53e 100644 --- a/src/components/Hint/types.ts +++ b/src/components/Hint/types.ts @@ -1,7 +1,16 @@ export interface HintProps extends React.HTMLAttributes { + /** Main text content of the hint. */ label: string; + + /** Optional title; if provided, shows a close button. */ title?: string; + + /** Optional icon node displayed next to the title or label. @default */ icon?: React.ReactNode; + + /** Callback fired when the close button is clicked. */ onRequestClose?: () => void; + + /** Optional additional content rendered under the label. */ children?: React.ReactNode; } diff --git a/src/components/Input/README.md b/src/components/Input/README.md new file mode 100644 index 0000000..f8ac888 --- /dev/null +++ b/src/components/Input/README.md @@ -0,0 +1,118 @@ +# BBBInput + +The `BBBInput` component provides a labeled, auto-growing text field with an inline action button. It unifies the recurring "text field + submit button" pattern used across chat, guest messages, breakouts and similar areas, modeled after the core chat text input. + +`BBBTextInput` and `BBBTextAreaInput` remain the right choice for standalone fields with no inline action button. + +## Usage Example + +### Basic usage — text field with a submit icon button +```jsx +import { BBBInput } from 'bbb-ui-components-react'; +import { MdSend } from 'react-icons/md'; + +} + buttonAriaLabel="Send message" + onSubmit={(value) => handleSend(value)} +/> +``` + +### With a text button label instead of an icon +```jsx + sendGuestMessage(value)} +/> +``` + +### Controlled +```jsx +const [value, setValue] = useState(''); + +} + buttonAriaLabel="Send message" + onSubmit={(submittedValue) => { + submitMessage(submittedValue); + setValue(''); + }} +/> +``` + +### With sent feedback +```jsx +} + buttonAriaLabel="Send message" + showSentFeedback + onSubmit={(value) => handleSend(value)} +/> +``` + +### With extra actions around the submit button +```jsx +import { MdEmojiEmotions } from 'react-icons/md'; + +} + buttonAriaLabel="Send message" + beforeButton={( + + )} + onSubmit={(value) => handleSend(value)} +/> +``` + +## Props + +| Property | Type | Default | Description | +| ----------------------- | --------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------ | +| `label` | `string` | | Label displayed above the field. | +| `placeholder` | `string` | | Placeholder text displayed when the field is empty. | +| `value` | `string` | | Controlled value. Enables controlled mode when provided together with `onChange`. | +| `onChange` | `(value: string) => void` | | Called on every keystroke. Required in controlled mode. | +| `onSubmit` | `(value: string) => void` | | Called with the trimmed, non-empty value when the field is submitted. **Required.** | +| `buttonIcon` | `React.ReactNode` | | Icon rendered inside the action button. | +| `buttonLabel` | `string` | | Text label rendered by the action button, instead of (or alongside) `buttonIcon`. | +| `buttonAriaLabel` | `string` | | Accessible name for the action button. **Required** when only `buttonIcon` is set (no `buttonLabel`). | +| `buttonVariant` | `'primary' \| 'secondary' \| 'tertiary' \| 'subtle'` | `'primary'` | Visual variant of the action button (same variants as the shared `Button`). | +| `dataTest` | `string` | | `data-test` attribute on the underlying `