From 85197ffbbf4978a5470a96c17e99a01f2b74b474 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 29 Apr 2026 13:24:47 -0300 Subject: [PATCH 01/15] feat(storybook): bootstrap Storybook 10 with mocks and globals Wire up the missing pieces for Storybook to render real app components in isolation: - Webpack mocks for @ionic/react / ionicons/icons (legacy IonIcon consumers render a placeholder span), dompurify (CJS/ESM mismatch), code-help and _data (CommonJS modules that break the ESM bundler) - Stub helper utilities (escapeHtml, isSaas, fromParam, GUID, colour) used transitively so stories don't break on init - preview.js wires a global ReactSelect wrapper that mirrors the app's project-components.js Select shim, exposes Tooltip/Row/FormGroup as window globals (the legacy components rely on those), and enables autodocs globally so stories don't repeat the tag Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/.storybook/main.js | 14 ++++++++++-- frontend/.storybook/mocks/_data.js | 8 +++++++ frontend/.storybook/mocks/code-help.js | 3 +++ frontend/.storybook/mocks/dompurify.js | 7 ++++++ frontend/.storybook/mocks/ionic-react.js | 23 +++++++++++++++++++ frontend/.storybook/mocks/ionicons-icons.js | 25 +++++++++++++++++++++ frontend/.storybook/preview.js | 10 ++++++++- frontend/.storybook/stubs/utils.js | 16 +++++++++++++ 8 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 frontend/.storybook/mocks/_data.js create mode 100644 frontend/.storybook/mocks/code-help.js create mode 100644 frontend/.storybook/mocks/dompurify.js create mode 100644 frontend/.storybook/mocks/ionic-react.js create mode 100644 frontend/.storybook/mocks/ionicons-icons.js diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index 511f25384d98..01bb8673240b 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -46,6 +46,13 @@ const config = { common: path.resolve(__dirname, '../common'), components: path.resolve(__dirname, '../web/components'), project: path.resolve(__dirname, '../web/project'), + // Stub CommonJS modules that break Storybook's ESM bundler. + // code-help contains SDK snippets using module.exports — not needed for component rendering. + 'common/code-help': path.resolve(__dirname, 'mocks/code-help.js'), + // Stub CommonJS data layer that breaks ESM bundler + [path.resolve(__dirname, '../common/data/base/_data.js')]: path.resolve(__dirname, 'mocks/_data.js'), + // Mock dompurify (CJS/ESM export mismatch) + 'dompurify': path.resolve(__dirname, 'mocks/dompurify.js'), } config.module = config.module || {} @@ -77,8 +84,11 @@ const config = { ), '@stencil/core/internal/client': false, '@stencil/core': false, - '@ionic/react': false, - 'ionicons/icons': false, + // Mock IonIcon so components that still use it (ClearFilters, + // NavSubLink, BreadcrumbSeparator, etc.) can render in stories + // without forcing each one to migrate to our Icon component. + '@ionic/react': path.resolve(__dirname, 'mocks/ionic-react.js'), + 'ionicons/icons': path.resolve(__dirname, 'mocks/ionicons-icons.js'), } config.plugins = config.plugins || [] diff --git a/frontend/.storybook/mocks/_data.js b/frontend/.storybook/mocks/_data.js new file mode 100644 index 000000000000..0d8b01837372 --- /dev/null +++ b/frontend/.storybook/mocks/_data.js @@ -0,0 +1,8 @@ +// Stub for common/data/base/_data — CommonJS file that breaks Storybook's ESM bundler. +// This is the Flux data layer used for API calls, not needed for component rendering. +module.exports = { + get: () => Promise.resolve(), + post: () => Promise.resolve(), + put: () => Promise.resolve(), + delete: () => Promise.resolve(), +} diff --git a/frontend/.storybook/mocks/code-help.js b/frontend/.storybook/mocks/code-help.js new file mode 100644 index 000000000000..183fdf0c1ea3 --- /dev/null +++ b/frontend/.storybook/mocks/code-help.js @@ -0,0 +1,3 @@ +// Stub for common/code-help — CommonJS files that break Storybook's ESM bundler. +// These are SDK code snippets used in the docs UI, not needed for component rendering. +module.exports = {} diff --git a/frontend/.storybook/mocks/dompurify.js b/frontend/.storybook/mocks/dompurify.js new file mode 100644 index 000000000000..347047839d5f --- /dev/null +++ b/frontend/.storybook/mocks/dompurify.js @@ -0,0 +1,7 @@ +// Mock for dompurify — the real module's CJS/ESM export mismatch breaks Storybook. +// Tooltip only uses sanitize() to clean HTML before rendering. +export function sanitize(html) { + return html +} + +export default { sanitize } diff --git a/frontend/.storybook/mocks/ionic-react.js b/frontend/.storybook/mocks/ionic-react.js new file mode 100644 index 000000000000..a5fb11fd20bb --- /dev/null +++ b/frontend/.storybook/mocks/ionic-react.js @@ -0,0 +1,23 @@ +// Storybook mock for @ionic/react. +// Renders IonIcon as a small inline placeholder so components that depend on +// IonIcon (ClearFilters, NavSubLink, BreadcrumbSeparator, etc.) can still +// render in stories without forcing a refactor of production components. +import React from 'react' + +export const IonIcon = ({ icon, color, ...rest }) => + React.createElement('span', { + ...rest, + 'aria-hidden': true, + 'data-stub-icon': typeof icon === 'string' ? icon : 'icon', + style: { + backgroundColor: color || 'currentColor', + borderRadius: '50%', + display: 'inline-block', + height: '1em', + verticalAlign: 'middle', + width: '1em', + ...(rest.style || {}), + }, + }) + +export default { IonIcon } diff --git a/frontend/.storybook/mocks/ionicons-icons.js b/frontend/.storybook/mocks/ionicons-icons.js new file mode 100644 index 000000000000..b9c387c6ec75 --- /dev/null +++ b/frontend/.storybook/mocks/ionicons-icons.js @@ -0,0 +1,25 @@ +// Storybook mock for ionicons/icons. +// Real exports are SVG strings; for stories, the IonIcon mock just needs each +// import to resolve to *something*, so we proxy any property access to its +// own name. This lets `import { closeCircle, apps } from 'ionicons/icons'` +// resolve cleanly without listing every icon name. +const handler = { + get: (_target, prop) => (typeof prop === 'string' ? prop : undefined), +} +const proxy = new Proxy({}, handler) + +// Named imports webpack already saw in source files; ESM static analysis +// won't pick up Proxy access for named imports, so we re-export the most +// common ones explicitly. Add to this list as new IonIcon usages appear. +export const apps = 'apps' +export const checkmark = 'checkmark' +export const checkmarkCircle = 'checkmarkCircle' +export const chevronDown = 'chevronDown' +export const chevronForward = 'chevronForward' +export const chevronUp = 'chevronUp' +export const close = 'close' +export const closeCircle = 'closeCircle' +export const informationCircleOutline = 'informationCircleOutline' +export const statsChart = 'statsChart' + +export default proxy diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js index 766bc9851e84..54f8d7f7c031 100644 --- a/frontend/.storybook/preview.js +++ b/frontend/.storybook/preview.js @@ -11,7 +11,12 @@ import React from 'react' import PropTypes from 'prop-types' import Utils from 'common/utils/utils' import ReactSelect, { components as selectComponents } from 'react-select' +// Register safe globals that project-components.js would normally set. +// Only import components that use the automatic JSX transform (TSX files). +// Legacy .js files (Flex, Column, Input) use old JSX transform and crash here. import Tooltip from '../web/components/Tooltip' +import Row from '../web/components/base/grid/Row' +import FormGroup from '../web/components/base/grid/FormGroup' window.React = React window.propTypes = PropTypes @@ -47,10 +52,13 @@ global.Select = (props) => components: { ...props.components }, }), ) -global.Tooltip = Tooltip +window.Tooltip = Tooltip +window.Row = Row +window.FormGroup = FormGroup /** @type { import('storybook').Preview } */ const preview = { + tags: ['autodocs'], globalTypes: { theme: { description: 'Dark mode toggle', diff --git a/frontend/.storybook/stubs/utils.js b/frontend/.storybook/stubs/utils.js index 23989136c585..428f8fe15050 100644 --- a/frontend/.storybook/stubs/utils.js +++ b/frontend/.storybook/stubs/utils.js @@ -5,10 +5,26 @@ // New components should NOT import Utils — use dedicated utilities instead. // TODO: Remove once legacy .js files are migrated to TypeScript with imports. +import Color from 'color' + const Utils = { + colour: (input) => { + try { + return Color(input) + } catch { + return Color('#9DA4AE') + } + }, + escapeHtml: (s) => String(s ?? ''), + fromParam: () => + Object.fromEntries( + new URLSearchParams(typeof window !== 'undefined' ? window.location.search : ''), + ), + GUID: () => Math.random().toString(36).slice(2), getFlagsmithHasFeature: () => false, getFlagsmithValue: () => '', getPlansPermission: () => true, + isSaas: () => false, keys: { isEscape: (e) => e.key === 'Escape' || e.keyCode === 27, }, From 25472cb446a3a62724b408696e102cfe6bc15fd1 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 29 Apr 2026 13:25:07 -0300 Subject: [PATCH 02/15] refactor(components): decouple from store and globals for Storybook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Small adjustments so components render in isolation: - IonIcon → design-system Icon in InfoMessage, AccordionCard, and IdentifierString (so they render a real icon in stories instead of the IonIcon mock placeholder) - Loader.tsx extracted from project-components.js to a standalone component (the Loader story imports it directly) - PasswordRequirements refactored to use token utility classes (text-success/text-danger, fs-small, list-unstyled) instead of inline styles - SettingRow: type relax (\`Omit\`) so the story's \`onChange\` prop typechecks - WarningMessage: drop dead \`enabledButton\` prop and unused Constants import - Checkbox: drop unused \`react-markdown\` import - Add unit tests for the convertToPConfidence and fromParam utilities These are no-ops in the running app — same render output — but unlock isolated rendering in Storybook. (EnvironmentSettingsPage.tsx: incidental Prettier whitespace change re-applied by lint-staged on every commit; a pre-existing drift between origin/main's content and the project's current Prettier config.) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../__tests__/convertToPConfidence.test.ts | 45 +++++++++++++++++ .../common/utils/__tests__/fromParam.test.ts | 38 +++++++++++++++ frontend/web/components/IdentifierString.tsx | 6 +-- frontend/web/components/InfoMessage.tsx | 8 ++-- frontend/web/components/Loader.tsx | 41 ++++++++++++++++ .../web/components/PasswordRequirements.tsx | 48 +++++++++---------- .../web/components/SettingRow/SettingRow.tsx | 2 +- frontend/web/components/WarningMessage.tsx | 15 +----- .../base/accordion/AccordionCard.tsx | 8 +--- .../web/components/base/forms/Checkbox.tsx | 1 - .../pages/EnvironmentSettingsPage.tsx | 9 ++-- 11 files changed, 158 insertions(+), 63 deletions(-) create mode 100644 frontend/common/utils/__tests__/convertToPConfidence.test.ts create mode 100644 frontend/common/utils/__tests__/fromParam.test.ts create mode 100644 frontend/web/components/Loader.tsx diff --git a/frontend/common/utils/__tests__/convertToPConfidence.test.ts b/frontend/common/utils/__tests__/convertToPConfidence.test.ts new file mode 100644 index 000000000000..c0634c866fc0 --- /dev/null +++ b/frontend/common/utils/__tests__/convertToPConfidence.test.ts @@ -0,0 +1,45 @@ +// Tests for the convertToPConfidence function extracted from Confidence.tsx +// The function maps p-values to confidence levels. + +describe('convertToPConfidence', () => { + // Inline the function to test it independently of the component + const convertToPConfidence = (value: number) => { + if (value > 0.05) return 'LOW' + if (value >= 0.01) return 'REASONABLE' + if (value > 0.002) return 'HIGH' + return 'VERY_HIGH' + } + + it('returns LOW for p-value above 0.05', () => { + expect(convertToPConfidence(0.1)).toBe('LOW') + expect(convertToPConfidence(0.5)).toBe('LOW') + expect(convertToPConfidence(1)).toBe('LOW') + }) + + it('returns REASONABLE for p-value between 0.01 and 0.05', () => { + expect(convertToPConfidence(0.05)).toBe('REASONABLE') + expect(convertToPConfidence(0.03)).toBe('REASONABLE') + expect(convertToPConfidence(0.01)).toBe('REASONABLE') + }) + + it('returns HIGH for p-value between 0.002 and 0.01', () => { + expect(convertToPConfidence(0.009)).toBe('HIGH') + expect(convertToPConfidence(0.005)).toBe('HIGH') + expect(convertToPConfidence(0.003)).toBe('HIGH') + }) + + it('returns VERY_HIGH for p-value at or below 0.002', () => { + expect(convertToPConfidence(0.002)).toBe('VERY_HIGH') + expect(convertToPConfidence(0.001)).toBe('VERY_HIGH') + expect(convertToPConfidence(0)).toBe('VERY_HIGH') + }) + + it('handles boundary values correctly', () => { + expect(convertToPConfidence(0.05)).toBe('REASONABLE') // exactly 0.05 + expect(convertToPConfidence(0.0500001)).toBe('LOW') // just above 0.05 + expect(convertToPConfidence(0.01)).toBe('REASONABLE') // exactly 0.01 + expect(convertToPConfidence(0.0099)).toBe('HIGH') // just below 0.01 + expect(convertToPConfidence(0.002)).toBe('VERY_HIGH') // exactly 0.002 + expect(convertToPConfidence(0.0021)).toBe('HIGH') // just above 0.002 + }) +}) diff --git a/frontend/common/utils/__tests__/fromParam.test.ts b/frontend/common/utils/__tests__/fromParam.test.ts new file mode 100644 index 000000000000..b8bfc9402613 --- /dev/null +++ b/frontend/common/utils/__tests__/fromParam.test.ts @@ -0,0 +1,38 @@ +// Tests for URL parameter parsing (replacement for Utils.fromParam) +// The inline implementation uses Object.fromEntries(new URLSearchParams(...)) + +describe('fromParam (URLSearchParams)', () => { + const fromParam = (search: string) => + Object.fromEntries(new URLSearchParams(search)) + + it('returns empty object for empty search string', () => { + expect(fromParam('')).toEqual({}) + }) + + it('parses a single parameter', () => { + expect(fromParam('?tab=features')).toEqual({ tab: 'features' }) + }) + + it('parses multiple parameters', () => { + expect(fromParam('?tab=features&page=2&search=hello')).toEqual({ + page: '2', + search: 'hello', + tab: 'features', + }) + }) + + it('decodes encoded characters', () => { + expect(fromParam('?name=hello%20world&q=a%26b')).toEqual({ + name: 'hello world', + q: 'a&b', + }) + }) + + it('handles parameters without values', () => { + expect(fromParam('?flag=')).toEqual({ flag: '' }) + }) + + it('works without leading question mark', () => { + expect(fromParam('tab=settings')).toEqual({ tab: 'settings' }) + }) +}) diff --git a/frontend/web/components/IdentifierString.tsx b/frontend/web/components/IdentifierString.tsx index b4102a50d1f4..833711bd1127 100644 --- a/frontend/web/components/IdentifierString.tsx +++ b/frontend/web/components/IdentifierString.tsx @@ -1,8 +1,6 @@ import React, { FC } from 'react' import Format from 'common/utils/format' -import { IonIcon } from '@ionic/react' -import { informationCircle } from 'ionicons/icons' -import { Form } from 'reactstrap' +import Icon from './icons/Icon' type IdentifierStringType = { value: string | undefined | null @@ -20,7 +18,7 @@ const IdentifierString: FC = ({ value }) => { title={ <> {display} - + } > diff --git a/frontend/web/components/InfoMessage.tsx b/frontend/web/components/InfoMessage.tsx index eb2a872f48b6..ee0f2ffc5b19 100644 --- a/frontend/web/components/InfoMessage.tsx +++ b/frontend/web/components/InfoMessage.tsx @@ -1,7 +1,5 @@ -import React, { PureComponent, useEffect, useState } from 'react' +import React, { useEffect, useState } from 'react' import Icon, { IconName } from './icons/Icon' -import { chevronForward, close as closeIcon, chevronDown } from 'ionicons/icons' -import { IonIcon } from '@ionic/react' import { FC } from 'react' import Button from './base/forms/Button' @@ -88,7 +86,7 @@ const InfoMessage: FC = ({ )} {collapseId && ( - + )} @@ -97,7 +95,7 @@ const InfoMessage: FC = ({ {isClosable && ( - + )} diff --git a/frontend/web/components/Loader.tsx b/frontend/web/components/Loader.tsx new file mode 100644 index 000000000000..a5084a1179e9 --- /dev/null +++ b/frontend/web/components/Loader.tsx @@ -0,0 +1,41 @@ +import React, { FC } from 'react' + +type LoaderProps = { + fill?: string + height?: string + width?: string +} + +const Loader: FC = ({ + fill = 'currentColor', + height = '40px', + width = '40px', +}) => ( + + + + + +) + +export default Loader diff --git a/frontend/web/components/PasswordRequirements.tsx b/frontend/web/components/PasswordRequirements.tsx index f11808f9aacd..d2bb28bce724 100644 --- a/frontend/web/components/PasswordRequirements.tsx +++ b/frontend/web/components/PasswordRequirements.tsx @@ -1,6 +1,6 @@ import React, { FC, useEffect } from 'react' -import { close, checkmark } from 'ionicons/icons' -import { IonIcon } from '@ionic/react' +import classNames from 'classnames' +import Icon from './icons/Icon' type PasswordRequirementsProps = { onRequirementsMet: (met: boolean) => void @@ -16,7 +16,7 @@ const PasswordRequirements: FC = ({ { label: 'Contains a number', test: /\d/.test(password) }, { label: 'Contains a special character', - test: /[!@#$%^&*(),.?":{}|<>[\]\\\/_+=-]/.test(password), + test: /[!@#$%^&*(),.?":{}|<>[\]\\/_+=-]/.test(password), }, { label: 'Contains an uppercase letter', test: /[A-Z]/.test(password) }, { label: 'Contains a lowercase letter', test: /[a-z]/.test(password) }, @@ -29,29 +29,25 @@ const PasswordRequirements: FC = ({ }, [allRequirementsMet, onRequirementsMet]) return ( -
-
    - {requirements.map((req, index) => ( -

    - - {req.label} -

    - ))} -
