diff --git a/.storybook-vue/main.ts b/.storybook-vue/main.ts new file mode 100644 index 00000000..f9fc3473 --- /dev/null +++ b/.storybook-vue/main.ts @@ -0,0 +1,27 @@ +import type { StorybookConfig } from '@storybook/vue3-vite'; +import vue from '@vitejs/plugin-vue'; + +const config: StorybookConfig = { + stories: [ + '../packages/@necto-vue/**/src/**/*.stories.@(js|jsx|mjs|ts|tsx)', + ], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + ], + framework: { + name: '@storybook/vue3-vite', + options: {}, + }, + core: { + disableTelemetry: true, + }, + viteFinal: async (config) => { + config.plugins = config.plugins || []; + config.plugins.push(vue()); + return config; + }, +}; + +export default config; diff --git a/.storybook-vue/preview.ts b/.storybook-vue/preview.ts new file mode 100644 index 00000000..cb0bffb4 --- /dev/null +++ b/.storybook-vue/preview.ts @@ -0,0 +1,15 @@ +import type { Preview } from '@storybook/vue3'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + layout: 'centered', + }, +}; + +export default preview; diff --git a/biome.json b/biome.json index b698a2dd..3b7dfa5f 100644 --- a/biome.json +++ b/biome.json @@ -28,6 +28,18 @@ "recommended": true } }, + "overrides": [ + { + "includes": ["**/*.vue"], + "linter": { + "rules": { + "correctness": { + "useHookAtTopLevel": "off" + } + } + } + } + ], "javascript": { "formatter": { "semicolons": "always", diff --git a/package.json b/package.json index 4e77c28d..13fd12f0 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,12 @@ "version": "0.0.0", "scripts": { "dev": "turbo run dev", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", + "storybook": "concurrently \"pnpm storybook:react\" \"pnpm storybook:vue\"", + "storybook:react": "storybook dev -p 6006", + "storybook:vue": "storybook dev -p 6007 -c .storybook-vue", + "build-storybook": "concurrently \"pnpm build-storybook:react\" \"pnpm build-storybook:vue\"", + "build-storybook:react": "storybook build", + "build-storybook:vue": "storybook build -c .storybook-vue", "changeset": "changeset", "test": "turbo run test", "test:visual": "turbo run test:visual", @@ -32,7 +36,11 @@ "@storybook/react": "^8.5.0", "@storybook/react-vite": "^8.5.0", "@storybook/test": "^8.5.0", + "@storybook/vue3": "^8.5.0", + "@storybook/vue3-vite": "^8.5.0", + "@vitejs/plugin-vue": "^5.2.1", "commitlint": "^19.8.1", + "concurrently": "^9.2.1", "lefthook": "^1.11.13", "markdownlint": "^0.38.0", "react": "^19.1.0", @@ -40,7 +48,8 @@ "storybook": "^8.5.0", "ts-node": "^10.9.2", "turbo": "^2.4.4", - "vitest": "^3.0.9" + "vitest": "^3.0.9", + "vue": "^3.5.13" }, "packageManager": "pnpm@9.15.3" } diff --git a/packages/@necto-react/necto-react-hooks/src/useAriaProps/index.ts b/packages/@necto-react/necto-react-hooks/src/useAriaProps/index.ts index cfc43a15..7306615f 100644 --- a/packages/@necto-react/necto-react-hooks/src/useAriaProps/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useAriaProps/index.ts @@ -9,6 +9,6 @@ export { useAriaProps } from './useAriaProps'; export type { - UseAriaPropsProps, + UseAriaPropsOptions, UseAriaPropsReturn } from './useAriaProps.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useAriaProps/useAriaProps.ts b/packages/@necto-react/necto-react-hooks/src/useAriaProps/useAriaProps.ts index 0e897515..8de5eeb6 100644 --- a/packages/@necto-react/necto-react-hooks/src/useAriaProps/useAriaProps.ts +++ b/packages/@necto-react/necto-react-hooks/src/useAriaProps/useAriaProps.ts @@ -13,7 +13,7 @@ import { useMemo } from 'react'; import { AriaProps } from '@necto/dom'; import type { - UseAriaPropsProps, + UseAriaPropsOptions, UseAriaPropsReturn } from './useAriaProps.types'; import type { AriaAttributes } from 'react'; @@ -22,11 +22,11 @@ import type { AriaAttributes } from 'react'; * Returns ARIA attributes based on the provided state flags. * Automatically filters out undefined values to avoid adding empty ARIA attributes. * - * @param {UseAriaPropsProps} props - State flags to convert to ARIA attributes. + * @param {UseAriaPropsOptions} options - State flags to convert to ARIA attributes. * @returns {UseAriaPropsReturn} The ARIA attributes object with only defined values. */ export function useAriaProps( - props: UseAriaPropsProps = {} + options: UseAriaPropsOptions = {} ): UseAriaPropsReturn { const { isInvalid, @@ -44,7 +44,7 @@ export function useAriaProps( valueMin, valueMax, valueText - } = props; + } = options; return useMemo((): AriaAttributes => { const ariaAttributes: Record = {}; diff --git a/packages/@necto-react/necto-react-hooks/src/useAriaProps/useAriaProps.types.ts b/packages/@necto-react/necto-react-hooks/src/useAriaProps/useAriaProps.types.ts index 47e7a5fa..b02cacd0 100644 --- a/packages/@necto-react/necto-react-hooks/src/useAriaProps/useAriaProps.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useAriaProps/useAriaProps.types.ts @@ -9,9 +9,9 @@ import type { AriaAttributes } from 'react'; /** - * Props for the useAriaProps hook. + * Options for the useAriaProps hook. */ -export interface UseAriaPropsProps { +export interface UseAriaPropsOptions { /** Whether the element is invalid. */ isInvalid?: boolean; diff --git a/packages/@necto-react/necto-react-hooks/src/useContextProps/index.ts b/packages/@necto-react/necto-react-hooks/src/useContextProps/index.ts index 894aea25..c9519c00 100644 --- a/packages/@necto-react/necto-react-hooks/src/useContextProps/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useContextProps/index.ts @@ -9,6 +9,6 @@ export { useContextProps } from './useContextProps'; export type { - UseContextPropsProps, + UseContextPropsOptions, UseContextPropsReturn } from './useContextProps.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useContextProps/useContextProps.ts b/packages/@necto-react/necto-react-hooks/src/useContextProps/useContextProps.ts index fa7bc1a3..145fd2ae 100644 --- a/packages/@necto-react/necto-react-hooks/src/useContextProps/useContextProps.ts +++ b/packages/@necto-react/necto-react-hooks/src/useContextProps/useContextProps.ts @@ -20,7 +20,7 @@ import { mergeProps } from '@necto/mergers'; import { useSlottedContext } from '@necto-react/hooks'; import type { - UseContextPropsProps, + UseContextPropsOptions, UseContextPropsReturn } from './useContextProps.types'; import type { RefObject } from 'react'; @@ -30,14 +30,14 @@ import type { RefObject } from 'react'; * * @template T The props type. * @template E The element type. - * @param {UseContextPropsProps} params - Component props, ref, and context. + * @param {UseContextPropsOptions} options - Component props, ref, and context. * @returns {UseContextPropsReturn} A tuple of merged props and merged ref. */ export function useContextProps({ props, ref, context -}: UseContextPropsProps): UseContextPropsReturn { +}: UseContextPropsOptions): UseContextPropsReturn { const ctx = useSlottedContext({ context, slot: props.slot }) || {}; const { ref: contextRef = null, ...contextProps } = ctx as { diff --git a/packages/@necto-react/necto-react-hooks/src/useContextProps/useContextProps.types.ts b/packages/@necto-react/necto-react-hooks/src/useContextProps/useContextProps.types.ts index 61eba8d6..9e7cc3c7 100644 --- a/packages/@necto-react/necto-react-hooks/src/useContextProps/useContextProps.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useContextProps/useContextProps.types.ts @@ -9,9 +9,9 @@ import type { ForwardedRef, Context } from 'react'; /** - * Props for the useContextProps hook. + * Options for the useContextProps hook. */ -export interface UseContextPropsProps { +export interface UseContextPropsOptions { /** * Component props, must include optional slot property and optional ref. */ diff --git a/packages/@necto-react/necto-react-hooks/src/useDisabled/index.ts b/packages/@necto-react/necto-react-hooks/src/useDisabled/index.ts index 4dfaaf24..f8ad0acd 100644 --- a/packages/@necto-react/necto-react-hooks/src/useDisabled/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useDisabled/index.ts @@ -9,6 +9,6 @@ export { useDisabled } from './useDisabled'; export type { - UseDisabledProps, - UseDisabledPropsReturn + UseDisabledOptions, + UseDisabledReturn } from './useDisabled.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useDisabled/useDisabled.ts b/packages/@necto-react/necto-react-hooks/src/useDisabled/useDisabled.ts index e61eeffd..d4779b7d 100644 --- a/packages/@necto-react/necto-react-hooks/src/useDisabled/useDisabled.ts +++ b/packages/@necto-react/necto-react-hooks/src/useDisabled/useDisabled.ts @@ -10,21 +10,20 @@ import { useContext } from 'react'; import { DisabledContext } from '@necto-react/contexts'; import type { - UseDisabledProps, - UseDisabledPropsReturn + UseDisabledOptions, + UseDisabledReturn } from './useDisabled.types'; /** * React hook to determine if a specific feature or component type is disabled. * - * @param {keyof DisabledFlags} type - The key of the disabled flag to check. Defaults to 'general'. - * @param {boolean} defaultFallback - The fallback value if the flag is not set. Defaults to false. + * @param {UseDisabledOptions} options - Options for the hook. * @returns {boolean} True if the specified type is disabled, otherwise the fallback value. */ export function useDisabled( - props: UseDisabledProps = {} -): UseDisabledPropsReturn { - const { type = 'general', defaultFallback = false } = props; + options: UseDisabledOptions = {} +): UseDisabledReturn { + const { type = 'general', defaultFallback = false } = options; const flags = useContext(DisabledContext) || {}; return flags[type] ?? defaultFallback; } diff --git a/packages/@necto-react/necto-react-hooks/src/useDisabled/useDisabled.types.ts b/packages/@necto-react/necto-react-hooks/src/useDisabled/useDisabled.types.ts index 2b5947b9..1f0d4562 100644 --- a/packages/@necto-react/necto-react-hooks/src/useDisabled/useDisabled.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useDisabled/useDisabled.types.ts @@ -8,9 +8,9 @@ import type { DisabledFlags } from '@necto-react/types'; /** - * Props for the useDisabled hook. + * Options for the useDisabled hook. */ -export interface UseDisabledProps { +export interface UseDisabledOptions { /** The key of the disabled flag to check. Defaults to 'general'. */ type?: keyof DisabledFlags; @@ -21,4 +21,4 @@ export interface UseDisabledProps { /** * Return type for the useDisabled hook. */ -export type UseDisabledPropsReturn = boolean; +export type UseDisabledReturn = boolean; diff --git a/packages/@necto-react/necto-react-hooks/src/useDisabledProps/index.ts b/packages/@necto-react/necto-react-hooks/src/useDisabledProps/index.ts index dda390cc..fca38f5d 100644 --- a/packages/@necto-react/necto-react-hooks/src/useDisabledProps/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useDisabledProps/index.ts @@ -9,6 +9,6 @@ export { useDisabledProps } from './useDisabledProps'; export type { - UseDisabledPropsProps, - UseDisabledPropsPropsReturn + UseDisabledPropsOptions, + UseDisabledPropsReturn } from './useDisabledProps.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useDisabledProps/useDisabledProps.ts b/packages/@necto-react/necto-react-hooks/src/useDisabledProps/useDisabledProps.ts index bc6d2625..ba58cf86 100644 --- a/packages/@necto-react/necto-react-hooks/src/useDisabledProps/useDisabledProps.ts +++ b/packages/@necto-react/necto-react-hooks/src/useDisabledProps/useDisabledProps.ts @@ -10,8 +10,8 @@ import { useMemo } from 'react'; import { useDisabled } from '@necto-react/hooks'; import type { - UseDisabledPropsProps, - UseDisabledPropsPropsReturn + UseDisabledPropsOptions, + UseDisabledPropsReturn } from './useDisabledProps.types'; import type { HTMLAttributes } from 'react'; @@ -22,9 +22,9 @@ import type { HTMLAttributes } from 'react'; * @returns {HTMLAttributes} The merged props including disabled and aria-disabled if applicable. */ export function useDisabledProps( - props: UseDisabledPropsProps = {} -): UseDisabledPropsPropsReturn { - const { type = 'general', extraProps = {} } = props; + options: UseDisabledPropsOptions = {} +): UseDisabledPropsReturn { + const { type = 'general', extraProps = {} } = options; const isDisabled = useDisabled({ type, defaultFallback: false }); return useMemo(() => { diff --git a/packages/@necto-react/necto-react-hooks/src/useDisabledProps/useDisabledProps.types.ts b/packages/@necto-react/necto-react-hooks/src/useDisabledProps/useDisabledProps.types.ts index e58932eb..a5434811 100644 --- a/packages/@necto-react/necto-react-hooks/src/useDisabledProps/useDisabledProps.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useDisabledProps/useDisabledProps.types.ts @@ -10,9 +10,9 @@ import type { HTMLAttributes } from 'react'; import type { DisabledFlags } from '@necto-react/types'; /** - * Props for the useDisabledProps hook. + * Options for the useDisabledProps hook. */ -export interface UseDisabledPropsProps { +export interface UseDisabledPropsOptions { /** The key of the disabled flag to check. Defaults to 'general'. */ type?: keyof DisabledFlags; @@ -23,4 +23,4 @@ export interface UseDisabledPropsProps { /** * Return type for the useDisabledProps hook. */ -export type UseDisabledPropsPropsReturn = HTMLAttributes; +export type UseDisabledPropsReturn = HTMLAttributes; diff --git a/packages/@necto-react/necto-react-hooks/src/useElementVisibility/index.ts b/packages/@necto-react/necto-react-hooks/src/useElementVisibility/index.ts index acfa5c13..7029a875 100644 --- a/packages/@necto-react/necto-react-hooks/src/useElementVisibility/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useElementVisibility/index.ts @@ -9,6 +9,6 @@ export { useElementVisibility } from './useElementVisibility'; export type { - UseElementVisibilityProps, + UseElementVisibilityOptions, UseElementVisibilityReturn } from './useElementVisibility.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useElementVisibility/useElementVisibility.ts b/packages/@necto-react/necto-react-hooks/src/useElementVisibility/useElementVisibility.ts index 709d12c8..cfd3da05 100644 --- a/packages/@necto-react/necto-react-hooks/src/useElementVisibility/useElementVisibility.ts +++ b/packages/@necto-react/necto-react-hooks/src/useElementVisibility/useElementVisibility.ts @@ -10,7 +10,7 @@ import { useEffectEvent } from '@necto-react/hooks'; import { useRef, useState, useCallback, useEffect } from 'react'; import type { - UseElementVisibilityProps, + UseElementVisibilityOptions, UseElementVisibilityReturn, IntersectionDetails } from './useElementVisibility.types'; @@ -19,11 +19,11 @@ import type { * React hook that observes the visibility of a DOM element using the Intersection Observer API. * * @template T The type of the element being observed. - * @param {UseElementVisibilityProps} [props] - Options to control visibility observation and callbacks. + * @param {UseElementVisibilityOptions} [options] - Options to control visibility observation and callbacks. * @returns {UseElementVisibilityReturn} A tuple containing the ref to the element, the visibility state, and intersection details. */ export function useElementVisibility( - props: UseElementVisibilityProps = {} + options: UseElementVisibilityOptions = {} ): UseElementVisibilityReturn { const { partialVisibility = false, @@ -33,7 +33,7 @@ export function useElementVisibility( active = true, once = false, onChange - } = props; + } = options; const [element, setElement] = useState(null); const [isVisible, setIsVisible] = useState(false); diff --git a/packages/@necto-react/necto-react-hooks/src/useElementVisibility/useElementVisibility.types.ts b/packages/@necto-react/necto-react-hooks/src/useElementVisibility/useElementVisibility.types.ts index bd36717f..f07fefb2 100644 --- a/packages/@necto-react/necto-react-hooks/src/useElementVisibility/useElementVisibility.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useElementVisibility/useElementVisibility.types.ts @@ -12,9 +12,9 @@ export type PartialVisibility = boolean | 'top' | 'right' | 'bottom' | 'left'; /** - * Props for the useElementVisibility hook. + * Options for the useElementVisibility hook. */ -export interface UseElementVisibilityProps { +export interface UseElementVisibilityOptions { /** Whether partial visibility is allowed, or which edge to check. */ partialVisibility?: PartialVisibility; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocus/index.ts b/packages/@necto-react/necto-react-hooks/src/useFocus/index.ts index c0f6c82b..0d556f31 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocus/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocus/index.ts @@ -8,4 +8,4 @@ export { useFocus } from './useFocus'; -export type { UseFocusProps, UseFocusReturn } from './useFocus.types'; +export type { UseFocusOptions, UseFocusReturn } from './useFocus.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocus/useFocus.ts b/packages/@necto-react/necto-react-hooks/src/useFocus/useFocus.ts index 10dc4c40..ecb81304 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocus/useFocus.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocus/useFocus.ts @@ -20,24 +20,24 @@ import { getOwnerDocument, getEventTarget, getActiveElement } from '@necto/dom'; import type { FocusableElement } from '@necto/types'; import type { DOMAttributes } from '@necto-react/types'; import type { FocusEvent as ReactFocusEvent } from 'react'; -import type { UseFocusProps, UseFocusReturn } from './useFocus.types'; +import type { UseFocusOptions, UseFocusReturn } from './useFocus.types'; /** * React hook that manages focus and blur event handling for a focusable element. * * @template T The type of the focusable element. - * @param {UseFocusProps} [props] - Options and event handlers for focus management. + * @param {UseFocusOptions} [options] - Options and event handlers for focus management. * @returns {UseFocusReturn} An object containing props to spread on the target element for focus management. */ export function useFocus( - props: UseFocusProps = {} + options: UseFocusOptions = {} ): UseFocusReturn { const { isDisabled, onFocus: onFocusProp, onBlur: onBlurProp, onFocusChange - } = props; + } = options; // Unified handler for focus change const handleFocusChange = useCallback( diff --git a/packages/@necto-react/necto-react-hooks/src/useFocus/useFocus.types.ts b/packages/@necto-react/necto-react-hooks/src/useFocus/useFocus.types.ts index d314b62a..c883300a 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocus/useFocus.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocus/useFocus.types.ts @@ -10,9 +10,9 @@ import type { FocusEvents, DOMAttributes } from '@necto-react/types'; import type { FocusableElement } from '@necto/types'; /** - * Props for the useFocus hook. + * Options for the useFocus hook. */ -export interface UseFocusProps extends FocusEvents { +export interface UseFocusOptions extends FocusEvents { /** Whether focus events are disabled. */ isDisabled?: boolean; } diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusRing/index.ts b/packages/@necto-react/necto-react-hooks/src/useFocusRing/index.ts index e4a30ec1..304e14d3 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusRing/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusRing/index.ts @@ -9,6 +9,6 @@ export { useFocusRing } from './useFocusRing'; export type { - UseFocusRingProps, + UseFocusRingOptions, UseFocusRingReturn } from './useFocusRing.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusRing/useFocusRing.ts b/packages/@necto-react/necto-react-hooks/src/useFocusRing/useFocusRing.ts index c2834488..34931faf 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusRing/useFocusRing.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusRing/useFocusRing.ts @@ -19,7 +19,7 @@ import { useRef, useState, useCallback } from 'react'; import { useFocusVisibleListener } from '@necto-react/hooks'; import type { - UseFocusRingProps, + UseFocusRingOptions, UseFocusRingReturn, Modality } from './useFocusRing.types'; @@ -30,13 +30,13 @@ const currentModality: null | Modality = null; /** * React hook that manages focus state and focus ring visibility for an element. * - * @param {UseFocusRingProps} [props] - Options to control focus ring behavior. + * @param {UseFocusRingOptions} [options] - Options to control focus ring behavior. * @returns {UseFocusRingReturn} An object containing focus state, focus visibility, and props to spread on the target element. */ export function useFocusRing( - props: UseFocusRingProps = {} + options: UseFocusRingOptions = {} ): UseFocusRingReturn { - const { within = false, isTextInput = false, autoFocus = false } = props; + const { within = false, isTextInput = false, autoFocus = false } = options; const state = useRef({ isFocused: autoFocus, isFocusVisible: autoFocus || currentModality !== 'pointer' diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusRing/useFocusRing.types.ts b/packages/@necto-react/necto-react-hooks/src/useFocusRing/useFocusRing.types.ts index 58c9fddf..0e896bab 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusRing/useFocusRing.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusRing/useFocusRing.types.ts @@ -9,9 +9,9 @@ import type { DOMAttributes } from '@necto-react/types'; /** - * Props for the useFocusRing hook. + * Options for the useFocusRing hook. */ -export interface UseFocusRingProps { +export interface UseFocusRingOptions { /** Whether to track focus visibility within a subtree instead of just the element. */ within?: boolean; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusVisible/index.ts b/packages/@necto-react/necto-react-hooks/src/useFocusVisible/index.ts index fab0afac..582a850f 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusVisible/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusVisible/index.ts @@ -9,6 +9,6 @@ export { useFocusVisible } from './useFocusVisible'; export type { - UseFocusVisibleProps, + UseFocusVisibleOptions, UseFocusVisibleReturn } from './useFocusVisible.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusVisible/useFocusVisible.ts b/packages/@necto-react/necto-react-hooks/src/useFocusVisible/useFocusVisible.ts index 9d0aeb56..ea234943 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusVisible/useFocusVisible.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusVisible/useFocusVisible.ts @@ -20,20 +20,20 @@ import { } from '@necto-react/hooks'; import type { - UseFocusVisibleProps, + UseFocusVisibleOptions, UseFocusVisibleReturn } from './useFocusVisible.types'; /** * React hook that provides focus visibility tracking. * - * @param {UseFocusVisibleProps} [props] - Options for focus visibility behavior. + * @param {UseFocusVisibleOptions} [options] - Options for focus visibility behavior. * @returns {UseFocusVisibleReturn} An object containing the focus visibility state. */ export function useFocusVisible( - props: UseFocusVisibleProps = {} + options: UseFocusVisibleOptions = {} ): UseFocusVisibleReturn { - const { isTextInput, autoFocus } = props; + const { isTextInput, autoFocus } = options; const [isFocusVisibleState, setFocusVisible] = useState( autoFocus || getInteractionModality() !== 'pointer' ); diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusVisible/useFocusVisible.types.ts b/packages/@necto-react/necto-react-hooks/src/useFocusVisible/useFocusVisible.types.ts index 42e8be8a..3452ad28 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusVisible/useFocusVisible.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusVisible/useFocusVisible.types.ts @@ -7,9 +7,9 @@ */ /** - * Props for the useFocusVisible hook. + * Options for the useFocusVisible hook. */ -export interface UseFocusVisibleProps { +export interface UseFocusVisibleOptions { /** Whether the target element is a text input. */ isTextInput?: boolean; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/index.ts b/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/index.ts index 7698599c..3342d82d 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/index.ts @@ -9,4 +9,4 @@ export { getInteractionModality } from './focusContext'; export { useFocusVisibleListener } from './useFocusVisibleListener'; -export type { UseFocusVisibleListenerProps } from './useFocusVisibleListener.types'; +export type { UseFocusVisibleListenerOptions } from './useFocusVisibleListener.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/useFocusVisibleListener.ts b/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/useFocusVisibleListener.ts index bb94f070..77f6fcfc 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/useFocusVisibleListener.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/useFocusVisibleListener.ts @@ -23,7 +23,7 @@ import { getOwnerDocument, getOwnerWindow } from '@necto/dom'; import { globalListeners, changeHandlers, focusState } from './focusContext'; import type { - UseFocusVisibleListenerProps, + UseFocusVisibleListenerOptions, Modality, HandlerEvent, Handler @@ -151,13 +151,13 @@ export function getInteractionModality(): Modality | null { /** * React hook that listens for focus visibility changes based on interaction modality. * - * @param {UseFocusVisibleListenerProps} props - The props for the focus visible listener. + * @param {UseFocusVisibleListenerOptions} options - The options for the focus visible listener. * @returns {void} */ export function useFocusVisibleListener( - props: UseFocusVisibleListenerProps + options: UseFocusVisibleListenerOptions ): void { - const { fn, deps, opts } = props; + const { fn, deps, opts } = options; if (typeof window === 'undefined' || typeof document === 'undefined') { return; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/useFocusVisibleListener.types.ts b/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/useFocusVisibleListener.types.ts index 294de761..2aaf65ab 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/useFocusVisibleListener.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusVisibleListener/useFocusVisibleListener.types.ts @@ -36,9 +36,9 @@ export type HandlerEvent = export type Handler = (modality: Modality, e: HandlerEvent) => void; /** - * Props for the useFocusVisibleListener hook. + * Options for the useFocusVisibleListener hook. */ -export interface UseFocusVisibleListenerProps { +export interface UseFocusVisibleListenerOptions { /** Function called when focus visibility changes. */ fn: (isFocusVisible: boolean) => void; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusWithin/index.ts b/packages/@necto-react/necto-react-hooks/src/useFocusWithin/index.ts index 9fb416fe..3f5c68e9 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusWithin/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusWithin/index.ts @@ -9,6 +9,6 @@ export { useFocusWithin } from './useFocusWithin'; export type { - UseFocusWithinProps, + UseFocusWithinOptions, FocusWithinReturn } from './useFocusWithin.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusWithin/useFocusWithin.ts b/packages/@necto-react/necto-react-hooks/src/useFocusWithin/useFocusWithin.ts index e51d16c3..1ae7c8cc 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusWithin/useFocusWithin.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusWithin/useFocusWithin.ts @@ -27,13 +27,15 @@ import type { RefObject, FocusEvent } from 'react'; import type { DOMAttributes } from '@necto-react/types'; import type { UseSyntheticBlurEventReturn } from '@necto-react/hooks'; import type { - UseFocusWithinProps, + UseFocusWithinOptions, FocusWithinReturn } from './useFocusWithin.types'; -export function useFocusWithin(props: UseFocusWithinProps): FocusWithinReturn { +export function useFocusWithin( + options: UseFocusWithinOptions +): FocusWithinReturn { const { isDisabled, onFocusWithin, onBlurWithin, onFocusWithinChange } = - props; + options; const state: RefObject<{ isFocusWithin: boolean }> = useRef({ isFocusWithin: false }); diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusWithin/useFocusWithin.types.ts b/packages/@necto-react/necto-react-hooks/src/useFocusWithin/useFocusWithin.types.ts index a5ed9627..bdcc24ee 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusWithin/useFocusWithin.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusWithin/useFocusWithin.types.ts @@ -10,9 +10,9 @@ import type { DOMAttributes } from '@necto-react/types'; import type { FocusEvent as ReactFocusEvent } from 'react'; /** - * Props for the useFocusWithin hook. + * Options for the useFocusWithin hook. */ -export interface UseFocusWithinProps { +export interface UseFocusWithinOptions { /** Whether the focus events should be disabled. */ isDisabled?: boolean; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusable/index.ts b/packages/@necto-react/necto-react-hooks/src/useFocusable/index.ts index 7c5dc4d4..74d56981 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusable/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusable/index.ts @@ -9,6 +9,6 @@ export { useFocusable } from './useFocusable'; export type { - UseFocusableProps, + UseFocusableOptions, UseFocusableReturn } from './useFocusable.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusable/useFocusable.ts b/packages/@necto-react/necto-react-hooks/src/useFocusable/useFocusable.ts index e904b4d2..37d09958 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusable/useFocusable.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusable/useFocusable.ts @@ -30,7 +30,7 @@ import { useEffect, useRef, useContext } from 'react'; import { FocusableContext } from '@necto-react/contexts'; import type { - UseFocusableProps, + UseFocusableOptions, UseFocusableReturn } from './useFocusable.types'; import type { RefObject } from 'react'; @@ -40,18 +40,18 @@ import type { FocusableElement } from '@necto/types'; * React hook that provides focus management and keyboard accessibility for a focusable element. * Handles autofocus, disabled state, tab order, and merges focus and keyboard props. * - * @param {UseFocusableProps} props - Props controlling focus behavior and accessibility. + * @param {UseFocusableOptions} options - Options controlling focus behavior and accessibility. * @param {RefObject} domRef - Ref to the DOM element to manage focus for. * @returns {UseFocusableReturn} Object containing merged props for focusable behavior. */ export function useFocusable( - props: UseFocusableProps, + options: UseFocusableOptions, domRef: RefObject ): UseFocusableReturn { - const { autoFocus, isDisabled, excludeFromTabOrder } = props; + const { autoFocus, isDisabled, excludeFromTabOrder } = options; - const { focusProps } = useFocus(props); - const { keyboardProps } = useKeyboard(props); + const { focusProps } = useFocus(options); + const { keyboardProps } = useKeyboard(options); const context = useContext(FocusableContext) || {}; const autoFocusRef: RefObject = useRef(autoFocus); diff --git a/packages/@necto-react/necto-react-hooks/src/useFocusable/useFocusable.types.ts b/packages/@necto-react/necto-react-hooks/src/useFocusable/useFocusable.types.ts index 5e9a659b..ae7eb284 100644 --- a/packages/@necto-react/necto-react-hooks/src/useFocusable/useFocusable.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useFocusable/useFocusable.types.ts @@ -8,7 +8,10 @@ import type { FocusableDOMProps, FocusEvents } from '@necto-react/types'; -export interface UseFocusableProps extends FocusableDOMProps, FocusEvents { +/** + * Options for the useFocusable hook. + */ +export interface UseFocusableOptions extends FocusableDOMProps, FocusEvents { isDisabled?: boolean; autoFocus?: boolean; diff --git a/packages/@necto-react/necto-react-hooks/src/useId/index.ts b/packages/@necto-react/necto-react-hooks/src/useId/index.ts index 15d5f077..4c3f08cd 100644 --- a/packages/@necto-react/necto-react-hooks/src/useId/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useId/index.ts @@ -8,4 +8,4 @@ export { useId } from './useId'; -export type { UseIdProps } from './useId.types'; +export type { UseIdOptions } from './useId.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useId/useId.ts b/packages/@necto-react/necto-react-hooks/src/useId/useId.ts index 5f8fc830..cdc22c24 100644 --- a/packages/@necto-react/necto-react-hooks/src/useId/useId.ts +++ b/packages/@necto-react/necto-react-hooks/src/useId/useId.ts @@ -13,16 +13,16 @@ import { isTest } from 'std-env'; import { registry, defaultContext, idsUpdaterMap } from './hookContext'; import React, { useRef, useState, useEffect, useId as useReactId } from 'react'; -import type { UseIdProps } from './useId.types'; +import type { UseIdOptions } from './useId.types'; /** * Generates a unique, stable ID for React components, optionally with a custom prefix. * - * @param {UseIdProps} [props] - Optional props object. You can provide a custom prefix and/or a defaultId. - * @returns {UseIdReturns} The generated or provided unique ID. + * @param {UseIdOptions} [options] - Optional options object. You can provide a custom prefix and/or a defaultId. + * @returns {string} The generated or provided unique ID. */ -export function useId(props: UseIdProps = {}): string { - const { prefix = 'necto', defaultId } = props; +export function useId(options: UseIdOptions = {}): string { + const { prefix = 'necto', defaultId } = options; // Initialize state only once with a function to avoid unnecessary calculations const [_, setValue] = useState(() => defaultId); diff --git a/packages/@necto-react/necto-react-hooks/src/useId/useId.types.ts b/packages/@necto-react/necto-react-hooks/src/useId/useId.types.ts index e04acf65..f7be8955 100644 --- a/packages/@necto-react/necto-react-hooks/src/useId/useId.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useId/useId.types.ts @@ -7,9 +7,9 @@ */ /** - * Props for the useId hook. + * Options for the useId hook. */ -export interface UseIdProps { +export interface UseIdOptions { /** Optional custom prefix for the generated ID. */ prefix?: string; diff --git a/packages/@necto-react/necto-react-hooks/src/useKeyboard/useKeyboard.ts b/packages/@necto-react/necto-react-hooks/src/useKeyboard/useKeyboard.ts index cb407b4a..0f4c7ab5 100644 --- a/packages/@necto-react/necto-react-hooks/src/useKeyboard/useKeyboard.ts +++ b/packages/@necto-react/necto-react-hooks/src/useKeyboard/useKeyboard.ts @@ -7,27 +7,30 @@ */ import { createEventHandler } from '@necto-react/helpers'; -import type { UseKeyboardProps, UseKeyboardReturn } from './useKeyboard.types'; +import type { + UseKeyboardOptions, + UseKeyboardReturn +} from './useKeyboard.types'; /** * React hook that provides keyboard event handlers based on the disabled state. * - * @param {UseKeyboardProps} props - Props controlling keyboard interaction. + * @param {UseKeyboardOptions} options - Options controlling keyboard interaction. * @returns {UseKeyboardReturn} Object containing keyboard event handler props. */ -export function useKeyboard(props: UseKeyboardProps): UseKeyboardReturn { +export function useKeyboard(options: UseKeyboardOptions): UseKeyboardReturn { return { - keyboardProps: props.isDisabled + keyboardProps: options.isDisabled ? {} : { - onKeyDown: props.onKeyDown + onKeyDown: options.onKeyDown ? createEventHandler((e) => - props.onKeyDown?.(e.nativeEvent as KeyboardEvent) + options.onKeyDown?.(e.nativeEvent as KeyboardEvent) ) : undefined, - onKeyUp: props.onKeyUp + onKeyUp: options.onKeyUp ? createEventHandler((e) => - props.onKeyUp?.(e.nativeEvent as KeyboardEvent) + options.onKeyUp?.(e.nativeEvent as KeyboardEvent) ) : undefined } diff --git a/packages/@necto-react/necto-react-hooks/src/useKeyboard/useKeyboard.types.ts b/packages/@necto-react/necto-react-hooks/src/useKeyboard/useKeyboard.types.ts index feb030ae..f02d1ef1 100644 --- a/packages/@necto-react/necto-react-hooks/src/useKeyboard/useKeyboard.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useKeyboard/useKeyboard.types.ts @@ -9,9 +9,9 @@ import type { KeyboardEvents, DOMAttributes } from '@necto-react/types'; /** - * Props for the useKeyboard hook. + * Options for the useKeyboard hook. */ -export interface UseKeyboardProps extends KeyboardEvents { +export interface UseKeyboardOptions extends KeyboardEvents { /** * Whether the keyboard interaction is disabled. * Defaults to false. diff --git a/packages/@necto-react/necto-react-hooks/src/useMounted/index.ts b/packages/@necto-react/necto-react-hooks/src/useMounted/index.ts index 8065b10a..74415f75 100644 --- a/packages/@necto-react/necto-react-hooks/src/useMounted/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useMounted/index.ts @@ -8,4 +8,4 @@ export { useMounted } from './useMounted'; -export type { UseMountedProps, UseMountedReturn } from './useMounted.types'; +export type { UseMountedOptions, UseMountedReturn } from './useMounted.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useMounted/useMounted.ts b/packages/@necto-react/necto-react-hooks/src/useMounted/useMounted.ts index 0fc65642..45d73c94 100644 --- a/packages/@necto-react/necto-react-hooks/src/useMounted/useMounted.ts +++ b/packages/@necto-react/necto-react-hooks/src/useMounted/useMounted.ts @@ -9,7 +9,7 @@ import { useRef, useState, useCallback, useEffect } from 'react'; import type { - UseMountedProps, + UseMountedOptions, UseMountedReturn, MountedAccessType } from './useMounted.types'; @@ -18,13 +18,15 @@ import type { * React hook that tracks whether a component is mounted. * * @template T The type of access to the mounted state (function, ref, or boolean). - * @param {UseMountedProps & { type: T }} [props] - Options to configure the hook behavior. + * @param {UseMountedOptions & { type: T }} [options] - Options to configure the hook behavior. * @returns {UseMountedReturn} The mounted state in the requested format. */ export function useMounted( - props: UseMountedProps & { type: T } = {} as UseMountedProps & { type: T } + options: UseMountedOptions & { type: T } = {} as UseMountedOptions & { + type: T; + } ): UseMountedReturn { - const { type = 'function' } = props; + const { type = 'function' } = options; const mountedRef = useRef(false); const [mountedState, setMountedState] = useState(false); diff --git a/packages/@necto-react/necto-react-hooks/src/useMounted/useMounted.types.ts b/packages/@necto-react/necto-react-hooks/src/useMounted/useMounted.types.ts index e44ff4fd..f35c72c2 100644 --- a/packages/@necto-react/necto-react-hooks/src/useMounted/useMounted.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useMounted/useMounted.types.ts @@ -14,9 +14,9 @@ import type { RefObject } from 'react'; export type MountedAccessType = 'function' | 'ref' | 'boolean'; /** - * Props for the useMounted hook. + * Options for the useMounted hook. */ -export interface UseMountedProps { +export interface UseMountedOptions { /** The type of access to the mounted state. */ type?: MountedAccessType; } diff --git a/packages/@necto-react/necto-react-hooks/src/useRenderer/index.ts b/packages/@necto-react/necto-react-hooks/src/useRenderer/index.ts index 5bb19d38..f9fc73e9 100644 --- a/packages/@necto-react/necto-react-hooks/src/useRenderer/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useRenderer/index.ts @@ -8,4 +8,7 @@ export { useRenderer } from './useRenderer'; -export type { UseRendererProps, UseRendererReturn } from './useRenderer.types'; +export type { + UseRendererOptions, + UseRendererReturn +} from './useRenderer.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useRenderer/useRenderer.ts b/packages/@necto-react/necto-react-hooks/src/useRenderer/useRenderer.ts index 3d8e518c..333d2562 100644 --- a/packages/@necto-react/necto-react-hooks/src/useRenderer/useRenderer.ts +++ b/packages/@necto-react/necto-react-hooks/src/useRenderer/useRenderer.ts @@ -9,9 +9,20 @@ import { useMemo } from 'react'; import type { ReactNode, CSSProperties } from 'react'; -import type { UseRendererProps, UseRendererReturn } from './useRenderer.types'; +import type { + UseRendererOptions, + UseRendererReturn +} from './useRenderer.types'; -export function useRenderer(props: UseRendererProps): UseRendererReturn { +/** + * React hook that handles rendering logic for components with render props. + * + * @param {UseRendererOptions} options - Options for the hook. + * @returns {UseRendererReturn} The resolved rendering properties. + */ +export function useRenderer( + options: UseRendererOptions +): UseRendererReturn { const { className, style, @@ -21,7 +32,7 @@ export function useRenderer(props: UseRendererProps): UseRendererReturn { defaultChildren, defaultStyle, values - } = props; + } = options; return useMemo(() => { // Compute classnames diff --git a/packages/@necto-react/necto-react-hooks/src/useRenderer/useRenderer.types.ts b/packages/@necto-react/necto-react-hooks/src/useRenderer/useRenderer.types.ts index 6e1eaab4..19bb2e82 100644 --- a/packages/@necto-react/necto-react-hooks/src/useRenderer/useRenderer.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useRenderer/useRenderer.types.ts @@ -10,9 +10,9 @@ import type { RenderProps } from '@necto-react/types'; import type { ReactNode, CSSProperties } from 'react'; /** - * Props for the useRendererHook + * Options for the useRenderer hook. */ -export interface UseRendererProps extends RenderProps { +export interface UseRendererOptions extends RenderProps { // Optional ID for the component. id?: string; diff --git a/packages/@necto-react/necto-react-hooks/src/useScrollLock/index.ts b/packages/@necto-react/necto-react-hooks/src/useScrollLock/index.ts index c268f893..c2d82b12 100644 --- a/packages/@necto-react/necto-react-hooks/src/useScrollLock/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useScrollLock/index.ts @@ -9,6 +9,6 @@ export { useScrollLock } from './useScrollLock'; export type { - UseScrollLockProps, + UseScrollLockOptions, UseScrollLockReturn } from './useScrollLock.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useScrollLock/useScrollLock.ts b/packages/@necto-react/necto-react-hooks/src/useScrollLock/useScrollLock.ts index 1e3e6a3b..02323011 100644 --- a/packages/@necto-react/necto-react-hooks/src/useScrollLock/useScrollLock.ts +++ b/packages/@necto-react/necto-react-hooks/src/useScrollLock/useScrollLock.ts @@ -11,20 +11,20 @@ import { useRef, useState, useEffect } from 'react'; import type { RefObject } from 'react'; import type { - UseScrollLockProps, + UseScrollLockOptions, UseScrollLockReturn } from './useScrollLock.types'; /** * React hook that locks scrolling on a target element or the entire page. * - * @param {UseScrollLockProps} [props] - Options to configure scroll lock behavior. + * @param {UseScrollLockOptions} [options] - Options to configure scroll lock behavior. * @returns {UseScrollLockReturn} An object with isLocked state and lock/unlock functions. */ export function useScrollLock( - props: UseScrollLockProps = {} + options: UseScrollLockOptions = {} ): UseScrollLockReturn { - const { autoLock = false, target: targetProp, widthReflow = true } = props; + const { autoLock = false, target: targetProp, widthReflow = true } = options; const [isLocked, setIsLocked] = useState(false); const target: RefObject = useRef( diff --git a/packages/@necto-react/necto-react-hooks/src/useScrollLock/useScrollLock.types.ts b/packages/@necto-react/necto-react-hooks/src/useScrollLock/useScrollLock.types.ts index 1ba788ef..ae5029c1 100644 --- a/packages/@necto-react/necto-react-hooks/src/useScrollLock/useScrollLock.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useScrollLock/useScrollLock.types.ts @@ -7,9 +7,9 @@ */ /** - * Props for the useScrollLock hook. + * Options for the useScrollLock hook. */ -export interface UseScrollLockProps { +export interface UseScrollLockOptions { /** Whether to automatically lock scroll on mount. */ autoLock?: boolean; diff --git a/packages/@necto-react/necto-react-hooks/src/useStyleInjection/index.ts b/packages/@necto-react/necto-react-hooks/src/useStyleInjection/index.ts index c67b8fc9..47505fe8 100644 --- a/packages/@necto-react/necto-react-hooks/src/useStyleInjection/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useStyleInjection/index.ts @@ -1,2 +1,2 @@ export { useStyleInjection } from './useStyleInjection'; -export type { UseStyleInjectionProps } from './useStyleInjection.types'; +export type { UseStyleInjectionOptions } from './useStyleInjection.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useStyleInjection/useStyleInjection.ts b/packages/@necto-react/necto-react-hooks/src/useStyleInjection/useStyleInjection.ts index 47bf49f0..07bb8e01 100644 --- a/packages/@necto-react/necto-react-hooks/src/useStyleInjection/useStyleInjection.ts +++ b/packages/@necto-react/necto-react-hooks/src/useStyleInjection/useStyleInjection.ts @@ -9,15 +9,20 @@ import { injectStyle } from '@necto/dom'; import { useIsomorphicInsertionEffect } from '@necto-react/hooks'; -import type { UseStyleInjectionProps } from './useStyleInjection.types'; +import type { UseStyleInjectionOptions } from './useStyleInjection.types'; +/** + * React hook that injects CSS styles into the document. + * + * @param {UseStyleInjectionOptions} options - Options for the hook. + */ export function useStyleInjection({ id, css, - window: targetWindow = typeof window !== 'undefined' ? window : null, insertionPoint, - enabled = true -}: UseStyleInjectionProps): void { + enabled = true, + window: targetWindow = typeof window !== 'undefined' ? window : null +}: UseStyleInjectionOptions): void { const cssString: string = Array.isArray(css) ? css.join('\n') : css; useIsomorphicInsertionEffect(() => { diff --git a/packages/@necto-react/necto-react-hooks/src/useStyleInjection/useStyleInjection.types.ts b/packages/@necto-react/necto-react-hooks/src/useStyleInjection/useStyleInjection.types.ts index 376ae06b..dd631f51 100644 --- a/packages/@necto-react/necto-react-hooks/src/useStyleInjection/useStyleInjection.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useStyleInjection/useStyleInjection.types.ts @@ -1,4 +1,15 @@ -export interface UseStyleInjectionProps { +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * Options for the useStyleInjection hook. + */ +export interface UseStyleInjectionOptions { id?: string; css: string | string[]; window?: Window | null; diff --git a/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/index.ts b/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/index.ts index 7dfa4aa4..2d089863 100644 --- a/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/index.ts +++ b/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/index.ts @@ -9,6 +9,6 @@ export { useSyntheticBlurEvent } from './useSyntheticBlurEvent'; export type { - UseSyntheticBlurEventProps, + UseSyntheticBlurEventOptions, UseSyntheticBlurEventReturn } from './useSyntheticBlurEvent.types'; diff --git a/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/useSyntheticBlurEvent.ts b/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/useSyntheticBlurEvent.ts index b558603f..e5c0391d 100644 --- a/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/useSyntheticBlurEvent.ts +++ b/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/useSyntheticBlurEvent.ts @@ -19,20 +19,20 @@ import { useRef, useCallback, useLayoutEffect } from 'react'; import type { FocusEvent } from 'react'; import type { - UseSyntheticBlurEventProps, + UseSyntheticBlurEventOptions, UseSyntheticBlurEventReturn } from './useSyntheticBlurEvent.types'; /** * React hook to handle synthetic blur events, particularly for disabled elements. * - * @param onBlur - Callback to handle the blur event. + * @param {UseSyntheticBlurEventOptions} options - Options for the hook. * @returns A callback to attach to the element's blur event. */ export function useSyntheticBlurEvent( - props: UseSyntheticBlurEventProps + options: UseSyntheticBlurEventOptions ): UseSyntheticBlurEventReturn { - const { onBlur } = props; + const { onBlur } = options; const stateRef = useRef<{ isFocused: boolean; observer: MutationObserver | null; diff --git a/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/useSyntheticBlurEvent.types.ts b/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/useSyntheticBlurEvent.types.ts index db9912cf..9cd35980 100644 --- a/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/useSyntheticBlurEvent.types.ts +++ b/packages/@necto-react/necto-react-hooks/src/useSyntheticBlurEvent/useSyntheticBlurEvent.types.ts @@ -9,9 +9,9 @@ import type { FocusEvent } from 'react'; /** - * Props for the useSyntheticBlurEvent hook. + * Options for the useSyntheticBlurEvent hook. */ -export interface UseSyntheticBlurEventProps { +export interface UseSyntheticBlurEventOptions { /** * Handler called when the blur event is triggered on the target element. * Receives a React FocusEvent for the target element. diff --git a/packages/@necto-react/necto-react-popper/src/components/index.ts b/packages/@necto-react/necto-react-popper/src/components/index.ts index 959ece44..31b698fc 100644 --- a/packages/@necto-react/necto-react-popper/src/components/index.ts +++ b/packages/@necto-react/necto-react-popper/src/components/index.ts @@ -1,7 +1,9 @@ /** - * Popper components + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ -// Export your components here -// Example: export * from './Popper'; -// Example: export * from './Tooltip'; +export {}; diff --git a/packages/@necto-react/necto-react-popper/src/contexts/index.ts b/packages/@necto-react/necto-react-popper/src/contexts/index.ts index 5a0cd165..31b698fc 100644 --- a/packages/@necto-react/necto-react-popper/src/contexts/index.ts +++ b/packages/@necto-react/necto-react-popper/src/contexts/index.ts @@ -1,6 +1,9 @@ /** - * Popper contexts + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ -// Export your contexts here -// Example: export * from './PopperContext'; +export {}; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/index.ts index f06f7b82..ecae4737 100644 --- a/packages/@necto-react/necto-react-popper/src/hooks/index.ts +++ b/packages/@necto-react/necto-react-popper/src/hooks/index.ts @@ -1,8 +1,24 @@ /** - * All hooks exports - * Each hook is in its own folder with its own types + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ export * from './usePopper'; -export * from './useFloating'; -export * from './usePopperWithInteractions'; +export * from './useClick'; +export * from './useFloatingHover'; +export * from './useFloatingFocus'; +export * from './useDismiss'; +export * from './useRole'; +export * from './useInteractions'; +export * from './useListNavigation'; +export * from './useTypeahead'; +export * from './useTransitionStatus'; +export * from './useClientPoint'; +export * from './useDelayGroup'; +export * from './useMergeRefs'; +export * from './useFloatingPortal'; + +export type { ElementProps, FloatingContext, InteractionProps } from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/types.ts new file mode 100644 index 00000000..3fd53844 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/types.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { MutableRefObject } from 'react'; + +export type ElementProps = Record; + +export interface FloatingContext { + open: boolean; + onOpenChange: (open: boolean) => void; + refs: { + reference: MutableRefObject; + floating: MutableRefObject; + }; + elements: { + reference: Element | null; + floating: HTMLElement | null; + }; + dataRef: MutableRefObject>; +} + +export interface InteractionProps { + reference: ElementProps; + floating: ElementProps; + item?: ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useClick/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useClick/index.ts new file mode 100644 index 00000000..5796951c --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useClick/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useClick } from './useClick'; + +export type { UseClickOptions, UseClickReturn } from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useClick/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useClick/types.ts new file mode 100644 index 00000000..990877df --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useClick/types.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ElementProps } from '../types'; + +export interface UseClickOptions { + /** + * Whether the floating element is open. + */ + open: boolean; + + /** + * Callback to set the open state. + */ + onOpenChange: (open: boolean) => void; + + /** + * Whether the hook is enabled. + * @default true + */ + enabled?: boolean; + + /** + * Whether to toggle on click or only open. + * @default true + */ + toggle?: boolean; + + /** + * The event type that triggers the click. + * @default 'click' + */ + event?: 'click' | 'mousedown'; + + /** + * Whether to ignore mouse events after touch events. + * @default true + */ + ignoreMouse?: boolean; + + /** + * The keyboard keys that trigger the click. + * @default ['Enter', ' '] + */ + keyboardHandlers?: boolean; +} + +export interface UseClickReturn { + reference: ElementProps; + floating: ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useClick/useClick.ts b/packages/@necto-react/necto-react-popper/src/hooks/useClick/useClick.ts new file mode 100644 index 00000000..f7056613 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useClick/useClick.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useRef, useCallback, useMemo } from 'react'; + +import type { UseClickOptions, UseClickReturn } from './types'; + +/** + * Provides click interaction for floating elements. + * @param options - Configuration options. + * @returns Props to spread on reference and floating elements. + */ +export function useClick(options: UseClickOptions): UseClickReturn { + const { + open, + onOpenChange, + enabled = true, + toggle = true, + event = 'click', + ignoreMouse = true, + keyboardHandlers = true + } = options; + + const pointerTypeRef = useRef(''); + const didKeyDownRef = useRef(false); + + const handleClick = useCallback(() => { + if (ignoreMouse && pointerTypeRef.current === 'mouse') { + return; + } + + if (toggle) { + onOpenChange(!open); + } else if (!open) { + onOpenChange(true); + } + }, [open, onOpenChange, toggle, ignoreMouse]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (!keyboardHandlers) return; + + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + didKeyDownRef.current = true; + } + }, + [keyboardHandlers] + ); + + const handleKeyUp = useCallback( + (e: React.KeyboardEvent) => { + if (!keyboardHandlers) return; + + if ((e.key === 'Enter' || e.key === ' ') && didKeyDownRef.current) { + didKeyDownRef.current = false; + handleClick(); + } + }, + [keyboardHandlers, handleClick] + ); + + const handlePointerDown = useCallback((e: React.PointerEvent) => { + pointerTypeRef.current = e.pointerType; + }, []); + + const reference = useMemo(() => { + if (!enabled) return {}; + + const eventProps: Record = { + onPointerDown: handlePointerDown + }; + + if (event === 'click') { + eventProps.onClick = handleClick; + } else { + eventProps.onMouseDown = handleClick; + } + + if (keyboardHandlers) { + eventProps.onKeyDown = handleKeyDown; + eventProps.onKeyUp = handleKeyUp; + } + + return eventProps; + }, [ + enabled, + event, + handleClick, + handlePointerDown, + handleKeyDown, + handleKeyUp, + keyboardHandlers + ]); + + return { + reference, + floating: {} + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useClientPoint/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useClientPoint/index.ts new file mode 100644 index 00000000..39b16f93 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useClientPoint/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useClientPoint } from './useClientPoint'; + +export type { UseClientPointOptions, UseClientPointReturn } from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useClientPoint/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useClientPoint/types.ts new file mode 100644 index 00000000..08df7bb8 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useClientPoint/types.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ElementProps } from '../types'; + +export interface UseClientPointOptions { + /** + * Whether the floating element is open. + */ + open: boolean; + + /** + * Whether the hook is enabled. + * @default true + */ + enabled?: boolean; + + /** + * The axis to position on. + * @default 'both' + */ + axis?: 'x' | 'y' | 'both'; + + /** + * Virtual element setter for positioning. + */ + setReference?: ( + reference: { + getBoundingClientRect: () => DOMRect; + } | null + ) => void; +} + +export interface UseClientPointReturn { + reference: ElementProps; + floating: ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useClientPoint/useClientPoint.ts b/packages/@necto-react/necto-react-popper/src/hooks/useClientPoint/useClientPoint.ts new file mode 100644 index 00000000..79af3771 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useClientPoint/useClientPoint.ts @@ -0,0 +1,111 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useCallback, useMemo, useRef, useEffect } from 'react'; + +import type { UseClientPointOptions, UseClientPointReturn } from './types'; + +/** + * Provides cursor-position-based positioning for floating elements. + * @param options - Configuration options. + * @returns Props to spread on reference and floating elements. + */ +export function useClientPoint( + options: UseClientPointOptions +): UseClientPointReturn { + const { open, enabled = true, axis = 'both', setReference } = options; + + const clientPointRef = useRef({ x: 0, y: 0 }); + const initialReferenceRect = useRef(null); + + const createVirtualElement = useCallback( + (x: number, y: number) => { + return { + getBoundingClientRect: () => { + const base = initialReferenceRect.current ?? { + x: 0, + y: 0, + width: 0, + height: 0, + top: 0, + right: 0, + bottom: 0, + left: 0 + }; + + const newX = axis === 'y' ? base.x : x; + const newY = axis === 'x' ? base.y : y; + + return { + x: newX, + y: newY, + width: 0, + height: 0, + top: newY, + right: newX, + bottom: newY, + left: newX, + toJSON: () => ({}) + } as DOMRect; + } + }; + }, + [axis] + ); + + const handlePointerMove = useCallback( + (e: PointerEvent) => { + if (!enabled) return; + + clientPointRef.current = { x: e.clientX, y: e.clientY }; + + if (setReference) { + setReference(createVirtualElement(e.clientX, e.clientY)); + } + }, + [enabled, setReference, createVirtualElement] + ); + + useEffect(() => { + if (!enabled || !open) return; + + document.addEventListener('pointermove', handlePointerMove); + + return () => { + document.removeEventListener('pointermove', handlePointerMove); + }; + }, [enabled, open, handlePointerMove]); + + const handlePointerDown = useCallback( + (e: React.PointerEvent) => { + if (!enabled) return; + + const target = e.currentTarget as Element; + initialReferenceRect.current = target.getBoundingClientRect(); + clientPointRef.current = { x: e.clientX, y: e.clientY }; + + if (setReference) { + setReference(createVirtualElement(e.clientX, e.clientY)); + } + }, + [enabled, setReference, createVirtualElement] + ); + + const reference = useMemo(() => { + if (!enabled) return {}; + + return { + onPointerDown: handlePointerDown + }; + }, [enabled, handlePointerDown]); + + return { + reference, + floating: {} + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useDelayGroup/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useDelayGroup/index.ts new file mode 100644 index 00000000..58b90595 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useDelayGroup/index.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { + DelayGroupContext, + UseDelayGroupOptions, + UseDelayGroupReturn, + DelayGroupProviderProps +} from './types'; + +export { DelayGroupProvider, useDelayGroup } from './useDelayGroup'; + +export type { + DelayGroupContext, + UseDelayGroupOptions, + UseDelayGroupReturn, + DelayGroupProviderProps +}; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useDelayGroup/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useDelayGroup/types.ts new file mode 100644 index 00000000..6f3380c6 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useDelayGroup/types.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface DelayGroupContext { + /** + * Current delay value for the group. + */ + delay: number | { open?: number; close?: number }; + + /** + * Sets the current delay for the group. + */ + setCurrentId: (id: string | null) => void; + + /** + * Currently active floating element ID. + */ + currentId: string | null; + + /** + * Whether any element in the group is open. + */ + isInstantPhase: boolean; + + /** + * Timeout duration before resetting to initial delay. + */ + timeoutMs: number; +} + +export interface UseDelayGroupOptions { + /** + * Unique ID for this floating element in the group. + */ + id: string; +} + +export interface UseDelayGroupReturn { + /** + * Delay value to use for this element. + */ + delay: number | { open?: number; close?: number }; + + /** + * Registers this element as the current one. + */ + setAsCurrentId: () => void; + + /** + * Whether this element is in instant phase. + */ + isInstantPhase: boolean; +} + +export interface DelayGroupProviderProps { + /** + * Initial delay configuration. + */ + delay: number | { open?: number; close?: number }; + + /** + * Timeout before resetting to initial delay after all elements close. + * @default 0 + */ + timeoutMs?: number; + + /** + * Children components. + */ + children: React.ReactNode; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useDelayGroup/useDelayGroup.tsx b/packages/@necto-react/necto-react-popper/src/hooks/useDelayGroup/useDelayGroup.tsx new file mode 100644 index 00000000..42d8bf36 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useDelayGroup/useDelayGroup.tsx @@ -0,0 +1,119 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React, { + createContext, + useContext, + useState, + useCallback, + useRef, + useEffect +} from 'react'; + +import type { + DelayGroupContext, + UseDelayGroupOptions, + UseDelayGroupReturn, + DelayGroupProviderProps +} from './types'; + +const DelayGroupCtx = createContext(null); + +/** + * Provides shared delay context for a group of floating elements. + * @param props - Configuration options. + * @returns Provider component wrapping children. + */ +export function DelayGroupProvider( + props: DelayGroupProviderProps +): React.ReactElement { + const { delay, timeoutMs = 0, children } = props; + + const [currentId, setCurrentId] = useState(null); + const [isInstantPhase, setIsInstantPhase] = useState(false); + const timeoutRef = useRef | null>(null); + + const handleSetCurrentId = useCallback( + (id: string | null) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + if (id === null) { + if (timeoutMs > 0) { + timeoutRef.current = setTimeout(() => { + setIsInstantPhase(false); + setCurrentId(null); + }, timeoutMs); + } else { + setIsInstantPhase(false); + setCurrentId(null); + } + } else { + setCurrentId(id); + setIsInstantPhase(true); + } + }, + [timeoutMs] + ); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const contextValue: DelayGroupContext = { + delay, + setCurrentId: handleSetCurrentId, + currentId, + isInstantPhase, + timeoutMs + }; + + return React.createElement( + DelayGroupCtx.Provider, + { value: contextValue }, + children + ); +} + +/** + * Provides delay group functionality for a floating element. + * @param options - Configuration options. + * @returns Delay value and group control functions. + */ +export function useDelayGroup( + options: UseDelayGroupOptions +): UseDelayGroupReturn { + const { id } = options; + + const context = useContext(DelayGroupCtx); + + const setAsCurrentId = useCallback(() => { + context?.setCurrentId(id); + }, [context, id]); + + if (!context) { + return { + delay: 0, + setAsCurrentId: () => {}, + isInstantPhase: false + }; + } + + const delay = context.isInstantPhase ? 0 : context.delay; + + return { + delay, + setAsCurrentId, + isInstantPhase: context.isInstantPhase + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useDismiss/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useDismiss/index.ts new file mode 100644 index 00000000..fd8b4309 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useDismiss/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useDismiss } from './useDismiss'; + +export type { UseDismissOptions, UseDismissReturn } from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useDismiss/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useDismiss/types.ts new file mode 100644 index 00000000..1f5c65ec --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useDismiss/types.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ElementProps } from '../types'; + +export interface UseDismissOptions { + /** + * Whether the floating element is open. + */ + open: boolean; + + /** + * Callback to set the open state. + */ + onOpenChange: (open: boolean) => void; + + /** + * Whether the hook is enabled. + * @default true + */ + enabled?: boolean; + + /** + * Whether to close on escape key. + * @default true + */ + escapeKey?: boolean; + + /** + * Whether to close on outside press. + * @default true + */ + outsidePress?: boolean | ((event: MouseEvent) => boolean); + + /** + * Whether to close on reference press. + * @default false + */ + referencePress?: boolean; + + /** + * Whether to close when the reference element is hidden. + * @default false + */ + ancestorScroll?: boolean; + + /** + * Whether to bubble the escape key event. + * @default false + */ + bubbles?: boolean | { escapeKey?: boolean; outsidePress?: boolean }; +} + +export interface UseDismissReturn { + reference: ElementProps; + floating: ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useDismiss/useDismiss.ts b/packages/@necto-react/necto-react-popper/src/hooks/useDismiss/useDismiss.ts new file mode 100644 index 00000000..6dd8cef8 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useDismiss/useDismiss.ts @@ -0,0 +1,166 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useRef, useCallback, useEffect, useMemo } from 'react'; +import { getOwnerDocument, nodeContains } from '@necto/dom'; + +import type { UseDismissOptions, UseDismissReturn } from './types'; + +/** + * Provides dismiss interaction (outside click, escape key) for floating elements. + * @param options - Configuration options. + * @returns Props to spread on reference and floating elements. + */ +export function useDismiss(options: UseDismissOptions): UseDismissReturn { + const { + open, + onOpenChange, + enabled = true, + escapeKey = true, + outsidePress = true, + referencePress = false, + ancestorScroll = false, + bubbles = false + } = options; + + const referenceRef = useRef(null); + const floatingRef = useRef(null); + const insideTreeRef = useRef(false); + + const escapeKeyBubbles = + typeof bubbles === 'boolean' ? bubbles : (bubbles.escapeKey ?? false); + const outsidePressBubbles = + typeof bubbles === 'boolean' ? bubbles : (bubbles.outsidePress ?? false); + + const closeOnEscapeKey = useCallback( + (event: KeyboardEvent) => { + if (!open || !escapeKey) return; + + if (event.key === 'Escape') { + if (!escapeKeyBubbles) { + event.stopPropagation(); + } + onOpenChange(false); + } + }, + [open, escapeKey, escapeKeyBubbles, onOpenChange] + ); + + const closeOnOutsidePress = useCallback( + (event: MouseEvent) => { + if (!open) return; + if (insideTreeRef.current) { + insideTreeRef.current = false; + return; + } + + const target = event.target as Element; + + if (referenceRef.current && nodeContains(referenceRef.current, target)) { + if (!referencePress) return; + } + + if (floatingRef.current && nodeContains(floatingRef.current, target)) { + return; + } + + if (typeof outsidePress === 'function' && !outsidePress(event)) { + return; + } + + if (!outsidePressBubbles) { + event.stopPropagation(); + } + + onOpenChange(false); + }, + [open, outsidePress, outsidePressBubbles, referencePress, onOpenChange] + ); + + useEffect(() => { + if (!enabled || !open) return; + + const doc = getOwnerDocument(floatingRef.current); + + if (escapeKey) { + doc.addEventListener('keydown', closeOnEscapeKey); + } + + if (outsidePress) { + doc.addEventListener('mousedown', closeOnOutsidePress); + } + + return () => { + doc.removeEventListener('keydown', closeOnEscapeKey); + doc.removeEventListener('mousedown', closeOnOutsidePress); + }; + }, [ + enabled, + open, + escapeKey, + outsidePress, + closeOnEscapeKey, + closeOnOutsidePress + ]); + + useEffect(() => { + if (!enabled || !open || !ancestorScroll) return; + + const scrollHandler = () => { + onOpenChange(false); + }; + + const reference = referenceRef.current; + if (!reference) return; + + let current: Element | null = reference.parentElement; + const cleanup: (() => void)[] = []; + + while (current) { + current.addEventListener('scroll', scrollHandler); + const el = current; + cleanup.push(() => el.removeEventListener('scroll', scrollHandler)); + current = current.parentElement; + } + + window.addEventListener('scroll', scrollHandler); + cleanup.push(() => window.removeEventListener('scroll', scrollHandler)); + + return () => { + cleanup.forEach((fn) => fn()); + }; + }, [enabled, open, ancestorScroll, onOpenChange]); + + const reference = useMemo(() => { + if (!enabled) return {}; + + return { + ref: (node: Element | null) => { + referenceRef.current = node; + } + }; + }, [enabled]); + + const floating = useMemo(() => { + if (!enabled) return {}; + + return { + ref: (node: HTMLElement | null) => { + floatingRef.current = node; + }, + onPointerDown: () => { + insideTreeRef.current = true; + } + }; + }, [enabled]); + + return { + reference, + floating + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useFloatingFocus/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingFocus/index.ts new file mode 100644 index 00000000..31b5e2f3 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingFocus/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useFloatingFocus } from './useFloatingFocus'; + +export type { UseFloatingFocusOptions, UseFloatingFocusReturn } from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useFloatingFocus/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingFocus/types.ts new file mode 100644 index 00000000..10e10a70 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingFocus/types.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ElementProps } from '../types'; + +export interface UseFloatingFocusOptions { + /** + * Whether the floating element is open. + */ + open: boolean; + + /** + * Callback to set the open state. + */ + onOpenChange: (open: boolean) => void; + + /** + * Whether the hook is enabled. + * @default true + */ + enabled?: boolean; + + /** + * Whether to only show on keyboard focus. + * @default true + */ + visibleOnly?: boolean; +} + +export interface UseFloatingFocusReturn { + reference: ElementProps; + floating: ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useFloatingFocus/useFloatingFocus.ts b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingFocus/useFloatingFocus.ts new file mode 100644 index 00000000..f37a33d2 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingFocus/useFloatingFocus.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useRef, useCallback, useMemo } from 'react'; + +import type { UseFloatingFocusOptions, UseFloatingFocusReturn } from './types'; + +/** + * Provides focus interaction for floating elements. + * @param options - Configuration options. + * @returns Props to spread on reference and floating elements. + */ +export function useFloatingFocus( + options: UseFloatingFocusOptions +): UseFloatingFocusReturn { + const { open, onOpenChange, enabled = true, visibleOnly = true } = options; + + const blockFocusRef = useRef(false); + + const handleFocus = useCallback( + (e: React.FocusEvent) => { + if (blockFocusRef.current) { + blockFocusRef.current = false; + return; + } + + if (visibleOnly) { + try { + if (!e.currentTarget.matches(':focus-visible')) { + return; + } + } catch { + // :focus-visible not supported + } + } + + onOpenChange(true); + }, + [visibleOnly, onOpenChange] + ); + + const handleBlur = useCallback( + (e: React.FocusEvent) => { + const relatedTarget = e.relatedTarget as Element | null; + const currentTarget = e.currentTarget as Element; + + if (relatedTarget && currentTarget.contains(relatedTarget)) { + return; + } + + onOpenChange(false); + }, + [onOpenChange] + ); + + const handlePointerDown = useCallback(() => { + blockFocusRef.current = true; + }, []); + + const reference = useMemo(() => { + if (!enabled) return {}; + + return { + onFocus: handleFocus, + onBlur: handleBlur, + onPointerDown: handlePointerDown + }; + }, [enabled, handleFocus, handleBlur, handlePointerDown]); + + return { + reference, + floating: {} + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useFloatingHover/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingHover/index.ts new file mode 100644 index 00000000..0d334ccf --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingHover/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useFloatingHover } from './useFloatingHover'; + +export type { UseFloatingHoverOptions, UseFloatingHoverReturn } from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useFloatingHover/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingHover/types.ts new file mode 100644 index 00000000..0bfefea5 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingHover/types.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ElementProps } from '../types'; + +export interface UseFloatingHoverOptions { + /** + * Whether the floating element is open. + */ + open: boolean; + + /** + * Callback to set the open state. + */ + onOpenChange: (open: boolean) => void; + + /** + * Whether the hook is enabled. + * @default true + */ + enabled?: boolean; + + /** + * Delay in ms before showing. + * @default 0 + */ + delay?: number | { open?: number; close?: number }; + + /** + * Whether hovering the floating element keeps it open. + * @default true + */ + handleClose?: boolean; + + /** + * Whether to ignore mouse events. + * @default false + */ + mouseOnly?: boolean; + + /** + * Delay before closing when leaving. + * @default 0 + */ + restMs?: number; +} + +export interface UseFloatingHoverReturn { + reference: ElementProps; + floating: ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useFloatingHover/useFloatingHover.ts b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingHover/useFloatingHover.ts new file mode 100644 index 00000000..d629da6c --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingHover/useFloatingHover.ts @@ -0,0 +1,136 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useRef, useCallback, useMemo, useEffect } from 'react'; + +import type { UseFloatingHoverOptions, UseFloatingHoverReturn } from './types'; + +/** + * Provides hover interaction with delay support for floating elements. + * @param options - Configuration options. + * @returns Props to spread on reference and floating elements. + */ +export function useFloatingHover( + options: UseFloatingHoverOptions +): UseFloatingHoverReturn { + const { + open, + onOpenChange, + enabled = true, + delay = 0, + handleClose = true, + mouseOnly = false, + restMs = 0 + } = options; + + const openDelay = typeof delay === 'number' ? delay : (delay.open ?? 0); + const closeDelay = typeof delay === 'number' ? delay : (delay.close ?? 0); + + const timeoutRef = useRef | null>(null); + const restTimeoutRef = useRef | null>(null); + const blockMouseRef = useRef(false); + const isHoveringRef = useRef(false); + + const clearTimeouts = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + if (restTimeoutRef.current) { + clearTimeout(restTimeoutRef.current); + restTimeoutRef.current = null; + } + }, []); + + useEffect(() => { + return () => clearTimeouts(); + }, [clearTimeouts]); + + const handleOpen = useCallback(() => { + clearTimeouts(); + if (openDelay > 0) { + timeoutRef.current = setTimeout(() => { + onOpenChange(true); + }, openDelay); + } else { + onOpenChange(true); + } + }, [openDelay, onOpenChange, clearTimeouts]); + + const handleClose_ = useCallback(() => { + clearTimeouts(); + if (closeDelay > 0) { + timeoutRef.current = setTimeout(() => { + onOpenChange(false); + }, closeDelay); + } else { + onOpenChange(false); + } + }, [closeDelay, onOpenChange, clearTimeouts]); + + const handlePointerEnter = useCallback( + (e: React.PointerEvent) => { + if (mouseOnly && e.pointerType !== 'mouse') return; + if (blockMouseRef.current && e.pointerType === 'mouse') return; + + isHoveringRef.current = true; + handleOpen(); + }, + [mouseOnly, handleOpen] + ); + + const handlePointerLeave = useCallback( + (e: React.PointerEvent) => { + if (mouseOnly && e.pointerType !== 'mouse') return; + + isHoveringRef.current = false; + + if (restMs > 0) { + restTimeoutRef.current = setTimeout(() => { + if (!isHoveringRef.current) { + handleClose_(); + } + }, restMs); + } else { + handleClose_(); + } + }, + [mouseOnly, restMs, handleClose_] + ); + + const handleTouchStart = useCallback(() => { + blockMouseRef.current = true; + }, []); + + const reference = useMemo(() => { + if (!enabled) return {}; + + return { + onPointerEnter: handlePointerEnter, + onPointerLeave: handlePointerLeave, + onTouchStart: handleTouchStart + }; + }, [enabled, handlePointerEnter, handlePointerLeave, handleTouchStart]); + + const floating = useMemo(() => { + if (!enabled || !handleClose) return {}; + + return { + onPointerEnter: () => { + isHoveringRef.current = true; + clearTimeouts(); + }, + onPointerLeave: handlePointerLeave + }; + }, [enabled, handleClose, clearTimeouts, handlePointerLeave]); + + return { + reference, + floating + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useFloatingPortal/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingPortal/index.ts new file mode 100644 index 00000000..1569b8af --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingPortal/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { + UseFloatingPortalOptions, + UseFloatingPortalReturn, + FloatingPortalProps +} from './types'; + +export { useFloatingPortal, FloatingPortal } from './useFloatingPortal'; + +export type { + UseFloatingPortalOptions, + UseFloatingPortalReturn, + FloatingPortalProps +}; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useFloatingPortal/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingPortal/types.ts new file mode 100644 index 00000000..5f4460be --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingPortal/types.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface UseFloatingPortalOptions { + /** + * Custom portal ID to use. + */ + id?: string; + + /** + * Whether the portal is enabled. + * @default true + */ + enabled?: boolean; + + /** + * Custom root element to render the portal into. + */ + root?: HTMLElement | null; + + /** + * Whether to preserve tab order for accessibility. + * @default true + */ + preserveTabOrder?: boolean; +} + +export interface UseFloatingPortalReturn { + /** + * The portal container element. + */ + portalNode: HTMLElement | null; + + /** + * Unique portal ID. + */ + portalId: string; +} + +export interface FloatingPortalProps { + /** + * Custom portal ID. + */ + id?: string; + + /** + * Custom root element. + */ + root?: HTMLElement | null; + + /** + * Whether to preserve tab order. + * @default true + */ + preserveTabOrder?: boolean; + + /** + * Children to render in the portal. + */ + children: React.ReactNode; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useFloatingPortal/useFloatingPortal.tsx b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingPortal/useFloatingPortal.tsx new file mode 100644 index 00000000..bf72e872 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useFloatingPortal/useFloatingPortal.tsx @@ -0,0 +1,103 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import React, { useState, useEffect, useId, useMemo } from 'react'; +import { createPortal } from 'react-dom'; + +import type { + UseFloatingPortalOptions, + UseFloatingPortalReturn, + FloatingPortalProps +} from './types'; + +const PORTAL_ROOT_ID = 'necto-floating-portal-root'; + +/** + * Gets or creates the default portal root element. + * @returns The portal root element. + */ +function getPortalRoot(): HTMLElement { + let root = document.getElementById(PORTAL_ROOT_ID); + + if (!root) { + root = document.createElement('div'); + root.id = PORTAL_ROOT_ID; + root.setAttribute('data-necto-portal', ''); + document.body.appendChild(root); + } + + return root; +} + +/** + * Provides portal functionality for floating elements. + * @param options - Configuration options. + * @returns Portal node and ID. + */ +export function useFloatingPortal( + options: UseFloatingPortalOptions = {} +): UseFloatingPortalReturn { + const { id, enabled = true, root } = options; + + const uniqueId = useId(); + const portalId = id ?? `necto-portal-${uniqueId}`; + + const [portalNode, setPortalNode] = useState(null); + + useEffect(() => { + if (!enabled || typeof document === 'undefined') { + setPortalNode(null); + return; + } + + const parentRoot = root ?? getPortalRoot(); + const node = document.createElement('div'); + node.id = portalId; + node.setAttribute('data-necto-floating-portal', ''); + parentRoot.appendChild(node); + setPortalNode(node); + + return () => { + if (node.parentNode) { + node.parentNode.removeChild(node); + } + }; + }, [enabled, portalId, root]); + + return { + portalNode, + portalId + }; +} + +/** + * Renders children into a portal at the end of the document body. + * @param props - Configuration options. + * @returns Portal component or null. + */ +export function FloatingPortal( + props: FloatingPortalProps +): React.ReactPortal | null { + const { id, root, preserveTabOrder = true, children } = props; + + const { portalNode } = useFloatingPortal({ id, root, preserveTabOrder }); + + const content = useMemo(() => { + if (!preserveTabOrder) { + return children; + } + + return React.createElement(React.Fragment, null, children); + }, [preserveTabOrder, children]); + + if (!portalNode) { + return null; + } + + return createPortal(content, portalNode); +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useInteractions/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useInteractions/index.ts new file mode 100644 index 00000000..4e06070d --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useInteractions/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useInteractions } from './useInteractions'; + +export type { InteractionReturn, UseInteractionsReturn } from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useInteractions/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useInteractions/types.ts new file mode 100644 index 00000000..f6a9d35f --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useInteractions/types.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ElementProps } from '../types'; + +export interface InteractionReturn { + reference: ElementProps; + floating: ElementProps; + item?: ElementProps; +} + +export interface UseInteractionsReturn { + getReferenceProps: (userProps?: ElementProps) => ElementProps; + getFloatingProps: (userProps?: ElementProps) => ElementProps; + getItemProps: (userProps?: ElementProps) => ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useInteractions/useInteractions.ts b/packages/@necto-react/necto-react-popper/src/hooks/useInteractions/useInteractions.ts new file mode 100644 index 00000000..076e1ad2 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useInteractions/useInteractions.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMemo } from 'react'; + +import type { ElementProps } from '../types'; +import type { InteractionReturn, UseInteractionsReturn } from './types'; + +/** + * Merges multiple interaction hooks into unified prop getters. + * @param interactions - Array of interaction hook results. + * @returns Prop getter functions for reference, floating, and item elements. + */ +export function useInteractions( + interactions: Array = [] +): UseInteractionsReturn { + const filteredInteractions = interactions.filter( + Boolean + ) as InteractionReturn[]; + + const mergeProps = useMemo(() => { + return ( + key: 'reference' | 'floating' | 'item', + userProps?: ElementProps + ): ElementProps => { + const merged: ElementProps = { ...userProps }; + + for (const interaction of filteredInteractions) { + const props = interaction[key]; + if (!props) continue; + + for (const [propKey, propValue] of Object.entries(props)) { + if (propKey === 'ref') { + const existingRef = merged.ref; + if (existingRef) { + merged.ref = (node: Element | null) => { + if (typeof existingRef === 'function') existingRef(node); + if (typeof propValue === 'function') propValue(node); + }; + } else { + merged.ref = propValue; + } + } else if ( + propKey.startsWith('on') && + typeof propValue === 'function' + ) { + const existingHandler = merged[propKey]; + if (typeof existingHandler === 'function') { + merged[propKey] = (...args: unknown[]) => { + (propValue as Function)(...args); + (existingHandler as Function)(...args); + }; + } else { + merged[propKey] = propValue; + } + } else { + merged[propKey] = propValue; + } + } + } + + return merged; + }; + }, [filteredInteractions]); + + const getReferenceProps = useMemo(() => { + return (userProps?: ElementProps) => mergeProps('reference', userProps); + }, [mergeProps]); + + const getFloatingProps = useMemo(() => { + return (userProps?: ElementProps) => mergeProps('floating', userProps); + }, [mergeProps]); + + const getItemProps = useMemo(() => { + return (userProps?: ElementProps) => mergeProps('item', userProps); + }, [mergeProps]); + + return { + getReferenceProps, + getFloatingProps, + getItemProps + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useListNavigation/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useListNavigation/index.ts new file mode 100644 index 00000000..528f068c --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useListNavigation/index.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useListNavigation } from './useListNavigation'; + +export type { + UseListNavigationOptions, + UseListNavigationReturn +} from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useListNavigation/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useListNavigation/types.ts new file mode 100644 index 00000000..57012904 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useListNavigation/types.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { MutableRefObject } from 'react'; +import type { ElementProps } from '../types'; + +export interface UseListNavigationOptions { + /** + * Whether the floating element is open. + */ + open: boolean; + + /** + * Callback to set the open state. + */ + onOpenChange: (open: boolean) => void; + + /** + * Ref to array of list item elements. + */ + listRef: MutableRefObject>; + + /** + * The active index. + */ + activeIndex: number | null; + + /** + * Callback when active index changes. + */ + onNavigate: (index: number | null) => void; + + /** + * Whether the hook is enabled. + * @default true + */ + enabled?: boolean; + + /** + * Whether the list is virtual. + * @default false + */ + virtual?: boolean; + + /** + * Whether to loop navigation. + * @default false + */ + loop?: boolean; + + /** + * Orientation of the list. + * @default 'vertical' + */ + orientation?: 'horizontal' | 'vertical' | 'both'; + + /** + * Whether to focus item on hover. + * @default true + */ + focusItemOnHover?: boolean; + + /** + * Whether to open on arrow key down. + * @default true + */ + openOnArrowKeyDown?: boolean; +} + +export interface UseListNavigationReturn { + reference: ElementProps; + floating: ElementProps; + item: ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useListNavigation/useListNavigation.ts b/packages/@necto-react/necto-react-popper/src/hooks/useListNavigation/useListNavigation.ts new file mode 100644 index 00000000..f34fe473 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useListNavigation/useListNavigation.ts @@ -0,0 +1,171 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useCallback, useMemo, useRef } from 'react'; + +import type { + UseListNavigationOptions, + UseListNavigationReturn +} from './types'; + +/** + * Provides keyboard navigation for list-based floating elements. + * @param options - Configuration options. + * @returns Props to spread on reference, floating, and item elements. + */ +export function useListNavigation( + options: UseListNavigationOptions +): UseListNavigationReturn { + const { + open, + onOpenChange, + listRef, + activeIndex, + onNavigate, + enabled = true, + virtual = false, + loop = false, + orientation = 'vertical', + focusItemOnHover = true, + openOnArrowKeyDown = true + } = options; + + const indexRef = useRef(activeIndex); + indexRef.current = activeIndex; + + const getNextIndex = useCallback( + (current: number | null, direction: 1 | -1): number | null => { + const items = listRef.current.filter(Boolean); + const itemCount = items.length; + + if (itemCount === 0) return null; + + const currentIndex = current ?? (direction === 1 ? -1 : itemCount); + let nextIndex = currentIndex + direction; + + if (loop) { + if (nextIndex < 0) nextIndex = itemCount - 1; + if (nextIndex >= itemCount) nextIndex = 0; + } else { + if (nextIndex < 0 || nextIndex >= itemCount) return current; + } + + return nextIndex; + }, + [listRef, loop] + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (!enabled) return; + + const isVertical = orientation === 'vertical' || orientation === 'both'; + const isHorizontal = + orientation === 'horizontal' || orientation === 'both'; + + const arrowUp = isVertical && e.key === 'ArrowUp'; + const arrowDown = isVertical && e.key === 'ArrowDown'; + const arrowLeft = isHorizontal && e.key === 'ArrowLeft'; + const arrowRight = isHorizontal && e.key === 'ArrowRight'; + + if (arrowUp || arrowDown || arrowLeft || arrowRight) { + e.preventDefault(); + + if (!open && openOnArrowKeyDown) { + onOpenChange(true); + return; + } + + const direction = arrowUp || arrowLeft ? -1 : 1; + const nextIndex = getNextIndex(indexRef.current, direction); + + onNavigate(nextIndex); + + if (!virtual && nextIndex !== null) { + const item = listRef.current[nextIndex]; + item?.focus(); + } + } + + if (e.key === 'Home') { + e.preventDefault(); + onNavigate(0); + if (!virtual) { + listRef.current[0]?.focus(); + } + } + + if (e.key === 'End') { + e.preventDefault(); + const lastIndex = listRef.current.filter(Boolean).length - 1; + onNavigate(lastIndex); + if (!virtual) { + listRef.current[lastIndex]?.focus(); + } + } + }, + [ + enabled, + open, + onOpenChange, + orientation, + openOnArrowKeyDown, + getNextIndex, + onNavigate, + virtual, + listRef + ] + ); + + const reference = useMemo(() => { + if (!enabled) return {}; + + return { + onKeyDown: handleKeyDown, + 'aria-activedescendant': + virtual && activeIndex !== null + ? listRef.current[activeIndex]?.id + : undefined + }; + }, [enabled, handleKeyDown, virtual, activeIndex, listRef]); + + const floating = useMemo(() => { + if (!enabled) return {}; + + return { + onKeyDown: handleKeyDown + }; + }, [enabled, handleKeyDown]); + + const item = useMemo(() => { + if (!enabled) return {}; + + return { + onPointerEnter: focusItemOnHover + ? (e: React.PointerEvent) => { + const target = e.currentTarget as HTMLElement; + const index = listRef.current.indexOf(target); + if (index !== -1) { + onNavigate(index); + } + } + : undefined, + onPointerLeave: focusItemOnHover + ? () => { + onNavigate(null); + } + : undefined + }; + }, [enabled, focusItemOnHover, listRef, onNavigate]); + + return { + reference, + floating, + item + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useMergeRefs/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useMergeRefs/index.ts new file mode 100644 index 00000000..90f5fb35 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useMergeRefs/index.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ReactRef } from './types'; + +export { useMergeRefs } from './useMergeRefs'; + +export type { ReactRef }; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useMergeRefs/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useMergeRefs/types.ts new file mode 100644 index 00000000..95ca99a5 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useMergeRefs/types.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Ref, MutableRefObject, RefCallback } from 'react'; + +export type ReactRef = + | Ref + | MutableRefObject + | RefCallback + | null + | undefined; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useMergeRefs/useMergeRefs.ts b/packages/@necto-react/necto-react-popper/src/hooks/useMergeRefs/useMergeRefs.ts new file mode 100644 index 00000000..68b60be6 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useMergeRefs/useMergeRefs.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMemo } from 'react'; + +import type { ReactRef } from './types'; + +/** + * Sets a ref value, handling both callback refs and ref objects. + * @param ref - The ref to set. + * @param value - The value to assign. + */ +function setRef(ref: ReactRef, value: T | null): void { + if (typeof ref === 'function') { + ref(value); + } else if (ref !== null && ref !== undefined) { + (ref as React.MutableRefObject).current = value; + } +} + +/** + * Merges multiple refs into a single callback ref. + * @param refs - Array of refs to merge. + * @returns A single callback ref that updates all provided refs. + */ +export function useMergeRefs( + ...refs: ReactRef[] +): React.RefCallback | null { + return useMemo(() => { + const filteredRefs = refs.filter(Boolean); + + if (filteredRefs.length === 0) { + return null; + } + + if (filteredRefs.length === 1) { + const ref = filteredRefs[0]; + return (value: T | null) => setRef(ref, value); + } + + return (value: T | null) => { + for (const ref of filteredRefs) { + setRef(ref, value); + } + }; + }, refs); +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/usePopper/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/usePopper/index.ts index 11505f9c..41e3dff2 100644 --- a/packages/@necto-react/necto-react-popper/src/hooks/usePopper/index.ts +++ b/packages/@necto-react/necto-react-popper/src/hooks/usePopper/index.ts @@ -1,6 +1,19 @@ /** - * usePopper hook exports + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ export { usePopper } from './usePopper'; -export type * from './usePopper.types'; + +export type { + UsePopperOptions, + UsePopperReturn, + ComputePositionOptions, + ComputePositionResult, + Placement, + Strategy, + Middleware +} from './usePopper.types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/usePopper/usePopper.ts b/packages/@necto-react/necto-react-popper/src/hooks/usePopper/usePopper.ts index 9fb9cfd2..71231489 100644 --- a/packages/@necto-react/necto-react-popper/src/hooks/usePopper/usePopper.ts +++ b/packages/@necto-react/necto-react-popper/src/hooks/usePopper/usePopper.ts @@ -3,7 +3,7 @@ import * as ReactDOM from 'react-dom'; import { defu } from 'defu'; import { useLatestRef } from '@necto-react/hooks'; import { computePosition } from '@necto/popper'; -import type { UsePopperProps, UsePopperReturn } from './usePopper.types'; +import type { UsePopperOptions, UsePopperReturn } from './usePopper.types'; import type { ComputePositionResult } from '@necto/popper'; function deepEqual(a: any, b: any): boolean { @@ -37,7 +37,7 @@ function roundByDPR(element: Element, value: number): number { return Math.round(value * dpr) / dpr; } -export function usePopper(props: UsePopperProps = {}): UsePopperReturn { +export function usePopper(options: UsePopperOptions = {}): UsePopperReturn { const { placement, strategy, @@ -47,7 +47,7 @@ export function usePopper(props: UsePopperProps = {}): UsePopperReturn { transform, whileElementsMounted, open - } = defu(props, { + } = defu(options, { placement: 'bottom' as const, strategy: 'absolute' as const, middleware: [], diff --git a/packages/@necto-react/necto-react-popper/src/hooks/usePopper/usePopper.types.ts b/packages/@necto-react/necto-react-popper/src/hooks/usePopper/usePopper.types.ts index 705554b1..889b4ac5 100644 --- a/packages/@necto-react/necto-react-popper/src/hooks/usePopper/usePopper.types.ts +++ b/packages/@necto-react/necto-react-popper/src/hooks/usePopper/usePopper.types.ts @@ -9,9 +9,9 @@ import type { } from '@necto/popper'; /** - * Props for usePopper hook + * Options for usePopper hook */ -export interface UsePopperProps extends ComputePositionOptions { +export interface UsePopperOptions extends ComputePositionOptions { /** * External reference element (instead of using refs) */ diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useRole/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useRole/index.ts new file mode 100644 index 00000000..580eee36 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useRole/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useRole } from './useRole'; + +export type { UseRoleOptions, UseRoleReturn, FloatingRole } from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useRole/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useRole/types.ts new file mode 100644 index 00000000..d58e76e3 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useRole/types.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { ElementProps } from '../types'; + +export type FloatingRole = + | 'tooltip' + | 'dialog' + | 'menu' + | 'listbox' + | 'tree' + | 'grid' + | 'alertdialog'; + +export interface UseRoleOptions { + /** + * Whether the floating element is open. + */ + open: boolean; + + /** + * Whether the hook is enabled. + * @default true + */ + enabled?: boolean; + + /** + * The ARIA role of the floating element. + * @default 'dialog' + */ + role?: FloatingRole; +} + +export interface UseRoleReturn { + reference: ElementProps; + floating: ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useRole/useRole.ts b/packages/@necto-react/necto-react-popper/src/hooks/useRole/useRole.ts new file mode 100644 index 00000000..7af03a9b --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useRole/useRole.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useId, useMemo } from 'react'; + +import type { UseRoleOptions, UseRoleReturn } from './types'; + +/** + * Provides ARIA role props for floating elements. + * @param options - Configuration options. + * @returns Props to spread on reference and floating elements. + */ +export function useRole(options: UseRoleOptions): UseRoleReturn { + const { open, enabled = true, role = 'dialog' } = options; + + const floatingId = useId(); + + const reference = useMemo(() => { + if (!enabled) return {}; + + if (role === 'tooltip') { + return { + 'aria-describedby': open ? floatingId : undefined + }; + } + + return { + 'aria-expanded': open, + 'aria-haspopup': + role === 'menu' || + role === 'listbox' || + role === 'tree' || + role === 'grid' + ? role + : 'dialog', + 'aria-controls': open ? floatingId : undefined + }; + }, [enabled, open, role, floatingId]); + + const floating = useMemo(() => { + if (!enabled) return {}; + + const baseProps: Record = { + id: floatingId, + role + }; + + if (role === 'tooltip') { + return baseProps; + } + + if (role === 'dialog' || role === 'alertdialog') { + return { + ...baseProps, + 'aria-modal': 'true' + }; + } + + return baseProps; + }, [enabled, floatingId, role]); + + return { + reference, + floating + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useTransitionStatus/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useTransitionStatus/index.ts new file mode 100644 index 00000000..96b1038d --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useTransitionStatus/index.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { + useTransitionStatus, + useTransitionStyles +} from './useTransitionStatus'; + +export type { + UseTransitionStatusOptions, + UseTransitionStatusReturn, + UseTransitionStylesOptions, + UseTransitionStylesReturn, + TransitionStatus +} from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useTransitionStatus/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useTransitionStatus/types.ts new file mode 100644 index 00000000..9e0568ea --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useTransitionStatus/types.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { CSSProperties } from 'react'; + +export type TransitionStatus = 'unmounted' | 'initial' | 'open' | 'close'; + +export interface UseTransitionStatusOptions { + /** + * Whether the floating element is open. + */ + open: boolean; + + /** + * Duration of the transition in ms. + * @default 250 + */ + duration?: number | { open?: number; close?: number }; +} + +export interface UseTransitionStatusReturn { + /** + * Whether the element should be mounted. + */ + isMounted: boolean; + + /** + * The current transition status. + */ + status: TransitionStatus; +} + +export interface UseTransitionStylesOptions extends UseTransitionStatusOptions { + /** + * Initial styles when opening. + */ + initial?: CSSProperties; + + /** + * Styles when open. + */ + openStyles?: CSSProperties; + + /** + * Styles when closing. + */ + closeStyles?: CSSProperties; + + /** + * Common styles applied to all states. + */ + common?: CSSProperties; +} + +export interface UseTransitionStylesReturn extends UseTransitionStatusReturn { + /** + * Styles to apply based on current status. + */ + styles: CSSProperties; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useTransitionStatus/useTransitionStatus.ts b/packages/@necto-react/necto-react-popper/src/hooks/useTransitionStatus/useTransitionStatus.ts new file mode 100644 index 00000000..d68a214e --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useTransitionStatus/useTransitionStatus.ts @@ -0,0 +1,113 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useState, useEffect, useLayoutEffect } from 'react'; + +import type { + UseTransitionStatusOptions, + UseTransitionStatusReturn, + UseTransitionStylesOptions, + UseTransitionStylesReturn, + TransitionStatus +} from './types'; + +const useIsomorphicLayoutEffect = + typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +/** + * Provides transition status for animating floating elements. + * @param options - Configuration options. + * @returns Mounted state and current transition status. + */ +export function useTransitionStatus( + options: UseTransitionStatusOptions +): UseTransitionStatusReturn { + const { open, duration = 250 } = options; + + const closeDuration = + typeof duration === 'number' ? duration : (duration.close ?? 250); + + const [status, setStatus] = useState('unmounted'); + const [isMounted, setIsMounted] = useState(false); + + useIsomorphicLayoutEffect(() => { + if (open) { + setIsMounted(true); + setStatus('initial'); + + const frame = requestAnimationFrame(() => { + setStatus('open'); + }); + + return () => cancelAnimationFrame(frame); + } else if (isMounted) { + setStatus('close'); + + const timeout = setTimeout(() => { + setStatus('unmounted'); + setIsMounted(false); + }, closeDuration); + + return () => clearTimeout(timeout); + } + }, [open, closeDuration, isMounted]); + + return { + isMounted, + status + }; +} + +/** + * Provides transition styles for animating floating elements. + * @param options - Configuration options including style definitions. + * @returns Mounted state, status, and computed styles. + */ +export function useTransitionStyles( + options: UseTransitionStylesOptions +): UseTransitionStylesReturn { + const { + open, + duration = 250, + initial = { opacity: 0 }, + openStyles = { opacity: 1 }, + closeStyles, + common = {} + } = options; + + const openDuration = + typeof duration === 'number' ? duration : (duration.open ?? 250); + const closeDuration = + typeof duration === 'number' ? duration : (duration.close ?? 250); + + const { isMounted, status } = useTransitionStatus({ open, duration }); + + const styles = (() => { + const baseTransition = { + transitionProperty: 'opacity, transform', + transitionDuration: `${status === 'close' ? closeDuration : openDuration}ms` + }; + + switch (status) { + case 'initial': + return { ...common, ...baseTransition, ...initial }; + case 'open': + return { ...common, ...baseTransition, ...openStyles }; + case 'close': + return { ...common, ...baseTransition, ...(closeStyles ?? initial) }; + default: + return common; + } + })(); + + return { + isMounted, + status, + styles + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useTypeahead/index.ts b/packages/@necto-react/necto-react-popper/src/hooks/useTypeahead/index.ts new file mode 100644 index 00000000..6fac254f --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useTypeahead/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { useTypeahead } from './useTypeahead'; + +export type { UseTypeaheadOptions, UseTypeaheadReturn } from './types'; diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useTypeahead/types.ts b/packages/@necto-react/necto-react-popper/src/hooks/useTypeahead/types.ts new file mode 100644 index 00000000..69d663e1 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useTypeahead/types.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { MutableRefObject } from 'react'; +import type { ElementProps } from '../types'; + +export interface UseTypeaheadOptions { + /** + * Whether the floating element is open. + */ + open: boolean; + + /** + * Ref to array of list item elements. + */ + listRef: MutableRefObject>; + + /** + * The active index. + */ + activeIndex: number | null; + + /** + * Callback when active index changes. + */ + onMatch: (index: number) => void; + + /** + * Whether the hook is enabled. + * @default true + */ + enabled?: boolean; + + /** + * Function to extract text from list item. + */ + findMatch?: ( + list: Array, + search: string + ) => number | null; + + /** + * Timeout in ms before clearing the search. + * @default 750 + */ + resetMs?: number; + + /** + * Callback when typing occurs. + */ + onTypingChange?: (isTyping: boolean) => void; +} + +export interface UseTypeaheadReturn { + reference: ElementProps; + floating: ElementProps; +} diff --git a/packages/@necto-react/necto-react-popper/src/hooks/useTypeahead/useTypeahead.ts b/packages/@necto-react/necto-react-popper/src/hooks/useTypeahead/useTypeahead.ts new file mode 100644 index 00000000..f6f4b316 --- /dev/null +++ b/packages/@necto-react/necto-react-popper/src/hooks/useTypeahead/useTypeahead.ts @@ -0,0 +1,128 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useCallback, useMemo, useRef, useEffect } from 'react'; + +import type { UseTypeaheadOptions, UseTypeaheadReturn } from './types'; + +function defaultFindMatch( + list: Array, + search: string +): number | null { + const lowercaseSearch = search.toLowerCase(); + + for (let i = 0; i < list.length; i++) { + const item = list[i]; + if (!item) continue; + + const text = item.textContent?.toLowerCase() ?? ''; + if (text.startsWith(lowercaseSearch)) { + return i; + } + } + + return null; +} + +/** + * Provides typeahead search for list-based floating elements. + * @param options - Configuration options. + * @returns Props to spread on reference and floating elements. + */ +export function useTypeahead(options: UseTypeaheadOptions): UseTypeaheadReturn { + const { + open, + listRef, + activeIndex, + onMatch, + enabled = true, + findMatch = defaultFindMatch, + resetMs = 750, + onTypingChange + } = options; + + const searchRef = useRef(''); + const timeoutRef = useRef | null>(null); + const prevIndexRef = useRef(activeIndex); + + useEffect(() => { + if (!open) { + searchRef.current = ''; + } + }, [open]); + + const clearSearch = useCallback(() => { + searchRef.current = ''; + onTypingChange?.(false); + }, [onTypingChange]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (!enabled || !open) return; + + if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) { + return; + } + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + + searchRef.current += e.key; + onTypingChange?.(true); + + timeoutRef.current = setTimeout(clearSearch, resetMs); + + const matchIndex = findMatch(listRef.current, searchRef.current); + + if (matchIndex !== null && matchIndex !== prevIndexRef.current) { + prevIndexRef.current = matchIndex; + onMatch(matchIndex); + } + }, + [ + enabled, + open, + findMatch, + listRef, + onMatch, + resetMs, + clearSearch, + onTypingChange + ] + ); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, []); + + const reference = useMemo(() => { + if (!enabled) return {}; + + return { + onKeyDown: handleKeyDown + }; + }, [enabled, handleKeyDown]); + + const floating = useMemo(() => { + if (!enabled) return {}; + + return { + onKeyDown: handleKeyDown + }; + }, [enabled, handleKeyDown]); + + return { + reference, + floating + }; +} diff --git a/packages/@necto-react/necto-react-popper/src/index.ts b/packages/@necto-react/necto-react-popper/src/index.ts index bcb2139d..66809f26 100644 --- a/packages/@necto-react/necto-react-popper/src/index.ts +++ b/packages/@necto-react/necto-react-popper/src/index.ts @@ -1,5 +1,24 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + export * from './hooks'; export * from './contexts'; export * from './components'; -export { offset, flip, shift } from '@necto/popper'; + +export { + offset, + flip, + shift, + arrow, + size, + autoPlacement, + hide, + autoUpdate +} from '@necto/popper'; + export type * from '@necto/popper'; diff --git a/packages/@necto-vue/necto-vue-components/package.json b/packages/@necto-vue/necto-vue-components/package.json new file mode 100644 index 00000000..577dd3c0 --- /dev/null +++ b/packages/@necto-vue/necto-vue-components/package.json @@ -0,0 +1,45 @@ +{ + "name": "@necto-vue/components", + "version": "0.0.1", + "type": "module", + "description": "Necto's standard library for providing utility components for Vue applications.", + "scripts": { + "build": "vite build", + "dev": "vite build --watch" + }, + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "author": "Corinvo OSS Team", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.14.1", + "@vitejs/plugin-vue": "^5.2.1", + "@vitejs/plugin-vue-jsx": "^5.1.2", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-dts": "^4.3.0", + "vue": "^3.5.13" + }, + "peerDependencies": { + "vue": "^3.5.0" + }, + "dependencies": { + "@necto-vue/composables": "workspace:*", + "@necto/constants": "workspace:*", + "@necto/dom": "workspace:*", + "@necto/image": "workspace:*" + } +} diff --git a/packages/@necto-vue/necto-vue-components/src/Image/Image.stories.ts b/packages/@necto-vue/necto-vue-components/src/Image/Image.stories.ts new file mode 100644 index 00000000..40bfba03 --- /dev/null +++ b/packages/@necto-vue/necto-vue-components/src/Image/Image.stories.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import ImageComponent from './component.vue'; + +import type { Meta, StoryObj } from '@storybook/vue3'; + +const meta: Meta = { + title: 'Components/Image', + component: ImageComponent, + tags: ['autodocs'], + argTypes: { + src: { control: 'text' }, + alt: { control: 'text' }, + width: { control: 'number' }, + height: { control: 'number' }, + aspectRatio: { control: 'number' }, + sizes: { control: 'text' }, + layout: { + control: 'select', + options: ['fixed', 'constrained', 'fullWidth'] + }, + priority: { control: 'boolean' }, + background: { control: 'color' }, + objectFit: { + control: 'select', + options: ['cover', 'contain', 'fill', 'none', 'scale-down'] + }, + unstyled: { control: 'boolean' }, + custom: { control: 'boolean' }, + inline: { control: 'boolean' } + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + src: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?w=800', + alt: 'A beautiful landscape', + width: 800, + height: 600 + } +}; diff --git a/packages/@necto-vue/necto-vue-components/src/Image/component.vue b/packages/@necto-vue/necto-vue-components/src/Image/component.vue new file mode 100644 index 00000000..4f8f6ef9 --- /dev/null +++ b/packages/@necto-vue/necto-vue-components/src/Image/component.vue @@ -0,0 +1,153 @@ + + + + + + + \ No newline at end of file diff --git a/packages/@necto-vue/necto-vue-components/src/Image/index.ts b/packages/@necto-vue/necto-vue-components/src/Image/index.ts new file mode 100644 index 00000000..e3e13047 --- /dev/null +++ b/packages/@necto-vue/necto-vue-components/src/Image/index.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export { + default as Image, + default as NectoImage +} from './component.vue'; + +export type * from './types'; diff --git a/packages/@necto-vue/necto-vue-components/src/Image/types.ts b/packages/@necto-vue/necto-vue-components/src/Image/types.ts new file mode 100644 index 00000000..798e4e62 --- /dev/null +++ b/packages/@necto-vue/necto-vue-components/src/Image/types.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { ImgHTMLAttributes } from 'vue'; + +/** Image layout mode */ +export type ImageLayout = 'fixed' | 'constrained' | 'fullWidth'; + +/** CSS object-fit mode */ +export type ObjectFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; + +export interface ImageProps + extends /* @vue-ignore */ Omit< + ImgHTMLAttributes, + 'src' | 'width' | 'height' | 'sizes' + > { + /** Image source URL (required) */ + src: string; + + /** Alt text for accessibility (required) */ + alt: string; + + /** Image size (sets both width and height) */ + size?: string | number; + + /** Image width */ + width?: string | number; + + /** Image height */ + height?: string | number; + + /** Aspect ratio (width/height) */ + aspectRatio?: number; + + /** Responsive sizes attribute */ + sizes?: string | string[]; + + /** Layout mode: 'fixed' | 'constrained' | 'fullWidth' (default: 'constrained') */ + layout?: ImageLayout; + + /** Loading priority - if true, disables lazy loading */ + priority?: boolean; + + /** Background color or image URL for placeholder */ + background?: string; + + /** CSS object-fit: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' (default: 'cover') */ + objectFit?: ObjectFit; + + /** Disable all automatic styling */ + unstyled?: boolean; + + /** Use custom slot instead of img element */ + custom?: boolean; + + /** Inline SVG directly into DOM (only works with SVG src) */ + inline?: boolean; +} + +export interface ImageEmits { + (event: 'load', payload: Event): void; + (event: 'error', payload: Event): void; +} + +export interface ImageSlotProps { + /** Computed image attributes */ + imgAttrs: ImgHTMLAttributes; + + /** Whether the main image has loaded */ + isLoaded: boolean; + + /** Whether the image failed to load */ + hasError: boolean; +} diff --git a/packages/@necto-vue/necto-vue-components/src/Picture/index.ts b/packages/@necto-vue/necto-vue-components/src/Picture/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/@necto-vue/necto-vue-components/src/index.ts b/packages/@necto-vue/necto-vue-components/src/index.ts new file mode 100644 index 00000000..39a1ee0c --- /dev/null +++ b/packages/@necto-vue/necto-vue-components/src/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export * from './Image'; diff --git a/packages/@necto-vue/necto-vue-components/tsconfig.json b/packages/@necto-vue/necto-vue-components/tsconfig.json new file mode 100644 index 00000000..bddbfc76 --- /dev/null +++ b/packages/@necto-vue/necto-vue-components/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/@necto-vue/necto-vue-components/vite.config.ts b/packages/@necto-vue/necto-vue-components/vite.config.ts new file mode 100644 index 00000000..49dbedfe --- /dev/null +++ b/packages/@necto-vue/necto-vue-components/vite.config.ts @@ -0,0 +1,33 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import vueJsx from '@vitejs/plugin-vue-jsx'; +import dts from 'vite-plugin-dts'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [ + vue(), + vueJsx(), + dts({ + insertTypesEntry: true, + rollupTypes: true + }) + ], + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'NectoVueComponents', + formats: ['es', 'cjs'], + fileName: (format) => (format === 'es' ? 'index.js' : 'index.cjs') + }, + rollupOptions: { + external: ['vue', 'vue-router', '@necto-vue/composables'], + output: { + globals: { + vue: 'Vue', + 'vue-router': 'VueRouter' + } + } + } + } +}); diff --git a/packages/@necto-vue/necto-vue-composables/package.json b/packages/@necto-vue/necto-vue-composables/package.json new file mode 100644 index 00000000..e47aa5c4 --- /dev/null +++ b/packages/@necto-vue/necto-vue-composables/package.json @@ -0,0 +1,40 @@ +{ + "name": "@necto-vue/composables", + "version": "0.0.1", + "type": "module", + "description": "Necto's standard library for Vue composables.", + "scripts": { + "build": "vite build", + "dev": "vite build --watch" + }, + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "author": "Corinvo OSS Team", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.14.1", + "typescript": "^5.9.3", + "vite": "^6.0.0", + "vite-plugin-dts": "^4.3.0", + "vue": "^3.5.13" + }, + "peerDependencies": { + "vue": "^3.5.0" + }, + "dependencies": { + "@vueuse/core": "^14.1.0" + } +} diff --git a/packages/@necto-vue/necto-vue-composables/src/index.ts b/packages/@necto-vue/necto-vue-composables/src/index.ts new file mode 100644 index 00000000..2010cb19 --- /dev/null +++ b/packages/@necto-vue/necto-vue-composables/src/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export * from './useImage'; +export * from './useMounted'; diff --git a/packages/@necto-vue/necto-vue-composables/src/useImage/index.ts b/packages/@necto-vue/necto-vue-composables/src/useImage/index.ts new file mode 100644 index 00000000..3ff5e75b --- /dev/null +++ b/packages/@necto-vue/necto-vue-composables/src/useImage/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export { useImage } from './useImage'; + +export type { UseImageOptions } from './types'; diff --git a/packages/@necto-vue/necto-vue-composables/src/useImage/types.ts b/packages/@necto-vue/necto-vue-composables/src/useImage/types.ts new file mode 100644 index 00000000..1df4553e --- /dev/null +++ b/packages/@necto-vue/necto-vue-composables/src/useImage/types.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export interface UseImageOptions { + /** The URL of the image to load */ + src: string; + + /** Comma-separated list of image URLs with descriptors for responsive images (e.g., "image-1x.jpg 1x, image-2x.jpg 2x") */ + srcset?: string; + + /** Comma-separated list of source sizes for responsive images (e.g., "(max-width: 600px) 100vw, 50vw") */ + sizes?: string; + + /** Alternative text describing the image for accessibility */ + alt?: string; + + /** CSS class name(s) to apply to the image element */ + class?: string; + + /** Loading behavior: 'eager' loads immediately, 'lazy' defers until near viewport */ + loading?: HTMLImageElement['loading']; + + /** CORS setting: 'anonymous' or 'use-credentials' for cross-origin requests */ + crossorigin?: string; + + /** Controls what referrer information is sent when fetching the image */ + referrerPolicy?: HTMLImageElement['referrerPolicy']; + + /** Intrinsic width of the image in pixels */ + width?: HTMLImageElement['width']; + + /** Intrinsic height of the image in pixels */ + height?: HTMLImageElement['height']; + + /** Decoding hint: 'sync', 'async', or 'auto' for how the browser should decode the image */ + decoding?: HTMLImageElement['decoding']; + + /** Fetch priority hint: 'high', 'low', or 'auto' */ + fetchPriority?: HTMLImageElement['fetchPriority']; + + /** Whether the image is part of a server-side image map */ + ismap?: HTMLImageElement['isMap']; + + /** The hash fragment of an associated image map element (e.g., "#mymap") */ + usemap?: HTMLImageElement['useMap']; +} diff --git a/packages/@necto-vue/necto-vue-composables/src/useImage/useImage.ts b/packages/@necto-vue/necto-vue-composables/src/useImage/useImage.ts new file mode 100644 index 00000000..1c18b4e7 --- /dev/null +++ b/packages/@necto-vue/necto-vue-composables/src/useImage/useImage.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { toValue, watch } from 'vue'; +import { useAsyncState } from '@vueuse/core'; + +import type { MaybeRefOrGetter } from 'vue'; +import type { UseImageOptions } from './types'; +import type { UseAsyncStateOptions } from '@vueuse/core'; + +const isBrowser: boolean = typeof window !== 'undefined'; + +/** + * Reactive load an image in the browser, you can wait the result to display it or show a fallback. + * + * @param options - Image attributes, as used in the tag + * @param asyncStateOptions - Options for useAsyncState + */ +export function useImage( + options: MaybeRefOrGetter, + asyncStateOptions: UseAsyncStateOptions = {} +): any { + const state: any = useAsyncState( + (): Promise => { + return new Promise((resolve, reject): void => { + if (!isBrowser) { + return; + } + + const img = new Image(); + const { + src, + srcset, + sizes, + class: clazz, + loading, + crossorigin, + referrerPolicy, + width, + height, + decoding, + fetchPriority, + ismap, + usemap + } = toValue(options); + + img.src = src; + + if (srcset != null) img.srcset = srcset; + if (sizes != null) img.sizes = sizes; + if (clazz != null) img.className = clazz; + if (loading != null) img.loading = loading; + if (crossorigin != null) img.crossOrigin = crossorigin; + if (referrerPolicy != null) img.referrerPolicy = referrerPolicy; + if (width != null) img.width = width; + if (height != null) img.height = height; + if (decoding != null) img.decoding = decoding; + if (fetchPriority != null) img.fetchPriority = fetchPriority; + if (ismap != null) img.isMap = ismap; + if (usemap != null) img.useMap = usemap; + + img.onload = (): void => resolve(img); + img.onerror = reject; + }); + }, + undefined, + { + resetOnExecute: true, + ...asyncStateOptions + } + ); + + if (isBrowser) { + watch( + (): UseImageOptions => toValue(options), + (): any => state.execute(asyncStateOptions.delay), + { deep: true } + ); + } + + return state; +} diff --git a/packages/@necto-vue/necto-vue-composables/src/useMounted.ts b/packages/@necto-vue/necto-vue-composables/src/useMounted.ts new file mode 100644 index 00000000..51a9070e --- /dev/null +++ b/packages/@necto-vue/necto-vue-composables/src/useMounted.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { getCurrentInstance, onMounted, shallowRef } from 'vue'; + +import type { ComponentInternalInstance, ShallowRef } from 'vue'; + +export function useMounted(): ShallowRef { + const isMounted: ShallowRef = shallowRef(false); + + const instance: ComponentInternalInstance | null = getCurrentInstance(); + + if (instance) { + onMounted((): void => { + isMounted.value = true; + }, instance); + } + + return isMounted; +} diff --git a/packages/@necto-vue/necto-vue-composables/tsconfig.json b/packages/@necto-vue/necto-vue-composables/tsconfig.json new file mode 100644 index 00000000..138c6e88 --- /dev/null +++ b/packages/@necto-vue/necto-vue-composables/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/@necto-vue/necto-vue-composables/vite.config.ts b/packages/@necto-vue/necto-vue-composables/vite.config.ts new file mode 100644 index 00000000..2bf786ad --- /dev/null +++ b/packages/@necto-vue/necto-vue-composables/vite.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [ + dts({ + insertTypesEntry: true, + rollupTypes: true + }) + ], + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: 'NectoVueComposables', + formats: ['es', 'cjs'], + fileName: (format) => (format === 'es' ? 'index.js' : 'index.cjs') + }, + rollupOptions: { + external: ['vue', '@vueuse/core'], + output: { + globals: { + vue: 'Vue' + } + } + } + } +}); diff --git a/packages/@necto/necto-dom/package.json b/packages/@necto/necto-dom/package.json index 2073ec25..86a94478 100644 --- a/packages/@necto/necto-dom/package.json +++ b/packages/@necto/necto-dom/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "@necto/constants": "workspace:*", + "@necto/file": "workspace:*", "@necto/types": "workspace:*", "@necto/platform": "workspace:*", "html-tags": "^4.0.0" diff --git a/packages/@necto/necto-dom/src/aria-props.ts b/packages/@necto/necto-dom/src/aria-props/index.ts similarity index 92% rename from packages/@necto/necto-dom/src/aria-props.ts rename to packages/@necto/necto-dom/src/aria-props/index.ts index 765e058c..b4d788ef 100644 --- a/packages/@necto/necto-dom/src/aria-props.ts +++ b/packages/@necto/necto-dom/src/aria-props/index.ts @@ -45,12 +45,7 @@ const createAriaPropsMap = (): Record => * }; * ``` */ -export const AriaProps = createAriaPropsMap(); - -/** - * Type representing valid ARIA attribute values (e.g., 'aria-pressed', 'aria-disabled'). - */ -export type AriaAttribute = (typeof DOM.ARIA_ATTRIBUTES)[number]; +export const AriaProps: Record = createAriaPropsMap(); /** * Array of all ARIA attribute values. @@ -94,3 +89,5 @@ export const isAriaAttribute = (prop: string): boolean => */ export const hasAriaPrefix = (prop: string): boolean => prop.startsWith('aria-'); + +export type { AriaAttribute } from './types'; diff --git a/packages/@necto/necto-dom/src/aria-props/types.ts b/packages/@necto/necto-dom/src/aria-props/types.ts new file mode 100644 index 00000000..db9e4875 --- /dev/null +++ b/packages/@necto/necto-dom/src/aria-props/types.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { DOM } from '@necto/constants'; + +/** + * Type representing valid ARIA attribute values (e.g., 'aria-pressed', 'aria-disabled'). + */ +export type AriaAttribute = (typeof DOM.ARIA_ATTRIBUTES)[number]; diff --git a/packages/@necto/necto-dom/src/containment.ts b/packages/@necto/necto-dom/src/containment/index.ts similarity index 81% rename from packages/@necto/necto-dom/src/containment.ts rename to packages/@necto/necto-dom/src/containment/index.ts index ed6ffea5..1ca5b94e 100644 --- a/packages/@necto/necto-dom/src/containment.ts +++ b/packages/@necto/necto-dom/src/containment/index.ts @@ -6,13 +6,15 @@ * */ -import { isNode } from './node'; -import { getOwnerDocument, getOwnerWindow } from './owner'; +import { isNode } from '../node'; +import { getOwnerDocument, getOwnerWindow } from '../owner'; + +import type { ContainmentRect } from './types'; export function getContainmentRect( containment: Element | null | undefined, fallbackElement?: Element | null -): { top: number; left: number; bottom: number; right: number } { +): ContainmentRect { if (containment && isNode(containment)) { const r = (containment as Element).getBoundingClientRect(); return { @@ -33,3 +35,5 @@ export function getContainmentRect( }; } } + +export type { ContainmentRect } from './types'; diff --git a/packages/@necto/necto-dom/src/containment/types.ts b/packages/@necto/necto-dom/src/containment/types.ts new file mode 100644 index 00000000..ad2af384 --- /dev/null +++ b/packages/@necto/necto-dom/src/containment/types.ts @@ -0,0 +1,14 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface ContainmentRect { + top: number; + left: number; + bottom: number; + right: number; +} diff --git a/packages/@necto/necto-dom/src/css.ts b/packages/@necto/necto-dom/src/css.ts new file mode 100644 index 00000000..73e26f24 --- /dev/null +++ b/packages/@necto/necto-dom/src/css.ts @@ -0,0 +1,12 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** Converts a number to CSS pixels, passes strings through unchanged */ +export function toPx(value?: number | string): string | undefined { + if (value === undefined) return undefined; + return typeof value === 'number' ? `${value}px` : value; +} diff --git a/packages/@necto/necto-dom/src/focus.ts b/packages/@necto/necto-dom/src/focus/index.ts similarity index 96% rename from packages/@necto/necto-dom/src/focus.ts rename to packages/@necto/necto-dom/src/focus/index.ts index d52b497d..39e9c611 100644 --- a/packages/@necto/necto-dom/src/focus.ts +++ b/packages/@necto/necto-dom/src/focus/index.ts @@ -13,7 +13,7 @@ * Modifications have been made to adapt the code for use in this project. */ -import { supportsPreventScroll } from './scroll'; +import { supportsPreventScroll } from '../scroll'; import type { ScrollableElement } from './types'; import type { FocusableElement } from '@necto/types'; @@ -79,3 +79,5 @@ export function getScrollableElements( return scrollableElements; } + +export type { ScrollableElement } from './types'; diff --git a/packages/@necto/necto-dom/src/focus/types.ts b/packages/@necto/necto-dom/src/focus/types.ts new file mode 100644 index 00000000..f249678d --- /dev/null +++ b/packages/@necto/necto-dom/src/focus/types.ts @@ -0,0 +1,28 @@ +/** + * Portions of this file are based on code from the React Aria Spectrum library by Adobe, + * licensed under the Apache License, Version 2.0. + * Copyright (c) Adobe. All rights reserved. + * See: https://github.com/adobe/react-spectrum + * + * Modifications copyright (c) Corinvo, LLC. and affiliates. All rights reserved. + * + * This file contains code licensed under: + * - The MIT License (see LICENSE in the root directory) for Corinvo modifications. + * - The Apache License, Version 2.0 for portions from Adobe. + * + * Modifications have been made to adapt the code for use in this project. + */ + +/** + * Represents an element with scroll position information. + */ +export interface ScrollableElement { + /** The HTML element that is scrollable. */ + element: HTMLElement; + + /** The vertical scroll position of the element. */ + scrollTop: number; + + /** The horizontal scroll position of the element. */ + scrollLeft: number; +} diff --git a/packages/@necto/necto-dom/src/html-elements.ts b/packages/@necto/necto-dom/src/html-elements/index.ts similarity index 80% rename from packages/@necto/necto-dom/src/html-elements.ts rename to packages/@necto/necto-dom/src/html-elements/index.ts index 9c9a603e..6155a1cd 100644 --- a/packages/@necto/necto-dom/src/html-elements.ts +++ b/packages/@necto/necto-dom/src/html-elements/index.ts @@ -1,3 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + import { DOM } from '@necto/constants'; import type { HTMLElementsMap } from '@necto/types'; diff --git a/packages/@necto/necto-dom/src/index.ts b/packages/@necto/necto-dom/src/index.ts index b12ce0d1..f5ccfe5d 100644 --- a/packages/@necto/necto-dom/src/index.ts +++ b/packages/@necto/necto-dom/src/index.ts @@ -3,16 +3,18 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * */ -export * from './node'; +export * from './css'; export * from './owner'; -export * from './focus'; export * from './scroll'; +export * from './shadow-dom'; +export * from './node'; +export * from './focus'; +export * from './style'; export * from './containment'; export * from './transitions'; export * from './html-elements'; -export * from './textSelection'; +export * from './text-selection'; export * from './aria-props'; -export * from './style'; +export * from './svg'; diff --git a/packages/@necto/necto-dom/src/node.ts b/packages/@necto/necto-dom/src/node/index.ts similarity index 97% rename from packages/@necto/necto-dom/src/node.ts rename to packages/@necto/necto-dom/src/node/index.ts index b7700f20..c723ce55 100644 --- a/packages/@necto/necto-dom/src/node.ts +++ b/packages/@necto/necto-dom/src/node/index.ts @@ -6,7 +6,7 @@ * */ -import { isShadowRoot } from './shadow-dom'; +import { isShadowRoot } from '../shadow-dom'; export function isNode(value: unknown): value is Node { return ( diff --git a/packages/@necto/necto-dom/src/owner.ts b/packages/@necto/necto-dom/src/owner.ts index d10fcd45..f593c874 100644 --- a/packages/@necto/necto-dom/src/owner.ts +++ b/packages/@necto/necto-dom/src/owner.ts @@ -3,20 +3,22 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * */ -export const getOwnerDocument = (el: Element | null | undefined): Document => { +/** Returns the ownerDocument of an element, or the global document */ +export function getOwnerDocument(el: Element | null | undefined): Document { return el?.ownerDocument ?? document; -}; +} -export const getOwnerWindow = ( +/** Returns the window object that owns an element */ +export function getOwnerWindow( el: (Window & typeof global) | Element | null | undefined -): Window & typeof global => { +): Window & typeof global { if (el && 'window' in el && el.window === el) { return el; } - const doc = getOwnerDocument(el as Element | null | undefined); - return doc.defaultView || window; -}; + return ( + getOwnerDocument(el as Element | null | undefined).defaultView || window + ); +} diff --git a/packages/@necto/necto-dom/src/scroll.ts b/packages/@necto/necto-dom/src/scroll.ts index 0bee44ce..c802dbe3 100644 --- a/packages/@necto/necto-dom/src/scroll.ts +++ b/packages/@necto/necto-dom/src/scroll.ts @@ -13,23 +13,14 @@ * Modifications have been made to adapt the code for use in this project. */ -/** - * Caches the result of the supportsPreventScroll feature detection. - * - * null indicates that the feature has not been checked yet. - */ let supportsPreventScrollCached: boolean | null = null; -/** - * Detects if the browser supports the preventScroll option in the focus() method. - * - * @returns {boolean} True if preventScroll is supported, otherwise false. - */ +/** Detects if the browser supports the preventScroll option in focus() */ export function supportsPreventScroll(): boolean { if (supportsPreventScrollCached == null) { supportsPreventScrollCached = false; try { - const focusElement = document.createElement('div'); + const focusElement: HTMLDivElement = document.createElement('div'); focusElement.focus({ get preventScroll(): boolean { supportsPreventScrollCached = true; @@ -37,7 +28,7 @@ export function supportsPreventScroll(): boolean { } }); } catch { - // Throw Formatted Errors Later + // Ignore } } diff --git a/packages/@necto/necto-dom/src/shadow-dom.ts b/packages/@necto/necto-dom/src/shadow-dom.ts index 6d5b74dc..a1bc578c 100644 --- a/packages/@necto/necto-dom/src/shadow-dom.ts +++ b/packages/@necto/necto-dom/src/shadow-dom.ts @@ -3,14 +3,14 @@ * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. - * */ -import { isNode } from './node'; - export function isShadowRoot(node: Node | null): node is ShadowRoot { return ( - isNode(node) && + node !== null && + typeof node === 'object' && + 'nodeType' in node && + typeof node.nodeType === 'number' && node.nodeType === Node.DOCUMENT_FRAGMENT_NODE && 'host' in node ); diff --git a/packages/@necto/necto-dom/src/style.ts b/packages/@necto/necto-dom/src/style/index.ts similarity index 79% rename from packages/@necto/necto-dom/src/style.ts rename to packages/@necto/necto-dom/src/style/index.ts index 1994eb6b..ca5dbbd7 100644 --- a/packages/@necto/necto-dom/src/style.ts +++ b/packages/@necto/necto-dom/src/style/index.ts @@ -1,16 +1,18 @@ -import { getOwnerDocument } from './owner'; - -export interface CreateStyleElementOptions { - id?: string; - insertionPoint?: HTMLElement | null; -} - -export interface StyleEntry { - element: HTMLStyleElement | null; - count: number; -} - -export type StyleMap = Map; +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { getOwnerDocument } from '../owner'; + +import type { + CreateStyleElementOptions, + InjectStyleOptions, + StyleMap +} from './types'; const STYLE_ATTRIBUTE = 'necto-style-id'; const DEFAULT_ID = 'necto-style'; @@ -49,7 +51,7 @@ export function createStyleElement( export function injectStyle( css: string, - options: CreateStyleElementOptions & { window?: Window | null } = {} + options: InjectStyleOptions = {} ): () => void { const { id = DEFAULT_ID, @@ -90,3 +92,10 @@ export function injectStyle( export function removeStyleElement(element: HTMLStyleElement): void { element.remove(); } + +export type { + CreateStyleElementOptions, + InjectStyleOptions, + StyleEntry, + StyleMap +} from './types'; diff --git a/packages/@necto/necto-dom/src/style/types.ts b/packages/@necto/necto-dom/src/style/types.ts new file mode 100644 index 00000000..4909ef91 --- /dev/null +++ b/packages/@necto/necto-dom/src/style/types.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface CreateStyleElementOptions { + id?: string; + insertionPoint?: HTMLElement | null; +} + +export interface StyleEntry { + element: HTMLStyleElement | null; + count: number; +} + +export type StyleMap = Map; + +export interface InjectStyleOptions extends CreateStyleElementOptions { + window?: Window | null; +} diff --git a/packages/@necto/necto-dom/src/svg/index.ts b/packages/@necto/necto-dom/src/svg/index.ts new file mode 100644 index 00000000..8b1cf207 --- /dev/null +++ b/packages/@necto/necto-dom/src/svg/index.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export { isSvg as isSvgContent, isSvgFast } from '@necto/file'; + +/** + * Injects width and height attributes into an SVG string + */ +export function injectSvgDimensions( + svg: string, + width: string | number = '100%', + height: string | number = '100%' +): string { + return svg.replace( + /]*)>/, + `` + ); +} + +/** + * Checks if an element is an SVG element + */ +export function isSvgElement(element: Element): element is SVGElement { + return element instanceof SVGElement; +} + +/** + * Checks if an element is a specific HTML element type + */ +export function isElementType( + element: Element, + tagName: K +): element is HTMLElementTagNameMap[K] { + return element.tagName.toLowerCase() === tagName.toLowerCase(); +} diff --git a/packages/@necto/necto-dom/src/textSelection.ts b/packages/@necto/necto-dom/src/text-selection/index.ts similarity index 96% rename from packages/@necto/necto-dom/src/textSelection.ts rename to packages/@necto/necto-dom/src/text-selection/index.ts index 0f39d835..2c8c98fe 100644 --- a/packages/@necto/necto-dom/src/textSelection.ts +++ b/packages/@necto/necto-dom/src/text-selection/index.ts @@ -14,13 +14,13 @@ */ import { isIOS } from '@necto/platform'; -import { getOwnerDocument } from './owner'; -import { runAfterTransition } from './transitions'; +import { getOwnerDocument } from '../owner'; +import { runAfterTransition } from '../transitions'; import type { TextSelectionStates } from './types'; +let savedUserSelect: string = ''; let state: TextSelectionStates = 'default'; -let savedUserSelect = ''; const modifiedElementMap = new WeakMap(); /** @@ -111,3 +111,5 @@ export function restoreTextSelection(target?: Element): void { } } } + +export type { TextSelectionStates } from './types'; diff --git a/packages/@necto/necto-dom/src/text-selection/types.ts b/packages/@necto/necto-dom/src/text-selection/types.ts new file mode 100644 index 00000000..f02d1da8 --- /dev/null +++ b/packages/@necto/necto-dom/src/text-selection/types.ts @@ -0,0 +1,19 @@ +/** + * Portions of this file are based on code from the React Aria Spectrum library by Adobe, + * licensed under the Apache License, Version 2.0. + * Copyright (c) Adobe. All rights reserved. + * See: https://github.com/adobe/react-spectrum + * + * Modifications copyright (c) Corinvo, LLC. and affiliates. All rights reserved. + * + * This file contains code licensed under: + * - The MIT License (see LICENSE in the root directory) for Corinvo modifications. + * - The Apache License, Version 2.0 for portions from Adobe. + * + * Modifications have been made to adapt the code for use in this project. + */ + +/** + * Text selection state + */ +export type TextSelectionStates = 'default' | 'disabled' | 'restoring'; diff --git a/packages/@necto/necto-dom/src/transitions.ts b/packages/@necto/necto-dom/src/transitions/index.ts similarity index 100% rename from packages/@necto/necto-dom/src/transitions.ts rename to packages/@necto/necto-dom/src/transitions/index.ts diff --git a/packages/@necto/necto-dom/src/types.ts b/packages/@necto/necto-dom/src/types.ts deleted file mode 100644 index 8ec4cecc..00000000 --- a/packages/@necto/necto-dom/src/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) Corinvo, LLC. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -/** - * Text selection state - */ -export type TextSelectionStates = 'default' | 'disabled' | 'restoring'; - -/** - * Represents an element with scroll position information. - */ -export interface ScrollableElement { - /** The HTML element that is scrollable. */ - element: HTMLElement; - - /** The vertical scroll position of the element. */ - scrollTop: number; - - /** The horizontal scroll position of the element. */ - scrollLeft: number; -} diff --git a/packages/@necto/necto-file/package.json b/packages/@necto/necto-file/package.json new file mode 100644 index 00000000..f68bef7d --- /dev/null +++ b/packages/@necto/necto-file/package.json @@ -0,0 +1,37 @@ +{ + "name": "@necto/file", + "version": "1.0.0", + "type": "module", + "description": "File type detection utilities for Necto", + "scripts": { + "build": "tsup --minify terser", + "dev": "tsup --watch" + }, + "files": [ + "dist" + ], + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "author": "Corinvo OSS Team", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.14.1", + "@types/sax": "^1.2.7", + "terser": "^5.39.0", + "tsup": "^8.5.0", + "typescript": "^5.9.3" + }, + "dependencies": { + "sax": "^1.4.1" + } +} diff --git a/packages/@necto/necto-file/src/index.ts b/packages/@necto/necto-file/src/index.ts new file mode 100644 index 00000000..66a9f0c5 --- /dev/null +++ b/packages/@necto/necto-file/src/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export * from './xml'; +export * from './svg'; diff --git a/packages/@necto/necto-file/src/svg/index.ts b/packages/@necto/necto-file/src/svg/index.ts new file mode 100644 index 00000000..09568bc5 --- /dev/null +++ b/packages/@necto/necto-file/src/svg/index.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { XmlTextDetector } from '../xml'; + +export interface IsSvgOptions { + /** Whether to fully validate the SVG structure (default: true) */ + validate?: boolean; +} + +/** + * Checks if a string contains valid SVG content using SAX parsing + */ +export function isSvg(content: string, options: IsSvgOptions = {}): boolean { + const { validate = true } = options; + + if (typeof content !== 'string') { + throw new TypeError(`Expected a string, got ${typeof content}`); + } + + const trimmed = content.trim(); + if (trimmed.length === 0) return false; + + const detector = new XmlTextDetector({ fullScan: validate }); + + if (validate) { + detector.write(trimmed); + if (!detector.isValid()) return false; + } else { + const chunkSize = 128; + let offset = 0; + + while (trimmed.length > offset && !detector.onEnd) { + detector.write( + trimmed.slice(offset, Math.min(offset + chunkSize, trimmed.length)) + ); + offset += chunkSize; + } + } + + return detector.fileType?.ext === 'svg'; +} + +/** + * Quick check if content looks like SVG (without full validation) + */ +export function isSvgFast(content: string): boolean { + return isSvg(content, { validate: false }); +} diff --git a/packages/@necto/necto-file/src/xml/detector.ts b/packages/@necto/necto-file/src/xml/detector.ts new file mode 100644 index 00000000..6f823c87 --- /dev/null +++ b/packages/@necto/necto-file/src/xml/detector.ts @@ -0,0 +1,184 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import sax from 'sax'; + +import type { + FileTypeResult, + XmlTextDetectorOptions, + XmlDetection, + XmlTextEncoding +} from './types'; + +/** Maps root element namespace to file type */ +const namespaceMapping: Record = { + 'http://www.w3.org/2000/svg': { ext: 'svg', mime: 'image/svg+xml' }, + 'http://www.w3.org/1999/xhtml': { + ext: 'xhtml', + mime: 'application/xhtml+xml' + }, + 'http://www.opengis.net/kml/2.2': { + ext: 'kml', + mime: 'application/vnd.google-earth.kml+xml' + }, + 'http://www.opengis.net/gml': { ext: 'gml', mime: 'application/gml+xml' } +}; + +/** Maps root element name to file type (for non-namespaced XML) */ +const rootNameMapping: Record = { + rss: { ext: 'rss', mime: 'application/rss+xml' }, + 'score-partwise': { + ext: 'musicxml', + mime: 'application/vnd.recordare.musicxml+xml' + }, + svg: { ext: 'svg', mime: 'image/svg+xml' } +}; + +function startsWith( + array: Uint8Array | number[], + prefix: Uint8Array | number[] +): boolean { + if (prefix.length > array.length) return false; + for (let i = 0; i < prefix.length; i++) { + if (array[i] !== prefix[i]) return false; + } + return true; +} + +function hasXmlTag(xmlString: string): boolean { + return /^<\s*\w+(?=\s+[^<>]*=|>)/.test(xmlString); +} + +function hasArrayXmlTag(array: Uint8Array, encoding: string): boolean { + const textDecoder = new TextDecoder(encoding); + return hasXmlTag(textDecoder.decode(array)); +} + +/** Detects if a byte array contains XML and its encoding */ +export function isXml(array: Uint8Array): XmlDetection { + // UTF-8 XML declaration + if (startsWith(array, [60, 63, 120, 109, 108, 32])) { + return { xml: true, encoding: 'utf-8', offset: 0 }; + } + + // UTF-8 BOM + if (startsWith(array, [0xef, 0xbb, 0xbf])) { + const encoding: XmlTextEncoding = 'utf-8'; + if ( + startsWith(array.subarray(3), [60, 63, 120, 109, 108, 32]) || + hasArrayXmlTag(array, encoding) + ) { + return { xml: true, encoding, offset: 3 }; + } + } + + // UTF-16BE BOM + if (startsWith(array, [0xfe, 0xff])) { + const encoding: XmlTextEncoding = 'utf-16be'; + if ( + startsWith( + array.subarray(2), + [0, 60, 0, 63, 0, 120, 0, 109, 0, 108, 0, 32] + ) || + hasArrayXmlTag(array, encoding) + ) { + return { xml: true, encoding, offset: 2 }; + } + } + + // UTF-16LE BOM + if (startsWith(array, [0xff, 0xfe])) { + const encoding: XmlTextEncoding = 'utf-16le'; + if ( + startsWith( + array.subarray(2), + [60, 0, 63, 0, 120, 0, 109, 0, 108, 0, 32, 0] + ) || + hasArrayXmlTag(array, encoding) + ) { + return { xml: true, encoding, offset: 2 }; + } + return { xml: true, encoding: 'utf-16le', offset: 2 }; + } + + // UTF-16BE without BOM + if (startsWith(array, [0, 60, 0, 63, 0, 120, 0, 109, 0, 108, 0, 32])) { + return { xml: true, encoding: 'utf-16be', offset: 0 }; + } + + // UTF-16LE without BOM + if (startsWith(array, [60, 0, 63, 0, 120, 0, 109, 0, 108, 0, 32, 0])) { + return { xml: true, encoding: 'utf-16le', offset: 0 }; + } + + // Fallback: check for XML tag in UTF-8 + if (hasArrayXmlTag(array, 'utf-8')) { + return { xml: true, encoding: 'utf-8', offset: 0 }; + } + + return { xml: false }; +} + +/** SAX-based XML text detector for determining file type from XML content */ +export class XmlTextDetector { + private options: XmlTextDetectorOptions; + private firstTag: boolean; + private parser: sax.SAXParser; + private nesting: number; + + public onEnd: boolean; + public fileType?: FileTypeResult; + + constructor(options?: XmlTextDetectorOptions) { + this.options = options ?? {}; + this.firstTag = true; + this.onEnd = false; + this.nesting = 0; + this.parser = sax.parser(true, { xmlns: true }); + + this.parser.onerror = (e) => { + // Allow entity reference errors + if (e.message.startsWith('Invalid character entity')) return; + this.fileType = undefined; + this.onEnd = true; + }; + + this.parser.onopentag = (node) => { + ++this.nesting; + if (!this.firstTag || this.onEnd) return; + + this.firstTag = false; + + if ((node as sax.QualifiedTag).uri) { + this.fileType = namespaceMapping[(node as sax.QualifiedTag).uri]; + } else if (node.name) { + this.fileType = rootNameMapping[node.name.toLowerCase()]; + } + + if (this.fileType && !this.options.fullScan) { + this.onEnd = true; + } + }; + + this.parser.onclosetag = () => { + --this.nesting; + }; + } + + write(text: string): void { + this.parser.write(text); + } + + close(): void { + this.parser.close(); + this.onEnd = true; + } + + isValid(): boolean { + return this.nesting === 0; + } +} diff --git a/packages/@necto/necto-file/src/xml/index.ts b/packages/@necto/necto-file/src/xml/index.ts new file mode 100644 index 00000000..9001bc26 --- /dev/null +++ b/packages/@necto/necto-file/src/xml/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export * from './types'; +export * from './detector'; diff --git a/packages/@necto/necto-file/src/xml/types.ts b/packages/@necto/necto-file/src/xml/types.ts new file mode 100644 index 00000000..1aa043d5 --- /dev/null +++ b/packages/@necto/necto-file/src/xml/types.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export type XmlTextEncoding = 'utf-8' | 'utf-16be' | 'utf-16le'; + +export interface FileTypeResult { + ext: string; + mime: string; +} + +export interface XmlDetectionResult { + xml: true; + encoding: XmlTextEncoding; + offset: number; +} + +export interface XmlDetectionFailed { + xml: false; +} + +export type XmlDetection = XmlDetectionResult | XmlDetectionFailed; + +export interface XmlTextDetectorOptions { + fullScan?: boolean; +} diff --git a/packages/@necto/necto-file/tsup.config.ts b/packages/@necto/necto-file/tsup.config.ts new file mode 100644 index 00000000..3ccdb578 --- /dev/null +++ b/packages/@necto/necto-file/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + outExtension({ format }) { + return { js: format === 'esm' ? '.mjs' : '.cjs' }; + }, + format: ['cjs', 'esm'], + dts: true, + splitting: false, + clean: true +}); diff --git a/packages/@necto/necto-image/package.json b/packages/@necto/necto-image/package.json new file mode 100644 index 00000000..a3710e41 --- /dev/null +++ b/packages/@necto/necto-image/package.json @@ -0,0 +1,33 @@ +{ + "name": "@necto/image", + "version": "1.0.0", + "description": "Framework-agnostic image transformation utilities for CDN providers", + "scripts": { + "build": "tsup --minify terser" + }, + "author": "Corinvo OSS Team", + "license": "MIT", + "dependencies": { + "@necto/dom": "workspace:*", + "@necto/url": "workspace:*" + }, + "devDependencies": { + "@types/node": "^22.14.1", + "tsup": "^8.4.0" + }, + "files": [ + "dist", + "README.md" + ], + "type": "module", + "types": "./dist/index.d.ts", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + } +} diff --git a/packages/@necto/necto-image/src/index.ts b/packages/@necto/necto-image/src/index.ts new file mode 100644 index 00000000..c83f3eca --- /dev/null +++ b/packages/@necto/necto-image/src/index.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export * from './style'; +export * from './transform'; diff --git a/packages/@necto/necto-image/src/style/index.ts b/packages/@necto/necto-image/src/style/index.ts new file mode 100644 index 00000000..e2a8aad5 --- /dev/null +++ b/packages/@necto/necto-image/src/style/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export * from './style'; + +export type * from './types'; diff --git a/packages/@necto/necto-image/src/style/style.ts b/packages/@necto/necto-image/src/style/style.ts new file mode 100644 index 00000000..f1ef767d --- /dev/null +++ b/packages/@necto/necto-image/src/style/style.ts @@ -0,0 +1,74 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { toPx } from '@necto/dom'; +import { isUrl } from '@necto/url'; + +import type { ImageLayout } from '../transform/types'; +import type { CSSProperties, StyleOptions } from './types'; + +/** Generates CSS styles for an image based on layout */ +export function getImageStyle({ + width, + height, + aspectRatio, + layout, + objectFit, + background +}: StyleOptions): CSSProperties | undefined { + const style: CSSProperties = {}; + + // Only apply object-fit if explicitly set + if (objectFit) { + style['object-fit'] = objectFit; + } + + if (isUrl(background)) { + style['background-image'] = `url(${background})`; + style['background-size'] = 'cover'; + style['background-repeat'] = 'no-repeat'; + } else if (background) { + style.background = background; + } + + // Only apply layout styles if layout is explicitly set or dimensions are provided + if (layout === 'fixed') { + if (width !== undefined) style.width = toPx(width)!; + if (height !== undefined) style.height = toPx(height)!; + } else if (layout === 'constrained') { + style.width = '100%'; + if (width !== undefined) style['max-width'] = toPx(width)!; + if (height !== undefined) style['max-height'] = toPx(height)!; + if (aspectRatio) style['aspect-ratio'] = String(aspectRatio); + } else if (layout === 'fullWidth') { + style.width = '100%'; + if (height !== undefined) style.height = toPx(height)!; + if (aspectRatio) style['aspect-ratio'] = String(aspectRatio); + } + + // Return undefined if no styles were applied + return Object.keys(style).length > 0 ? style : undefined; +} + +/** Generates the HTML sizes attribute for responsive images */ +export function getImageSizes( + width?: number, + layout?: ImageLayout +): string | undefined { + if (!width || !layout) return undefined; + + switch (layout) { + case 'constrained': + return `(min-width: ${width}px) ${width}px, 100vw`; + case 'fixed': + return `${width}px`; + case 'fullWidth': + return '100vw'; + default: + return undefined; + } +} diff --git a/packages/@necto/necto-image/src/style/types.ts b/packages/@necto/necto-image/src/style/types.ts new file mode 100644 index 00000000..5c91fcbc --- /dev/null +++ b/packages/@necto/necto-image/src/style/types.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { ImageLayout } from '../transform/types'; + +/** CSS properties object */ +export type CSSProperties = Record; + +/** Options for generating image styles */ +export interface StyleOptions { + width?: number | string; + height?: number | string; + aspectRatio?: number; + layout?: ImageLayout; + objectFit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; + background?: string; +} + +// Backwards compatibility alias +export type ImageStyleProps = StyleOptions; diff --git a/packages/@necto/necto-image/src/transform/index.ts b/packages/@necto/necto-image/src/transform/index.ts new file mode 100644 index 00000000..b179b8fe --- /dev/null +++ b/packages/@necto/necto-image/src/transform/index.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export * from './srcset'; +export * from './props'; + +export type * from './types'; diff --git a/packages/@necto/necto-image/src/transform/props.ts b/packages/@necto/necto-image/src/transform/props.ts new file mode 100644 index 00000000..d8c8bd11 --- /dev/null +++ b/packages/@necto/necto-image/src/transform/props.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { getImageStyle } from '../style'; + +import type { ImageAttributes, TransformOptions } from './types'; + +/** Transforms image props into img element attributes */ +export function transformProps(props: TransformOptions): ImageAttributes { + const { + src, + width, + height, + aspectRatio, + layout, + priority = false, + background, + objectFit, + unstyled = false + } = props; + + const result: ImageAttributes = { + src, + loading: priority ? 'eager' : 'lazy', + decoding: priority ? 'sync' : 'async', + fetchpriority: priority ? 'high' : undefined + }; + + // Generate style if not unstyled + if (!unstyled) { + const style = getImageStyle({ + width, + height, + aspectRatio, + layout, + objectFit, + background + }); + if (style) { + result.style = style; + } + } + + // Set dimensions based on layout + if (layout === 'fixed') { + result.width = width; + result.height = height; + } + + return result; +} diff --git a/packages/@necto/necto-image/src/transform/srcset.ts b/packages/@necto/necto-image/src/transform/srcset.ts new file mode 100644 index 00000000..cba54821 --- /dev/null +++ b/packages/@necto/necto-image/src/transform/srcset.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** Parses a dimension value to a number */ +export function parseDimension( + value: number | string | undefined +): number | undefined { + if (value === undefined) return undefined; + if (typeof value === 'number') return value; + const parsed: number = Number.parseInt(value, 10); + + return Number.isNaN(parsed) ? undefined : parsed; +} + +/** Infers width/height from props and aspect ratio */ +export function inferDimensions(props: { + width?: number | string; + height?: number | string; + aspectRatio?: number; +}): { width?: number; height?: number } { + let width: number | undefined = parseDimension(props.width); + let height: number | undefined = parseDimension(props.height); + + if (props.aspectRatio) { + if (width && !height) { + height = Math.round(width / props.aspectRatio); + } else if (height && !width) { + width = Math.round(height * props.aspectRatio); + } + } + + return { width, height }; +} diff --git a/packages/@necto/necto-image/src/transform/types.ts b/packages/@necto/necto-image/src/transform/types.ts new file mode 100644 index 00000000..3949b868 --- /dev/null +++ b/packages/@necto/necto-image/src/transform/types.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { CSSProperties } from '../style/types'; + +/** HTML image element attributes (output of transform) */ +export interface ImageAttributes { + src?: string; + srcset?: string; + sizes?: string; + width?: number | string; + height?: number | string; + alt?: string; + loading?: 'lazy' | 'eager'; + decoding?: 'sync' | 'async' | 'auto'; + fetchpriority?: 'high' | 'low' | 'auto'; + style?: CSSProperties; +} + +/** Image layout mode */ +export type ImageLayout = 'fixed' | 'constrained' | 'fullWidth'; + +/** CSS object-fit mode */ +export type ObjectFit = 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'; + +/** Options for the transform function */ +export interface TransformOptions { + src: string; + width?: number | string; + height?: number | string; + aspectRatio?: number; + layout?: ImageLayout; + priority?: boolean; + background?: string; + objectFit?: ObjectFit; + unstyled?: boolean; +} diff --git a/packages/@necto/necto-image/tsup.config.ts b/packages/@necto/necto-image/tsup.config.ts new file mode 100644 index 00000000..a4bd57ae --- /dev/null +++ b/packages/@necto/necto-image/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig([ + { + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + sourcemap: false, + splitting: false, + clean: true, + cjsInterop: true, + platform: 'neutral', + outExtension({ format }) { + return { + js: format === 'cjs' ? '.cjs' : '.mjs' + }; + } + } +]); diff --git a/packages/@necto/necto-popper/src/core/computePosition.ts b/packages/@necto/necto-popper/src/core/computePosition.ts index bae2c93e..4a8f5ef8 100644 --- a/packages/@necto/necto-popper/src/core/computePosition.ts +++ b/packages/@necto/necto-popper/src/core/computePosition.ts @@ -1,28 +1,30 @@ /** - * Core positioning computation - * Pure functional approach - no classes needed! + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ import { DEFAULT_OPTIONS } from '../types'; import { getElementRects } from './getElementRects'; import { computeCoords } from '../utils/getPlacementCoords'; -import type { ComputePositionResult, ComputePositionOptions } from '../types'; +import type { + Placement, + Coordinates, + ElementRects, + MiddlewareResult, + ComputePositionResult, + ComputePositionOptions +} from '../types'; /** * Computes the position of a floating element relative to a reference element. - * - * This is the main entry point - a pure function that: - * 1. Gets element measurements - * 2. Computes initial position based on placement - * 3. Applies middleware (modifiers) in sequence - * 4. Returns final coordinates - * - * @example - * const position = await computePosition(button, tooltip, { - * placement: 'top', - * middleware: [offset(8), flip(), shift()] - * }); + * @param reference - The reference element to position against. + * @param floating - The floating element to be positioned. + * @param options - Configuration options for placement, strategy, and middleware. + * @returns A promise resolving to the computed position result. */ export async function computePosition( reference: Element, @@ -35,18 +37,13 @@ export async function computePosition( middleware = [] } = options; - // 1. Get element measurements - const rects = getElementRects(reference, floating); - - // 2. Compute initial coordinates based on placement + const rects: ElementRects = getElementRects(reference, floating); let { x, y } = computeCoords(placement, rects); - - let currentPlacement = placement; + let currentPlacement: Placement = placement; const middlewareData: Record = {}; - // 3. Apply middleware in sequence (functional pipeline pattern) for (const mw of middleware) { - const result = await mw.fn({ + const result: MiddlewareResult = await mw.fn({ x, y, placement: currentPlacement, @@ -55,25 +52,21 @@ export async function computePosition( elements: { reference, floating } }); - // Middleware can modify x, y, or even change placement if (result.x !== undefined) x = result.x; if (result.y !== undefined) y = result.y; if (result.placement !== undefined) currentPlacement = result.placement; - // Store middleware data if (result.data) { middlewareData[mw.name] = result.data; } - // Reset allows middleware to restart the pipeline (e.g., flip changed placement) if (result.reset) { - const newCoords = computeCoords(currentPlacement, rects); + const newCoords: Coordinates = computeCoords(currentPlacement, rects); x = newCoords.x; y = newCoords.y; } } - // 4. Return final result return { x, y, diff --git a/packages/@necto/necto-popper/src/core/getElementRects.ts b/packages/@necto/necto-popper/src/core/getElementRects.ts index 7a4a6b23..355fea05 100644 --- a/packages/@necto/necto-popper/src/core/getElementRects.ts +++ b/packages/@necto/necto-popper/src/core/getElementRects.ts @@ -1,15 +1,20 @@ /** - * Get element bounding rectangles + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. * - * Uses @necto/dom utilities - reusing existing code! */ import { isNode } from '@necto/dom'; + import type { ElementRects, Rect } from '../types'; /** * Gets the bounding rectangles for reference and floating elements. - * Pure function - just reads DOM and returns data. + * @param reference - The reference element. + * @param floating - The floating element. + * @returns The bounding rectangles for both elements. */ export function getElementRects( reference: Element, @@ -22,12 +27,11 @@ export function getElementRects( } /** - * Converts a DOMRect to our Rect type - * - * Uses @necto/dom's isNode for validation + * Converts a DOMRect to a Rect type. + * @param element - The element to get the bounding rect from. + * @returns The element's bounding rectangle. */ function getRectFromElement(element: Element): Rect { - // Validate element using @necto/dom utility if (!isNode(element)) { throw new Error('Invalid element provided to getRectFromElement'); } diff --git a/packages/@necto/necto-popper/src/index.ts b/packages/@necto/necto-popper/src/index.ts index 54c6c2db..6b960c7f 100644 --- a/packages/@necto/necto-popper/src/index.ts +++ b/packages/@necto/necto-popper/src/index.ts @@ -1,27 +1,17 @@ /** - * @necto/popper + * Copyright (c) Corinvo, LLC. and affiliates. * - * Core positioning engine for popovers, tooltips, dropdowns, and floating elements. - * Framework-agnostic positioning utilities. + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. * - * Built on top of @necto/dom for robust DOM manipulation. */ -// ============================================================================ -// TYPES - Comprehensive type system for positioning -// ============================================================================ export type * from './types'; -export * from './types'; // Export utilities and constants +export * from './types'; -// ============================================================================ -// CORE - Main positioning engine -// ============================================================================ export { computePosition } from './core/computePosition'; export { getElementRects } from './core/getElementRects'; -// ============================================================================ -// MIDDLEWARE - Composable position modifiers -// ============================================================================ export { offset } from './middlewares/offset'; export type { OffsetOptions } from './middlewares/offset'; @@ -31,15 +21,23 @@ export type { FlipOptions } from './middlewares/flip'; export { shift } from './middlewares/shift'; export type { ShiftOptions } from './middlewares/shift'; -// ============================================================================ -// UTILITIES - Helper functions -// ============================================================================ +export { arrow } from './middlewares/arrow'; +export type { ArrowOptions } from './middlewares/arrow'; + +export { size } from './middlewares/size'; +export type { SizeOptions } from './middlewares/size'; + +export { autoPlacement } from './middlewares/autoPlacement'; +export type { AutoPlacementOptions } from './middlewares/autoPlacement'; + +export { hide } from './middlewares/hide'; +export type { HideOptions } from './middlewares/hide'; + export { computeCoords } from './utils/getPlacementCoords'; export { detectOverflow, hasOverflow } from './utils/detectOverflow'; +export { autoUpdate } from './utils/autoUpdate'; +export type { AutoUpdateOptions } from './utils/autoUpdate'; -// ============================================================================ -// RE-EXPORTS - Commonly used @necto/dom utilities for convenience -// ============================================================================ export { getContainmentRect, isNode, diff --git a/packages/@necto/necto-popper/src/middlewares/arrow.ts b/packages/@necto/necto-popper/src/middlewares/arrow.ts new file mode 100644 index 00000000..2e6b636f --- /dev/null +++ b/packages/@necto/necto-popper/src/middlewares/arrow.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { createMiddleware, getSide, clamp } from '../types'; + +import type { Middleware } from '../types'; + +export interface ArrowOptions { + /** + * The arrow element to position. + */ + element: HTMLElement | null; + + /** + * Padding from the edges of the floating element. + * @default 0 + */ + padding?: number; +} + +/** + * Creates an arrow middleware that positions an arrow element. + * @param options - The arrow element and padding options. + * @returns A middleware that computes arrow positioning data. + */ +export function arrow(options: ArrowOptions): Middleware { + const { element, padding = 0 } = options; + + return createMiddleware('arrow', (state) => { + if (!element) { + return {}; + } + + const { x, y, placement, rects } = state; + const side = getSide(placement); + const isVertical = side === 'top' || side === 'bottom'; + + const arrowRect = element.getBoundingClientRect(); + const arrowLength = isVertical ? arrowRect.width : arrowRect.height; + + const floatingLength = isVertical + ? rects.floating.width + : rects.floating.height; + const referenceLength = isVertical + ? rects.reference.width + : rects.reference.height; + + const minPadding = padding; + const maxPadding = floatingLength - arrowLength - padding; + + const referenceCenter = isVertical + ? rects.reference.x + rects.reference.width / 2 + : rects.reference.y + rects.reference.height / 2; + + const floatingStart = isVertical ? x : y; + + const center = referenceCenter - floatingStart - arrowLength / 2; + const arrowOffset = clamp(center, minPadding, maxPadding); + + const shouldCenter = referenceLength < floatingLength; + + return { + data: { + x: isVertical ? arrowOffset : undefined, + y: isVertical ? undefined : arrowOffset, + centerOffset: shouldCenter ? center - arrowOffset : 0 + } + }; + }); +} diff --git a/packages/@necto/necto-popper/src/middlewares/autoPlacement.ts b/packages/@necto/necto-popper/src/middlewares/autoPlacement.ts new file mode 100644 index 00000000..dfb7f2e1 --- /dev/null +++ b/packages/@necto/necto-popper/src/middlewares/autoPlacement.ts @@ -0,0 +1,113 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { detectOverflow } from '../utils/detectOverflow'; +import { createMiddleware, getSide, getAlignment } from '../types'; + +import type { + Middleware, + Placement, + Side, + Alignment, + BoundaryOptions +} from '../types'; + +export interface AutoPlacementOptions extends BoundaryOptions { + /** + * Allowed placements to choose from. + */ + allowedPlacements?: Placement[]; + + /** + * Whether to also auto-align. + * @default true + */ + autoAlignment?: boolean; +} + +const ALL_SIDES: Side[] = ['top', 'right', 'bottom', 'left']; +const ALL_ALIGNMENTS: Array = [ + undefined, + 'start', + 'end' +]; + +/** + * Creates an auto-placement middleware that picks the best placement. + * @param options - Configuration options. + * @returns A middleware that automatically selects optimal placement. + */ +export function autoPlacement(options: AutoPlacementOptions = {}): Middleware { + const { + allowedPlacements, + autoAlignment = true, + ...detectOverflowOptions + } = options; + + return createMiddleware('autoPlacement', (state) => { + const { rects, placement } = state; + const currentSide = getSide(placement); + const currentAlignment = getAlignment(placement); + + const placements: Placement[] = + allowedPlacements ?? + (autoAlignment + ? ALL_SIDES.flatMap((side) => + ALL_ALIGNMENTS.map((alignment) => + alignment ? (`${side}-${alignment}` as Placement) : side + ) + ) + : ALL_SIDES); + + const overflows: Array<{ placement: Placement; overflow: number }> = []; + + for (const p of placements) { + const overflow = detectOverflow(rects.floating, detectOverflowOptions); + const side = getSide(p); + + let overflowAmount = 0; + switch (side) { + case 'top': + overflowAmount = overflow.top; + break; + case 'bottom': + overflowAmount = overflow.bottom; + break; + case 'left': + overflowAmount = overflow.left; + break; + case 'right': + overflowAmount = overflow.right; + break; + } + + overflows.push({ placement: p, overflow: overflowAmount }); + } + + const sorted = overflows.sort((a, b) => a.overflow - b.overflow); + const bestPlacement = sorted[0]?.placement ?? placement; + + if (bestPlacement !== placement) { + return { + placement: bestPlacement, + reset: true, + data: { + placements: sorted.map((o) => o.placement), + overflows: sorted.map((o) => o.overflow) + } + }; + } + + return { + data: { + placements: sorted.map((o) => o.placement), + overflows: sorted.map((o) => o.overflow) + } + }; + }); +} diff --git a/packages/@necto/necto-popper/src/middlewares/flip.ts b/packages/@necto/necto-popper/src/middlewares/flip.ts index 920485b6..e5a57df7 100644 --- a/packages/@necto/necto-popper/src/middlewares/flip.ts +++ b/packages/@necto/necto-popper/src/middlewares/flip.ts @@ -1,10 +1,12 @@ /** - * Flip middleware - flips placement when there's not enough space - * Example of a more complex middleware + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ import { detectOverflow, hasOverflow } from '../utils/detectOverflow'; -import type { Middleware, Placement, BoundaryOptions } from '../types'; import { createMiddleware, getSide, @@ -12,42 +14,35 @@ import { getOppositeSide } from '../types'; +import type { Middleware, Placement, BoundaryOptions } from '../types'; + export interface FlipOptions extends BoundaryOptions { /** - * Fallback placements to try if the main placement doesn't fit - * If not provided, will just flip to the opposite side + * Fallback placements to try if the main placement doesn't fit. */ fallbackPlacements?: Placement[]; } /** * Creates a flip middleware that changes placement when overflowing. - * - * Uses @necto/dom's getContainmentRect under the hood! - * - * @example - * computePosition(ref, floating, { - * middleware: [flip()] // Auto-flips if no space - * }); + * @param options - Configuration options for flip behavior. + * @returns A middleware that flips placement when there's insufficient space. */ export function flip(options: FlipOptions = {}): Middleware { return createMiddleware('flip', (state) => { const { placement, rects } = state; const floatingRect = rects.floating; - // Check if current placement causes overflow using our utility const overflow = detectOverflow(floatingRect, options); const isOverflowing = hasOverflow(floatingRect, options); if (!isOverflowing) { - return {}; // No changes needed + return {}; } - // Determine which side has the most overflow const side = getSide(placement); const alignment = getAlignment(placement); - // Check if we should flip to the opposite side let shouldFlip = false; switch (side) { @@ -69,7 +64,6 @@ export function flip(options: FlipOptions = {}): Middleware { return {}; } - // Flip to opposite side const oppositeSide = getOppositeSide(side); const oppositePlacement: Placement = alignment ? `${oppositeSide}-${alignment}` @@ -77,7 +71,7 @@ export function flip(options: FlipOptions = {}): Middleware { return { placement: oppositePlacement, - reset: true, // Tell computePosition to recalculate coords + reset: true, data: { flipped: true, originalPlacement: placement } }; }); diff --git a/packages/@necto/necto-popper/src/middlewares/hide.ts b/packages/@necto/necto-popper/src/middlewares/hide.ts new file mode 100644 index 00000000..049d7b65 --- /dev/null +++ b/packages/@necto/necto-popper/src/middlewares/hide.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { detectOverflow } from '../utils/detectOverflow'; +import { createMiddleware } from '../types'; + +import type { Middleware, BoundaryOptions } from '../types'; + +export interface HideOptions extends BoundaryOptions { + /** + * The strategy to use for hiding. + * - 'referenceHidden': Hide when the reference is fully clipped. + * - 'escaped': Hide when the floating element has escaped the boundary. + * @default 'referenceHidden' + */ + strategy?: 'referenceHidden' | 'escaped'; +} + +/** + * Creates a hide middleware that determines visibility state. + * @param options - Configuration options. + * @returns A middleware that computes hide data. + */ +export function hide(options: HideOptions = {}): Middleware { + const { strategy = 'referenceHidden', ...detectOverflowOptions } = options; + + return createMiddleware('hide', (state) => { + const { rects, elements } = state; + + if (strategy === 'referenceHidden') { + const referenceRect = elements.reference.getBoundingClientRect(); + const overflow = detectOverflow( + { + x: referenceRect.x, + y: referenceRect.y, + width: referenceRect.width, + height: referenceRect.height + }, + detectOverflowOptions + ); + + const referenceHidden = + overflow.top >= referenceRect.height || + overflow.bottom >= referenceRect.height || + overflow.left >= referenceRect.width || + overflow.right >= referenceRect.width; + + return { + data: { + referenceHidden, + referenceHiddenOffsets: overflow + } + }; + } + + const overflow = detectOverflow(rects.floating, detectOverflowOptions); + const escaped = + overflow.top < 0 || + overflow.bottom < 0 || + overflow.left < 0 || + overflow.right < 0; + + return { + data: { + escaped, + escapedOffsets: overflow + } + }; + }); +} diff --git a/packages/@necto/necto-popper/src/middlewares/offset.ts b/packages/@necto/necto-popper/src/middlewares/offset.ts index 4df9f276..958c7bc4 100644 --- a/packages/@necto/necto-popper/src/middlewares/offset.ts +++ b/packages/@necto/necto-popper/src/middlewares/offset.ts @@ -1,33 +1,30 @@ /** - * Offset middleware - adds distance between reference and floating element - * Example of a simple middleware function + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ -import type { Middleware } from '../types'; import { createMiddleware, getSide } from '../types'; +import type { Middleware } from '../types'; + export interface OffsetOptions { /** - * The offset distance + * The offset distance in pixels. */ value: number; } /** - * Creates an offset middleware. - * This is a "middleware factory" - a function that returns a middleware function. - * - * Pattern: Configuration → Middleware Function → Result - * - * @example - * computePosition(ref, floating, { - * middleware: [offset({ value: 10 })] // Adds 10px gap - * }); + * Creates an offset middleware that adds distance between reference and floating elements. + * @param options - The offset value or options object. + * @returns A middleware that applies the specified offset. */ export function offset(options: OffsetOptions | number): Middleware { const value = typeof options === 'number' ? options : options.value; - // Return the actual middleware using createMiddleware helper return createMiddleware('offset', (state) => { const { x, y, placement } = state; const side = getSide(placement); @@ -35,7 +32,6 @@ export function offset(options: OffsetOptions | number): Middleware { let newX = x; let newY = y; - // Apply offset based on side switch (side) { case 'top': newY -= value; diff --git a/packages/@necto/necto-popper/src/middlewares/shift.ts b/packages/@necto/necto-popper/src/middlewares/shift.ts index 4dcae34f..2e716230 100644 --- a/packages/@necto/necto-popper/src/middlewares/shift.ts +++ b/packages/@necto/necto-popper/src/middlewares/shift.ts @@ -1,33 +1,33 @@ /** - * Shift middleware - shifts the floating element to stay in view + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ import { detectOverflow } from '../utils/detectOverflow'; -import type { Middleware, BoundaryOptions } from '../types'; import { createMiddleware } from '../types'; +import type { Middleware, BoundaryOptions } from '../types'; + export interface ShiftOptions extends BoundaryOptions { /** - * Maximum distance to shift (prevents shifting too far) + * Maximum distance to shift in pixels. */ maxShift?: number; /** - * Which axes to shift on + * Which axes to shift on. * @default 'both' */ axis?: 'x' | 'y' | 'both'; } /** - * Creates a shift middleware that keeps element within viewport. - * - * Uses @necto/dom's getContainmentRect for boundary detection! - * - * @example - * computePosition(ref, floating, { - * middleware: [shift({ padding: 8 })] - * }); + * Creates a shift middleware that keeps the floating element within the viewport. + * @param options - Configuration options for shift behavior. + * @returns A middleware that shifts position to prevent overflow. */ export function shift(options: ShiftOptions = {}): Middleware { return createMiddleware('shift', (state) => { @@ -35,7 +35,6 @@ export function shift(options: ShiftOptions = {}): Middleware { const { maxShift, axis = 'both' } = options; const floatingRect = rects.floating; - // Detect overflow using our utility (which uses @necto/dom!) const overflow = detectOverflow( { ...floatingRect, x, y }, { boundary: options.boundary, padding: options.padding } @@ -44,7 +43,6 @@ export function shift(options: ShiftOptions = {}): Middleware { let newX = x; let newY = y; - // Shift horizontally if needed and allowed if (axis === 'x' || axis === 'both') { if (overflow.left > 0) { const shiftAmount = Math.min(overflow.left, maxShift ?? overflow.left); @@ -58,7 +56,6 @@ export function shift(options: ShiftOptions = {}): Middleware { } } - // Shift vertically if needed and allowed if (axis === 'y' || axis === 'both') { if (overflow.top > 0) { const shiftAmount = Math.min(overflow.top, maxShift ?? overflow.top); @@ -72,7 +69,6 @@ export function shift(options: ShiftOptions = {}): Middleware { } } - // Only return new coordinates if they changed if (newX !== x || newY !== y) { return { x: newX, diff --git a/packages/@necto/necto-popper/src/middlewares/size.ts b/packages/@necto/necto-popper/src/middlewares/size.ts new file mode 100644 index 00000000..da8b9b8f --- /dev/null +++ b/packages/@necto/necto-popper/src/middlewares/size.ts @@ -0,0 +1,75 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { detectOverflow } from '../utils/detectOverflow'; +import { createMiddleware, getSide } from '../types'; + +import type { Middleware, BoundaryOptions } from '../types'; + +export interface SizeOptions extends BoundaryOptions { + /** + * Function to apply size styles to the floating element. + */ + apply?: (args: { + availableWidth: number; + availableHeight: number; + elements: { reference: Element; floating: HTMLElement }; + }) => void; +} + +/** + * Creates a size middleware that constrains the floating element dimensions. + * @param options - Configuration options for size constraints. + * @returns A middleware that computes available space. + */ +export function size(options: SizeOptions = {}): Middleware { + const { apply, ...detectOverflowOptions } = options; + + return createMiddleware('size', (state) => { + const { x, y, rects, elements, placement } = state; + const side = getSide(placement); + + const overflow = detectOverflow( + { ...rects.floating, x, y }, + detectOverflowOptions + ); + + const availableHeight = + side === 'top' + ? rects.reference.y - overflow.top + : side === 'bottom' + ? window.innerHeight - + rects.reference.y - + rects.reference.height - + overflow.bottom + : rects.floating.height - Math.max(overflow.top, overflow.bottom); + + const availableWidth = + side === 'left' + ? rects.reference.x - overflow.left + : side === 'right' + ? window.innerWidth - + rects.reference.x - + rects.reference.width - + overflow.right + : rects.floating.width - Math.max(overflow.left, overflow.right); + + apply?.({ + availableWidth: Math.max(0, availableWidth), + availableHeight: Math.max(0, availableHeight), + elements + }); + + return { + data: { + availableWidth, + availableHeight + } + }; + }); +} diff --git a/packages/@necto/necto-popper/src/types/geometry.ts b/packages/@necto/necto-popper/src/types/geometry.ts index 4f38c337..90665113 100644 --- a/packages/@necto/necto-popper/src/types/geometry.ts +++ b/packages/@necto/necto-popper/src/types/geometry.ts @@ -1,56 +1,33 @@ /** - * Geometry and boundary types for positioning calculations - * Consolidated for simplicity - boundaries are geometric concepts + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ -// ============================================================================ -// BASIC GEOMETRY -// ============================================================================ - -/** - * 2D coordinates - */ export interface Coordinates { x: number; y: number; } -/** - * Element dimensions - */ export interface Dimensions { width: number; height: number; } -/** - * A rectangle with position and dimensions - */ export interface Rect extends Coordinates, Dimensions {} -/** - * Bounding rectangles for reference and floating elements - */ export interface ElementRects { readonly reference: Rect; readonly floating: Rect; } -/** - * The actual HTML elements being positioned - */ export interface Elements { readonly reference: Element; readonly floating: HTMLElement; } -// ============================================================================ -// PADDING & OVERFLOW -// ============================================================================ - -/** - * Padding configuration - can be a number or per-side object - */ export type Padding = | number | Partial<{ @@ -61,7 +38,9 @@ export type Padding = }>; /** - * Resolve padding to all four sides + * Resolves padding to all four sides. + * @param padding - The padding value or object. + * @returns Resolved padding for all sides. */ export function resolvePadding(padding: Padding = 0): Required<{ top: number; @@ -86,9 +65,6 @@ export function resolvePadding(padding: Padding = 0): Required<{ }; } -/** - * Overflow amounts on each side - */ export interface OverflowData { readonly top: number; readonly right: number; @@ -97,7 +73,9 @@ export interface OverflowData { } /** - * Check if there's any overflow + * Checks if there's any overflow on any side. + * @param overflow - The overflow data to check. + * @returns True if any side has overflow. */ export function hasAnyOverflow(overflow: OverflowData): boolean { return ( @@ -108,46 +86,33 @@ export function hasAnyOverflow(overflow: OverflowData): boolean { ); } -// ============================================================================ -// BOUNDARY (merged from boundary.ts) -// ============================================================================ - -/** - * Root boundary - can be viewport, document, or a specific element - */ export type RootBoundary = 'viewport' | 'document' | Element; -/** - * Boundary options for overflow detection - * Used by flip(), shift(), and other middleware - */ export interface BoundaryOptions { /** - * The boundary element to check overflow against - * If undefined, uses the viewport + * The boundary element to check overflow against. */ boundary?: Element; /** - * Padding around the boundary - * Can be a single number or per-side values + * Padding around the boundary. * @default 0 */ padding?: Padding; /** - * The root boundary to use + * The root boundary to use. * @default 'viewport' */ rootBoundary?: RootBoundary; } -// ============================================================================ -// UTILITIES -// ============================================================================ - /** - * Clamp a value between min and max + * Clamps a value between min and max. + * @param value - The value to clamp. + * @param min - The minimum value. + * @param max - The maximum value. + * @returns The clamped value. */ export function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(max, value)); diff --git a/packages/@necto/necto-popper/src/types/index.ts b/packages/@necto/necto-popper/src/types/index.ts index 510667fb..eb552672 100644 --- a/packages/@necto/necto-popper/src/types/index.ts +++ b/packages/@necto/necto-popper/src/types/index.ts @@ -1,17 +1,12 @@ /** - * Central export for all types - * Organized and consolidated for clarity + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ -// ============================================================================ -// PLACEMENT - Placement-related types and utilities -// ============================================================================ -export type { - Side, - Alignment, - Placement, - Strategy -} from './placement'; +export type { Side, Alignment, Placement, Strategy } from './placement'; export { isSide, @@ -24,9 +19,6 @@ export { getOppositeAlignment } from './placement'; -// ============================================================================ -// GEOMETRY - Geometric types, boundaries, padding, overflow -// ============================================================================ export type { Coordinates, Dimensions, @@ -36,18 +28,11 @@ export type { Padding, OverflowData, RootBoundary, - BoundaryOptions // Moved from boundary.ts + BoundaryOptions } from './geometry'; -export { - resolvePadding, - hasAnyOverflow, - clamp -} from './geometry'; +export { resolvePadding, hasAnyOverflow, clamp } from './geometry'; -// ============================================================================ -// MIDDLEWARE - Middleware system types -// ============================================================================ export type { MiddlewareState, MiddlewareResult, @@ -59,12 +44,6 @@ export type { export { createMiddleware } from './middleware'; -// ============================================================================ -// OPTIONS - Main API options and results -// ============================================================================ -export type { - ComputePositionOptions, - ComputePositionResult -} from './options'; +export type { ComputePositionOptions, ComputePositionResult } from './options'; export { DEFAULT_OPTIONS } from './options'; diff --git a/packages/@necto/necto-popper/src/types/middleware.ts b/packages/@necto/necto-popper/src/types/middleware.ts index f8b1d1c7..ce864f24 100644 --- a/packages/@necto/necto-popper/src/types/middleware.ts +++ b/packages/@necto/necto-popper/src/types/middleware.ts @@ -1,15 +1,14 @@ /** - * Middleware system types - * Advanced TypeScript patterns for composable positioning modifiers + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ import type { Placement, Strategy } from './placement'; import type { ElementRects, Elements } from './geometry'; -/** - * The state passed to middleware functions - * Readonly to prevent mutations - pure functional approach - */ export interface MiddlewareState { readonly x: number; readonly y: number; @@ -19,58 +18,37 @@ export interface MiddlewareState { readonly elements: Elements; } -/** - * The result returned by middleware functions - * All properties are optional - only specify what you want to change - */ export interface MiddlewareResult { - /** New x coordinate (if changed) */ x?: number; - /** New y coordinate (if changed) */ y?: number; - /** New placement (if changed, e.g., after flipping) */ placement?: Placement; - /** If true, restart the middleware pipeline from the beginning */ reset?: boolean; - /** Additional data to pass to other middleware */ data?: Record; } -/** - * Middleware function type - * Pure function: takes state, returns modifications - */ export type MiddlewareFn = ( state: MiddlewareState ) => MiddlewareResult | Promise; -/** - * Middleware with metadata - */ export interface Middleware { - /** The name of this middleware (for debugging) */ readonly name: string; - /** The middleware function */ readonly fn: MiddlewareFn; } /** - * Helper to create named middleware + * Creates a named middleware. + * @param name - The middleware name for debugging. + * @param fn - The middleware function. + * @returns A middleware object. */ export function createMiddleware(name: string, fn: MiddlewareFn): Middleware { return { name, fn }; } -/** - * Type for middleware factory functions - */ export type MiddlewareFactory = TOptions extends void ? () => Middleware : (options: TOptions) => Middleware; -/** - * Utility type to extract options type from a middleware factory - */ export type MiddlewareOptions = T extends MiddlewareFactory ? O : never; diff --git a/packages/@necto/necto-popper/src/types/options.ts b/packages/@necto/necto-popper/src/types/options.ts index 028e7174..7f395cbf 100644 --- a/packages/@necto/necto-popper/src/types/options.ts +++ b/packages/@necto/necto-popper/src/types/options.ts @@ -1,67 +1,42 @@ /** - * Configuration options for computePosition + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ import type { Placement, Strategy } from './placement'; import type { Middleware } from './middleware'; -/** - * Options for computing position - */ export interface ComputePositionOptions { /** - * Where to place the floating element relative to the reference + * Where to place the floating element relative to the reference. * @default 'bottom' */ placement?: Placement; /** - * The CSS position property to use + * The CSS position property to use. * @default 'absolute' */ strategy?: Strategy; /** - * Array of middleware to apply - * Middleware functions are applied in order + * Array of middleware to apply in order. * @default [] */ middleware?: Middleware[]; } -/** - * The result of computing position - */ export interface ComputePositionResult { - /** - * The x coordinate for the floating element - */ x: number; - - /** - * The y coordinate for the floating element - */ y: number; - - /** - * The final placement (may differ from initial if flipped) - */ placement: Placement; - - /** - * The strategy used - */ strategy: Strategy; - - /** - * Additional data from middleware - */ middlewareData?: Record; } -/** - * Default values for options - */ export const DEFAULT_OPTIONS: Required< Omit > = { diff --git a/packages/@necto/necto-popper/src/types/placement.ts b/packages/@necto/necto-popper/src/types/placement.ts index c1fe4083..1a7ab842 100644 --- a/packages/@necto/necto-popper/src/types/placement.ts +++ b/packages/@necto/necto-popper/src/types/placement.ts @@ -1,45 +1,41 @@ /** - * Placement and positioning types - * Using advanced TypeScript for type safety + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ -/** - * The four primary sides where an element can be placed - */ export type Side = 'top' | 'right' | 'bottom' | 'left'; -/** - * Alignment within a side - */ export type Alignment = 'start' | 'end'; -/** - * All possible placements using template literal types - * Generates: 'top', 'top-start', 'top-end', 'bottom', 'bottom-start', etc. - */ export type Placement = Side | `${Side}-${Alignment}`; -/** - * Positioning strategy for the floating element - */ export type Strategy = 'absolute' | 'fixed'; /** - * Type guard to check if a string is a valid Side + * Checks if a string is a valid Side. + * @param value - The string to check. + * @returns True if the value is a valid Side. */ export function isSide(value: string): value is Side { return ['top', 'right', 'bottom', 'left'].includes(value); } /** - * Type guard to check if a string is a valid Alignment + * Checks if a string is a valid Alignment. + * @param value - The string to check. + * @returns True if the value is a valid Alignment. */ export function isAlignment(value: string): value is Alignment { return ['start', 'end'].includes(value); } /** - * Type guard to check if a string is a valid Placement + * Checks if a string is a valid Placement. + * @param value - The string to check. + * @returns True if the value is a valid Placement. */ export function isPlacement(value: string): value is Placement { const parts = value.split('-'); @@ -53,20 +49,18 @@ export function isPlacement(value: string): value is Placement { } /** - * Extract the side from a placement - * @example - * getSide('top-start') // 'top' - * getSide('bottom') // 'bottom' + * Extracts the side from a placement. + * @param placement - The placement to extract from. + * @returns The side component of the placement. */ export function getSide(placement: Placement): Side { return placement.split('-')[0] as Side; } /** - * Extract the alignment from a placement (if any) - * @example - * getAlignment('top-start') // 'start' - * getAlignment('bottom') // undefined + * Extracts the alignment from a placement. + * @param placement - The placement to extract from. + * @returns The alignment component, or undefined if none. */ export function getAlignment(placement: Placement): Alignment | undefined { const parts = placement.split('-'); @@ -74,14 +68,18 @@ export function getAlignment(placement: Placement): Alignment | undefined { } /** - * Get the axis for a given side + * Gets the axis for a given side. + * @param side - The side to get the axis for. + * @returns 'x' for left/right, 'y' for top/bottom. */ export function getAxis(side: Side): 'x' | 'y' { return side === 'top' || side === 'bottom' ? 'y' : 'x'; } /** - * Get the opposite side + * Gets the opposite side. + * @param side - The side to get the opposite of. + * @returns The opposite side. */ export function getOppositeSide(side: Side): Side { const opposites: Record = { @@ -94,7 +92,9 @@ export function getOppositeSide(side: Side): Side { } /** - * Get the opposite alignment + * Gets the opposite alignment. + * @param alignment - The alignment to get the opposite of. + * @returns The opposite alignment. */ export function getOppositeAlignment(alignment: Alignment): Alignment { return alignment === 'start' ? 'end' : 'start'; diff --git a/packages/@necto/necto-popper/src/utils/autoUpdate.ts b/packages/@necto/necto-popper/src/utils/autoUpdate.ts new file mode 100644 index 00000000..8c77664a --- /dev/null +++ b/packages/@necto/necto-popper/src/utils/autoUpdate.ts @@ -0,0 +1,172 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { getOwnerWindow, getOwnerDocument } from '@necto/dom'; + +export interface AutoUpdateOptions { + /** + * Whether to update on ancestor scroll events. + * @default true + */ + ancestorScroll?: boolean; + + /** + * Whether to update on ancestor resize events. + * @default true + */ + ancestorResize?: boolean; + + /** + * Whether to update when the reference element resizes. + * @default true + */ + elementResize?: boolean; + + /** + * Whether to update on layout shifts. + * @default true + */ + layoutShift?: boolean; + + /** + * Whether to update using requestAnimationFrame polling. + * @default false + */ + animationFrame?: boolean; +} + +/** + * Gets all scroll ancestor elements of a given element. + * @param element - The element to get scroll ancestors for. + * @returns Array of scrollable ancestor elements. + */ +function getScrollAncestors(element: Element): Array { + const ancestors: Array = []; + let current: Element | null = element.parentElement; + + while (current) { + const { overflow, overflowX, overflowY } = getComputedStyle(current); + if (/auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX)) { + ancestors.push(current); + } + current = current.parentElement; + } + + ancestors.push(getOwnerWindow(element)); + return ancestors; +} + +/** + * Automatically updates the floating element position when necessary. + * @param reference - The reference element. + * @param floating - The floating element. + * @param update - The update function to call. + * @param options - Configuration options. + * @returns A cleanup function to stop auto-updating. + */ +export function autoUpdate( + reference: Element, + floating: HTMLElement, + update: () => void, + options: AutoUpdateOptions = {} +): () => void { + const { + ancestorScroll = true, + ancestorResize = true, + elementResize = true, + layoutShift = true, + animationFrame = false + } = options; + + const ancestors = + ancestorScroll || ancestorResize + ? [...getScrollAncestors(reference), ...getScrollAncestors(floating)] + : []; + + const cleanupFns: Array<() => void> = []; + + if (ancestorScroll) { + for (const ancestor of ancestors) { + ancestor.addEventListener('scroll', update, { passive: true }); + cleanupFns.push(() => ancestor.removeEventListener('scroll', update)); + } + } + + if (ancestorResize) { + const win = getOwnerWindow(reference); + win.addEventListener('resize', update); + cleanupFns.push(() => win.removeEventListener('resize', update)); + } + + let resizeObserver: ResizeObserver | null = null; + if (elementResize && typeof ResizeObserver !== 'undefined') { + resizeObserver = new ResizeObserver(() => { + update(); + }); + resizeObserver.observe(reference); + resizeObserver.observe(floating); + cleanupFns.push(() => resizeObserver?.disconnect()); + } + + let intersectionObserver: IntersectionObserver | null = null; + if (layoutShift && typeof IntersectionObserver !== 'undefined') { + let prevRect = reference.getBoundingClientRect(); + intersectionObserver = new IntersectionObserver( + () => { + const newRect = reference.getBoundingClientRect(); + if ( + prevRect.x !== newRect.x || + prevRect.y !== newRect.y || + prevRect.width !== newRect.width || + prevRect.height !== newRect.height + ) { + update(); + } + prevRect = newRect; + }, + { + root: getOwnerDocument(reference), + threshold: Array.from({ length: 101 }, (_, i) => i / 100) + } + ); + intersectionObserver.observe(reference); + cleanupFns.push(() => intersectionObserver?.disconnect()); + } + + let frameId: number | null = null; + if (animationFrame) { + let prevRect = reference.getBoundingClientRect(); + const frameLoop = () => { + const newRect = reference.getBoundingClientRect(); + if ( + prevRect.x !== newRect.x || + prevRect.y !== newRect.y || + prevRect.width !== newRect.width || + prevRect.height !== newRect.height + ) { + update(); + } + prevRect = newRect; + frameId = requestAnimationFrame(frameLoop); + }; + frameId = requestAnimationFrame(frameLoop); + cleanupFns.push(() => { + if (frameId !== null) { + cancelAnimationFrame(frameId); + } + }); + } + + update(); + + return () => { + for (const cleanup of cleanupFns) { + cleanup(); + } + }; +} diff --git a/packages/@necto/necto-popper/src/utils/detectOverflow.ts b/packages/@necto/necto-popper/src/utils/detectOverflow.ts index 05eebfea..69fbeab8 100644 --- a/packages/@necto/necto-popper/src/utils/detectOverflow.ts +++ b/packages/@necto/necto-popper/src/utils/detectOverflow.ts @@ -1,34 +1,28 @@ /** - * Utilities for detecting overflow and viewport boundaries + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. * - * Uses @necto/dom utilities - perfect example of package reuse! */ import { getContainmentRect } from '@necto/dom'; -import type { Rect, OverflowData, BoundaryOptions } from '../types'; import { resolvePadding, hasAnyOverflow } from '../types'; +import type { Rect, OverflowData, BoundaryOptions } from '../types'; + /** * Detects how much a rect overflows its boundary. - * - * Uses your existing @necto/dom getContainmentRect! - * - * @example - * const overflow = detectOverflow(tooltipRect, { boundary: container }) - * if (overflow.top > 0) { - * // Tooltip is overflowing the top by `overflow.top` pixels - * } + * @param rect - The rectangle to check for overflow. + * @param options - Boundary and padding options. + * @returns Overflow amounts for each side (positive values indicate overflow). */ export function detectOverflow( rect: Rect, options: BoundaryOptions = {} ): OverflowData { const { boundary, padding: paddingOption = 0 } = options; - - // Resolve padding using our utility const padding = resolvePadding(paddingOption); - - // ✅ Using your existing @necto/dom utility! const boundaryRect = getContainmentRect(boundary); return { @@ -40,7 +34,10 @@ export function detectOverflow( } /** - * Check if a rect is overflowing on any side + * Checks if a rect is overflowing on any side. + * @param rect - The rectangle to check. + * @param options - Boundary and padding options. + * @returns True if the rect overflows on any side. */ export function hasOverflow( rect: Rect, diff --git a/packages/@necto/necto-popper/src/utils/getPlacementCoords.ts b/packages/@necto/necto-popper/src/utils/getPlacementCoords.ts index 77a72272..7237c8e0 100644 --- a/packages/@necto/necto-popper/src/utils/getPlacementCoords.ts +++ b/packages/@necto/necto-popper/src/utils/getPlacementCoords.ts @@ -1,29 +1,32 @@ /** - * Calculate coordinates based on placement - * Pure math functions - easy to test! + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * */ -import type { Placement, Coordinates, ElementRects } from '../types'; import { getSide, getAlignment, getAxis } from '../types'; +import type { Placement, Coordinates, ElementRects } from '../types'; + /** * Computes x,y coordinates for a given placement. - * Pure function: same inputs always produce same outputs. + * @param placement - The desired placement position. + * @param rects - The bounding rectangles of reference and floating elements. + * @returns The computed x,y coordinates. */ export function computeCoords( placement: Placement, rects: ElementRects ): Coordinates { const { reference, floating } = rects; - - // Extract the side and alignment from placement using our utility functions const side = getSide(placement); const alignment = getAlignment(placement); let x = 0; let y = 0; - // Position on the correct side switch (side) { case 'top': x = reference.x + reference.width / 2 - floating.width / 2; @@ -43,19 +46,16 @@ export function computeCoords( break; } - // Apply alignment (start/end) using our utility function if (alignment) { const axis = getAxis(side); if (axis === 'x') { - // Horizontal alignment if (alignment === 'start') { x = reference.x; } else if (alignment === 'end') { x = reference.x + reference.width - floating.width; } } else { - // Vertical alignment if (alignment === 'start') { y = reference.y; } else if (alignment === 'end') { diff --git a/packages/@necto/necto-react/src/components/index.ts b/packages/@necto/necto-react/src/components/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/@necto/necto-react/src/hooks/index.ts b/packages/@necto/necto-react/src/hooks/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/@necto/necto-react/src/index.ts b/packages/@necto/necto-react/src/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/@necto/necto-url/package.json b/packages/@necto/necto-url/package.json new file mode 100644 index 00000000..6f007d77 --- /dev/null +++ b/packages/@necto/necto-url/package.json @@ -0,0 +1,29 @@ +{ + "name": "@necto/url", + "version": "1.0.0", + "description": "URL parsing and validation utilities", + "scripts": { + "build": "tsup --minify terser" + }, + "author": "Corinvo OSS Team", + "license": "MIT", + "devDependencies": { + "@types/node": "^22.14.1", + "tsup": "^8.4.0" + }, + "files": [ + "dist", + "README.md" + ], + "type": "module", + "types": "./dist/index.d.ts", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + } +} diff --git a/packages/@necto/necto-url/src/index.ts b/packages/@necto/necto-url/src/index.ts new file mode 100644 index 00000000..270d031d --- /dev/null +++ b/packages/@necto/necto-url/src/index.ts @@ -0,0 +1,8 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export * from './validate'; diff --git a/packages/@necto/necto-url/src/parse.ts b/packages/@necto/necto-url/src/parse.ts new file mode 100644 index 00000000..f2000602 --- /dev/null +++ b/packages/@necto/necto-url/src/parse.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import type { ParsedUrl } from './types'; + +/** + * Parses a URL string using the WHATWG URL API and returns its key components. + * + * @param url - The URL to parse. Can be absolute, or relative if `base` is provided. + * @param base - Optional base URL used to resolve relative `url` (e.g., "https://example.com"). + * @returns A parsed URL object on success, or `null` if the URL is invalid. + */ +export function parseUrl(url: string, base?: string): ParsedUrl | null { + try { + const parsed = new URL(url, base); + + const searchParams: Record = {}; + parsed.searchParams.forEach((value: string, key: string): void => { + searchParams[key] = value; + }); + + return { + href: parsed.href, + protocol: parsed.protocol, + host: parsed.host, + hostname: parsed.hostname, + port: parsed.port, + pathname: parsed.pathname, + search: parsed.search, + hash: parsed.hash, + origin: parsed.origin, + searchParams + }; + } catch { + return null; + } +} diff --git a/packages/@necto/necto-url/src/types.ts b/packages/@necto/necto-url/src/types.ts new file mode 100644 index 00000000..3ade4ce9 --- /dev/null +++ b/packages/@necto/necto-url/src/types.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export interface ParsedUrl { + href: string; + protocol: string; + host: string; + hostname: string; + port: string; + pathname: string; + search: string; + hash: string; + origin: string; + searchParams: Record; +} diff --git a/packages/@necto/necto-url/src/validate.ts b/packages/@necto/necto-url/src/validate.ts new file mode 100644 index 00000000..7888ace9 --- /dev/null +++ b/packages/@necto/necto-url/src/validate.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) Corinvo, LLC. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +const ABSOLUTE_URL_REGEX: RegExp = /^[A-Za-z][A-Za-z0-9+.-]*:/; +const DATA_URL_REGEX: RegExp = /^data:/i; +const BLOB_URL_REGEX: RegExp = /^blob:/i; +const ALLOWED_SCHEMES_REGEX: RegExp = /^(https?|data|blob):/i; +const RELATIVE_PATH_REGEX: RegExp = /^(?:\/|\.\/|\.\.\/)/; +const PROTOCOL_RELATIVE_REGEX: RegExp = /^\/\//; +const WINDOWS_BACKSLASH_PATH_REGEX: RegExp = /^(?:\\|\.\\|\.\.\\)/; + +/** + * Checks if value is a URL (absolute, relative path, or data URL) + */ +export function isUrl(value?: string | null): value is string { + if (!value) return false; + + const input: string = value.trim(); + if (!input) return false; + + // Explicitly reject Windows backslash paths + if (WINDOWS_BACKSLASH_PATH_REGEX.test(input)) return false; + + return ( + ALLOWED_SCHEMES_REGEX.test(input) || + RELATIVE_PATH_REGEX.test(input) || + PROTOCOL_RELATIVE_REGEX.test(input) + ); +} + +/** + * Checks if value is an absolute URL with a protocol + */ +export function isAbsoluteUrl(value?: string | null): value is string { + if (!value) return false; + return ABSOLUTE_URL_REGEX.test(value.trim()); +} + +/** + * Checks if value is a relative URL (starts with /, ./, or ../) + */ +export function isRelativeUrl(value?: string | null): value is string { + if (!value) return false; + const input: string = value.trim(); + + return RELATIVE_PATH_REGEX.test(input) || PROTOCOL_RELATIVE_REGEX.test(input); +} +/** + * Checks if value is a data URL + */ +export function isDataUrl(value?: string | null): value is string { + if (!value) return false; + return DATA_URL_REGEX.test(value.trim()); +} + +/** + * Checks if value is a blob URL + */ +export function isBlobUrl(value?: string | null): value is string { + if (!value) return false; + return BLOB_URL_REGEX.test(value.trim()); +} diff --git a/packages/@necto/necto-url/tsup.config.ts b/packages/@necto/necto-url/tsup.config.ts new file mode 100644 index 00000000..a4bd57ae --- /dev/null +++ b/packages/@necto/necto-url/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig([ + { + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + sourcemap: false, + splitting: false, + clean: true, + cjsInterop: true, + platform: 'neutral', + outExtension({ format }) { + return { + js: format === 'cjs' ? '.cjs' : '.mjs' + }; + } + } +]); diff --git a/packages/@necto/necto-vue/src/components/index.ts b/packages/@necto/necto-vue/src/components/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/@necto/necto-vue/src/composables/index.ts b/packages/@necto/necto-vue/src/composables/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/@necto/necto-vue/src/idnex.ts b/packages/@necto/necto-vue/src/idnex.ts new file mode 100644 index 00000000..e69de29b diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25a582fd..2ec59f2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,9 +39,21 @@ importers: '@storybook/test': specifier: ^8.5.0 version: 8.6.14(storybook@8.6.14(prettier@2.8.8)) + '@storybook/vue3': + specifier: ^8.5.0 + version: 8.6.14(storybook@8.6.14(prettier@2.8.8))(vue@3.5.25(typescript@5.9.3)) + '@storybook/vue3-vite': + specifier: ^8.5.0 + version: 8.6.14(storybook@8.6.14(prettier@2.8.8))(vite@7.2.2(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3)) + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.4(vite@7.2.2(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3)) commitlint: specifier: ^19.8.1 version: 19.8.1(@types/node@22.19.0)(typescript@5.9.3) + concurrently: + specifier: ^9.2.1 + version: 9.2.1 lefthook: specifier: ^1.11.13 version: 1.13.6 @@ -66,6 +78,9 @@ importers: vitest: specifier: ^3.0.9 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jiti@2.6.1)(jsdom@26.1.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) + vue: + specifier: ^3.5.13 + version: 3.5.25(typescript@5.9.3) packages/@necto-react/necto-react-components: dependencies: @@ -126,7 +141,7 @@ importers: version: 1.93.3 tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) vitest: specifier: ^3.0.9 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.0)(jiti@2.6.1)(jsdom@26.1.0)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) @@ -151,7 +166,7 @@ importers: version: 19.2.0 tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto-react/necto-react-helpers: dependencies: @@ -185,7 +200,7 @@ importers: version: 19.2.0(react@19.2.0) tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto-react/necto-react-hooks: dependencies: @@ -249,7 +264,7 @@ importers: version: link:../../shared tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto-react/necto-react-popper: dependencies: @@ -292,7 +307,7 @@ importers: version: 26.1.0 tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto-react/necto-react-types: dependencies: @@ -314,7 +329,66 @@ importers: version: 19.1.2(@types/react@19.1.2) tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + + packages/@necto-vue/necto-vue-components: + dependencies: + '@necto-vue/composables': + specifier: workspace:* + version: link:../necto-vue-composables + '@necto/constants': + specifier: workspace:* + version: link:../../@necto/necto-constants + '@necto/dom': + specifier: workspace:* + version: link:../../@necto/necto-dom + '@necto/image': + specifier: workspace:* + version: link:../../@necto/necto-image + devDependencies: + '@types/node': + specifier: ^22.14.1 + version: 22.19.0 + '@vitejs/plugin-vue': + specifier: ^5.2.1 + version: 5.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3)) + '@vitejs/plugin-vue-jsx': + specifier: ^5.1.2 + version: 5.1.2(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3)) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) + vite-plugin-dts: + specifier: ^4.3.0 + version: 4.5.4(@types/node@22.19.0)(rollup@4.53.2)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1)) + vue: + specifier: ^3.5.13 + version: 3.5.25(typescript@5.9.3) + + packages/@necto-vue/necto-vue-composables: + dependencies: + '@vueuse/core': + specifier: ^14.1.0 + version: 14.1.0(vue@3.5.25(typescript@5.9.3)) + devDependencies: + '@types/node': + specifier: ^22.14.1 + version: 22.19.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^6.0.0 + version: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) + vite-plugin-dts: + specifier: ^4.3.0 + version: 4.5.4(@types/node@22.19.0)(rollup@4.53.2)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1)) + vue: + specifier: ^3.5.13 + version: 3.5.25(typescript@5.9.3) packages/@necto/necto-assert: devDependencies: @@ -375,13 +449,16 @@ importers: devDependencies: tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto/necto-dom: dependencies: '@necto/constants': specifier: workspace:* version: link:../necto-constants + '@necto/file': + specifier: workspace:* + version: link:../necto-file '@necto/platform': specifier: workspace:* version: link:../necto-platform @@ -397,7 +474,7 @@ importers: version: 22.19.0 tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto/necto-env: dependencies: @@ -416,7 +493,29 @@ importers: version: 22.19.0 tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + + packages/@necto/necto-file: + dependencies: + sax: + specifier: ^1.4.1 + version: 1.4.3 + devDependencies: + '@types/node': + specifier: ^22.14.1 + version: 22.19.0 + '@types/sax': + specifier: ^1.2.7 + version: 1.2.7 + terser: + specifier: ^5.39.0 + version: 5.44.1 + tsup: + specifier: ^8.5.0 + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 packages/@necto/necto-flags: {} @@ -437,7 +536,23 @@ importers: version: 2.0.3 tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + + packages/@necto/necto-image: + dependencies: + '@necto/dom': + specifier: workspace:* + version: link:../necto-dom + '@necto/url': + specifier: workspace:* + version: link:../necto-url + devDependencies: + '@types/node': + specifier: ^22.14.1 + version: 22.19.0 + tsup: + specifier: ^8.4.0 + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto/necto-mergers: devDependencies: @@ -452,13 +567,13 @@ importers: version: 19.2.0 tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto/necto-platform: devDependencies: tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto/necto-popper: dependencies: @@ -477,19 +592,28 @@ importers: version: 22.19.0 tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto/necto-strings: devDependencies: tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto/necto-types: devDependencies: tsup: specifier: ^8.4.0 - version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) + + packages/@necto/necto-url: + devDependencies: + '@types/node': + specifier: ^22.14.1 + version: 22.19.0 + tsup: + specifier: ^8.4.0 + version: 8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3) packages/@necto/necto-uuid: {} @@ -1543,6 +1667,19 @@ packages: '@types/react': 19.1.2 react: '>=16' + '@microsoft/api-extractor-model@7.32.1': + resolution: {integrity: sha512-u4yJytMYiUAnhcNQcZDTh/tVtlrzKlyKrQnLOV+4Qr/5gV+cpufWzCYAB1Q23URFqD6z2RoL2UYncM9xJVGNKA==} + + '@microsoft/api-extractor@7.55.1': + resolution: {integrity: sha512-l8Z+8qrLkZFM3HM95Dbpqs6G39fpCa7O5p8A7AkA6hSevxkgwsOlLrEuPv0ADOyj5dI1Af5WVDiwpKG/ya5G3w==} + hasBin: true + + '@microsoft/tsdoc-config@0.18.0': + resolution: {integrity: sha512-8N/vClYyfOH+l4fLkkr9+myAoR6M7akc8ntBJ4DJdWH2b09uVfr71+LTMpNyG19fNqWDg8KEDZhx5wxuqHyGjw==} + + '@microsoft/tsdoc@0.16.0': + resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2194,6 +2331,9 @@ packages: '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + '@rolldown/pluginutils@1.0.0-beta.53': + resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + '@rollup/plugin-babel@6.1.0': resolution: {integrity: sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==} engines: {node: '>=14.0.0'} @@ -2366,6 +2506,36 @@ packages: cpu: [x64] os: [win32] + '@rushstack/node-core-library@5.19.0': + resolution: {integrity: sha512-BxAopbeWBvNJ6VGiUL+5lbJXywTdsnMeOS8j57Cn/xY10r6sV/gbsTlfYKjzVCUBZATX2eRzJHSMCchsMTGN6A==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/problem-matcher@0.1.1': + resolution: {integrity: sha512-Fm5XtS7+G8HLcJHCWpES5VmeMyjAKaWeyZU5qPzZC+22mPlJzAsOxymHiWIfuirtPckX3aptWws+K2d0BzniJA==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/rig-package@0.6.0': + resolution: {integrity: sha512-ZQmfzsLE2+Y91GF15c65L/slMRVhF6Hycq04D4TwtdGaUAbIXXg9c5pKA5KFU7M4QMaihoobp9JJYpYcaY3zOw==} + + '@rushstack/terminal@0.19.4': + resolution: {integrity: sha512-f4XQk02CrKfrMgyOfhYd3qWI944dLC21S4I/LUhrlAP23GTMDNG6EK5effQtFkISwUKCgD9vMBrJZaPSUquxWQ==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + + '@rushstack/ts-command-line@5.1.4': + resolution: {integrity: sha512-H0I6VdJ6sOUbktDFpP2VW5N29w8v4hRoNZOQz02vtEi6ZTYL1Ju8u+TcFiFawUDrUsx/5MQTUhd79uwZZVwVlA==} + '@storybook/addon-actions@8.6.14': resolution: {integrity: sha512-mDQxylxGGCQSK7tJPkD144J8jWh9IU9ziJMHfB84PKpI/V5ZgqMDnpr2bssTrUaGDqU5e1/z8KcRF+Melhs9pQ==} peerDependencies: @@ -2536,6 +2706,20 @@ packages: peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + '@storybook/vue3-vite@8.6.14': + resolution: {integrity: sha512-3BclEv7SzHuw8eC9mFsAuH3EjEf4eCb0FxY3SoyTagNX14WjCE5cV2AK9RpWh6e5kQZiTzF8NiYq6AJqi5ebbw==} + engines: {node: '>=18.0.0'} + peerDependencies: + storybook: ^8.6.14 + vite: ^4.0.0 || ^5.0.0 || ^6.0.0 + + '@storybook/vue3@8.6.14': + resolution: {integrity: sha512-T9ORF734iBqYf2Sw/L/6qQL3FvBH9q6dHh8AFGkqTL/cluy0VxW55B6QLBvLAMS2OeMFB5dXRli5MFfw5njjQw==} + engines: {node: '>=18.0.0'} + peerDependencies: + storybook: ^8.6.14 + vue: ^3.0.0 + '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} @@ -2580,6 +2764,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/argparse@1.0.38': + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} @@ -2667,18 +2854,38 @@ packages: '@types/resolve@1.20.6': resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + '@vitejs/plugin-vue-jsx@5.1.2': + resolution: {integrity: sha512-3a2BOryRjG/Iih87x87YXz5c8nw27eSlHytvSKYfp8ZIsp5+FgFQoKeA7k2PnqWpjJrv6AoVTMnvmuKUXb771A==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.0.0 + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + '@vitest/expect@2.0.5': resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} @@ -2726,6 +2933,101 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + + '@volar/language-core@2.4.26': + resolution: {integrity: sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==} + + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + + '@volar/source-map@2.4.26': + resolution: {integrity: sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==} + + '@volar/typescript@2.4.15': + resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + + '@volar/typescript@2.4.26': + resolution: {integrity: sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA==} + + '@vue/babel-helper-vue-transform-on@2.0.1': + resolution: {integrity: sha512-uZ66EaFbnnZSYqYEyplWvn46GhZ1KuYSThdT68p+am7MgBNbQ3hphTL9L+xSIsWkdktwhPYLwPgVWqo96jDdRA==} + + '@vue/babel-plugin-jsx@2.0.1': + resolution: {integrity: sha512-a8CaLQjD/s4PVdhrLD/zT574ZNPnZBOY+IhdtKWRB4HRZ0I2tXBi5ne7d9eCfaYwp5gU5+4KIyFTV1W1YL9xZA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@2.0.1': + resolution: {integrity: sha512-ybwgIuRGRRBhOU37GImDoWQoz+TlSqap65qVI6iwg/J7FfLTLmMf97TS7xQH9I7Qtr/gp161kYVdhr1ZMraSYQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.25': + resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==} + + '@vue/compiler-dom@3.5.25': + resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==} + + '@vue/compiler-sfc@3.5.25': + resolution: {integrity: sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==} + + '@vue/compiler-ssr@3.5.25': + resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.0': + resolution: {integrity: sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@2.2.12': + resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.25': + resolution: {integrity: sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==} + + '@vue/runtime-core@3.5.25': + resolution: {integrity: sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==} + + '@vue/runtime-dom@3.5.25': + resolution: {integrity: sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==} + + '@vue/server-renderer@3.5.25': + resolution: {integrity: sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==} + peerDependencies: + vue: 3.5.25 + + '@vue/shared@3.5.25': + resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==} + + '@vueuse/core@14.1.0': + resolution: {integrity: sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.1.0': + resolution: {integrity: sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==} + + '@vueuse/shared@14.1.0': + resolution: {integrity: sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==} + peerDependencies: + vue: ^3.5.0 + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -2734,6 +3036,11 @@ packages: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -2743,9 +3050,37 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ajv@8.13.0: + resolution: {integrity: sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + alien-signals@0.4.14: + resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -2808,6 +3143,12 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assert-never@1.4.0: + resolution: {integrity: sha512-5oJg84os6NMQNl27T9LnZkvvqzvAnHu03ShCnoj6bsJwS7L8AO4lf+C/XjK/nvzEqQB744moC6V128RucQd1jA==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2843,6 +3184,10 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-walk@3.0.0-canary-5: + resolution: {integrity: sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==} + engines: {node: '>= 10.0.0'} + babel@6.23.0: resolution: {integrity: sha512-ZDcCaI8Vlct8PJ3DvmyqUz+5X2Ylz3ZuuItBe/74yXosk2dwyVo/aN7MCJ8HJzhnnJ+6yP4o+lDgG9MBe91DLA==} deprecated: In 6.x, the babel package has been deprecated in favor of babel-cli. Check https://opencollective.com/babel to support the Babel maintainers @@ -2947,6 +3292,9 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + character-parser@2.2.0: + resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} @@ -3023,16 +3371,30 @@ packages: compare-func@2.0.0: resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + constantinople@4.0.1: + resolution: {integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==} + conventional-changelog-angular@7.0.0: resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} engines: {node: '>=16'} @@ -3113,6 +3475,9 @@ packages: resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} engines: {node: '>= 0.4'} + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -3171,6 +3536,10 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + diff@8.0.2: + resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3179,6 +3548,9 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + doctypes@1.1.0: + resolution: {integrity: sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -3209,6 +3581,10 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -3275,6 +3651,9 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + esm-resolve@1.0.11: + resolution: {integrity: sha512-LxF0wfUQm3ldUDHkkV2MIbvvY0TgzIpJ420jHSV1Dm+IlplBEWiJTKWM61GtxUfvjV6iD4OtTYFGAGM2uuIUWg==} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -3294,6 +3673,9 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -3323,6 +3705,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-package-json@1.2.0: + resolution: {integrity: sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==} + find-root@1.1.0: resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} @@ -3349,6 +3734,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -3471,10 +3860,17 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash-sum@2.0.0: + resolution: {integrity: sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -3520,6 +3916,10 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -3598,6 +3998,9 @@ packages: engines: {node: '>=8'} hasBin: true + is-expression@4.0.0: + resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3651,6 +4054,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3720,10 +4126,16 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} + js-stringify@1.0.2: + resolution: {integrity: sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3773,14 +4185,23 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jstransformer@1.0.0: + resolution: {integrity: sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==} + katex@0.16.25: resolution: {integrity: sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==} hasBin: true + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + lefthook-darwin-arm64@1.13.6: resolution: {integrity: sha512-m6Lb77VGc84/Qo21Lhq576pEvcgFCnvloEiP02HbAHcIXD0RTLy9u2yAInrixqZeaz13HYtdDaI7OBYAAdVt8A==} cpu: [arm64] @@ -3850,6 +4271,10 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -3907,6 +4332,14 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@8.0.5: + resolution: {integrity: sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==} + engines: {node: '>=16.14'} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -4033,6 +4466,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minimatch@10.1.1: resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} @@ -4061,6 +4498,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -4179,6 +4619,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -4252,6 +4695,9 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.56.1: resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} engines: {node: '>=18'} @@ -4305,6 +4751,45 @@ packages: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} + promise@7.3.1: + resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} + + pug-attrs@3.0.0: + resolution: {integrity: sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==} + + pug-code-gen@3.0.3: + resolution: {integrity: sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==} + + pug-error@2.1.0: + resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==} + + pug-filters@4.0.0: + resolution: {integrity: sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==} + + pug-lexer@5.0.1: + resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} + + pug-linker@4.0.0: + resolution: {integrity: sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==} + + pug-load@3.0.0: + resolution: {integrity: sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==} + + pug-parser@6.0.0: + resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} + + pug-runtime@3.0.1: + resolution: {integrity: sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==} + + pug-strip-comments@2.0.0: + resolution: {integrity: sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==} + + pug-walk@2.0.0: + resolution: {integrity: sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==} + + pug@3.0.3: + resolution: {integrity: sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4593,6 +5078,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + sax@1.4.3: + resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -4608,6 +5096,11 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -4736,6 +5229,10 @@ packages: prettier: optional: true + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4780,6 +5277,10 @@ packages: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} @@ -4889,6 +5390,9 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + token-stream@1.0.0: + resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -4911,6 +5415,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-map@1.0.3: + resolution: {integrity: sha512-vDWbsl26LIcPGmDpoVzjEP6+hvHZkBkLW7JpvwbCv/5IYPJlsbzCVXY3wsCeAxAUeTclNOUZxnLdGh3VBD/J6w==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -4985,6 +5492,10 @@ packages: resolution: {integrity: sha512-kC5VJqOXo50k0/0jnJDDjibLAXalqT9j7PQ56so0pN+81VR4Fwb2QgIE9dTzT3phqOTQuEXkPh3sCpnv5Isz2g==} hasBin: true + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -5001,6 +5512,11 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -5040,6 +5556,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unplugin@1.16.1: resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==} engines: {node: '>=14.0.0'} @@ -5050,6 +5570,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -5076,6 +5599,15 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-plugin-dts@4.5.4: + resolution: {integrity: sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg==} + peerDependencies: + typescript: '*' + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite@6.4.1: resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -5184,6 +5716,45 @@ packages: jsdom: optional: true + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-meta@2.2.12: + resolution: {integrity: sha512-dQU6/obNSNbennJ1xd+rhDid4g3vQro+9qUBBIg8HMZH2Zs1jTpkFNxuQ3z77bOlU+ew08Qck9sbYkdSePr0Pw==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + + vue-component-type-helpers@3.1.7: + resolution: {integrity: sha512-SxZOijzRU0LiTYARVZNYf//21bZRLJ26P2prcA6a+FfDQxJtan4tEnsryIY7aiLh2LVoet0ci2G+6RrArQ+7tA==} + + vue-docgen-api@4.79.2: + resolution: {integrity: sha512-n9ENAcs+40awPZMsas7STqjkZiVlIjxIKgiJr5rSohDP0/JCrD9VtlzNojafsA1MChm/hz2h3PDtUedx3lbgfA==} + peerDependencies: + vue: '>=2' + + vue-inbrowser-compiler-independent-utils@4.71.1: + resolution: {integrity: sha512-K3wt3iVmNGaFEOUR4JIThQRWfqokxLfnPslD41FDZB2ajXp789+wCqJyGYlIFsvEQ2P61PInw6/ph5iiqg51gg==} + peerDependencies: + vue: '>=2' + + vue@3.5.25: + resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -5243,6 +5814,10 @@ packages: engines: {node: '>=8'} hasBin: true + with@7.0.2: + resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} + engines: {node: '>= 10.0.0'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5280,6 +5855,9 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} @@ -6608,9 +7186,45 @@ snapshots: '@types/react': 19.1.2 react: 19.2.0 - '@nodelib/fs.scandir@2.1.5': + '@microsoft/api-extractor-model@7.32.1(@types/node@22.19.0)': dependencies: - '@nodelib/fs.stat': 2.0.5 + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.0 + '@rushstack/node-core-library': 5.19.0(@types/node@22.19.0) + transitivePeerDependencies: + - '@types/node' + + '@microsoft/api-extractor@7.55.1(@types/node@22.19.0)': + dependencies: + '@microsoft/api-extractor-model': 7.32.1(@types/node@22.19.0) + '@microsoft/tsdoc': 0.16.0 + '@microsoft/tsdoc-config': 0.18.0 + '@rushstack/node-core-library': 5.19.0(@types/node@22.19.0) + '@rushstack/rig-package': 0.6.0 + '@rushstack/terminal': 0.19.4(@types/node@22.19.0) + '@rushstack/ts-command-line': 5.1.4(@types/node@22.19.0) + diff: 8.0.2 + lodash: 4.17.21 + minimatch: 10.0.3 + resolve: 1.22.11 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.8.2 + transitivePeerDependencies: + - '@types/node' + + '@microsoft/tsdoc-config@0.18.0': + dependencies: + '@microsoft/tsdoc': 0.16.0 + ajv: 8.12.0 + jju: 1.4.0 + resolve: 1.22.11 + + '@microsoft/tsdoc@0.16.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 '@nodelib/fs.stat@2.0.5': {} @@ -7684,6 +8298,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.27': {} + '@rolldown/pluginutils@1.0.0-beta.53': {} + '@rollup/plugin-babel@6.1.0(@babel/core@7.28.5)(@types/babel__core@7.20.5)(rollup@4.53.2)': dependencies: '@babel/core': 7.28.5 @@ -7801,6 +8417,45 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.53.2': optional: true + '@rushstack/node-core-library@5.19.0(@types/node@22.19.0)': + dependencies: + ajv: 8.13.0 + ajv-draft-04: 1.0.0(ajv@8.13.0) + ajv-formats: 3.0.1(ajv@8.13.0) + fs-extra: 11.3.2 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.11 + semver: 7.5.4 + optionalDependencies: + '@types/node': 22.19.0 + + '@rushstack/problem-matcher@0.1.1(@types/node@22.19.0)': + optionalDependencies: + '@types/node': 22.19.0 + + '@rushstack/rig-package@0.6.0': + dependencies: + resolve: 1.22.11 + strip-json-comments: 3.1.1 + + '@rushstack/terminal@0.19.4(@types/node@22.19.0)': + dependencies: + '@rushstack/node-core-library': 5.19.0(@types/node@22.19.0) + '@rushstack/problem-matcher': 0.1.1(@types/node@22.19.0) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 22.19.0 + + '@rushstack/ts-command-line@5.1.4(@types/node@22.19.0)': + dependencies: + '@rushstack/terminal': 0.19.4(@types/node@22.19.0) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.2 + transitivePeerDependencies: + - '@types/node' + '@storybook/addon-actions@8.6.14(storybook@8.6.14(prettier@2.8.8))': dependencies: '@storybook/global': 5.0.0 @@ -8022,6 +8677,34 @@ snapshots: dependencies: storybook: 8.6.14(prettier@2.8.8) + '@storybook/vue3-vite@8.6.14(storybook@8.6.14(prettier@2.8.8))(vite@7.2.2(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@storybook/builder-vite': 8.6.14(storybook@8.6.14(prettier@2.8.8))(vite@7.2.2(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1)) + '@storybook/vue3': 8.6.14(storybook@8.6.14(prettier@2.8.8))(vue@3.5.25(typescript@5.9.3)) + find-package-json: 1.2.0 + magic-string: 0.30.21 + storybook: 8.6.14(prettier@2.8.8) + typescript: 5.9.3 + vite: 7.2.2(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) + vue-component-meta: 2.2.12(typescript@5.9.3) + vue-docgen-api: 4.79.2(vue@3.5.25(typescript@5.9.3)) + transitivePeerDependencies: + - vue + + '@storybook/vue3@8.6.14(storybook@8.6.14(prettier@2.8.8))(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@storybook/components': 8.6.14(storybook@8.6.14(prettier@2.8.8)) + '@storybook/global': 5.0.0 + '@storybook/manager-api': 8.6.14(storybook@8.6.14(prettier@2.8.8)) + '@storybook/preview-api': 8.6.14(storybook@8.6.14(prettier@2.8.8)) + '@storybook/theming': 8.6.14(storybook@8.6.14(prettier@2.8.8)) + '@vue/compiler-core': 3.5.25 + storybook: 8.6.14(prettier@2.8.8) + ts-dedent: 2.2.0 + type-fest: 2.19.0 + vue: 3.5.25(typescript@5.9.3) + vue-component-type-helpers: 3.1.7 + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -8069,6 +8752,8 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@types/argparse@1.0.38': {} + '@types/aria-query@5.0.4': {} '@types/babel__core@7.20.5': @@ -8164,10 +8849,16 @@ snapshots: '@types/resolve@1.20.6': {} + '@types/sax@1.2.7': + dependencies: + '@types/node': 22.19.0 + '@types/unist@2.0.11': {} '@types/uuid@9.0.8': {} + '@types/web-bluetooth@0.0.21': {} + '@vitejs/plugin-react@4.7.0(vite@7.2.2(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1))': dependencies: '@babel/core': 7.28.5 @@ -8180,6 +8871,28 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-vue-jsx@5.1.2(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.53 + '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.5) + vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) + vue: 3.5.25(typescript@5.9.3) + transitivePeerDependencies: + - supports-color + + '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3))': + dependencies: + vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) + vue: 3.5.25(typescript@5.9.3) + + '@vitejs/plugin-vue@5.2.4(vite@7.2.2(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1))(vue@3.5.25(typescript@5.9.3))': + dependencies: + vite: 7.2.2(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) + vue: 3.5.25(typescript@5.9.3) + '@vitest/expect@2.0.5': dependencies: '@vitest/spy': 2.0.5 @@ -8254,6 +8967,157 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + '@volar/language-core@2.4.15': + dependencies: + '@volar/source-map': 2.4.15 + + '@volar/language-core@2.4.26': + dependencies: + '@volar/source-map': 2.4.26 + + '@volar/source-map@2.4.15': {} + + '@volar/source-map@2.4.26': {} + + '@volar/typescript@2.4.15': + dependencies: + '@volar/language-core': 2.4.15 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@volar/typescript@2.4.26': + dependencies: + '@volar/language-core': 2.4.26 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@2.0.1': {} + + '@vue/babel-plugin-jsx@2.0.1(@babel/core@7.28.5)': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@vue/babel-helper-vue-transform-on': 2.0.1 + '@vue/babel-plugin-resolve-type': 2.0.1(@babel/core@7.28.5) + '@vue/shared': 3.5.25 + optionalDependencies: + '@babel/core': 7.28.5 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@2.0.1(@babel/core@7.28.5)': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/parser': 7.28.5 + '@vue/compiler-sfc': 3.5.25 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.25 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.25': + dependencies: + '@vue/compiler-core': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/compiler-sfc@3.5.25': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.25 + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-ssr': 3.5.25 + '@vue/shared': 3.5.25 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.25': + dependencies: + '@vue/compiler-dom': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.0(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.26 + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.25 + alien-signals: 0.4.14 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/language-core@2.2.12(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.15 + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.25 + alien-signals: 1.0.13 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/reactivity@3.5.25': + dependencies: + '@vue/shared': 3.5.25 + + '@vue/runtime-core@3.5.25': + dependencies: + '@vue/reactivity': 3.5.25 + '@vue/shared': 3.5.25 + + '@vue/runtime-dom@3.5.25': + dependencies: + '@vue/reactivity': 3.5.25 + '@vue/runtime-core': 3.5.25 + '@vue/shared': 3.5.25 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.25(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.25 + '@vue/shared': 3.5.25 + vue: 3.5.25(typescript@5.9.3) + + '@vue/shared@3.5.25': {} + + '@vueuse/core@14.1.0(vue@3.5.25(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.1.0 + '@vueuse/shared': 14.1.0(vue@3.5.25(typescript@5.9.3)) + vue: 3.5.25(typescript@5.9.3) + + '@vueuse/metadata@14.1.0': {} + + '@vueuse/shared@14.1.0(vue@3.5.25(typescript@5.9.3))': + dependencies: + vue: 3.5.25(typescript@5.9.3) + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -8263,10 +9127,34 @@ snapshots: dependencies: acorn: 8.15.0 + acorn@7.4.1: {} + acorn@8.15.0: {} agent-base@7.1.4: {} + ajv-draft-04@1.0.0(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + + ajv-formats@3.0.1(ajv@8.13.0): + optionalDependencies: + ajv: 8.13.0 + + ajv@8.12.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + + ajv@8.13.0: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -8274,6 +9162,10 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + alien-signals@0.4.14: {} + + alien-signals@1.0.13: {} + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -8327,6 +9219,10 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + asap@2.0.6: {} + + assert-never@1.4.0: {} + assertion-error@2.0.1: {} ast-types@0.16.1: @@ -8369,6 +9265,10 @@ snapshots: transitivePeerDependencies: - supports-color + babel-walk@3.0.0-canary-5: + dependencies: + '@babel/types': 7.28.5 + babel@6.23.0: {} balanced-match@1.0.2: {} @@ -8473,6 +9373,10 @@ snapshots: character-entities@2.0.2: {} + character-parser@2.2.0: + dependencies: + is-regex: 1.2.1 + character-reference-invalid@2.0.1: {} chardet@2.1.1: {} @@ -8538,12 +9442,30 @@ snapshots: array-ify: 1.0.0 dot-prop: 5.3.0 + compare-versions@6.1.1: {} + concat-map@0.0.1: {} + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + confbox@0.1.8: {} + confbox@0.2.2: {} + consola@3.4.2: {} + constantinople@4.0.1: + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + conventional-changelog-angular@7.0.0: dependencies: compare-func: 2.0.0 @@ -8637,6 +9559,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 + de-indent@1.0.2: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -8680,6 +9604,8 @@ snapshots: diff@4.0.2: {} + diff@8.0.2: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -8688,6 +9614,8 @@ snapshots: dependencies: esutils: 2.0.3 + doctypes@1.1.0: {} + dom-accessibility-api@0.5.16: {} dom-accessibility-api@0.6.3: {} @@ -8715,6 +9643,8 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 + entities@4.5.0: {} + entities@6.0.1: {} env-paths@2.2.1: {} @@ -8853,6 +9783,8 @@ snapshots: escape-string-regexp@4.0.0: {} + esm-resolve@1.0.11: {} + esprima@4.0.1: {} estree-walker@2.0.2: {} @@ -8865,6 +9797,8 @@ snapshots: expect-type@1.2.2: {} + exsolve@1.0.8: {} + extendable-error@0.1.7: {} fast-deep-equal@3.1.3: {} @@ -8891,6 +9825,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-package-json@1.2.0: {} + find-root@1.1.0: {} find-up@4.1.0: @@ -8924,6 +9860,12 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -9068,10 +10010,14 @@ snapshots: dependencies: has-symbols: 1.1.0 + hash-sum@2.0.0: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 + he@1.2.0: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -9117,6 +10063,8 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-lazy@4.0.0: {} + import-meta-resolve@4.2.0: {} indent-string@4.0.0: {} @@ -9201,6 +10149,11 @@ snapshots: is-docker@2.2.1: {} + is-expression@4.0.0: + dependencies: + acorn: 7.4.1 + object-assign: 4.1.1 + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -9242,6 +10195,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@2.2.2: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -9313,8 +10268,12 @@ snapshots: jiti@2.6.1: {} + jju@1.4.0: {} + joycon@3.1.1: {} + js-stringify@1.0.2: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -9371,12 +10330,25 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jsonparse@1.3.1: {} + jstransformer@1.0.0: + dependencies: + is-promise: 2.2.2 + promise: 7.3.1 + katex@0.16.25: dependencies: commander: 8.3.0 + kolorist@1.8.0: {} + lefthook-darwin-arm64@1.13.6: optional: true @@ -9433,6 +10405,12 @@ snapshots: load-tsconfig@0.2.5: {} + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -9477,6 +10455,12 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@8.0.5: {} + lz-string@1.5.0: {} magic-string@0.27.0: @@ -9697,6 +10681,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -9724,6 +10712,8 @@ snapshots: ms@2.1.3: {} + muggle-string@0.4.1: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -9861,6 +10851,8 @@ snapshots: dependencies: entities: 6.0.1 + path-browserify@1.0.1: {} + path-exists@4.0.0: {} path-exists@5.0.0: {} @@ -9908,6 +10900,12 @@ snapshots: mlly: 1.8.0 pathe: 2.0.3 + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + playwright-core@1.56.1: {} playwright@1.56.1: @@ -9945,6 +10943,77 @@ snapshots: process@0.11.10: {} + promise@7.3.1: + dependencies: + asap: 2.0.6 + + pug-attrs@3.0.0: + dependencies: + constantinople: 4.0.1 + js-stringify: 1.0.2 + pug-runtime: 3.0.1 + + pug-code-gen@3.0.3: + dependencies: + constantinople: 4.0.1 + doctypes: 1.1.0 + js-stringify: 1.0.2 + pug-attrs: 3.0.0 + pug-error: 2.1.0 + pug-runtime: 3.0.1 + void-elements: 3.1.0 + with: 7.0.2 + + pug-error@2.1.0: {} + + pug-filters@4.0.0: + dependencies: + constantinople: 4.0.1 + jstransformer: 1.0.0 + pug-error: 2.1.0 + pug-walk: 2.0.0 + resolve: 1.22.11 + + pug-lexer@5.0.1: + dependencies: + character-parser: 2.2.0 + is-expression: 4.0.0 + pug-error: 2.1.0 + + pug-linker@4.0.0: + dependencies: + pug-error: 2.1.0 + pug-walk: 2.0.0 + + pug-load@3.0.0: + dependencies: + object-assign: 4.1.1 + pug-walk: 2.0.0 + + pug-parser@6.0.0: + dependencies: + pug-error: 2.1.0 + token-stream: 1.0.0 + + pug-runtime@3.0.1: {} + + pug-strip-comments@2.0.0: + dependencies: + pug-error: 2.1.0 + + pug-walk@2.0.0: {} + + pug@3.0.3: + dependencies: + pug-code-gen: 3.0.3 + pug-filters: 4.0.0 + pug-lexer: 5.0.1 + pug-linker: 4.0.0 + pug-load: 3.0.0 + pug-parser: 6.0.0 + pug-runtime: 3.0.1 + pug-strip-comments: 2.0.0 + punycode@2.3.1: {} quansync@0.2.11: {} @@ -10302,6 +11371,8 @@ snapshots: optionalDependencies: '@parcel/watcher': 2.5.1 + sax@1.4.3: {} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -10312,6 +11383,10 @@ snapshots: semver@6.3.1: {} + semver@7.5.4: + dependencies: + lru-cache: 6.0.0 + semver@7.7.3: {} serialize-javascript@4.0.0: @@ -10445,6 +11520,8 @@ snapshots: - supports-color - utf-8-validate + string-argv@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -10503,6 +11580,8 @@ snapshots: strip-indent@4.1.1: {} + strip-json-comments@3.1.1: {} + strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 @@ -10595,6 +11674,8 @@ snapshots: dependencies: is-number: 7.0.0 + token-stream@1.0.0: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -10613,6 +11694,8 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-map@1.0.3: {} + ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -10639,7 +11722,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.0(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3): + tsup@8.5.0(@microsoft/api-extractor@7.55.1(@types/node@22.19.0))(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.25.12) cac: 6.7.14 @@ -10659,6 +11742,7 @@ snapshots: tinyglobby: 0.2.15 tree-kill: 1.2.2 optionalDependencies: + '@microsoft/api-extractor': 7.55.1(@types/node@22.19.0) postcss: 8.5.6 typescript: 5.9.3 transitivePeerDependencies: @@ -10694,6 +11778,8 @@ snapshots: turbo-windows-64: 2.6.0 turbo-windows-arm64: 2.6.0 + type-fest@2.19.0: {} + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -10727,6 +11813,8 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 + typescript@5.8.2: {} + typescript@5.9.3: {} ufo@1.6.1: {} @@ -10755,6 +11843,8 @@ snapshots: universalify@0.1.2: {} + universalify@2.0.1: {} + unplugin@1.16.1: dependencies: acorn: 8.15.0 @@ -10766,6 +11856,10 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + use-sync-external-store@1.6.0(react@19.2.0): dependencies: react: 19.2.0 @@ -10795,7 +11889,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.2.2(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) + vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) transitivePeerDependencies: - '@types/node' - jiti @@ -10810,6 +11904,25 @@ snapshots: - tsx - yaml + vite-plugin-dts@4.5.4(@types/node@22.19.0)(rollup@4.53.2)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1)): + dependencies: + '@microsoft/api-extractor': 7.55.1(@types/node@22.19.0) + '@rollup/pluginutils': 5.3.0(rollup@4.53.2) + '@volar/typescript': 2.4.26 + '@vue/language-core': 2.2.0(typescript@5.9.3) + compare-versions: 6.1.1 + debug: 4.4.3 + kolorist: 1.8.0 + local-pkg: 1.1.2 + magic-string: 0.30.21 + typescript: 5.9.3 + optionalDependencies: + vite: 6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite@6.4.1(@types/node@22.19.0)(jiti@2.6.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.44.1): dependencies: esbuild: 0.25.12 @@ -10885,6 +11998,53 @@ snapshots: - tsx - yaml + void-elements@3.1.0: {} + + vscode-uri@3.1.0: {} + + vue-component-meta@2.2.12(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.15 + '@vue/language-core': 2.2.12(typescript@5.9.3) + path-browserify: 1.0.1 + vue-component-type-helpers: 2.2.12 + optionalDependencies: + typescript: 5.9.3 + + vue-component-type-helpers@2.2.12: {} + + vue-component-type-helpers@3.1.7: {} + + vue-docgen-api@4.79.2(vue@3.5.25(typescript@5.9.3)): + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-sfc': 3.5.25 + ast-types: 0.16.1 + esm-resolve: 1.0.11 + hash-sum: 2.0.0 + lru-cache: 8.0.5 + pug: 3.0.3 + recast: 0.23.11 + ts-map: 1.0.3 + vue: 3.5.25(typescript@5.9.3) + vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.25(typescript@5.9.3)) + + vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.25(typescript@5.9.3)): + dependencies: + vue: 3.5.25(typescript@5.9.3) + + vue@3.5.25(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.25 + '@vue/compiler-sfc': 3.5.25 + '@vue/runtime-dom': 3.5.25 + '@vue/server-renderer': 3.5.25(vue@3.5.25(typescript@5.9.3)) + '@vue/shared': 3.5.25 + optionalDependencies: + typescript: 5.9.3 + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 @@ -10966,6 +12126,13 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + with@7.0.2: + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + assert-never: 1.4.0 + babel-walk: 3.0.0-canary-5 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -10990,6 +12157,8 @@ snapshots: yallist@3.1.1: {} + yallist@4.0.0: {} + yaml@1.10.2: {} yargs-parser@21.1.1: {}