diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c07536e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,54 @@ +# CLAUDE.md + +Guidance for Claude Code when working in this repository. + +## Project overview + +`@mconf/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 '@mconf/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/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 6aa8e05..aea570a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@mconf/bbb-ui-components-react", - "version": "2.2.0", + "version": "2.3.0", "license": "LGPL-3.0", "dependencies": { "@tippyjs/react": "^4.2.6", @@ -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", @@ -3326,6 +3364,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" }, @@ -3345,8 +3384,7 @@ "@types/react": { "optional": true } - }, - "peer": true + } }, "node_modules/@mui/material": { "version": "6.5.0", @@ -3610,6 +3648,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", @@ -5928,6 +5979,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", @@ -6930,6 +6991,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", @@ -8279,6 +8350,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", @@ -11500,6 +11644,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", @@ -11820,6 +11971,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", @@ -11838,6 +11999,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", @@ -12781,6 +12949,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", @@ -13468,6 +13649,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", @@ -14171,6 +14377,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 c867055..48b774d 100644 --- a/package.json +++ b/package.json @@ -240,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", @@ -257,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", @@ -272,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", @@ -279,8 +282,6 @@ "globals": "^16.3.0", "path": "^0.12.7", "storybook": "^8.6.18", - "@storybook/addon-a11y": "^8.6.18", - "@storybook/addon-actions": "^8.6.18", "style-loader": "^4.0.0", "styled-components": "6.1.13", "ts-loader": "^9.5.1", 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/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/component.stories.tsx b/src/components/Button/component.stories.tsx index d393508..50a6554 100644 --- a/src/components/Button/component.stories.tsx +++ b/src/components/Button/component.stories.tsx @@ -115,6 +115,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 +135,7 @@ export const Primary: Story = { ), }; +/** Renders the `secondary` variant across all available colors. */ export const Secondary: Story = { name: 'Secondary', args: { @@ -153,6 +155,7 @@ export const Secondary: Story = { ), }; +/** Renders the `tertiary` variant across all available colors. */ export const Tertiary: Story = { name: 'Tertiary', args: { @@ -172,6 +175,7 @@ export const Tertiary: Story = { ), }; +/** Renders the `subtle` variant across all available colors. */ export const Subtle: Story = { name: 'Subtle', args: { @@ -191,6 +195,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 +217,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 +231,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 +254,7 @@ 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: { @@ -269,6 +277,7 @@ export const Squared: Story = { ), }; +/** Shows a labeled button with a leading icon via `iconStart`. */ export const WithIcon: Story = { args: { label: 'Like', @@ -277,6 +286,7 @@ export const WithIcon: Story = { }, }; +/** Renders every available `size` value side by side. */ export const AllSizes: Story = { name: 'All Sizes', args: { @@ -296,6 +306,7 @@ export const AllSizes: Story = { ), }; +/** Shows the button in its disabled state. */ export const Disabled: Story = { args: { label: 'Disabled Button', @@ -304,6 +315,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: { @@ -324,6 +336,7 @@ export const WithTooltip: Story = { ), }; +/** Compares buttons using `iconStart`, `iconEnd`, and both together. */ export const WithStartAndEndIcons: Story = { name: 'With Start and End Icons', args: { diff --git a/src/components/Button/type.ts b/src/components/Button/type.ts index 29857ef..8b63960 100644 --- a/src/components/Button/type.ts +++ b/src/components/Button/type.ts @@ -61,44 +61,93 @@ 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; } 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; }; 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/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/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/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/types.ts b/src/components/Input/types.ts index 8b7e8b1..2d516b0 100644 --- a/src/components/Input/types.ts +++ b/src/components/Input/types.ts @@ -25,10 +25,7 @@ export interface BBBInputProps { /** Text label rendered by the action button, instead of (or alongside) `buttonIcon`. */ buttonLabel?: string; - /** - * Accessible name for the action button. Falls back to `buttonLabel` when set. - * Required for the button to be accessible when only `buttonIcon` is provided. - */ + /** Accessible name for the action button. Falls back to `buttonLabel` when set. Required for the button to be accessible when only `buttonIcon` is provided. */ buttonAriaLabel?: string; /** Visual variant of the action button (same variants as the shared `Button`). @default 'primary' */ @@ -46,18 +43,10 @@ export interface BBBInputProps { /** Extra content (icon, button, or any other element) rendered to the right of the action button. */ afterButton?: React.ReactNode; - /** - * When `true`, pressing Enter submits the field (Shift+Enter still inserts a newline). - * When `false`, Enter always inserts a newline and only the button submits. - * @default true - */ + /** When `true`, pressing Enter submits the field (Shift+Enter still inserts a newline). When `false`, Enter always inserts a newline and only the button submits. @default true */ submitOnEnter?: boolean; - /** - * When `true`, shows `sentFeedbackContent` in place of `helperText` for `sentFeedbackDuration` - * after a successful submit. - * @default false - */ + /** When `true`, shows `sentFeedbackContent` in place of `helperText` for `sentFeedbackDuration` after a successful submit. @default false */ showSentFeedback?: boolean; /** Content rendered while the sent feedback is visible. @default 'Message sent' */ @@ -90,10 +79,13 @@ export interface BBBInputProps { /** Ref forwarded to the underlying `