@heliosgraphics/ui 2.0.0-alpha.77 → 2.0.0-alpha.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import type { FC } from "react"
6
6
  import type { ClockProps } from "./Clock.types"
7
7
 
8
8
  export const Clock: FC<ClockProps> = () => {
9
- const [time, setTime] = useState(new Date())
9
+ const [time, setTime] = useState(() => new Date())
10
10
 
11
11
  useEffect(() => {
12
12
  const timerId = globalThis.setInterval(() => {
@@ -1,10 +1,10 @@
1
1
  import { getClasses } from "@heliosgraphics/utils/classnames"
2
2
  import { icons } from "@heliosgraphics/icons"
3
3
  import styles from "./Icon.module.css"
4
- import type { FC } from "react"
4
+ import { memo, type FC } from "react"
5
5
  import type { IconProps } from "./Icon.types"
6
6
 
7
- export const Icon: FC<IconProps> = ({ icon, className, emphasis, size }) => {
7
+ export const Icon: FC<IconProps> = memo(({ icon, className, emphasis, size }) => {
8
8
  const iconSizeStyle = {
9
9
  height: size + "px",
10
10
  minHeight: size + "px",
@@ -26,4 +26,4 @@ export const Icon: FC<IconProps> = ({ icon, className, emphasis, size }) => {
26
26
  dangerouslySetInnerHTML={{ __html: icons[icon] || "" }}
27
27
  />
28
28
  )
29
- }
29
+ })
@@ -1,9 +1,9 @@
1
1
  import { getClasses } from "@heliosgraphics/utils/classnames"
2
2
  import styles from "./Loading.module.css"
3
- import type { FC } from "react"
3
+ import { memo, type FC } from "react"
4
4
  import type { LoadingProps } from "./Loading.types"
5
5
 
6
- export const Loading: FC<LoadingProps> = ({ className, size, emphasis }) => {
6
+ export const Loading: FC<LoadingProps> = memo(({ className, size, emphasis }) => {
7
7
  const rSize = size / 2
8
8
  const cSize = rSize + 2
9
9
  const dashSize = size + cSize
@@ -40,4 +40,4 @@ export const Loading: FC<LoadingProps> = ({ className, size, emphasis }) => {
40
40
  />
41
41
  </svg>
42
42
  )
43
- }
43
+ })
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { useEffect, useState, type FC } from "react"
3
+ import { useState, type FC } from "react"
4
4
  import { Flex } from "../../../Flex"
5
5
  import { Icon } from "../../../Icon"
6
6
  import { Text } from "../../../Text"
@@ -8,9 +8,23 @@ import { getClasses } from "@heliosgraphics/utils"
8
8
  import styles from "./MenuCategory.module.css"
9
9
  import type { MenuCategoryProps } from "./MenuCategory.types"
10
10
 
11
- export const MenuCategory: FC<MenuCategoryProps> = ({ category, children, isFolder, isOpen: isIncomingOpen }) => {
12
- const [isOpen, setOpen] = useState<boolean>(!!isIncomingOpen)
13
- const onToggle = (): void => setOpen(!isOpen)
11
+ export const MenuCategory: FC<MenuCategoryProps> = ({
12
+ category,
13
+ children,
14
+ isFolder,
15
+ isOpen: controlledIsOpen,
16
+ isInitiallyClosed,
17
+ }) => {
18
+ const [localIsOpen, setLocalIsOpen] = useState<boolean>(!isInitiallyClosed)
19
+
20
+ const isControlled: boolean = controlledIsOpen !== undefined
21
+ const isOpen: boolean = isControlled ? !!controlledIsOpen : localIsOpen
22
+
23
+ const onToggle = (): void => {
24
+ if (!isControlled) {
25
+ setLocalIsOpen((prev) => !prev)
26
+ }
27
+ }
14
28
 
15
29
  const menuCategoryClasses: string = getClasses(styles.menuCategory, {
16
30
  [styles.menuCategoryFolder]: category,
@@ -28,10 +42,6 @@ export const MenuCategory: FC<MenuCategoryProps> = ({ category, children, isFold
28
42
 
29
43
  const showHeader: boolean = Boolean(category || isFolder)
30
44
 
31
- useEffect(() => {
32
- setOpen(!!isIncomingOpen)
33
- }, [isIncomingOpen])
34
-
35
45
  return (
36
46
  <Flex isColumn={true} isXCentered={true} data-ui-component="Menu.Category" className={menuCategoryClasses}>
37
47
  {showHeader && (
@@ -4,7 +4,7 @@ import { Flex } from "../Flex"
4
4
  import { Text } from "../Text"
5
5
  import { Icon } from "../Icon"
6
6
  import styles from "./Pill.module.css"
7
- import type { FC } from "react"
7
+ import { memo, type FC } from "react"
8
8
  import type { PillProps } from "./Pill.types"
9
9
 
10
10
  const PILL_ICON_SIZE: Record<string, number> = {
@@ -13,43 +13,45 @@ const PILL_ICON_SIZE: Record<string, number> = {
13
13
  normal: 24,
14
14
  }
15
15
 
16
- export const Pill: FC<PillProps> = ({
17
- appearance = "light",
18
- color = "gray",
19
- className,
20
- icon,
21
- isLabelHidden,
22
- isMono,
23
- isRounded,
24
- label,
25
- onClick,
26
- size = "normal",
27
- }) => {
28
- const pillColorClasses: Array<string> = getColorClasses(color, appearance)
29
- const pillClasses = getClasses(styles.pill, "non-selectable break-word", ...pillColorClasses, className, {
30
- [styles.pillRounded]: isRounded,
31
- [`radius-md`]: !isRounded,
16
+ export const Pill: FC<PillProps> = memo(
17
+ ({
18
+ appearance = "light",
19
+ color = "gray",
20
+ className,
21
+ icon,
22
+ isLabelHidden,
23
+ isMono,
24
+ isRounded,
25
+ label,
26
+ onClick,
27
+ size = "normal",
28
+ }) => {
29
+ const pillColorClasses: Array<string> = getColorClasses(color, appearance)
30
+ const pillClasses = getClasses(styles.pill, "non-selectable break-word", ...pillColorClasses, className, {
31
+ [styles.pillRounded]: isRounded,
32
+ [`radius-md`]: !isRounded,
32
33
 
33
- [styles.pillLight]: appearance === "light",
34
+ [styles.pillLight]: appearance === "light",
34
35
 
35
- [styles.pillNormal]: !size || size === "normal",
36
- [styles.pillSmall]: size === "small",
37
- [styles.pillTiny]: size === "tiny",
36
+ [styles.pillNormal]: !size || size === "normal",
37
+ [styles.pillSmall]: size === "small",
38
+ [styles.pillTiny]: size === "tiny",
38
39
 
39
- [styles.pillIconOnly]: !!icon && isLabelHidden,
40
- })
40
+ [styles.pillIconOnly]: !!icon && isLabelHidden,
41
+ })
41
42
 
42
- const isSmall: boolean = size !== "normal"
43
- const pillTextSize = isSmall ? "tiny" : "small"
43
+ const isSmall: boolean = size !== "normal"
44
+ const pillTextSize = isSmall ? "tiny" : "small"
44
45
 
45
- return (
46
- <Flex {...(onClick && { onClick })} className={pillClasses} isCentered={true} gap={2} data-ui-component="Pill">
47
- {icon && <Icon size={PILL_ICON_SIZE[size] || 16} icon={icon} />}
48
- {!isLabelHidden && (
49
- <Text type={pillTextSize} whiteSpace="nowrap" {...(isMono && { fontFamily: "mono" })} fontWeight="medium">
50
- {label}
51
- </Text>
52
- )}
53
- </Flex>
54
- )
55
- }
46
+ return (
47
+ <Flex {...(onClick && { onClick })} className={pillClasses} isCentered={true} gap={2} data-ui-component="Pill">
48
+ {icon && <Icon size={PILL_ICON_SIZE[size] || 16} icon={icon} />}
49
+ {!isLabelHidden && (
50
+ <Text type={pillTextSize} whiteSpace="nowrap" {...(isMono && { fontFamily: "mono" })} fontWeight="medium">
51
+ {label}
52
+ </Text>
53
+ )}
54
+ </Flex>
55
+ )
56
+ },
57
+ )
@@ -3,32 +3,27 @@ import { VerticalSeparator } from "./components/VerticalSeparator"
3
3
  import styles from "./Separator.module.css"
4
4
  import { getClasses } from "@heliosgraphics/utils"
5
5
  import type { SeparatorProps } from "./Separator.types"
6
- import type { FC } from "react"
6
+ import { memo, type FC } from "react"
7
7
 
8
- export const Separator: FC<SeparatorProps> = ({
9
- className,
10
- emphasis = "primary",
11
- isVertical,
12
- height,
13
- lineStyle = "solid",
14
- width,
15
- }) => {
16
- const separatorClasses: string = getClasses(className, {
17
- [styles.separatorPrimary]: emphasis === "primary",
18
- [styles.separatorSecondary]: emphasis === "secondary",
19
- [styles.separatorTertiary]: emphasis === "tertiary",
20
- })
8
+ export const Separator: FC<SeparatorProps> = memo(
9
+ ({ className, emphasis = "primary", isVertical, height, lineStyle = "solid", width }) => {
10
+ const separatorClasses: string = getClasses(className, {
11
+ [styles.separatorPrimary]: emphasis === "primary",
12
+ [styles.separatorSecondary]: emphasis === "secondary",
13
+ [styles.separatorTertiary]: emphasis === "tertiary",
14
+ })
21
15
 
22
- const separatorProps: SeparatorProps = {
23
- className: separatorClasses,
24
- emphasis,
25
- ...(isVertical !== undefined && { isVertical }),
26
- ...(height !== undefined && { height }),
27
- lineStyle,
28
- ...(width !== undefined && { width }),
29
- }
16
+ const separatorProps: SeparatorProps = {
17
+ className: separatorClasses,
18
+ emphasis,
19
+ ...(isVertical !== undefined && { isVertical }),
20
+ ...(height !== undefined && { height }),
21
+ lineStyle,
22
+ ...(width !== undefined && { width }),
23
+ }
30
24
 
31
- if (isVertical) return <VerticalSeparator {...separatorProps} />
25
+ if (isVertical) return <VerticalSeparator {...separatorProps} />
32
26
 
33
- return <HorizontalSeparator {...separatorProps} />
34
- }
27
+ return <HorizontalSeparator {...separatorProps} />
28
+ },
29
+ )
@@ -1,6 +1,6 @@
1
- import type { FC } from "react"
1
+ import { memo, type FC } from "react"
2
2
  import type { SpacerProps } from "./Spacer.types"
3
3
 
4
- export const Spacer: FC<SpacerProps> = ({ gap }) => {
4
+ export const Spacer: FC<SpacerProps> = memo(({ gap }) => {
5
5
  return <div style={{ height: `${gap ?? 0}px` }} />
6
- }
6
+ })
@@ -4,12 +4,12 @@ import { getTypographyUtility } from "./Text.utils"
4
4
  import { P } from "./components/P"
5
5
  import { Small } from "./components/Small"
6
6
  import { Tiny } from "./components/Tiny"
7
- import { type FC } from "react"
7
+ import { memo, type FC } from "react"
8
8
  import styles from "./Text.module.css"
9
9
  import type { TextProps } from "./Text.types"
10
10
  import { Micro } from "./components/Micro"
11
11
 
12
- export const Text: FC<TextProps> = (props) => {
12
+ export const Text: FC<TextProps> = memo((props) => {
13
13
  const textClasses: string = getClasses(props.className, styles.text, {
14
14
  [styles.textPrimary]: props.emphasis === "primary",
15
15
  [styles.textSecondary]: props.emphasis === "secondary",
@@ -51,4 +51,4 @@ export const Text: FC<TextProps> = (props) => {
51
51
  default:
52
52
  return <Div {...baseTextProps} />
53
53
  }
54
- }
54
+ })
@@ -19,6 +19,7 @@ const LayoutProvider: FC<LayoutProviderProps> = ({ children, breakpoint = 960 })
19
19
  const [hasMounted, setHasMounted] = useState<boolean>(false)
20
20
  const asideRef = useRef<HTMLElement | null>(null)
21
21
  const isMenuVisibleRef = useRef<boolean>(false)
22
+ const windowWidthRef = useRef<number>(0)
22
23
 
23
24
  useEffect(() => {
24
25
  isMenuVisibleRef.current = isMenuVisible
@@ -29,6 +30,7 @@ const LayoutProvider: FC<LayoutProviderProps> = ({ children, breakpoint = 960 })
29
30
 
30
31
  setHasMounted(true)
31
32
  setWindowWidth(initialWidth)
33
+ windowWidthRef.current = initialWidth
32
34
 
33
35
  const initialMenuVisible = initialWidth >= breakpoint
34
36
 
@@ -37,10 +39,11 @@ const LayoutProvider: FC<LayoutProviderProps> = ({ children, breakpoint = 960 })
37
39
 
38
40
  const handleResize = (): void => {
39
41
  const newWidth: number = globalThis.innerWidth
40
- const wasWideEnough: boolean = windowWidth >= breakpoint
42
+ const wasWideEnough: boolean = windowWidthRef.current >= breakpoint
41
43
  const isNowWideEnough: boolean = newWidth >= breakpoint
42
44
 
43
45
  setWindowWidth(newWidth)
46
+ windowWidthRef.current = newWidth
44
47
 
45
48
  if (isNowWideEnough && !wasWideEnough) {
46
49
  setIsMenuVisible(true)
@@ -71,7 +74,7 @@ const LayoutProvider: FC<LayoutProviderProps> = ({ children, breakpoint = 960 })
71
74
  globalThis.removeEventListener("resize", handleResize)
72
75
  globalThis.document.removeEventListener("mousedown", handleClickOutside)
73
76
  }
74
- }, [breakpoint, windowWidth])
77
+ }, [breakpoint])
75
78
 
76
79
  const isWideEnough: boolean = hasMounted ? windowWidth >= breakpoint : false
77
80
  const shouldShowNavigation: boolean = isWideEnough || isMenuVisible
@@ -19,11 +19,11 @@ export const useChatScroll = (
19
19
  onResetManual: () => void
20
20
  } => {
21
21
  const [isManual, setManual] = useState<boolean>(false)
22
- const [scrollPos, setScrollPos] = useState<ScrollPosition>({
22
+ const [scrollPos, setScrollPos] = useState<ScrollPosition>(() => ({
23
23
  top: 0,
24
24
  bottom: 0,
25
25
  height: 0,
26
- })
26
+ }))
27
27
  const ref = useRef<HTMLDivElement>(null)
28
28
  const wasAtBottom = useRef<boolean>(true)
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heliosgraphics/ui",
3
- "version": "2.0.0-alpha.77",
3
+ "version": "2.0.0-alpha.78",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "author": "Chris Puska <chris@puska.org>",
@@ -37,23 +37,23 @@
37
37
  "react-plock": "^3.6.1"
38
38
  },
39
39
  "devDependencies": {
40
- "@testing-library/react": "^16.3.1",
40
+ "@testing-library/react": "^16.3.2",
41
41
  "@types/node": "^25",
42
- "@typescript-eslint/eslint-plugin": "^8.50.1",
42
+ "@typescript-eslint/eslint-plugin": "^8.53.1",
43
43
  "@vitejs/plugin-react": "^5.1.2",
44
- "@vitest/coverage-v8": "^4.0.16",
44
+ "@vitest/coverage-v8": "^4.0.18",
45
45
  "esbuild": "latest",
46
46
  "esbuild-css-modules-plugin": "latest",
47
47
  "eslint": "^9.39.2",
48
48
  "eslint-config-prettier": "^10.1.8",
49
- "eslint-plugin-prettier": "^5.5.4",
49
+ "eslint-plugin-prettier": "^5.5.5",
50
50
  "glob": "latest",
51
- "jsdom": "^27.3.0",
52
- "next": "^16.1.1",
53
- "prettier": "^3.7.4",
51
+ "jsdom": "^27.4.0",
52
+ "next": "^16.1.4",
53
+ "prettier": "^3.8.1",
54
54
  "typescript": "^5.9.3",
55
- "vite": "^7.3.0",
56
- "vitest": "^4.0.16"
55
+ "vite": "^7.3.1",
56
+ "vitest": "^4.0.18"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "@types/react": "^19",