diff --git a/.github/workflows/design-system-preview.yaml b/.github/workflows/design-system-preview.yaml new file mode 100644 index 0000000000..58d46981eb --- /dev/null +++ b/.github/workflows/design-system-preview.yaml @@ -0,0 +1,37 @@ +name: Design system / Storybook preview + +on: + pull_request: + branches: + - "**" + paths: + - "assets/design-system/**" + - ".github/workflows/design-system-preview.yaml" + +permissions: + checks: write + contents: read + pull-requests: write + +jobs: + preview: + name: Create preview + if: ${{ github.actor != 'renovate[bot]' }} + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: assets + steps: + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0 + - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: 24.11.1 + - run: yarn --immutable + - run: yarn workspace @pluralsh/design-system build:storybook + - uses: FirebaseExtended/action-hosting-deploy@500ac625ca2dd40cbd15f7659af953801858032a # v0.11.0 + with: + repoToken: ${{ secrets.GITHUB_TOKEN }} + firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_PLURALSH_DESIGN }} + projectId: pluralsh-design + entryPoint: assets/design-system diff --git a/assets/design-system/.firebaserc b/assets/design-system/.firebaserc new file mode 100644 index 0000000000..f10f72dc6d --- /dev/null +++ b/assets/design-system/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "pluralsh-design" + } +} diff --git a/assets/design-system/firebase.json b/assets/design-system/firebase.json new file mode 100644 index 0000000000..e253544eb7 --- /dev/null +++ b/assets/design-system/firebase.json @@ -0,0 +1,20 @@ +{ + "emulators": { + "hosting": { + "port": 5000, + "host": "0.0.0.0" + } + }, + "hosting": { + "public": "storybook-static", + "ignore": [ + "firebase.json" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } +} diff --git a/assets/design-system/src/GlobalStyle.tsx b/assets/design-system/src/GlobalStyle.tsx index f13de40957..6d5231f3db 100644 --- a/assets/design-system/src/GlobalStyle.tsx +++ b/assets/design-system/src/GlobalStyle.tsx @@ -22,6 +22,10 @@ const { export const colorsToCSSVars: (colors: unknown) => any = (colors) => { function inner(colors: unknown, prefix = '') { + if (!colors || typeof colors !== 'object') { + return + } + Object.entries(colors).forEach(([key, value]) => { if (typeof value === 'string') { ;(cssVars as any)[`--color-${prefix}${key}`] = value diff --git a/assets/design-system/src/components/AppIcon.tsx b/assets/design-system/src/components/AppIcon.tsx index a5e9ab6d47..64a70a11c1 100644 --- a/assets/design-system/src/components/AppIcon.tsx +++ b/assets/design-system/src/components/AppIcon.tsx @@ -85,7 +85,7 @@ export function toInitials(name: string) { .map((n) => n.charAt(0).toUpperCase()) if (initials.length > 2) { - initials = [initials[0], last(initials)] + initials = [initials[0], last(initials) ?? ''] } return initials.join('') @@ -101,17 +101,17 @@ const AppIconSC = styled.div<{ display: 'flex', alignItems: 'center', justifyContent: 'center', - backgroundColor: theme.colors[$color], + backgroundColor: $color ? theme.colors[$color] : undefined, borderRadius: theme.borderRadiuses.medium, border: $hasBorder ? theme.borders.default : 'none', - borderColor: theme.colors[$borderColor], + borderColor: $borderColor ? theme.colors[$borderColor] : undefined, width: $boxSize, height: $boxSize, minWidth: $boxSize, minHeight: $boxSize, cursor: $clickable ? 'pointer' : 'auto', overflow: 'hidden', - _hover: $clickable ? { backgroundColor: $borderColor } : null, + _hover: $clickable ? { backgroundColor: $borderColor } : undefined, })) const InitialsSC = styled.div<{ @@ -141,7 +141,7 @@ function AppIcon({ hue, clickable = false, url, - icon = null, + icon, alt, name, initials, @@ -175,13 +175,13 @@ function AppIcon({ $hasBorder={hasBorder} $boxSize={boxSize} $clickable={clickable} - onClick={clickable ? onClose : null} + onClick={clickable ? onClose : undefined} {...props} > {url ? ( ) : ( diff --git a/assets/design-system/src/components/ArrowScroll.tsx b/assets/design-system/src/components/ArrowScroll.tsx index c062d16928..8268c8633e 100644 --- a/assets/design-system/src/components/ArrowScroll.tsx +++ b/assets/design-system/src/components/ArrowScroll.tsx @@ -17,7 +17,7 @@ function ArrowScroll({ color?: SemanticColorKey opacity?: number } & ComponentPropsWithRef<'div'>) { - const containerRef = useRef(undefined) + const containerRef = useRef(null) const [showLeftGradient, setShowLeftGradient] = useState(false) const [showRightGradient, setShowRightGradient] = useState(false) diff --git a/assets/design-system/src/components/Breadcrumbs.tsx b/assets/design-system/src/components/Breadcrumbs.tsx index bf16d3ff58..721d402b0d 100644 --- a/assets/design-system/src/components/Breadcrumbs.tsx +++ b/assets/design-system/src/components/Breadcrumbs.tsx @@ -304,14 +304,14 @@ export function DynamicBreadcrumbs({ minLength = 0, maxLength = Infinity, collapsible = true, - breadcrumbs, + breadcrumbs = [], wrapperRef: transitionRef, ...props }: BreadcrumbPropsBase & { - wrapperRef?: RefObject + wrapperRef?: RefObject style: any }) { - const wrapperRef = useRef(undefined) + const wrapperRef = useRef(null) const [visibleListId, setVisibleListId] = useState('') const children: ReactNode[] = [] @@ -403,7 +403,7 @@ export function Breadcrumbs({ }: BreadcrumbsProps & Omit) { const contextCrumbs = useContext(BreadcrumbsContext)?.breadcrumbs const breadcrumbs = propsCrumbs || contextCrumbs - const nodeRef = useRef(undefined) + const nodeRef = useRef(null) if (!breadcrumbs) { throw Error( diff --git a/assets/design-system/src/components/Button.tsx b/assets/design-system/src/components/Button.tsx index 441486813c..b420ac5aa9 100644 --- a/assets/design-system/src/components/Button.tsx +++ b/assets/design-system/src/components/Button.tsx @@ -123,7 +123,7 @@ const Button = memo( $size={buttonSize} $type={buttonType} $noPadding={props.padding === 'none'} - disabled={disabled} + disabled={disabled ?? undefined} css={{ width, minWidth, diff --git a/assets/design-system/src/components/Card.tsx b/assets/design-system/src/components/Card.tsx index 25bc850b2b..c8c7a94ea9 100644 --- a/assets/design-system/src/components/Card.tsx +++ b/assets/design-system/src/components/Card.tsx @@ -209,7 +209,7 @@ function Card({ diff --git a/assets/design-system/src/components/Checkbox.tsx b/assets/design-system/src/components/Checkbox.tsx index 0d2443e2c0..e77ce834d7 100644 --- a/assets/design-system/src/components/Checkbox.tsx +++ b/assets/design-system/src/components/Checkbox.tsx @@ -175,7 +175,7 @@ function Checkbox({ } const labelId = useId() const toggleState = useToggleState(toggleStateProps) - const inputRef = useRef(undefined) + const inputRef = useRef(null) const { isFocusVisible, focusProps } = useFocusRing() const { inputProps } = useCheckbox( { @@ -195,9 +195,9 @@ function Checkbox({ ) const icon = indeterminate ? ( - + ) : toggleState.isSelected ? ( - + ) : null return ( @@ -209,8 +209,8 @@ function Checkbox({ indeterminate, })} $isFocusVisible={isFocusVisible} - $small={small} - $disabled={disabled} + $small={!!small} + $disabled={!!disabled} display="flex" marginBottom="0" {...props} @@ -224,7 +224,7 @@ function Checkbox({ if (typeof onChange === 'function') { onChange(e) } - inputProps.onChange(e) + inputProps.onChange?.(e) }} ref={inputRef} /> diff --git a/assets/design-system/src/components/Checklist.tsx b/assets/design-system/src/components/Checklist.tsx index 5aee7696bb..5b8f27c547 100644 --- a/assets/design-system/src/components/Checklist.tsx +++ b/assets/design-system/src/components/Checklist.tsx @@ -84,10 +84,10 @@ type ChecklistProps = ComponentPropsWithRef<'div'> & { } type ChecklistStateProps = { - onSelectionChange?: Dispatch + onSelectionChange?: Dispatch onFocusChange?: Dispatch onOpenChange?: Dispatch - selectedKey?: number + selectedKey?: number | null focusedKey?: number completedKey?: number isOpen?: boolean @@ -123,13 +123,17 @@ function ChecklistUnstyled({ useState(-1) const onSelectionChangeWrapper = useCallback( - (idx: number) => - idx < children.length && idx > -1 ? onSelectionChange(idx) : undefined, + (idx: number | null) => { + if (idx !== null && (idx >= children.length || idx < 0)) return + onSelectionChange?.(idx) + }, [children, onSelectionChange] ) const onFocusChangeWrapper = useCallback( - (idx: number) => - idx < children.length && idx > -1 ? onFocusChange(idx) : undefined, + (idx: number) => { + if (idx >= children.length || idx < -1) return + onFocusChange?.(idx) + }, [children, onFocusChange] ) @@ -142,7 +146,7 @@ function ChecklistUnstyled({ index={index} selected={selectedKey === index} focused={focusedKey === index} - completed={completedKey >= index} + completed={(completedKey ?? -1) >= index} onSelectionChange={onSelectionChangeWrapper} onFocusChange={onFocusChangeWrapper} > @@ -170,6 +174,8 @@ function ChecklistUnstyled({ }) useEffect(() => { + if (!itemsContainerRef.current || !finishedContainerRef.current) return + const maxItemContainerHeight = Math.max( itemContainerHeight, itemsContainerRef.current.getBoundingClientRect().height @@ -202,7 +208,7 @@ function ChecklistUnstyled({ >
onOpenChange(!isOpen)} + onClick={() => onOpenChange?.(!isOpen)} >
{label}
diff --git a/assets/design-system/src/components/ChecklistItem.tsx b/assets/design-system/src/components/ChecklistItem.tsx index 9c5263a2d9..2879257871 100644 --- a/assets/design-system/src/components/ChecklistItem.tsx +++ b/assets/design-system/src/components/ChecklistItem.tsx @@ -170,7 +170,7 @@ type ChecklistItemInnerProps = Omit & { selected?: boolean focused?: boolean completed?: boolean - onSelectionChange: Dispatch + onSelectionChange: Dispatch onFocusChange: Dispatch } @@ -184,7 +184,7 @@ function ChecklistItemInnerUnstyled({ onFocusChange, ...props }: ChecklistItemInnerProps): JSX.Element { - const headerRef = useRef(undefined) + const headerRef = useRef(null) const { keyboardProps } = useKeyboard({ onKeyDown: (e) => { switch (e.key) { @@ -209,7 +209,7 @@ function ChecklistItemInnerUnstyled({ useEffect(() => { if (headerRef.current && focused) { - setTimeout(() => headerRef.current.focus()) + setTimeout(() => headerRef.current?.focus()) } }, [headerRef, focused]) diff --git a/assets/design-system/src/components/ChipList.tsx b/assets/design-system/src/components/ChipList.tsx index 02faa4915a..dfe7134d23 100644 --- a/assets/design-system/src/components/ChipList.tsx +++ b/assets/design-system/src/components/ChipList.tsx @@ -40,7 +40,7 @@ function ChipList({ clickable && onClick(v)} + onClick={() => clickable && onClick?.(v)} {...props} > {transformValue ? transformValue(v) : `${v}`} diff --git a/assets/design-system/src/components/Code.tsx b/assets/design-system/src/components/Code.tsx index ce8e25d872..b383731bb8 100644 --- a/assets/design-system/src/components/Code.tsx +++ b/assets/design-system/src/components/Code.tsx @@ -58,7 +58,7 @@ type CodeProps = Omit & { type TabInterfaceT = 'tabs' | 'dropdown' type TabsContext = { - tabInterface: TabInterfaceT + tabInterface?: TabInterfaceT setTabInterface: (arg: TabInterfaceT) => void tabStateRef?: RefObject selectedKey?: string @@ -206,12 +206,13 @@ function CodeTabs() { setTabInterface, tabStateRef, - tabs, + tabs: tabsProp, selectedKey, onSelectionChange, } = useContext(TabsContext) - const tabsRef = useRef(undefined) - const tabsWrapRef = useRef(undefined) + const tabs = tabsProp ?? [] + const tabsRef = useRef(null) + const tabsWrapRef = useRef(null) const tabListStateProps: TabListStateProps = { keyboardActivation: 'manual', orientation: 'horizontal', @@ -236,10 +237,12 @@ function CodeTabs() { }, [setTabInterface]) ) + if (!tabStateRef) return null + return ( ) { - const { tabs, selectedKey, onSelectionChange } = useContext(TabsContext) + const { + tabs: tabsProp, + selectedKey, + onSelectionChange, + } = useContext(TabsContext) + const tabs = tabsProp ?? [] const selectedTab = tabs.find((tab) => tab.key === selectedKey) || tabs[0] @@ -283,7 +291,7 @@ function CodeSelectUnstyled({ className }: ComponentProps<'div'>) { width="max-content" placement="right" triggerButton={ - {selectedTab.label} + {selectedTab?.label} } > {tabs.map((tab) => ( @@ -409,12 +417,12 @@ function CodeUnstyled({ }: CodeProps) { const parentFillLevel = useFillLevel() const inferredFillLevel = fillLevelProp ?? parentFillLevel - const tabStateRef = useRef(undefined) + const tabStateRef = useRef(null) const [selectedTabKey, setSelectedTabKey] = useState( tabs?.[0]?.key || '' ) const theme = useTheme() - const [tabInterface, setTabInterface] = useState() + const [tabInterface, setTabInterface] = useState() props.height = props.height || undefined const hasSetHeight = !!props.height || !!props.minHeight @@ -508,7 +516,7 @@ function CodeUnstyled({ isStreaming={isStreaming} setMermaidError={setMermaidError} > - {children} + {children ?? ''} )} diff --git a/assets/design-system/src/components/CodeEditor.tsx b/assets/design-system/src/components/CodeEditor.tsx index e761476cb4..fe0411b026 100644 --- a/assets/design-system/src/components/CodeEditor.tsx +++ b/assets/design-system/src/components/CodeEditor.tsx @@ -49,9 +49,9 @@ export default function CodeEditor({ const parentFillLevel = useFillLevel() const theme = useTheme() const monaco = useMonaco() - const [current, setCurrent] = useState(value) + const [current, setCurrent] = useState(value ?? '') const [copied, setCopied] = useState(false) - const changed = current !== value + const changed = current !== (value ?? '') const onEditorMount = useCallback( (editor: any) => { @@ -115,8 +115,8 @@ export default function CodeEditor({ language={language} value={value} onChange={(v) => { - setCurrent(v) - if (onChange) onChange(v) + setCurrent(v ?? '') + if (onChange) onChange(v ?? '') }} options={mergedOptions} theme={theme.mode === 'light' ? 'plural-light' : 'plural-dark'} diff --git a/assets/design-system/src/components/ComboBox.tsx b/assets/design-system/src/components/ComboBox.tsx index 0bb8df94b8..ec09941389 100644 --- a/assets/design-system/src/components/ComboBox.tsx +++ b/assets/design-system/src/components/ComboBox.tsx @@ -105,20 +105,22 @@ const OpenButtonSC = styled.div(({ theme }) => ({ function OpenButton({ buttonRef, - buttonProps, + buttonProps = {}, ...props }: HTMLAttributes & { - buttonRef: RefObject - buttonProps: AriaButtonProps + buttonRef?: RefObject + buttonProps?: AriaButtonProps }) { + const fallbackRef = useRef(null) + const resolvedButtonRef = buttonRef ?? fallbackRef const { buttonProps: useButtonProps } = useButton( { ...buttonProps, elementType: 'div' }, - buttonRef + resolvedButtonRef ) return ( @@ -237,7 +239,7 @@ function ComboBox({ containerProps, ...props }: ComboBoxProps) { - const nextFocusedKeyRef = useRef(null) + const nextFocusedKeyRef = useRef(null) const stateRef = useRef | null>(null) const [isOpenUncontrolled, setIsOpenUncontrolled] = useState(false) const previousInputValue = useRef(inputValue) @@ -246,7 +248,7 @@ function ComboBox({ isOpen = isOpenUncontrolled } - const wrappedOnOpenChange: typeof onOpenChange = useCallback( + const wrappedOnOpenChange: NonNullable = useCallback( (nextIsOpen, menuTrigger) => { setIsOpenUncontrolled(nextIsOpen) if (nextIsOpen !== isOpen) { @@ -263,40 +265,38 @@ function ComboBox({ [wrappedOnOpenChange] ) - const wrappedOnSelectionChange: typeof onSelectionChange = useCallback( - (newKey, ...args) => { - if (onSelectionChange) { - onSelectionChange.apply(this, [ - typeof newKey === 'string' ? newKey : '', - ...args, - ]) - setIsOpen(false) - } - }, - [onSelectionChange, setIsOpen] - ) + const wrappedOnSelectionChange: NonNullable = + useCallback( + (newKey) => { + if (onSelectionChange) { + onSelectionChange(typeof newKey === 'string' ? newKey : '') + setIsOpen(false) + } + }, + [onSelectionChange, setIsOpen] + ) - const wrappedOnFocusChange: typeof onFocusChange = useCallback( - (isFocused, ...args) => { + const wrappedOnFocusChange: NonNullable = useCallback( + (isFocused) => { // Enforce open on focus if (isFocused && !isOpen) { setIsOpen(true) } if (onFocusChange) { - onFocusChange(isFocused, ...args) + onFocusChange(isFocused) } }, [isOpen, onFocusChange, setIsOpen] ) - const wrappedOnInputChange: typeof onInputChange = useCallback( - (input, ...args) => { + const wrappedOnInputChange: NonNullable = useCallback( + (input) => { if (input !== previousInputValue.current) { previousInputValue.current = input setIsOpen(true) } if (onInputChange) { - onInputChange(input, ...args) + onInputChange(input) } }, [onInputChange, setIsOpen] @@ -341,16 +341,16 @@ function ComboBox({ } }, [state, isOpen]) - const buttonRef = useRef(null) - const inputRef = useRef(null) - const inputInnerRef = useRef(null) - const listBoxRef = useRef(null) - const popoverRef = useRef(null) + const buttonRef = useRef(null) + const triggerElRef = useRef(null) + const inputInnerRef = useRef(null) + const listBoxRef = useRef(null) + const popoverRef = useRef(null) const { buttonProps, inputProps, listBoxProps } = useComboBox( { ...comboStateProps, - inputRef, + inputRef: inputInnerRef, buttonRef, listBoxRef, popoverRef, @@ -363,7 +363,7 @@ function ComboBox({ } const { floating, triggerRef } = useFloatingDropdown({ - triggerRef: inputRef, + triggerRef: triggerElRef, width, maxHeight, placement, @@ -390,7 +390,7 @@ function ComboBox({ if (nextChipClose instanceof HTMLElement) { nextChipClose.focus?.() } else { - inputRef.current?.querySelector('input')?.focus?.() + inputInnerRef.current?.focus?.() } } @@ -424,20 +424,24 @@ function ComboBox({ ) { const chip = document.activeElement?.closest(`[${CHIP_ATTR_KEY}]`) + if (!chip) return + if (dir === 1) { if (!chip.nextElementSibling) { inputInnerRef.current?.focus() } else { - chip?.nextElementSibling - ?.querySelector(`[${CHIP_CLOSE_ATTR_KEY}]`) - // @ts-ignore - ?.focus?.() + ;( + chip.nextElementSibling.querySelector( + `[${CHIP_CLOSE_ATTR_KEY}]` + ) as HTMLElement | null + )?.focus?.() } } else if (dir === -1) { - chip.previousElementSibling - ?.querySelector(`[${CHIP_CLOSE_ATTR_KEY}]`) - // @ts-ignore - ?.focus?.() + ;( + chip.previousElementSibling?.querySelector( + `[${CHIP_CLOSE_ATTR_KEY}]` + ) as HTMLElement | null + )?.focus?.() } } }, []) @@ -452,7 +456,7 @@ function ComboBox({ ref={chipListRef} onKeyDown={handleKeyDown} > - {chips.map((chipProps) => ( + {chips?.map((chipProps) => ( ({ function EmptyState({ message, description, - icon = null, + icon, children, ...props }: EmptyStateProps) { diff --git a/assets/design-system/src/components/Flex.tsx b/assets/design-system/src/components/Flex.tsx index 448412519c..4bfc9afa9f 100644 --- a/assets/design-system/src/components/Flex.tsx +++ b/assets/design-system/src/components/Flex.tsx @@ -72,7 +72,7 @@ function BaseFlex({ return ( } + wrapper={} > children.split(/\r?\n/), [children]) useLayoutEffect(() => { - if (hljs.getLanguage(language) && codeRef.current) { + if (language && hljs.getLanguage(language) && codeRef.current) { delete codeRef.current.dataset.highlighted hljs.highlightElement(codeRef.current) } diff --git a/assets/design-system/src/components/IconFrame.tsx b/assets/design-system/src/components/IconFrame.tsx index e7af163958..c68bab9824 100644 --- a/assets/design-system/src/components/IconFrame.tsx +++ b/assets/design-system/src/components/IconFrame.tsx @@ -15,7 +15,7 @@ import Tooltip, { type TooltipProps } from './Tooltip' type Size = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' type Type = 'secondary' | 'tertiary' | 'floating' -function typeToBG(theme: DefaultTheme): Record { +function typeToBG(theme: DefaultTheme): Record { return { secondary: 'transparent', tertiary: 'transparent', @@ -26,7 +26,7 @@ function typeToBG(theme: DefaultTheme): Record { } } -function typeToHoverBG(theme: DefaultTheme): Record { +function typeToHoverBG(theme: DefaultTheme): Record { return { secondary: theme.colors['action-input-hover'], tertiary: theme.colors['action-input-hover'], @@ -37,7 +37,9 @@ function typeToHoverBG(theme: DefaultTheme): Record { } } -function typeToSelectedBG(theme: DefaultTheme): Record { +function typeToSelectedBG( + theme: DefaultTheme +): Record { return { secondary: undefined, tertiary: undefined, @@ -48,7 +50,7 @@ function typeToSelectedBG(theme: DefaultTheme): Record { } } -function typeToFocusBG(theme: DefaultTheme): Record { +function typeToFocusBG(theme: DefaultTheme): Record { return { secondary: undefined, tertiary: undefined, diff --git a/assets/design-system/src/components/Input2.tsx b/assets/design-system/src/components/Input2.tsx index e9aae3822d..d06895c7e3 100644 --- a/assets/design-system/src/components/Input2.tsx +++ b/assets/design-system/src/components/Input2.tsx @@ -211,13 +211,13 @@ const InputAreaSC = styled.div((_) => ({ flex: '1 1', overflowX: 'auto', })) -const InputContentSC = styled.div<{ $padStart: keyof DefaultTheme['spacing'] }>( - ({ theme, $padStart }) => ({ - display: 'flex', - alignSelf: 'stretch', - paddingLeft: theme.spacing[$padStart], - }) -) +const InputContentSC = styled.div<{ + $padStart?: keyof DefaultTheme['spacing'] | null +}>(({ theme, $padStart }) => ({ + display: 'flex', + alignSelf: 'stretch', + ...($padStart ? { paddingLeft: theme.spacing[$padStart] } : {}), +})) function Input2({ ref, @@ -266,7 +266,7 @@ function Input2({ (inputAreaRef.current?.getBoundingClientRect().width ?? 0) if (scrollDiff > 0) { - inputAreaRef.current.scrollTo({ + inputAreaRef.current?.scrollTo({ left: scrollDiff + 1, behavior: 'smooth', }) @@ -293,33 +293,37 @@ function Input2({ const inputPadStart = startIcon ? null : hasStartContent ? 'small' : 'medium' const inputPadEnd = endIcon ? null : hasEndContent ? 'small' : 'medium' - const wrappedOnChange: InputPropsFull['onChange'] = useCallback( + const wrappedOnChange: NonNullable = useCallback( (e) => { onChange?.(e) }, [onChange] ) - const wrappedOnKeyDown: InputPropsFull['onKeyDown'] = useCallback( + const wrappedOnKeyDown: NonNullable = + useCallback( + (e) => { + if (e.key === 'Enter' && typeof onEnter === 'function') { + onEnter?.(e) + } + if (e.key === 'Backspace' && inputRef?.current?.selectionStart === 0) { + onDeleteInputContent?.(e) + } + if (typeof onKeyDown === 'function') { + onKeyDown?.(e) + } + }, + [onDeleteInputContent, onEnter, onKeyDown] + ) + + const outerOnClick: NonNullable = useCallback( (e) => { - if (e.key === 'Enter' && typeof onEnter === 'function') { - onEnter?.(e) - } - if (e.key === 'Backspace' && inputRef?.current?.selectionStart === 0) { - onDeleteInputContent?.(e) - } - if (typeof onKeyDown === 'function') { - onKeyDown?.(e) - } + e.preventDefault() + inputRef?.current?.focus() }, - [onDeleteInputContent, onEnter, onKeyDown] + [] ) - const outerOnClick: InputPropsFull['onClick'] = useCallback((e) => { - e.preventDefault() - inputRef?.current?.focus() - }, []) - return ( & { - selectedKey: Key + selectedKey?: Key | null onSelectionChange: (key: Key) => unknown onHeaderClick?: () => unknown onFooterClick?: () => unknown @@ -109,7 +109,8 @@ function propsToTextValue(props: Record | null | undefined) { } function useItemWrappedChildren( - children: ReactElement | (ReactElement | false)[], + children?: + ReactElement | (ReactElement | false | null | undefined)[] | null, header?: ReactElement, footer?: ReactElement ) { @@ -238,11 +239,8 @@ function ListBoxUnmanaged({ const theme = useTheme() // Get props for the listbox element - let ref = useRef(undefined) - - if (listBoxRef) { - ref = listBoxRef - } + const fallbackRef = useRef(null) + const ref = listBoxRef ?? fallbackRef const { listBoxProps } = useListBox(props, state, ref) return ( @@ -253,7 +251,7 @@ function ListBoxUnmanaged({ > {headerFixed &&
{headerFixed}
} } extendStyle={{ paddingTop: headerFixed ? 0 : theme.spacing.xxxsmall, paddingBottom: footerFixed ? 0 : theme.spacing.xxxsmall, @@ -275,7 +273,7 @@ function ListBoxUnmanaged({ function Option({ item, state }: { item: any; state: ListState }) { // Get props for the option element - const ref = useRef(undefined) + const ref = useRef(null) const { optionProps, isSelected, diff --git a/assets/design-system/src/components/ListBoxItem.tsx b/assets/design-system/src/components/ListBoxItem.tsx index b60405acf4..31e2de32a0 100644 --- a/assets/design-system/src/components/ListBoxItem.tsx +++ b/assets/design-system/src/components/ListBoxItem.tsx @@ -216,7 +216,9 @@ function ListBoxFooterPlus({ leftContent || ( {children || 'Add'} diff --git a/assets/design-system/src/components/LoadingSpinner.tsx b/assets/design-system/src/components/LoadingSpinner.tsx index 965a68d474..6ea394d003 100644 --- a/assets/design-system/src/components/LoadingSpinner.tsx +++ b/assets/design-system/src/components/LoadingSpinner.tsx @@ -142,10 +142,12 @@ function CenteringWrapper({ children }: { children: ReactNode }) { const [windowHeight, setWindowHeight] = useState( window.innerHeight ) - const ref = useRef(undefined) + const ref = useRef(null) const onSizeChange = useCallback(() => { - const nextTop = ref.current.getBoundingClientRect().top + const nextTop = ref.current?.getBoundingClientRect().top + + if (nextTop == null) return if (nextTop !== top) { setTop(nextTop) diff --git a/assets/design-system/src/components/LoopingLogo.tsx b/assets/design-system/src/components/LoopingLogo.tsx index 8eaae2e2c2..88cb7da3b2 100644 --- a/assets/design-system/src/components/LoopingLogo.tsx +++ b/assets/design-system/src/components/LoopingLogo.tsx @@ -14,8 +14,8 @@ export type LoopingLogoProps = ComponentPropsWithRef<'div'> & { scale?: number } -export const scaling = (scale: number): { transform: string } => - scale ? { transform: `scale(${scale})` } : null +export const scaling = (scale?: number): { transform: string } | undefined => + scale ? { transform: `scale(${scale})` } : undefined function LoopingLogo({ ref, diff --git a/assets/design-system/src/components/LoopingLogoAlternative.tsx b/assets/design-system/src/components/LoopingLogoAlternative.tsx index 9a20e019a0..00438ab663 100644 --- a/assets/design-system/src/components/LoopingLogoAlternative.tsx +++ b/assets/design-system/src/components/LoopingLogoAlternative.tsx @@ -14,8 +14,8 @@ export type LoopingLogoAlternativeProps = ComponentPropsWithRef<'div'> & { scale?: number } -export const scaling = (scale: number): { transform: string } => - scale ? { transform: `scale(${scale})` } : null +export const scaling = (scale?: number): { transform: string } | undefined => + scale ? { transform: `scale(${scale})` } : undefined const GradientBackground = styled.div<{ $scale?: number }>` @keyframes rotateGradient { diff --git a/assets/design-system/src/components/Markdown.tsx b/assets/design-system/src/components/Markdown.tsx index 2fc962e0e0..6d589b6bbf 100644 --- a/assets/design-system/src/components/Markdown.tsx +++ b/assets/design-system/src/components/Markdown.tsx @@ -25,7 +25,7 @@ type MarkdownProps = { export const markdownSanitizeSchema: SanitizeOptions = { ...defaultSchema } export function getLastStringChild(children: any, depth = 0): any { - let lastChild = null + let lastChild: string | null = null Children.forEach(children, (child) => { if (typeof child === 'string') { @@ -300,8 +300,8 @@ function MarkdownLink({ ...props }: { href?: string - gitUrl: string - mainBranch: string + gitUrl?: string + mainBranch?: string }) { // Convert local readme hrefs to full path on github if (gitUrl && href && !isExternalUrl(href)) { diff --git a/assets/design-system/src/components/Modal.tsx b/assets/design-system/src/components/Modal.tsx index 6bf9b1c1e2..de1d6d7799 100644 --- a/assets/design-system/src/components/Modal.tsx +++ b/assets/design-system/src/components/Modal.tsx @@ -165,8 +165,8 @@ function Modal({ = 1 || complete ? completeColor : progressColor + (progress ?? 0) >= 1 || complete ? completeColor : progressColor } - right={`${(1 - progress) * 100}%`} + right={`${(1 - (progress ?? 0)) * 100}%`} /> ) } diff --git a/assets/design-system/src/components/Radio.tsx b/assets/design-system/src/components/Radio.tsx index 09527239e7..1bc682d5d0 100644 --- a/assets/design-system/src/components/Radio.tsx +++ b/assets/design-system/src/components/Radio.tsx @@ -9,6 +9,8 @@ import { } from 'react-aria' import styled from 'styled-components' +import { type RadioGroupState } from 'react-stately' + import { RadioContext } from './RadioGroup' const CheckedIcon = memo(({ small }: { small: boolean }) => { @@ -140,17 +142,17 @@ function Radio({ ...props }: RadioProps) { const [checked, setChecked] = useState(defaultChecked || checkedProp) - const state = useContext(RadioContext) || { + const state = (useContext(RadioContext) || { setSelectedValue: () => {}, selectedValue: checkedProp || checked ? value : undefined, - } + }) as RadioGroupState useEffect(() => { setChecked(checkedProp) }, [checkedProp]) const labelId = useId() - const inputRef = useRef(undefined) + const inputRef = useRef(null) const { isFocusVisible, focusProps } = useFocusRing() const { inputProps, isSelected, isDisabled } = useRadio( { @@ -167,7 +169,7 @@ function Radio({ inputRef ) - const icon = isSelected ? : null + const icon = isSelected ? : null return ( diff --git a/assets/design-system/src/components/RadioGroup.tsx b/assets/design-system/src/components/RadioGroup.tsx index dda5ac3aa1..de59354951 100644 --- a/assets/design-system/src/components/RadioGroup.tsx +++ b/assets/design-system/src/components/RadioGroup.tsx @@ -1,9 +1,9 @@ import { type PropsWithChildren, createContext } from 'react' import { Div, type DivProps } from 'honorable' import { type AriaRadioGroupProps, useRadioGroup } from 'react-aria' -import { useRadioGroupState } from 'react-stately' +import { type RadioGroupState, useRadioGroupState } from 'react-stately' -export const RadioContext = createContext(null) +export const RadioContext = createContext(null) type RadioGroupProps = AriaRadioGroupProps & PropsWithChildren diff --git a/assets/design-system/src/components/ReactAriaPopover.tsx b/assets/design-system/src/components/ReactAriaPopover.tsx index 3633594eb7..ea1e8114c9 100644 --- a/assets/design-system/src/components/ReactAriaPopover.tsx +++ b/assets/design-system/src/components/ReactAriaPopover.tsx @@ -10,7 +10,7 @@ type PopoverProps = { } function Popover({ ...props }: PopoverProps) { - const ref = useRef(undefined) + const ref = useRef(null) const { popoverRef = ref, isOpen, onClose, children } = props // Handle events that should cause the popup to close, @@ -31,7 +31,7 @@ function Popover({ ...props }: PopoverProps) { void - inputRef: React.RefObject + inputRef: React.RefObject setValue: (val: string) => void } diff --git a/assets/design-system/src/components/Select.tsx b/assets/design-system/src/components/Select.tsx index 011fa9b27d..9c4e2c9247 100644 --- a/assets/design-system/src/components/Select.tsx +++ b/assets/design-system/src/components/Select.tsx @@ -83,7 +83,10 @@ type TriggerProps = { function Trigger({ buttonElt, isOpen, ...props }: TriggerProps) { const ref = props.buttonRef - const { buttonProps } = useButton(props, ref) + const { buttonProps } = useButton( + props as unknown as Parameters[0], + ref + ) const theme = useTheme() return cloneElement(buttonElt, { @@ -178,7 +181,6 @@ const SelectButtonInner = styled.div<{ marginLeft: theme.spacing.medium, }, '.arrow': { - transition: 'transform 0.1s ease', display: 'flex', marginLeft: theme.spacing.medium, alignItems: 'center', @@ -223,7 +225,7 @@ function SelectButton({ return ( | null>(null) const [isOpenUncontrolled, setIsOpen] = useState(false) - const nextFocusedKeyRef = useRef(null) + const nextFocusedKeyRef = useRef(null) if (typeof isOpen !== 'boolean') { isOpen = isOpenUncontrolled @@ -337,7 +339,7 @@ function Select({ setNextFocusedKey({ nextFocusedKeyRef, state, stateRef }) // Get props for the listbox element - const ref = useRef(undefined) + const ref = useRef(null) const { triggerProps, menuProps } = useSelect(selectStateProps, state, ref) label = label || ' ' diff --git a/assets/design-system/src/components/SelectComboShared.tsx b/assets/design-system/src/components/SelectComboShared.tsx index afa145475d..6dc58b0499 100644 --- a/assets/design-system/src/components/SelectComboShared.tsx +++ b/assets/design-system/src/components/SelectComboShared.tsx @@ -1,12 +1,6 @@ import { type Key, type Selection } from '@react-types/shared' import { isNil } from 'lodash-es' -import { - type Dispatch, - type RefObject, - type SetStateAction, - useCallback, - useRef, -} from 'react' +import { type RefObject, useCallback, useRef } from 'react' import { type ListState } from 'react-stately' import { type ComboBoxProps } from './ComboBox' @@ -25,9 +19,9 @@ type UseSelectComboStatePropsArgs = Pick< | 'onHeaderClick' | 'children' > & { - setIsOpen: Dispatch> + setIsOpen: (open: boolean) => void stateRef: RefObject | null> - nextFocusedKeyRef: RefObject + nextFocusedKeyRef: RefObject } type UseSelectComboStatePropsReturn = Pick< @@ -66,10 +60,10 @@ function useSelectComboStateProps({ } setIsOpen(open) if (onOpenChange) { - onOpenChange.apply(this, [open, ...args]) + onOpenChange(open, ...args) } }, - onSelectionChange: (newKeyOrKeys: Key | Selection, ...args: any) => { + onSelectionChange: (newKeyOrKeys: Key | Selection) => { let newKey: Key = '' if ( @@ -93,11 +87,13 @@ function useSelectComboStateProps({ onFooterClick?.() if (stateRef.current) { nextFocusedKeyRef.current = - stateRef?.current?.collection?.getKeyBefore(FOOTER_KEY) + stateRef.current.collection.getKeyBefore(FOOTER_KEY) ?? null } break default: - onSelectionChange?.apply(this, [newKeyOrKeys, ...args]) + ;( + onSelectionChange as ((key: Key | Selection) => void) | undefined + )?.(newKeyOrKeys) break } }, @@ -110,9 +106,9 @@ const setNextFocusedKey = ({ state, stateRef, }: { - nextFocusedKeyRef: RefObject + nextFocusedKeyRef: RefObject state: ListState - stateRef: RefObject> + stateRef: RefObject | null> }) => { stateRef.current = state diff --git a/assets/design-system/src/components/SelectItem.tsx b/assets/design-system/src/components/SelectItem.tsx index 94aaa0d4c0..9c550d8c06 100644 --- a/assets/design-system/src/components/SelectItem.tsx +++ b/assets/design-system/src/components/SelectItem.tsx @@ -9,6 +9,8 @@ import { } from 'react' import styled from 'styled-components' +import { type RadioGroupState } from 'react-stately' + import { RadioContext } from './RadioGroup' type SelectItemWrapProps = { @@ -59,8 +61,8 @@ function SelectItem({ className, ...props }: SelectItemProps) { - const state = useContext(RadioContext) - const inputRef = useRef(undefined) + const state = useContext(RadioContext) as RadioGroupState + const inputRef = useRef(null) const { isFocusVisible, focusProps } = useFocusRing() const { inputProps, isSelected } = useRadio( { diff --git a/assets/design-system/src/components/Slider.tsx b/assets/design-system/src/components/Slider.tsx index 744416a90f..821785adcd 100644 --- a/assets/design-system/src/components/Slider.tsx +++ b/assets/design-system/src/components/Slider.tsx @@ -202,7 +202,7 @@ function Slider({ return ( diff --git a/assets/design-system/src/components/Spinner.tsx b/assets/design-system/src/components/Spinner.tsx index 1fff817e10..195468b853 100644 --- a/assets/design-system/src/components/Spinner.tsx +++ b/assets/design-system/src/components/Spinner.tsx @@ -40,7 +40,7 @@ const SpinnerSC = styled( ` export function Spinner({ - color, + color = '', size = 16, ...props }: { color?: string; size?: number } & ComponentPropsWithRef<'div'>) { @@ -88,7 +88,7 @@ export function SpinnerAlt({ cx={size / 2} cy={size / 2} r={radius} - stroke={colors[color] ?? colors['icon-info']} + stroke={(color ? colors[color] : undefined) ?? colors['icon-info']} strokeWidth={strokeWidth} fill="none" strokeDasharray={`${circumference * 0.33} ${circumference * 0.8}`} diff --git a/assets/design-system/src/components/Stepper.tsx b/assets/design-system/src/components/Stepper.tsx index cfaf6b8f12..65efb748b6 100644 --- a/assets/design-system/src/components/Stepper.tsx +++ b/assets/design-system/src/components/Stepper.tsx @@ -79,7 +79,7 @@ function Stepper({ forceCollapse = false, compact = false, }: StepperProps) { - const eltRef = useRef(undefined) + const eltRef = useRef(null) const mergedRef = mergeRefs(ref, eltRef) const [collapseTitles, setCollapseTitles] = useState(true) @@ -90,7 +90,7 @@ function Stepper({ return } setCollapseTitles( - forceCollapse || eltRef?.current?.clientWidth < collapseAtWidth + !!forceCollapse || (eltRef.current?.clientWidth ?? 0) < collapseAtWidth ) }, [forceCollapse, eltRef, collapseAtWidth, vertical]) diff --git a/assets/design-system/src/components/SubTab.tsx b/assets/design-system/src/components/SubTab.tsx index 921caf14aa..586beae7e1 100644 --- a/assets/design-system/src/components/SubTab.tsx +++ b/assets/design-system/src/components/SubTab.tsx @@ -91,8 +91,8 @@ function SubTab({ diff --git a/assets/design-system/src/components/Switch.tsx b/assets/design-system/src/components/Switch.tsx index 8f33db8840..f2f10bfda0 100644 --- a/assets/design-system/src/components/Switch.tsx +++ b/assets/design-system/src/components/Switch.tsx @@ -152,7 +152,7 @@ export const useSwitch = ({ $disabled: isDisabled, $checked: isSelected, $readOnly: isReadOnly, - $variant: variant, + $variant: variant ?? 'default', }, state, }), diff --git a/assets/design-system/src/components/Tab.tsx b/assets/design-system/src/components/Tab.tsx index 23b1e85121..4f8dd72f9b 100644 --- a/assets/design-system/src/components/Tab.tsx +++ b/assets/design-system/src/components/Tab.tsx @@ -42,7 +42,9 @@ function Tab({ userSelect="none" cursor="pointer" borderBottom={ - vertical ? null : `1px solid ${active ? 'border-primary' : 'border'}` + vertical + ? undefined + : `1px solid ${active ? 'border-primary' : 'border'}` } borderRight={ vertical @@ -53,7 +55,7 @@ function Tab({ ? 'border-fill-two' : 'border' }` - : null + : undefined } {...borderRadiuses} _focusVisible={{ @@ -70,7 +72,7 @@ function Tab({ align="center" borderBottom={ vertical - ? null + ? undefined : `${TAB_INDICATOR_THICKNESS - 1}px solid ${ active ? theme.colors['border-primary'] : 'transparent' }` @@ -84,7 +86,7 @@ function Tab({ ? theme.colors['border-fill-two'] : 'transparent' }` - : null + : undefined } {...borderRadiuses} color={ diff --git a/assets/design-system/src/components/TabList.tsx b/assets/design-system/src/components/TabList.tsx index c9ab38740f..f10861c047 100644 --- a/assets/design-system/src/components/TabList.tsx +++ b/assets/design-system/src/components/TabList.tsx @@ -91,13 +91,15 @@ function TabList({ const state = useTabListState(finalStateProps) stateRef.current = { - updateTabPanel: () => { - console.warn("TabPanel didn't set stateRef.current.updateTabPanel") - }, ...(stateRef.current || {}), state, stateProps: finalStateProps, tabProps: stateRef?.current?.tabProps || {}, + updateTabPanel: + stateRef.current?.updateTabPanel ?? + (() => { + console.warn("TabPanel didn't set stateRef.current.updateTabPanel") + }), } useEffect(() => { stateRef?.current?.updateTabPanel() @@ -135,15 +137,17 @@ function TabList({ return ( } > {props.children} diff --git a/assets/design-system/src/components/TagMultiSelect.tsx b/assets/design-system/src/components/TagMultiSelect.tsx index 0ca996f30e..0a271c7f17 100644 --- a/assets/design-system/src/components/TagMultiSelect.tsx +++ b/assets/design-system/src/components/TagMultiSelect.tsx @@ -85,14 +85,16 @@ function TagMultiSelectUnstyled({