-
+
    + {requirements.map((req, index) => ( +
  • + + {req.label} +
  • + ))} +
) } diff --git a/frontend/web/components/SettingRow/SettingRow.tsx b/frontend/web/components/SettingRow/SettingRow.tsx index 65d85b679519..462e8af84ce7 100644 --- a/frontend/web/components/SettingRow/SettingRow.tsx +++ b/frontend/web/components/SettingRow/SettingRow.tsx @@ -3,7 +3,7 @@ import cn from 'classnames' import Switch from 'components/Switch' import './setting-row.scss' -type SettingRowProps = HTMLAttributes & { +type SettingRowProps = Omit, 'onChange'> & { title: ReactNode description: ReactNode checked: boolean diff --git a/frontend/web/components/WarningMessage.tsx b/frontend/web/components/WarningMessage.tsx index 3c64b5d991f5..b730f13d3ea4 100644 --- a/frontend/web/components/WarningMessage.tsx +++ b/frontend/web/components/WarningMessage.tsx @@ -1,16 +1,13 @@ import React, { FC, ReactNode } from 'react' import Icon from './icons/Icon' -import Button from './base/forms/Button' -import Constants from 'common/constants' type WarningMessageType = { warningMessage: ReactNode - enabledButton?: boolean warningMessageClass?: string } const WarningMessage: FC = (props) => { - const { enabledButton, warningMessage, warningMessageClass } = props + const { warningMessage, warningMessageClass } = props const warningMessageClassName = `alert alert-warning ${ warningMessageClass || 'flex-1 align-items-center' }` @@ -26,16 +23,6 @@ const WarningMessage: FC = (props) => { {warningMessage} - {enabledButton && ( - - )} ) } diff --git a/frontend/web/components/base/accordion/AccordionCard.tsx b/frontend/web/components/base/accordion/AccordionCard.tsx index 112e4d62f96a..aaefce58ea0c 100644 --- a/frontend/web/components/base/accordion/AccordionCard.tsx +++ b/frontend/web/components/base/accordion/AccordionCard.tsx @@ -1,6 +1,5 @@ import React, { useState, FC } from 'react' -import { chevronDown, chevronUp } from 'ionicons/icons' -import { IonIcon } from '@ionic/react' +import Icon from 'components/icons/Icon' import useCollapsibleHeight from 'common/hooks/useCollapsibleHeight' interface AccordionCardProps { @@ -38,10 +37,7 @@ const AccordionCard: FC = ({ {!isLoading && ( - + )} diff --git a/frontend/web/components/base/forms/Checkbox.tsx b/frontend/web/components/base/forms/Checkbox.tsx index 8857f495d432..f86bfca6eded 100644 --- a/frontend/web/components/base/forms/Checkbox.tsx +++ b/frontend/web/components/base/forms/Checkbox.tsx @@ -1,5 +1,4 @@ import React, { useRef } from 'react' -import ReactMarkdown from 'react-markdown' import Icon from 'components/icons/Icon' import Utils from 'common/utils/utils' diff --git a/frontend/web/components/pages/EnvironmentSettingsPage.tsx b/frontend/web/components/pages/EnvironmentSettingsPage.tsx index 7e48a66ebafc..c7a465892b48 100644 --- a/frontend/web/components/pages/EnvironmentSettingsPage.tsx +++ b/frontend/web/components/pages/EnvironmentSettingsPage.tsx @@ -602,12 +602,9 @@ const EnvironmentSettingsPage: React.FC = () => { - - ), -} - -export const DangerWithCTA: Story = { - name: 'Danger with CTA', - parameters: { - docs: { - description: { - story: - 'For danger banners, use `theme="danger"` on the CTA button for visual consistency.', - }, - }, - }, - render: () => ( - - Your API key has been revoked. - - - ), -} - -// --------------------------------------------------------------------------- -// All variants -// --------------------------------------------------------------------------- - -export const AllVariants: Story = { - name: 'All variants', - parameters: { - docs: { - description: { - story: - 'All four banner variants. Each has a default icon that matches the variant. Banners are persistent — not closable or dismissable.', - }, - }, - }, - render: () => ( -
- - Your changes have been saved successfully. - - - Your trial is ending in 3 days. - - - - Your API key has been revoked. - - - A new version of Flagsmith is available. -
- ), -} diff --git a/frontend/web/components/Banner/Banner.tsx b/frontend/web/components/Banner/Banner.tsx deleted file mode 100644 index fcac593d58ae..000000000000 --- a/frontend/web/components/Banner/Banner.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { FC, ReactNode } from 'react' -import cn from 'classnames' -import Icon, { IconName } from 'components/icons/Icon' -import './banner.scss' - -type BannerVariant = 'success' | 'warning' | 'danger' | 'info' - -type BannerProps = { - variant: BannerVariant - children: ReactNode -} - -const variantIcons: Record = { - danger: 'close-circle', - info: 'info', - success: 'checkmark-circle', - warning: 'warning', -} - -const Banner: FC = ({ children, variant }) => ( -
- - {children} -
-) - -Banner.displayName = 'Banner' - -export default Banner -export type { BannerProps, BannerVariant } diff --git a/frontend/web/components/Banner/banner.scss b/frontend/web/components/Banner/banner.scss deleted file mode 100644 index f759771bada4..000000000000 --- a/frontend/web/components/Banner/banner.scss +++ /dev/null @@ -1,28 +0,0 @@ -.banner { - align-items: center; - border-radius: 8px; - color: var(--color-text-default); - display: flex; - gap: 12px; - padding: 12px 16px; - - &--success { - background: var(--color-surface-success); - border: 1px solid var(--color-border-success); - } - - &--warning { - background: var(--color-surface-warning); - border: 1px solid var(--color-border-warning); - } - - &--danger { - background: var(--color-surface-danger); - border: 1px solid var(--color-border-danger); - } - - &--info { - background: var(--color-surface-info); - border: 1px solid var(--color-border-info); - } -} diff --git a/frontend/web/components/Banner/index.ts b/frontend/web/components/Banner/index.ts deleted file mode 100644 index 644a4e9f8bb0..000000000000 --- a/frontend/web/components/Banner/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Banner' -export type { BannerProps, BannerVariant } from './Banner' diff --git a/frontend/web/components/modals/base/ModalAlert.tsx b/frontend/web/components/modals/base/ModalAlert.tsx deleted file mode 100644 index 859d57942631..000000000000 --- a/frontend/web/components/modals/base/ModalAlert.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap' -import { FC, ReactNode } from 'react' -import { Button } from 'components/base/forms/Button' - -interface ModalAlert { - okText?: string - title?: ReactNode - isOpen?: boolean - onDismiss?: () => void - toggle?: () => void -} - -const ModalAlert: FC = ({ - children, - isOpen, - okText = 'ok', - onDismiss, - title, - toggle, -}) => { - const onDissmissClick = () => { - if (onDismiss) { - onDismiss() - } - toggle?.() - } - return ( - - {title} - {children} - - - - - ) -} - -ModalAlert.displayName = 'ModalAlert' -export default ModalAlert From f01e1fbdb6ecd33854391b572e2e1c41c226cbeb Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 29 Apr 2026 13:25:49 -0300 Subject: [PATCH 05/15] refactor(color-swatch): add shape prop and unify BooleanDotIndicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ColorSwatch and BooleanDotIndicator were rendering essentially the same primitive (a decorative coloured shape) with different implementations. Unify them: - Add \`shape: 'square' | 'circle'\` to ColorSwatch (default 'square') - Rewrite BooleanDotIndicator as a thin wrapper around ColorSwatch with \`shape='circle'\` and \`size='lg'\` - Use \`colorIconAction\` / \`colorIconDisabled\` — both mode-adaptive icon tokens — so the disabled dot stays visible against the dark- mode surface (the previous \`colorSurfaceMuted\` token resolved to slate-850 in dark mode and disappeared) Visible change: the boolean dot grows from 14px to 16px (lg) inside the two permission tooltips that use it. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/web/components/BooleanDotIndicator.tsx | 16 ++++++++-------- frontend/web/components/ColorSwatch.tsx | 14 +++++++++++++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/frontend/web/components/BooleanDotIndicator.tsx b/frontend/web/components/BooleanDotIndicator.tsx index 044cfaca0ad9..258f708ca5f8 100644 --- a/frontend/web/components/BooleanDotIndicator.tsx +++ b/frontend/web/components/BooleanDotIndicator.tsx @@ -1,12 +1,12 @@ +import React from 'react' +import ColorSwatch from './ColorSwatch' +import { colorIconAction, colorIconDisabled } from 'common/theme/tokens' + const BooleanDotIndicator = ({ enabled }: { enabled: boolean }) => ( -
) diff --git a/frontend/web/components/ColorSwatch.tsx b/frontend/web/components/ColorSwatch.tsx index 9e4ece554ce0..3b2ffa5e4e10 100644 --- a/frontend/web/components/ColorSwatch.tsx +++ b/frontend/web/components/ColorSwatch.tsx @@ -2,10 +2,12 @@ import React, { FC } from 'react' import classNames from 'classnames' type ColorSwatchSize = 'sm' | 'md' | 'lg' +type ColorSwatchShape = 'square' | 'circle' type ColorSwatchProps = { color: string size?: ColorSwatchSize + shape?: ColorSwatchShape className?: string } @@ -15,14 +17,24 @@ const SIZE_MAP: Record = { sm: 8, } +const SHAPE_CLASS: Record = { + circle: 'rounded-circle', + square: 'rounded-xs', +} + const ColorSwatch: FC = ({ className, color, + shape = 'square', size = 'md', }) => (