From ccd1baa20d006a5b62afff53e0198b35a5e59c06 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Tue, 28 Jul 2026 14:51:30 -0300 Subject: [PATCH 1/7] feat(BBBHint): allow uncontrolled open/close state Adds an optional `open` prop so BBBHint can manage its own visibility and close itself when the close button is clicked, removing the need for external useState boilerplate in simple cases. Passing `open` switches to the existing controlled behavior, where the parent decides visibility via `onRequestClose`. Closes #29 --- src/components/Hint/README.md | 32 +++++++++++++++- src/components/Hint/component.stories.tsx | 46 ++++++++++++++++++++++- src/components/Hint/component.tsx | 19 ++++++++-- src/components/Hint/types.ts | 7 +++- 4 files changed, 95 insertions(+), 9 deletions(-) diff --git a/src/components/Hint/README.md b/src/components/Hint/README.md index 33a25ca..755acb6 100644 --- a/src/components/Hint/README.md +++ b/src/components/Hint/README.md @@ -26,13 +26,41 @@ import { BBBHint } from 'bbb-ui-components-react'; /> ``` +### Uncontrolled Hint (default) + +Without an `open` prop, the hint manages its own visibility and closes itself when the close button is clicked — no external state required. + +```jsx +import { BBBHint } from 'bbb-ui-components-react'; + + +``` + +### Controlled Hint + +Pass `open` to drive visibility externally; the hint calls `onRequestClose` instead of hiding itself, leaving the parent in charge of updating `open`. + +```jsx +import { useState } from 'react'; +import { BBBHint } from 'bbb-ui-components-react'; + +const [open, setOpen] = useState(true); + + setOpen(false)} + label="This hint's visibility is controlled externally." +/> +``` + ## Props | Property | Type | Default | Description | | ---------------- | -------------------------------- | ------- | ------------------------------------------------------------------------------------ | | `label` | `string` | | The main text content of the hint. | -| `title` | `string` | | An optional title for the hint. If provided, a close button will be displayed. | +| `title` | `string` | | An optional title shown in the header; when set, `label` renders as a separate line below instead of inline. | | `icon` | `React.ReactNode` | | An optional icon to be displayed next to the title or label. | -| `onRequestClose` | `() => void` | | A callback function to be called when the close button is clicked. | +| `open` | `boolean` | | Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally. | +| `onRequestClose` | `() => void` | | A callback function to be called when the close button is clicked, in both controlled and uncontrolled mode. | | `children` | `React.ReactNode` | | Optional additional content to be displayed below the label. | | `...props` | `HTMLAttributes` | | Any other props will be passed down to the underlying container div. | diff --git a/src/components/Hint/component.stories.tsx b/src/components/Hint/component.stories.tsx index 65a8a14..7a1a555 100644 --- a/src/components/Hint/component.stories.tsx +++ b/src/components/Hint/component.stories.tsx @@ -1,5 +1,7 @@ +import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import BBBHint from './component'; +import { BBButton } from '../Button'; const meta = { title: 'BBBHint', @@ -12,15 +14,19 @@ const meta = { }, title: { control: 'text', - description: 'Optional title; if provided, shows a close button.', + description: 'Optional title shown in the header; when set, label renders as a separate line below instead of inline.', }, icon: { control: false, description: 'Optional icon node displayed next to the title or label.', }, + open: { + control: 'boolean', + description: 'Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally.', + }, onRequestClose: { control: false, - description: 'Callback fired when the close button is clicked.', + description: 'Callback fired when the close button is clicked, in both controlled and uncontrolled mode.', }, children: { control: false, @@ -32,9 +38,45 @@ const meta = { export default meta; type Story = StoryObj; +/** + * Wrapper component so hooks can be used inside Storybook's render function, + * demonstrating the `open` prop driving visibility from outside the hint. + */ +const ControlledHintStory: React.FC> = (args) => { + const [open, setOpen] = useState(true); + + return ( +
+ setOpen(true)} /> + setOpen(false)} + /> +
+ ); +}; + /** Basic hint rendering with only a label and the default info icon. */ export const Default: Story = { args: { label: 'Helpful hint', }, }; + +/** Uncontrolled hint (no `open` prop): it manages its own visibility and closes itself when dismissed. */ +export const Uncontrolled: Story = { + args: { + title: 'Uncontrolled', + label: 'This hint manages its own visibility and closes itself when the close button is clicked.', + }, +}; + +/** Controlled hint: visibility is driven by the `open` prop, so the parent decides when it reappears. */ +export const Controlled: Story = { + args: { + title: 'Controlled', + label: "This hint's visibility is controlled externally via the open prop.", + }, + render: (args) => , +}; diff --git a/src/components/Hint/component.tsx b/src/components/Hint/component.tsx index 50bb01b..a45518e 100644 --- a/src/components/Hint/component.tsx +++ b/src/components/Hint/component.tsx @@ -1,4 +1,4 @@ -import React, { JSX } from 'react'; +import React, { JSX, useState } from 'react'; import * as Styled from './styles'; import { HintProps } from './types'; import { MdClose, MdInfo } from 'react-icons/md'; @@ -9,15 +9,28 @@ import { MdClose, MdInfo } from 'react-icons/md'; * This component provides a small contextual hint used to surface tips, short help text or dismissible messages. * It can be displayed with a title, an icon, and a close button. * + * Visibility is uncontrolled by default (the hint closes itself when its close button is clicked); pass `open` to control it externally instead. */ function Hint({ title, label, icon = , + open, onRequestClose, children, ...rest -}: HintProps): JSX.Element { +}: HintProps): JSX.Element | null { + const isControlled = open !== undefined; + const [internalOpen, setInternalOpen] = useState(true); + const isOpen = isControlled ? open : internalOpen; + + const handleClose = (): void => { + if (!isControlled) setInternalOpen(false); + onRequestClose?.(); + }; + + if (!isOpen) return null; + const renderedLabel = {label}{children}; return ( diff --git a/src/components/Hint/types.ts b/src/components/Hint/types.ts index eddf53e..2b9e4c6 100644 --- a/src/components/Hint/types.ts +++ b/src/components/Hint/types.ts @@ -2,13 +2,16 @@ export interface HintProps extends React.HTMLAttributes { /** Main text content of the hint. */ label: string; - /** Optional title; if provided, shows a close button. */ + /** Optional title shown in the header; when set, `label` renders as a separate line below instead of inline. */ title?: string; /** Optional icon node displayed next to the title or label. @default */ icon?: React.ReactNode; - /** Callback fired when the close button is clicked. */ + /** Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally. */ + open?: boolean; + + /** Callback fired when the close button is clicked, in both controlled and uncontrolled mode. */ onRequestClose?: () => void; /** Optional additional content rendered under the label. */ From 10c72c7d87d122b046cfb9365c7c6602dc15c26c Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Tue, 28 Jul 2026 14:56:28 -0300 Subject: [PATCH 2/7] feat(BBBHint): add hideCloseButton prop Adds a `hideCloseButton` prop to hide the close (X) button for hints that shouldn't be manually dismissed, e.g. ones dismissed via another UI element or tooltip-style hints with no explicit dismiss action. The close button no longer depends on `title` being set, so label-only hints can now show one too unless hidden. Closes #30 --- src/components/Hint/README.md | 29 ++++++++++++++++------- src/components/Hint/component.stories.tsx | 13 ++++++++++ src/components/Hint/component.tsx | 3 ++- src/components/Hint/types.ts | 3 +++ 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/src/components/Hint/README.md b/src/components/Hint/README.md index 755acb6..c1b1bf5 100644 --- a/src/components/Hint/README.md +++ b/src/components/Hint/README.md @@ -53,14 +53,25 @@ const [open, setOpen] = useState(true); /> ``` +### Hint Without a Close Button + +Pass `hideCloseButton` for hints that shouldn't be manually dismissed — e.g. ones dismissed by interacting with another UI element, or tooltip-style hints with no explicit dismiss action. + +```jsx +import { BBBHint } from 'bbb-ui-components-react'; + + +``` + ## Props -| Property | Type | Default | Description | -| ---------------- | -------------------------------- | ------- | ------------------------------------------------------------------------------------ | -| `label` | `string` | | The main text content of the hint. | -| `title` | `string` | | An optional title shown in the header; when set, `label` renders as a separate line below instead of inline. | -| `icon` | `React.ReactNode` | | An optional icon to be displayed next to the title or label. | -| `open` | `boolean` | | Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally. | -| `onRequestClose` | `() => void` | | A callback function to be called when the close button is clicked, in both controlled and uncontrolled mode. | -| `children` | `React.ReactNode` | | Optional additional content to be displayed below the label. | -| `...props` | `HTMLAttributes` | | Any other props will be passed down to the underlying container div. | +| Property | Type | Default | Description | +| ----------------- | -------------------------------- | ------- | ------------------------------------------------------------------------------------ | +| `label` | `string` | | The main text content of the hint. | +| `title` | `string` | | An optional title shown in the header; when set, `label` renders as a separate line below instead of inline. | +| `icon` | `React.ReactNode` | | An optional icon to be displayed next to the title or label. | +| `open` | `boolean` | | Whether the hint is visible. Omit to let the hint manage its own visibility, closing itself when the close button is clicked; pass a boolean to control visibility externally. | +| `onRequestClose` | `() => void` | | A callback function to be called when the close button is clicked, in both controlled and uncontrolled mode. | +| `hideCloseButton` | `boolean` | `false` | Hides the close (X) button, for hints that shouldn't be manually dismissed. | +| `children` | `React.ReactNode` | | Optional additional content to be displayed below the label. | +| `...props` | `HTMLAttributes` | | Any other props will be passed down to the underlying container div. | diff --git a/src/components/Hint/component.stories.tsx b/src/components/Hint/component.stories.tsx index 7a1a555..a8790e9 100644 --- a/src/components/Hint/component.stories.tsx +++ b/src/components/Hint/component.stories.tsx @@ -28,6 +28,10 @@ const meta = { control: false, description: 'Callback fired when the close button is clicked, in both controlled and uncontrolled mode.', }, + hideCloseButton: { + control: 'boolean', + description: "Hides the close (X) button, for hints that shouldn't be manually dismissed.", + }, children: { control: false, description: 'Optional additional content rendered under the label.', @@ -80,3 +84,12 @@ export const Controlled: Story = { }, render: (args) => , }; + +/** Hint with `hideCloseButton`: no close (X) button, for hints that shouldn't be manually dismissed. */ +export const WithoutCloseButton: Story = { + args: { + title: 'No close button', + label: 'This hint cannot be manually dismissed.', + hideCloseButton: true, + }, +}; diff --git a/src/components/Hint/component.tsx b/src/components/Hint/component.tsx index a45518e..ee61d3a 100644 --- a/src/components/Hint/component.tsx +++ b/src/components/Hint/component.tsx @@ -17,6 +17,7 @@ function Hint({ icon = , open, onRequestClose, + hideCloseButton = false, children, ...rest }: HintProps): JSX.Element | null { @@ -48,7 +49,7 @@ function Hint({ {title && {title}} {!title && renderedLabel} - {title && ( + {!hideCloseButton && ( { /** Callback fired when the close button is clicked, in both controlled and uncontrolled mode. */ onRequestClose?: () => void; + /** Hides the close (X) button, for hints that shouldn't be manually dismissed. @default false */ + hideCloseButton?: boolean; + /** Optional additional content rendered under the label. */ children?: React.ReactNode; } From 082a09e96ead8447903d0d5d8b563e32741747a5 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Tue, 28 Jul 2026 16:15:40 -0300 Subject: [PATCH 3/7] feat(BBBAccordion): add buttonHeaderPosition prop buttonHeader always rendered immediately next to the title, with no way to push it to the far edge of the header row for secondary actions like edit/delete. Adds buttonHeaderPosition ('left' | 'right', default 'left') to control this, preserving existing layout by default. --- src/components/Accordion/README.md | 32 +++++++++++++------ .../Accordion/component.stories.tsx | 31 +++++++++++++++++- src/components/Accordion/component.tsx | 7 ++-- src/components/Accordion/constants.ts | 10 ++++++ src/components/Accordion/styles.ts | 8 ++++- src/components/Accordion/types.ts | 10 +++++- 6 files changed, 83 insertions(+), 15 deletions(-) diff --git a/src/components/Accordion/README.md b/src/components/Accordion/README.md index 3711f94..d110204 100644 --- a/src/components/Accordion/README.md +++ b/src/components/Accordion/README.md @@ -38,15 +38,27 @@ import { MdFavorite } from 'react-icons/md'; ``` +### Accordion with a right-aligned button header + +```jsx +import { BBBAccordion } from 'bbb-ui-components-react'; +import { MdEdit } from 'react-icons/md'; + +} buttonHeaderPosition="right"> +

Content for the accordion.

+
+``` + ## Props -| Property | Type | Default | Description | -| ------------------ | -------------------------------------- | ----------- | ------------------------------------------------------------------------------ | -| `title` | `string` | | The text to be displayed in the accordion header. | -| `tooltipLabel` | `string` | `null` | An optional label for the tooltip that appears on hover. | -| `tooltipPlacement` | `import('@tippyjs/react').Placement` | `'bottom'` | The placement of the tooltip. | -| `ariaLabel` | `string` | | The accessible name for the expand button. | -| `ariaLabelledBy` | `string` | | The ID of the element that labels the expand button. | -| `ariaDescribedBy` | `string` | | The ID of the element that describes the expand button. | -| `buttonHeader` | `React.ReactNode` | `null` | Optional content to be rendered inside the button header. | -| `children` | `React.ReactNode` | | The content to be displayed when the accordion is expanded. | +| Property | Type | Default | Description | +| ----------------------- | -------------------------------------- | ----------- | ------------------------------------------------------------------------------ | +| `title` | `string` | | The text to be displayed in the accordion header. | +| `tooltipLabel` | `string` | `null` | An optional label for the tooltip that appears on hover. | +| `tooltipPlacement` | `import('@tippyjs/react').Placement` | `'bottom'` | The placement of the tooltip. | +| `ariaLabel` | `string` | | The accessible name for the expand button. | +| `ariaLabelledBy` | `string` | | The ID of the element that labels the expand button. | +| `ariaDescribedBy` | `string` | | The ID of the element that describes the expand button. | +| `buttonHeader` | `React.ReactNode` | `null` | Optional content to be rendered inside the button header. | +| `buttonHeaderPosition` | `'left' \| 'right'` | `'left'` | Position of `buttonHeader` within the header row. | +| `children` | `React.ReactNode` | | The content to be displayed when the accordion is expanded. | diff --git a/src/components/Accordion/component.stories.tsx b/src/components/Accordion/component.stories.tsx index 7b54464..7a05344 100644 --- a/src/components/Accordion/component.stories.tsx +++ b/src/components/Accordion/component.stories.tsx @@ -1,7 +1,14 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import BBBAccordion from './component'; -import { TOOLTIP_PLACEMENT_VALUES, DEFAULT_TOOLTIP_PLACEMENT } from './constants'; +import { + TOOLTIP_PLACEMENT_VALUES, + DEFAULT_TOOLTIP_PLACEMENT, + BUTTON_HEADER_POSITIONS, + BUTTON_HEADER_POSITION_VALUES, + DEFAULT_BUTTON_HEADER_POSITION, +} from './constants'; +import { MdEdit } from 'react-icons/md'; import Typography from '../Typography/component'; const meta = { @@ -44,6 +51,14 @@ const meta = { control: false, description: 'Optional React node rendered inside the button header.', }, + buttonHeaderPosition: { + control: 'select', + options: BUTTON_HEADER_POSITION_VALUES, + description: 'Position of `buttonHeader` within the header row.', + table: { + defaultValue: { summary: `${DEFAULT_BUTTON_HEADER_POSITION}` }, + }, + }, children: { control: false, description: 'Content shown when the accordion is expanded.', @@ -79,3 +94,17 @@ export const WithTooltip: Story = { ), }, }; + +/** Shows `buttonHeaderPosition="right"` pushing the button header to the far edge of the header row. */ +export const WithRightAlignedButtonHeader: Story = { + args: { + title: 'Right-aligned Button Header', + buttonHeader: , + buttonHeaderPosition: BUTTON_HEADER_POSITIONS.RIGHT, + children: ( +
+ Accordion content goes here. +
+ ), + }, +}; diff --git a/src/components/Accordion/component.tsx b/src/components/Accordion/component.tsx index 5fc0753..9524240 100644 --- a/src/components/Accordion/component.tsx +++ b/src/components/Accordion/component.tsx @@ -4,7 +4,7 @@ import * as Styled from './styles'; import { MdExpandMore } from 'react-icons/md'; import Tippy from '@tippyjs/react'; import 'tippy.js/dist/tippy.css'; -import { DEFAULT_TOOLTIP_PLACEMENT } from './constants'; +import { DEFAULT_TOOLTIP_PLACEMENT, DEFAULT_BUTTON_HEADER_POSITION } from './constants'; /** * A customizable Accordion component that allows expanding and collapsing content. @@ -21,6 +21,7 @@ function Accordion({ ariaLabelledBy, ariaDescribedBy, buttonHeader = null, + buttonHeaderPosition = DEFAULT_BUTTON_HEADER_POSITION, children, }: AccordionProps): JSX.Element { const [isExpanded, setIsExpanded] = useState(false); @@ -38,7 +39,9 @@ function Accordion({ {title} - {buttonHeader} + + {buttonHeader} + ); diff --git a/src/components/Accordion/constants.ts b/src/components/Accordion/constants.ts index 79011e3..6dff066 100644 --- a/src/components/Accordion/constants.ts +++ b/src/components/Accordion/constants.ts @@ -7,8 +7,18 @@ const TOOLTIP_PLACEMENTS = { const TOOLTIP_PLACEMENT_VALUES = Object.values(TOOLTIP_PLACEMENTS); const DEFAULT_TOOLTIP_PLACEMENT = TOOLTIP_PLACEMENTS.TOP; +const BUTTON_HEADER_POSITIONS = { + LEFT: 'left', + RIGHT: 'right', +} as const; +const BUTTON_HEADER_POSITION_VALUES = Object.values(BUTTON_HEADER_POSITIONS); +const DEFAULT_BUTTON_HEADER_POSITION = BUTTON_HEADER_POSITIONS.LEFT; + export { TOOLTIP_PLACEMENTS, TOOLTIP_PLACEMENT_VALUES, DEFAULT_TOOLTIP_PLACEMENT, + BUTTON_HEADER_POSITIONS, + BUTTON_HEADER_POSITION_VALUES, + DEFAULT_BUTTON_HEADER_POSITION, } \ No newline at end of file diff --git a/src/components/Accordion/styles.ts b/src/components/Accordion/styles.ts index 721cbb7..0850835 100644 --- a/src/components/Accordion/styles.ts +++ b/src/components/Accordion/styles.ts @@ -2,7 +2,7 @@ import styled from 'styled-components'; 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'; +import { StyledAccordionContent, StyledExpandIcon, StyledButtonHeaderWrapper } from './types'; export const ExpandButton = styled.button` display: flex; @@ -51,6 +51,12 @@ export const ExpandIcon = styled.div` } `; +export const ButtonHeaderWrapper = styled.span` + display: flex; + align-items: center; + margin-left: ${({ $position }) => ($position === 'right' ? 'auto' : '0')}; +`; + export const TitleText = styled.span` font-size: ${fontSizeDefault}; font-weight: 400; diff --git a/src/components/Accordion/types.ts b/src/components/Accordion/types.ts index 87b09e4..2e61837 100644 --- a/src/components/Accordion/types.ts +++ b/src/components/Accordion/types.ts @@ -1,4 +1,4 @@ -import { TOOLTIP_PLACEMENT_VALUES } from './constants'; +import { TOOLTIP_PLACEMENT_VALUES, BUTTON_HEADER_POSITION_VALUES } from './constants'; import * as React from 'react'; export interface StyledExpandIcon { @@ -10,7 +10,12 @@ export interface StyledAccordionContent { $scrollHeight: number; } +export interface StyledButtonHeaderWrapper { + $position: ButtonHeaderPositionType; +} + type TooltipPlacementType = typeof TOOLTIP_PLACEMENT_VALUES[number]; +type ButtonHeaderPositionType = typeof BUTTON_HEADER_POSITION_VALUES[number]; export interface AccordionProps { /** The text to be displayed in the accordion header. */ @@ -34,6 +39,9 @@ export interface AccordionProps { /** Optional React node rendered inside the button header, alongside the title. @default null */ buttonHeader?: React.ReactNode; + /** Position of `buttonHeader` within the header row. @default 'left' */ + buttonHeaderPosition?: ButtonHeaderPositionType; + /** Content shown when the accordion is expanded. */ children?: React.ReactNode; }; From 07b5016203a79b2ad68beac5b4f67122ad3f5230 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Wed, 29 Jul 2026 16:39:40 -0300 Subject: [PATCH 4/7] feat: export standardized color palette object Adds a nested `colors` object (colors.text.default, colors.border.selected, ...) grouping the existing palette.ts tokens semantically, reachable from the package root and from a new tree-shakeable /colors subpath. --- README.md | 22 ++++++++++++++ package.json | 10 +++++++ src/index.ts | 1 + src/stylesheets/colors.ts | 61 +++++++++++++++++++++++++++++++++++++++ webpack.config.babel.js | 1 + 5 files changed, 95 insertions(+) create mode 100644 src/stylesheets/colors.ts diff --git a/README.md b/README.md index e9068ae..771f18e 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,28 @@ The following table lists the supported CSS variables for color overriding, extr If you need to override colors for specific components or add new variables, refer to the component's `styles.ts` file for implementation details. +### Importing Color Tokens in JS + +In addition to CSS variables, the same color tokens used internally by every component are also +exported as a nested `colors` object, for use directly in JS/TS (e.g. in your own +styled-components): + +```jsx +// From the package root +import { colors } from '@bigbluebutton/bbb-ui-components-react'; + +// Or from the dedicated, tree-shakeable subpath +import { colors } from '@bigbluebutton/bbb-ui-components-react/colors'; + +const StyledDiv = styled.div` + color: ${colors.text.default}; + background: ${colors.background.white}; +`; +``` + +`colors` is grouped the same way as the table above: `neutral`, `brand`, `semantic`, `background`, +`border`, `text`, `icon`, `hover`. + ## Installation You can install the library directly from npm: diff --git a/package.json b/package.json index d10ece2..aabb42e 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,13 @@ "require": "./dist/components/Typography.js", "default": "./dist/components/Typography.js" }, + "./colors": { + "types": "./dist/types/stylesheets/colors.d.ts", + "node": "./dist/components/colors.js", + "import": "./dist/esm/stylesheets/colors.js", + "require": "./dist/components/colors.js", + "default": "./dist/components/colors.js" + }, "./dist/components": { "types": "./dist/types/index.d.ts", "require": "./dist/components/index.js", @@ -208,6 +215,9 @@ ], "Typography": [ "dist/types/components/Typography/index.d.ts" + ], + "colors": [ + "dist/types/stylesheets/colors.d.ts" ] } }, diff --git a/src/index.ts b/src/index.ts index 8d8604e..2ca8450 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ export * from './components'; export * from './stylesheets/palette'; export * from './stylesheets/sizing'; +export * from './stylesheets/colors'; diff --git a/src/stylesheets/colors.ts b/src/stylesheets/colors.ts new file mode 100644 index 0000000..fb3a4fe --- /dev/null +++ b/src/stylesheets/colors.ts @@ -0,0 +1,61 @@ +import { + colorNeutral2, colorNeutral3, colorNeutral4, colorWhite, colorLightGray, colorGray, colorDarkGray, + colorBrand1, colorBrand2, colorBrand3, colorBrandLight, colorBrandAux, + colorSuccess, colorWarning, colorError, colorErrorDark, + colorBackgroundWhite, colorBackgroundLight, colorBackgroundBlue, + colorBorderDefault, colorBorderSelected, colorBorderError, + colorTextDefault, colorTextLight, + colorIconDefault, colorIconBlue, colorIconWhite, + colorHoverDark, colorHoverLight, colorHoverNeutral, +} from './palette'; + +export const colors = { + neutral: { + neutral2: colorNeutral2, + neutral3: colorNeutral3, + neutral4: colorNeutral4, + white: colorWhite, + lightGray: colorLightGray, + gray: colorGray, + darkGray: colorDarkGray, + }, + brand: { + brand1: colorBrand1, + brand2: colorBrand2, + brand3: colorBrand3, + light: colorBrandLight, + aux: colorBrandAux, + }, + semantic: { + success: colorSuccess, + warning: colorWarning, + error: colorError, + errorDark: colorErrorDark, + }, + background: { + white: colorBackgroundWhite, + light: colorBackgroundLight, + blue: colorBackgroundBlue, + }, + border: { + default: colorBorderDefault, + selected: colorBorderSelected, + error: colorBorderError, + }, + text: { + default: colorTextDefault, + light: colorTextLight, + }, + icon: { + default: colorIconDefault, + blue: colorIconBlue, + white: colorIconWhite, + }, + hover: { + dark: colorHoverDark, + light: colorHoverLight, + neutral: colorHoverNeutral, + }, +} as const; + +export type Colors = typeof colors; diff --git a/webpack.config.babel.js b/webpack.config.babel.js index c51f380..61ffdf7 100644 --- a/webpack.config.babel.js +++ b/webpack.config.babel.js @@ -17,6 +17,7 @@ export default { TextInput: './src/components/TextInput/index.ts', Toggle: './src/components/Toggle/index.ts', Typography: './src/components/Typography/index.ts', + colors: './src/stylesheets/colors.ts', index: './src/index.ts', }, output: { From c903b08abc39081862ba3337c6d44ff41f04f015 Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Fri, 31 Jul 2026 13:28:37 -0300 Subject: [PATCH 5/7] refactor: source remaining hardcoded colors from the shared palette Promotes 5 hardcoded color literals in Modal, Input, Search, and Toggle to named palette.ts consts (colorBorderFocusRing, colorIconDefaultDark, colorOverlay, colorShadowDefault), reachable from the exported colors object. No visual change. Co-Authored-By: Claude Sonnet 5 --- README.md | 4 ++++ src/components/Input/styles.ts | 3 ++- src/components/Modal/styles.ts | 4 ++-- src/components/Search/styles.ts | 3 ++- src/components/Toggle/styles.ts | 9 ++++++--- src/stylesheets/colors.ts | 14 ++++++++++++-- src/stylesheets/palette.ts | 8 ++++++++ 7 files changed, 36 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 771f18e..37c765f 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ The following table lists the supported CSS variables for color overriding, extr | `--color-hover-dark` | No | #0C57A7 | | `--color-hover-light` | No | #D4E5FA | | `--color-hover-neutral` | No | #DCE4EC | +| `--color-border-focus-ring`| No | rgba(29, 101, 212, 0.15) | +| `--color-icon-default-dark`| No | rgba(255, 255, 255, 0.35) | +| `--color-overlay` | No | rgba(0, 0, 0, 0.75) | +| `--color-shadow-default` | No | rgb(0 35 11 / 20%) | **Example Usage**: ```css diff --git a/src/components/Input/styles.ts b/src/components/Input/styles.ts index 47afd8d..0afe933 100644 --- a/src/components/Input/styles.ts +++ b/src/components/Input/styles.ts @@ -3,6 +3,7 @@ import { colorBorderDefault, colorBorderSelected, colorBorderError, + colorBorderFocusRing, colorTextDefault, colorTextLight, colorError, @@ -41,7 +42,7 @@ export const FieldContainer = styled.div` &:focus-within { border-color: ${colorBorderSelected}; - box-shadow: 0 0 0 3px var(--color-border-focus-ring, rgba(29, 101, 212, 0.15)); + box-shadow: 0 0 0 3px ${colorBorderFocusRing}; } ${({ $error }) => diff --git a/src/components/Modal/styles.ts b/src/components/Modal/styles.ts index 19c0eb3..708c871 100644 --- a/src/components/Modal/styles.ts +++ b/src/components/Modal/styles.ts @@ -2,7 +2,7 @@ import styled from 'styled-components'; import { Styles } from 'react-modal'; import * as React from 'react'; import { spacingLarge, spacingMedium, spacingSmallMedium, borderRadiusDefault } from '../../stylesheets/sizing'; -import { colorWhite } from '../../stylesheets/palette'; +import { colorWhite, colorOverlay } from '../../stylesheets/palette'; import { StyledModalBodyProps, StyledModalFooterProps } from './types'; export const modalStyles: Styles = { @@ -12,7 +12,7 @@ export const modalStyles: Styles = { left: 0, right: 0, bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.75)', + backgroundColor: colorOverlay, zIndex: 100, display: 'flex', alignItems: 'center', diff --git a/src/components/Search/styles.ts b/src/components/Search/styles.ts index a076a5a..b8d9788 100644 --- a/src/components/Search/styles.ts +++ b/src/components/Search/styles.ts @@ -2,6 +2,7 @@ import styled, { css } from 'styled-components'; import { colorBorderDefault, colorBorderSelected, + colorBorderFocusRing, colorTextDefault, colorTextLight, colorIconDefault, @@ -36,7 +37,7 @@ export const Container = styled.div` &:focus-within { border-color: ${colorBorderSelected}; - box-shadow: 0 0 0 3px var(--color-border-focus-ring, rgba(29, 101, 212, 0.15)); + box-shadow: 0 0 0 3px ${colorBorderFocusRing}; } ${({ $disabled }) => diff --git a/src/components/Toggle/styles.ts b/src/components/Toggle/styles.ts index 5815a1f..9bbf73a 100644 --- a/src/components/Toggle/styles.ts +++ b/src/components/Toggle/styles.ts @@ -1,7 +1,10 @@ import { Switch } from '@mui/material'; import { styled as materialStyled } from '@mui/material/styles'; import styled, { css } from 'styled-components'; -import { colorBrand1, colorIconDefault, colorTextDefault, colorTextLight, colorWhite } from '../../stylesheets/palette'; +import { + colorBrand1, colorIconDefault, colorTextDefault, colorTextLight, colorWhite, + colorShadowDefault, colorIconDefaultDark, +} from '../../stylesheets/palette'; import { TEXT_POSITIONS } from './constants'; import { StyledTextWrapperProps, StyledToggleWrapperProps } from './types'; import { fontSizeBig, fontSizeDefault } from '../../stylesheets/typography'; @@ -103,7 +106,7 @@ export const MaterialToggle = materialStyled(Switch)(({ theme }) => ({ }, }, '& .MuiSwitch-thumb': { - boxShadow: '0 2px 4px 0 rgb(0 35 11 / 20%)', + boxShadow: `0 2px 4px 0 ${colorShadowDefault}`, width: '0.6rem', height: '0.6rem', borderRadius: '0.5rem', @@ -118,7 +121,7 @@ export const MaterialToggle = materialStyled(Switch)(({ theme }) => ({ backgroundColor: colorIconDefault, boxSizing: 'border-box', ...theme.applyStyles('dark', { - backgroundColor: 'rgba(255,255,255,.35)', + backgroundColor: colorIconDefaultDark, }), }, })); \ No newline at end of file diff --git a/src/stylesheets/colors.ts b/src/stylesheets/colors.ts index fb3a4fe..84251c7 100644 --- a/src/stylesheets/colors.ts +++ b/src/stylesheets/colors.ts @@ -3,10 +3,12 @@ import { colorBrand1, colorBrand2, colorBrand3, colorBrandLight, colorBrandAux, colorSuccess, colorWarning, colorError, colorErrorDark, colorBackgroundWhite, colorBackgroundLight, colorBackgroundBlue, - colorBorderDefault, colorBorderSelected, colorBorderError, + colorBorderDefault, colorBorderSelected, colorBorderError, colorBorderFocusRing, colorTextDefault, colorTextLight, - colorIconDefault, colorIconBlue, colorIconWhite, + colorIconDefault, colorIconBlue, colorIconWhite, colorIconDefaultDark, colorHoverDark, colorHoverLight, colorHoverNeutral, + colorOverlay, + colorShadowDefault, } from './palette'; export const colors = { @@ -41,6 +43,7 @@ export const colors = { default: colorBorderDefault, selected: colorBorderSelected, error: colorBorderError, + focusRing: colorBorderFocusRing, }, text: { default: colorTextDefault, @@ -50,12 +53,19 @@ export const colors = { default: colorIconDefault, blue: colorIconBlue, white: colorIconWhite, + defaultDark: colorIconDefaultDark, }, hover: { dark: colorHoverDark, light: colorHoverLight, neutral: colorHoverNeutral, }, + overlay: { + default: colorOverlay, + }, + shadow: { + default: colorShadowDefault, + }, } as const; export type Colors = typeof colors; diff --git a/src/stylesheets/palette.ts b/src/stylesheets/palette.ts index deaaa9f..1867278 100644 --- a/src/stylesheets/palette.ts +++ b/src/stylesheets/palette.ts @@ -43,6 +43,7 @@ export const colorBorderSelected = `var(--color-border-selected, ${colorBrand1_b export const colorBorderError = `var(--color-border-error, ${colorError_base})`; // Mapped to core css vars export const colorBorderDefault = `var(--default-border, ${colorBorderDefault_base})`; +export const colorBorderFocusRing = 'var(--color-border-focus-ring, rgba(29, 101, 212, 0.15))'; // Text colors @@ -53,8 +54,15 @@ export const colorTextLight = `var(--color-text-light, ${colorNeutral2})`; export const colorIconDefault = `var(--color-icon-default, ${colorNeutral2})`; export const colorIconBlue = `var(--color-icon-blue, ${colorBrand1_base})`; export const colorIconWhite = `var(--color-icon-white, ${colorWhite})`; +export const colorIconDefaultDark = 'var(--color-icon-default-dark, rgba(255, 255, 255, 0.35))'; //Hover colors export const colorHoverDark = 'var(--color-hover-dark, #0C57A7)'; export const colorHoverLight = 'var(--color-hover-light, #D4E5FA)'; export const colorHoverNeutral = `var(--color-hover-neutral, ${colorNeutral4})`; + +//Overlay colors +export const colorOverlay = 'var(--color-overlay, rgba(0, 0, 0, 0.75))'; + +//Shadow colors +export const colorShadowDefault = 'var(--color-shadow-default, rgb(0 35 11 / 20%))'; From d0eb867d6e453a5dba5cd765e6a5c5c18c21e74c Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Fri, 31 Jul 2026 16:21:37 -0300 Subject: [PATCH 6/7] feat(BBBScrollArea): add standardized scroll area component Wraps scrollable content with the chat's scrollbar style (thin rounded thumb + optional edge fade mask/shadow) so chat, lists, and side panels no longer each roll their own scrollbar look. Configurable per axis (verticalScroll/horizontalScroll), size (maxHeight/maxWidth), edge fade (fadeEdges/fadeColor), and edge padding (paddingTop/Right/Bottom/Left) so the fade doesn't overlap the first/last item. --- README.md | 1 + package.json | 10 ++ src/components/ScrollArea/README.md | 67 +++++++++ .../ScrollArea/component.stories.tsx | 130 ++++++++++++++++++ src/components/ScrollArea/component.tsx | 47 +++++++ src/components/ScrollArea/constants.ts | 3 + src/components/ScrollArea/index.ts | 1 + src/components/ScrollArea/styles.ts | 84 +++++++++++ src/components/ScrollArea/types.ts | 49 +++++++ src/components/index.ts | 1 + webpack.config.babel.js | 1 + 11 files changed, 394 insertions(+) create mode 100644 src/components/ScrollArea/README.md create mode 100644 src/components/ScrollArea/component.stories.tsx create mode 100644 src/components/ScrollArea/component.tsx create mode 100644 src/components/ScrollArea/constants.ts create mode 100644 src/components/ScrollArea/index.ts create mode 100644 src/components/ScrollArea/styles.ts create mode 100644 src/components/ScrollArea/types.ts diff --git a/README.md b/README.md index 37c765f..223b759 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Below is a list of the components available in this library. Each component has - [BBBInput](./src/components/Input/README.md) - [BBBModal](./src/components/Modal//README.md) - [BBBNavigation](./src/components/Navigation/README.md) +- [BBBScrollArea](./src/components/ScrollArea/README.md) - [BBBSearch](./src/components/Search/README.md) - [BBBSelect](./src/components/Select/README.md) - [BBBSpinner](./src/components/Spinner//README.md) diff --git a/package.json b/package.json index aabb42e..5537f94 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,13 @@ "require": "./dist/components/Navigation.js", "default": "./dist/components/Navigation.js" }, + "./ScrollArea": { + "types": "./dist/types/components/ScrollArea/index.d.ts", + "node": "./dist/components/ScrollArea.js", + "import": "./dist/esm/components/ScrollArea/index.js", + "require": "./dist/components/ScrollArea.js", + "default": "./dist/components/ScrollArea.js" + }, "./Search": { "types": "./dist/types/components/Search/index.d.ts", "node": "./dist/components/Search.js", @@ -195,6 +202,9 @@ "Navigation": [ "dist/types/components/Navigation/index.d.ts" ], + "ScrollArea": [ + "dist/types/components/ScrollArea/index.d.ts" + ], "Search": [ "dist/types/components/Search/index.d.ts" ], diff --git a/src/components/ScrollArea/README.md b/src/components/ScrollArea/README.md new file mode 100644 index 0000000..4ea7534 --- /dev/null +++ b/src/components/ScrollArea/README.md @@ -0,0 +1,67 @@ +# BBBScrollArea + +`BBBScrollArea` is a wrapper that applies standardized scrollbar styling to its content — based on the chat's scrollbar style — so that chat, lists, side panels, and any other scrollable content look consistent throughout the application. + +## Usage Example + +### Basic usage + +```jsx +import { BBBScrollArea } from 'bbb-ui-components-react'; + + + + +``` + +### Horizontal scroll + +```jsx +import { BBBScrollArea } from 'bbb-ui-components-react'; + + + + +``` + +### Without edge fade + +```jsx +import { BBBScrollArea } from 'bbb-ui-components-react'; + + + + +``` + +### With edge padding + +```jsx +import { BBBScrollArea } from 'bbb-ui-components-react'; + + + + +``` + +## Props + +| Property | Type | Default | Description | +| -------------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------------- | +| `children` | `React.ReactNode` | | Scrollable content. | +| `verticalScroll` | `boolean` | `true` | Enables vertical scrolling. | +| `horizontalScroll` | `boolean` | `false` | Enables horizontal scrolling. | +| `maxHeight` | `string` | | Caps the content height; scrolling kicks in past this value (e.g. `'400px'`, `'50vh'`). | +| `maxWidth` | `string` | | Caps the content width; scrolling kicks in past this value (e.g. `'600px'`, `'100%'`). | +| `fadeEdges` | `boolean` | `true` | Fades the scrollable edges with a color mask + shadow, matching the chat's scroll style. | +| `fadeColor` | `string` | | Background color the fade blends into. Required in practice when `fadeEdges` is `true` — if omitted, the fade is silently skipped. | +| `paddingTop` | `string` | | Space reserved above the content, before the scrollable area's top edge (e.g. `'1rem'`). | +| `paddingRight` | `string` | | Space reserved to the right of the content, before the scrollable area's right edge (e.g. `'1rem'`). | +| `paddingBottom` | `string` | | Space reserved below the content, before the scrollable area's bottom edge (e.g. `'1rem'`). | +| `paddingLeft` | `string` | | Space reserved to the left of the content, before the scrollable area's left edge (e.g. `'1rem'`). | + +## Notes + +- Without `maxHeight`/`maxWidth`, the area grows with its content — scrolling only kicks in once a parent container or one of these props constrains its size, same as native `overflow: auto`. +- `fadeColor` must match the background behind `BBBScrollArea` (e.g. the surrounding panel color) so the fade blends in seamlessly instead of showing a mismatched edge. +- The fade mask overlays directly on top of the content near each active edge; set the matching `padding*` prop (e.g. `paddingTop`/`paddingBottom` for a vertical area) so the first/last item isn't partially covered by it. diff --git a/src/components/ScrollArea/component.stories.tsx b/src/components/ScrollArea/component.stories.tsx new file mode 100644 index 0000000..a6184e7 --- /dev/null +++ b/src/components/ScrollArea/component.stories.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import BBBScrollArea from './component'; +import { colorWhite } from '../../stylesheets/palette'; + +const meta = { + title: 'BBBScrollArea', + component: BBBScrollArea, + tags: ['autodocs'], + argTypes: { + children: { + control: false, + description: 'Scrollable content.', + }, + verticalScroll: { + control: 'boolean', + description: 'Enables vertical scrolling.', + table: { defaultValue: { summary: 'true' } }, + }, + horizontalScroll: { + control: 'boolean', + description: 'Enables horizontal scrolling.', + table: { defaultValue: { summary: 'false' } }, + }, + maxHeight: { + control: 'text', + description: 'Caps the content height; scrolling kicks in past this value.', + }, + maxWidth: { + control: 'text', + description: 'Caps the content width; scrolling kicks in past this value.', + }, + fadeEdges: { + control: 'boolean', + description: "Fades the scrollable edges with a color mask + shadow, matching the chat's scroll style.", + table: { defaultValue: { summary: 'true' } }, + }, + fadeColor: { + control: 'color', + description: 'Background color the fade blends into. Required in practice when fadeEdges is true.', + }, + paddingTop: { + control: 'text', + description: "Space reserved above the content, before the scrollable area's top edge.", + }, + paddingRight: { + control: 'text', + description: "Space reserved to the right of the content, before the scrollable area's right edge.", + }, + paddingBottom: { + control: 'text', + description: "Space reserved below the content, before the scrollable area's bottom edge.", + }, + paddingLeft: { + control: 'text', + description: "Space reserved to the left of the content, before the scrollable area's left edge.", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const VERTICAL_ITEMS = Array.from({ length: 20 }, (_, index) => `Message ${index + 1}`); +const HORIZONTAL_ITEMS = Array.from({ length: 12 }, (_, index) => `Card ${index + 1}`); + +const VerticalList = () => ( +
+ {VERTICAL_ITEMS.map((item) => ( +
+ {item} +
+ ))} +
+); + +const HorizontalList = () => ( +
+ {HORIZONTAL_ITEMS.map((item) => ( +
+ {item} +
+ ))} +
+); + +/** Default vertical scroll area with the chat-style scrollbar and top/bottom edge fade. */ +export const Default: Story = { + args: { + maxHeight: '250px', + fadeColor: colorWhite, + children: , + }, +}; + +/** Vertical list with top/bottom padding, so the fade blends over empty space instead of the first/last item. */ +export const WithEdgePadding: Story = { + args: { + maxHeight: '250px', + fadeColor: colorWhite, + paddingTop: '1rem', + paddingBottom: '1rem', + children: , + }, +}; + +/** Same vertical list with `fadeEdges` off — only the scrollbar styling remains. */ +export const NoFade: Story = { + args: { + maxHeight: '250px', + fadeEdges: false, + children: , + }, +}; + +/** Horizontal scroll area with left/right edge fade instead of top/bottom. */ +export const Horizontal: Story = { + args: { + verticalScroll: false, + horizontalScroll: true, + maxWidth: '400px', + fadeColor: colorWhite, + children: , + }, +}; diff --git a/src/components/ScrollArea/component.tsx b/src/components/ScrollArea/component.tsx new file mode 100644 index 0000000..db46434 --- /dev/null +++ b/src/components/ScrollArea/component.tsx @@ -0,0 +1,47 @@ +import React, { JSX } from 'react'; +import { ScrollAreaProps } from './types'; +import * as Styled from './styles'; +import { DEFAULT_VERTICAL_SCROLL, DEFAULT_HORIZONTAL_SCROLL, DEFAULT_FADE_EDGES } from './constants'; + +/** + * A wrapper that applies standardized scrollbar styling to its content. + * + * Use it around any scrollable area (chat, lists, side panels) to get a consistent + * scrollbar look, with optional axis control, size capping, and edge fading. + * + */ +function ScrollArea({ + children, + verticalScroll = DEFAULT_VERTICAL_SCROLL, + horizontalScroll = DEFAULT_HORIZONTAL_SCROLL, + maxHeight, + maxWidth, + fadeEdges = DEFAULT_FADE_EDGES, + fadeColor, + paddingTop, + paddingRight, + paddingBottom, + paddingLeft, +}: ScrollAreaProps): JSX.Element { + const isScrollable = verticalScroll || horizontalScroll; + + return ( + + {children} + + ); +} + +export default ScrollArea; diff --git a/src/components/ScrollArea/constants.ts b/src/components/ScrollArea/constants.ts new file mode 100644 index 0000000..1fb2cfc --- /dev/null +++ b/src/components/ScrollArea/constants.ts @@ -0,0 +1,3 @@ +export const DEFAULT_VERTICAL_SCROLL = true; +export const DEFAULT_HORIZONTAL_SCROLL = false; +export const DEFAULT_FADE_EDGES = true; diff --git a/src/components/ScrollArea/index.ts b/src/components/ScrollArea/index.ts new file mode 100644 index 0000000..25d0af5 --- /dev/null +++ b/src/components/ScrollArea/index.ts @@ -0,0 +1 @@ +export { default as BBBScrollArea } from './component'; diff --git a/src/components/ScrollArea/styles.ts b/src/components/ScrollArea/styles.ts new file mode 100644 index 0000000..da1ed03 --- /dev/null +++ b/src/components/ScrollArea/styles.ts @@ -0,0 +1,84 @@ +import styled, { css } from 'styled-components'; +import { StyledScrollAreaWrapperProps } from './types'; + +const FADE_MASK_SIZE = '40px'; +const FADE_SHADOW_SIZE = '14px'; +const FADE_SHADOW_COLOR = 'rgba(0, 0, 0, .2)'; + +interface FadeLayer { + image: string; + position: string; + size: string; + attachment: string; +} + +const verticalFadeLayers = (color: string): FadeLayer[] => [ + { image: `linear-gradient(${color} 30%, transparent)`, position: '0 0', size: `100% ${FADE_MASK_SIZE}`, attachment: 'local' }, + { image: `linear-gradient(transparent, ${color} 70%)`, position: '0 100%', size: `100% ${FADE_MASK_SIZE}`, attachment: 'local' }, + { image: `radial-gradient(farthest-side at 50% 0, ${FADE_SHADOW_COLOR}, transparent)`, position: '0 0', size: `100% ${FADE_SHADOW_SIZE}`, attachment: 'scroll' }, + { image: `radial-gradient(farthest-side at 50% 100%, ${FADE_SHADOW_COLOR}, transparent)`, position: '0 100%', size: `100% ${FADE_SHADOW_SIZE}`, attachment: 'scroll' }, +]; + +const horizontalFadeLayers = (color: string): FadeLayer[] => [ + { image: `linear-gradient(to right, ${color} 30%, transparent)`, position: '0 0', size: `${FADE_MASK_SIZE} 100%`, attachment: 'local' }, + { image: `linear-gradient(to right, transparent, ${color} 70%)`, position: '100% 0', size: `${FADE_MASK_SIZE} 100%`, attachment: 'local' }, + { image: `radial-gradient(farthest-side at 0 50%, ${FADE_SHADOW_COLOR}, transparent)`, position: '0 0', size: `${FADE_SHADOW_SIZE} 100%`, attachment: 'scroll' }, + { image: `radial-gradient(farthest-side at 100% 50%, ${FADE_SHADOW_COLOR}, transparent)`, position: '100% 0', size: `${FADE_SHADOW_SIZE} 100%`, attachment: 'scroll' }, +]; + +const fadeBackground = ({ + $verticalScroll, $horizontalScroll, $fadeEdges, $fadeColor, +}: StyledScrollAreaWrapperProps) => { + if (!$fadeEdges || !$fadeColor) return ''; + + const layers = [ + ...($verticalScroll ? verticalFadeLayers($fadeColor) : []), + ...($horizontalScroll ? horizontalFadeLayers($fadeColor) : []), + ]; + if (!layers.length) return ''; + + return css` + background-image: ${layers.map((layer) => layer.image).join(', ')}; + background-position: ${layers.map((layer) => layer.position).join(', ')}; + background-size: ${layers.map((layer) => layer.size).join(', ')}; + background-attachment: ${layers.map((layer) => layer.attachment).join(', ')}; + background-repeat: no-repeat; + background-color: transparent; + `; +}; + +export const ScrollAreaWrapper = styled.div` + overflow-y: ${({ $verticalScroll }) => ($verticalScroll ? 'auto' : 'hidden')}; + overflow-x: ${({ $horizontalScroll }) => ($horizontalScroll ? 'auto' : 'hidden')}; + ${({ $maxHeight }) => $maxHeight && css`max-height: ${$maxHeight};`} + ${({ $maxWidth }) => $maxWidth && css`max-width: ${$maxWidth};`} + ${({ $paddingTop }) => $paddingTop && css`padding-top: ${$paddingTop};`} + ${({ $paddingRight }) => $paddingRight && css`padding-right: ${$paddingRight};`} + ${({ $paddingBottom }) => $paddingBottom && css`padding-bottom: ${$paddingBottom};`} + ${({ $paddingLeft }) => $paddingLeft && css`padding-left: ${$paddingLeft};`} + ${fadeBackground} + + &::-webkit-scrollbar { + width: 5px; + height: 5px; + } + &::-webkit-scrollbar-button { + width: 0; + height: 0; + } + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, .25); + border: none; + border-radius: 50px; + } + &::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, .5); } + &::-webkit-scrollbar-thumb:active { background: rgba(0, 0, 0, .25); } + &::-webkit-scrollbar-track { + background: rgba(0, 0, 0, .25); + border: none; + border-radius: 50px; + } + &::-webkit-scrollbar-track:hover { background: rgba(0, 0, 0, .25); } + &::-webkit-scrollbar-track:active { background: rgba(0, 0, 0, .25); } + &::-webkit-scrollbar-corner { background: 0 0; } +`; diff --git a/src/components/ScrollArea/types.ts b/src/components/ScrollArea/types.ts new file mode 100644 index 0000000..02196c9 --- /dev/null +++ b/src/components/ScrollArea/types.ts @@ -0,0 +1,49 @@ +import React from 'react'; + +export interface StyledScrollAreaWrapperProps { + $verticalScroll: boolean; + $horizontalScroll: boolean; + $maxHeight?: string; + $maxWidth?: string; + $fadeEdges: boolean; + $fadeColor?: string; + $paddingTop?: string; + $paddingRight?: string; + $paddingBottom?: string; + $paddingLeft?: string; +} + +export interface ScrollAreaProps { + /** Scrollable content. */ + children: React.ReactNode; + + /** Enables vertical scrolling. @default true */ + verticalScroll?: boolean; + + /** Enables horizontal scrolling. @default false */ + horizontalScroll?: boolean; + + /** Caps the content height; scrolling kicks in past this value (e.g. '400px', '50vh'). */ + maxHeight?: string; + + /** Caps the content width; scrolling kicks in past this value (e.g. '600px', '100%'). */ + maxWidth?: string; + + /** Fades the scrollable edges with a color mask + shadow, matching the chat's scroll style. @default true */ + fadeEdges?: boolean; + + /** Background color the fade blends into. Required in practice when `fadeEdges` is `true` — if omitted, the fade is silently skipped. */ + fadeColor?: string; + + /** Space reserved above the content, before the scrollable area's top edge (e.g. '1rem'). Keeps the first item clear of the top fade instead of butting against it. */ + paddingTop?: string; + + /** Space reserved to the right of the content, before the scrollable area's right edge (e.g. '1rem'). Keeps the last item clear of the right fade instead of butting against it. */ + paddingRight?: string; + + /** Space reserved below the content, before the scrollable area's bottom edge (e.g. '1rem'). Keeps the last item clear of the bottom fade instead of butting against it. */ + paddingBottom?: string; + + /** Space reserved to the left of the content, before the scrollable area's left edge (e.g. '1rem'). Keeps the first item clear of the left fade instead of butting against it. */ + paddingLeft?: string; +} diff --git a/src/components/index.ts b/src/components/index.ts index b5e6250..230d706 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -6,6 +6,7 @@ export { BBBHint } from './Hint'; export { BBBInput } from './Input'; export { BBBModal } from './Modal'; export { BBBNavigation } from './Navigation'; +export { BBBScrollArea } from './ScrollArea'; export { BBBSearch } from './Search'; export { BBBSelect } from './Select'; export { BBBSpinner } from './Spinner'; diff --git a/webpack.config.babel.js b/webpack.config.babel.js index 61ffdf7..461ae7f 100644 --- a/webpack.config.babel.js +++ b/webpack.config.babel.js @@ -10,6 +10,7 @@ export default { Input: './src/components/Input/index.ts', Modal: './src/components/Modal/index.ts', Navigation: './src/components/Navigation/index.ts', + ScrollArea: './src/components/ScrollArea/index.ts', Search: './src/components/Search/index.ts', Select: './src/components/Select/index.ts', Spinner: './src/components/Spinner/index.ts', From b67eb255ff243ac4f64b9e3649e713f922fb04ad Mon Sep 17 00:00:00 2001 From: Arthurk12 Date: Fri, 31 Jul 2026 18:20:07 -0300 Subject: [PATCH 7/7] fix(BBBTextAreaInput): use standard palette colors for text and placeholder Text color used colorLightGray (near-white), making typed content nearly invisible against the white background, and the placeholder had no explicit color at all. Align with BBBInput by using colorTextDefault for text and colorTextLight for the placeholder. --- src/components/TextAreaInput/styles.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/TextAreaInput/styles.ts b/src/components/TextAreaInput/styles.ts index ffd9f2d..31d83fc 100644 --- a/src/components/TextAreaInput/styles.ts +++ b/src/components/TextAreaInput/styles.ts @@ -1,5 +1,7 @@ import styled from 'styled-components'; -import { colorBorderDefault, colorBrand1, colorBrand2, colorLightGray, colorWhite } from '../../stylesheets/palette'; +import { + colorBorderDefault, colorBrand1, colorBrand2, colorTextDefault, colorTextLight, colorWhite, +} from '../../stylesheets/palette'; import { borderRadiusDefault } from '../../stylesheets/sizing'; export const TextAreaInput = styled.textarea` @@ -7,7 +9,7 @@ export const TextAreaInput = styled.textarea` background: ${colorWhite}; background-clip: padding-box; margin: 0; - color: ${colorLightGray}; + color: ${colorTextDefault}; padding: calc(.3rem* 2.5) calc(.75rem* 1.25); resize: none; -webkit-transition: none; @@ -20,6 +22,11 @@ export const TextAreaInput = styled.textarea` border: 1px solid ${colorBorderDefault}; overflow-y: hidden; margin: 0.3em; + + &::placeholder { + color: ${colorTextLight}; + } + &:focus { outline: ${colorBrand1} solid 2px; box-shadow: 0 0 0 2px ${colorBrand2} inset 0 0 0 1px ${colorBrand1};