@hakit/components
Version:
A series of components to work with @hakit/core
1 lines • 1.1 MB
Source Map (JSON)
{"version":3,"file":"ThemeControlsModal-6PLKXzj7.cjs","sources":["../src/hooks/useBreakpoint.ts","../../../node_modules/@emotion/weak-memoize/dist/emotion-weak-memoize.esm.js","../src/Cards/CardBase/index.tsx","../src/Cards/CardBase/RelatedEntity/index.tsx","../src/Cards/CardBase/FeatureEntity/index.tsx","../src/Shared/Tooltip/index.tsx","../src/Shared/ErrorBoundary/index.tsx","../src/Cards/ButtonCard/index.tsx","../src/Shared/RangeSlider/index.tsx","../src/Shared/Ripples/index.tsx","../src/Cards/TriggerCard/index.tsx","../src/Group/index.tsx","../src/Cards/WeatherCard/index.tsx","../src/Cards/WeatherCard/WeatherCardDetail.tsx","../src/Cards/GarbageCollectionCard/garbage-bin.svg?react","../src/Cards/GarbageCollectionCard/index.tsx","../src/Cards/TimeCard/index.tsx","../src/Cards/AreaCard/index.tsx","../src/Cards/VacuumCard/index.tsx","../src/Cards/PictureCard/index.tsx","../src/Cards/FabCard/index.tsx","../src/Cards/FamilyCard/index.tsx","../src/Cards/FamilyCard/PersonCard/index.tsx","../src/Cards/SidebarCard/index.tsx","../src/Shared/Entity/Climate/ClimateControls/ClimateControlSlider.tsx","../src/Shared/Entity/Climate/ClimateControls/ClimateHumiditySlider.tsx","../src/Shared/Entity/Climate/ClimateControls/index.tsx","../src/Shared/Entity/Light/LightControls/color_wheel.png","../src/Shared/Entity/Light/LightControls/index.tsx","../src/Shared/Entity/Alarm/AlarmControls/index.tsx","../src/Shared/Entity/Cover/CoverControls/index.tsx","../src/Cards/MediaPlayerCard/Fab.tsx","../src/Shared/Entity/MediaPlayer/MediaPlayerControls/index.tsx","../src/Shared/Entity/Person/PersonControls/index.tsx","../src/Cards/ClimateCard/index.tsx","../src/Cards/EntitiesCard/index.tsx","../src/Cards/EntitiesCard/EntitiesCardRow.tsx","../src/Cards/MediaPlayerCard/VolumeControls/index.tsx","../src/Cards/MediaPlayerCard/PlaybackControls/index.tsx","../src/Cards/MediaPlayerCard/AlternateControls.tsx","../src/Cards/MediaPlayerCard/index.tsx","../../../node_modules/zustand/esm/vanilla/shallow.mjs","../../../node_modules/zustand/esm/react/shallow.mjs","../src/Cards/CalendarCard/index.tsx","../src/Shared/Entity/Miscellaneous/ButtonBar/index.tsx","../src/Shared/Entity/Miscellaneous/ButtonBar/ButtonBarButton/index.tsx","../src/Shared/Entity/Miscellaneous/ButtonGroup/index.tsx","../src/Shared/Entity/Miscellaneous/ButtonGroup/ButtonGroupButton/index.tsx","../src/Cards/CameraCard/players/hls.tsx","../src/Cards/CameraCard/players/webrtc.tsx","../src/Cards/CameraCard/stream/index.tsx","../src/Cards/CameraCard/index.tsx","../src/Shared/Entity/Miscellaneous/LogBookRenderer/index.tsx","../src/Shared/Modal/index.tsx","../../core/src/utils/computeDomain.ts","../src/Shared/Modal/ModalByEntityDomain/index.tsx","../src/Shared/Entity/Vacuum/VacuumControls/index.tsx","../src/Shared/ControlSlider/index.tsx","../src/Shared/ControlToggle/index.tsx","../src/Shared/ControlSliderCircular/index.tsx","../../../node_modules/@floating-ui/utils/dist/floating-ui.utils.dom.mjs","../../../node_modules/@floating-ui/utils/dist/floating-ui.utils.mjs","../../../node_modules/@floating-ui/core/dist/floating-ui.core.mjs","../../../node_modules/@floating-ui/dom/dist/floating-ui.dom.mjs","../../../node_modules/@floating-ui/react-dom/dist/floating-ui.react-dom.mjs","../../../node_modules/@floating-ui/react/dist/floating-ui.react.mjs","../src/Shared/Menu/index.tsx","../src/Shared/Entity/Light/ColorTempPicker/index.tsx","../src/Shared/Entity/Light/ColorPicker/index.tsx","../src/Shared/Entity/Miscellaneous/EntityAttributes/index.tsx","../src/ThemeProvider/index.tsx","../src/ThemeProvider/ThemeControls.tsx","../src/ThemeProvider/ThemeControlsModal.tsx"],"sourcesContent":["import { useEffect, useState, useMemo } from \"react\";\nimport { getBreakpoints, allBreakpoints, type BreakPoint, useThemeStore } from \"@components\";\nimport { useStore } from \"@hakit/core\";\n\n/**\n * @description This hook can be used to programmatically change the layout/content or functionality based on the current breakpoint.\n * This will return an object with all breakpoint key names and their active state.\n *\n * NOTE: If you're running hakit within an iframe, you'll need to update the window context in the HassConnect.options.windowContext property\n *\n * @example\n * ```tsx\n * import { useBreakpoint } from \"@hakit/components\";\n * function SomeComponent() {\n * const bp = useBreakpoint();\n * return (\n * <div>\n * {bp.xxs && <p>Extra small</p>}\n * {bp.xs && <p>Small</p>}\n * {bp.sm && <p>Medium</p>}\n * {bp.md && <p>Large</p>}\n * {bp.lg && <p>Extra large</p>}\n * </div>\n * );\n * }\n *\n * @returns { [key in BreakPoint]: boolean } - Object containing the breakpoint keys and if they're active or not.\n */\nexport function useBreakpoint(): { [key in BreakPoint]: boolean } {\n const breakpoints = useThemeStore((store) => store.breakpoints);\n const windowContext = useStore((store) => store.windowContext);\n const win = windowContext ?? window;\n const queries = useMemo(() => getBreakpoints(breakpoints), [breakpoints]);\n const [matches, setMatches] = useState(() => Object.fromEntries(allBreakpoints.map((bp) => [bp, false])) as Record<BreakPoint, boolean>);\n\n useEffect(() => {\n const context = win || window;\n const mqlMap = new Map<BreakPoint, MediaQueryList>();\n\n const updateMatches = () => {\n const newMatches = Object.fromEntries(allBreakpoints.map((bp) => [bp, false])) as Record<BreakPoint, boolean>;\n\n for (const bp of allBreakpoints) {\n const query = queries[bp];\n if (typeof query === \"string\") {\n const mql = mqlMap.get(bp);\n if (mql?.matches) {\n newMatches[bp] = true;\n // Only one should be active at a time, so we can break here\n break;\n }\n }\n }\n\n setMatches(newMatches);\n };\n\n for (const bp of allBreakpoints) {\n const query = queries[bp];\n if (typeof query === \"string\") {\n const mql = context.matchMedia(query);\n // when dynamically switch context (windows) the mql will be null\n // let the next iteration handle the mql\n if (!mql) continue;\n mqlMap.set(bp, mql);\n mql.addEventListener(\"change\", updateMatches);\n }\n }\n\n // Set initial matches\n updateMatches();\n\n return () => {\n for (const mql of mqlMap.values()) {\n if (!mql) continue;\n mql.removeEventListener(\"change\", updateMatches);\n }\n };\n }, [queries, win]);\n\n return matches;\n}\n","var weakMemoize = function weakMemoize(func) {\n var cache = new WeakMap();\n return function (arg) {\n if (cache.has(arg)) {\n // Use non-null assertion because we just checked that the cache `has` it\n // This allows us to remove `undefined` from the return value\n return cache.get(arg);\n }\n\n var ret = func(arg);\n cache.set(arg, ret);\n return ret;\n };\n};\n\nexport { weakMemoize as default };\n","import { css } from \"@emotion/react\";\nimport { useLongPress, type LongPressReactEvents } from \"use-long-press\";\nimport { lowerCase, startCase } from \"lodash\";\nimport {\n memo,\n useMemo,\n useId,\n useState,\n useCallback,\n Children,\n isValidElement,\n cloneElement,\n type ReactElement,\n type ReactNode,\n CSSProperties,\n useRef,\n} from \"react\";\nimport {\n type EntityName,\n type DomainService,\n type ExtractDomain,\n type ServiceData,\n type HassEntityWithService,\n type HistoryOptions,\n computeDomain,\n isUnavailableState,\n useEntity,\n useStore,\n localize,\n} from \"@hakit/core\";\nimport { CSSInterpolation } from \"@emotion/serialize\";\nimport {\n ModalByEntityDomain,\n Ripples,\n fallback,\n type RipplesProps,\n type AvailableQueries,\n type ModalByEntityDomainProps,\n type BreakPoint,\n Alert,\n SvgGraph,\n ButtonBar,\n ButtonBarButton,\n ButtonBarProps,\n SvgGraphProps,\n type RelatedEntity,\n type RelatedEntityProps,\n} from \"@components\";\nimport { type FeatureEntity, type FeatureEntityProps } from \"./FeatureEntity\";\nimport { ErrorBoundary } from \"react-error-boundary\";\nimport { isValidProp } from \"../../utils/isValidProp\";\nimport styled from \"@emotion/styled\";\nimport { useResizeDetector } from \"react-resize-detector\";\nimport { useResizeDetectorProps } from \"react-resize-detector\";\nimport { SVG_HEIGHT, SVG_WIDTH } from \"../../Shared/SvgGraph/constants\";\n\nconst getBaseElement = <C extends keyof React.JSX.IntrinsicElements = \"div\">(as: C, onlyFunctionality?: boolean) => {\n if (onlyFunctionality) {\n return styled(as)``;\n }\n return styled(as, {\n shouldForwardProp: (prop) => isValidProp(prop),\n })<{\n disableActiveState: boolean;\n disabled?: boolean;\n }>`\n outline: none;\n border: 0;\n box-sizing: border-box;\n padding: 0;\n position: relative;\n overflow: hidden;\n display: flex;\n width: 100%;\n cursor: pointer;\n background-color: var(--ha-S300);\n box-shadow: 0px 0px 0px rgba(0, 0, 0, 0);\n transform: scale(1) translate3d(0, 0, 0);\n transition: var(--ha-transition-duration) var(--ha-easing);\n transition-property: transform, background-color, background-image;\n color: var(--ha-S200-contrast);\n flex-shrink: 1;\n user-select: none;\n svg {\n color: var(--ha-S200-contrast);\n transition: color var(--ha-transition-duration) var(--ha-easing);\n }\n .graph-element {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n }\n &:not(.disabled):not(:disabled):not(:focus):hover {\n background-color: var(--ha-S400);\n color: var(--ha-500-contrast);\n svg {\n color: var(--ha-S400-contrast);\n }\n }\n &:disabled,\n &.disabled {\n cursor: not-allowed;\n opacity: 0.8;\n }\n &.card-base-active:active:not(.disable-scale-effect):not(.disabled):not(.unavailable) {\n transform: scale(0.9) translate3d(0, 0, 0);\n }\n ${(props) =>\n !props.disableActiveState &&\n `\n &.active, &:active {\n background-color: var(--ha-A400);\n color: var(--ha-900-contrast);\n svg {\n color: var(--ha-900-contrast);\n }\n &:not(:disabled):not(.disabled):hover {\n background-color: var(--ha-A700);\n color: var(--ha-900-contrast);\n }\n }\n `}\n `;\n};\n\nconst StyledRipples = styled((props: RipplesProps) => <Ripples {...props} />)`\n flex-shrink: 1;\n width: 100%;\n height: 100%;\n`;\n\nconst Trigger = styled.div`\n width: 100%;\n height: 100%;\n &:has(.features) {\n display: flex;\n flex-direction: column;\n }\n > .features {\n padding: 0 1rem 1rem;\n > .fit-content > .button-group-inner > * {\n width: auto;\n flex-grow: 1;\n > .button-bar-button {\n width: auto;\n flex-grow: 1;\n }\n }\n }\n`;\n\ntype Extendable<T extends keyof React.JSX.IntrinsicElements> = Omit<\n React.ComponentPropsWithRef<T>,\n \"onClick\" | \"disabled\" | \"title\" | \"children\" | \"active\"\n>;\n\n// Define the allowed children types\ntype AllowedFeaturedEntity = ReactElement<typeof FeatureEntity> | false | null | undefined;\ntype AllowedFeaturedEntities = AllowedFeaturedEntity | AllowedFeaturedEntity[];\n\n// Define the allowed children types\ntype AllowedRelatedEntity = ReactElement<typeof RelatedEntity> | false | null | undefined;\ntype AllowedRelatedEntities = AllowedRelatedEntity | AllowedRelatedEntity[];\n\nexport type CardBaseProps<T extends keyof React.JSX.IntrinsicElements = \"div\", E extends EntityName = EntityName> = Extendable<T> &\n AvailableQueries & {\n /** convert the component type to something else @default \"div\" */\n as?: T;\n /** children to render alongside the card */\n children?: ReactNode;\n /** should the card be disabled, this will disable any click events, service calls and scale effects */\n disabled?: boolean;\n /** Optional active param, By default this is updated via home assistant @default undefined */\n active?: boolean;\n /** By default, the title is retrieved from the domain name, or you can specify a manual title @default undefined */\n title?: ReactNode;\n /** The name of your entity */\n entity?: E;\n /** The service name to call */\n service?: DomainService<ExtractDomain<E>>;\n /** The data to pass to the service */\n serviceData?: ServiceData<ExtractDomain<E>, DomainService<ExtractDomain<E>>>;\n /** allows you to place a fully functional and interactive element at predefined zones of the card, like displaying an icon in the top left which might be a sensor indicating battery level */\n relatedEntities?: AllowedRelatedEntities;\n /** provide a FeatureEntity component as a list or individual components to render a bar at the bottom of the card */\n features?: AllowedFeaturedEntities;\n /** props to pass to the feature bar */\n featureBarProps?: Omit<ButtonBarProps, \"children\">;\n /** callback to fire after a long press event */\n longPressCallback?: E extends undefined\n ? (entity: null, event: LongPressReactEvents) => void\n : (entity: HassEntityWithService<ExtractDomain<E>>, event: LongPressReactEvents) => void;\n /** The onClick handler is called when the card is pressed, the first argument will be entity object with api methods if entity is provided */\n /** The onClick handler is called when the button is pressed, the first argument will be entity object with api methods if entity is provided */\n onClick?: E extends undefined\n ? (entity: null, event: React.MouseEvent<HTMLElement>) => void\n : (entity: HassEntityWithService<ExtractDomain<E>>, event: React.MouseEvent<HTMLElement>) => void;\n /** disable the modal opening functionality @default false */\n disableModal?: boolean;\n /** props to pass to the modal */\n modalProps?: Partial<ModalByEntityDomainProps<E>>;\n /** include ripples or not */\n disableRipples?: boolean;\n /** disable the scale effect on the card when clicked */\n disableScale?: boolean;\n /** disable the styles of the card when in the active state */\n disableActiveState?: boolean;\n /** This also controls the animated modal border-radius, update the border radius of the card @default \"16px\" */\n borderRadius?: CSSProperties[\"borderRadius\"];\n /** completely disable the automated column sizes, this will default to whatever width is provided by the user or the card @default false */\n disableColumns?: boolean;\n /** props to pass to the ripple component if enabled */\n rippleProps?: Omit<RipplesProps, \"children\">;\n /** className to provide to the trigger element */\n triggerClass?: string;\n /** the graph settings containing the entity to display a graph in the background of the card */\n graph?: {\n /** the entity to display the graph for */\n entity: EntityName;\n /** the props to pass to the svg graph, control the styles and colors of the graph from here */\n props?: SvgGraphProps;\n /** the space at the bottom of the card is automatically calculated by the available height, you can adjust this by manipulating the value here @default 0px */\n adjustGraphSpaceBy?: CSSProperties[\"paddingBottom\"];\n /** options to pass to the history request */\n historyOptions?: HistoryOptions;\n };\n /**\n *\n * A css string to update the card, this is similar to how you'd write scss.\n *\n * ```jsx\n * export const MyComponent = (otherProps) => {\n * return <SomeCard cssStyles={`\n * color: var(--ha-900-contrast);\n * .some-selector {\n * &:hover {\n * background-color: var(--ha-A400);\n * }\n * }\n * `} {...otherProps} />\n * }\n * ```\n */\n cssStyles?: CSSInterpolation;\n /** remove all base styles of the card and just use the inbuilt functionality */\n onlyFunctionality?: boolean;\n /** props to pass to the resize detector, this is useful if you want to trigger something whenever the card resizes */\n resizeDetectorProps?: useResizeDetectorProps<HTMLElement>;\n /** ref callback to get the reference to the card element */\n refCallback?: (ref: React.RefObject<HTMLElement | null>) => void;\n };\n\nconst DEFAULT_SIZES: Required<AvailableQueries> = {\n xxs: 12,\n xs: 6,\n sm: 6,\n md: 4,\n lg: 4,\n xlg: 3,\n};\n\nconst CardBaseInternal = function CardBase<T extends keyof React.JSX.IntrinsicElements = \"div\", E extends EntityName = EntityName>({\n as = \"div\" as T,\n entity: _entity,\n title: _title,\n active,\n service,\n serviceData,\n children,\n disabled,\n longPressCallback,\n onClick,\n disableModal = false,\n modalProps,\n disableRipples = false,\n disableScale = false,\n disableActiveState = false,\n onlyFunctionality = false,\n id,\n className,\n cssStyles,\n style,\n borderRadius = \"16px\",\n rippleProps,\n disableColumns,\n refCallback,\n key,\n relatedEntities,\n features,\n featureBarProps,\n graph,\n resizeDetectorProps,\n triggerClass,\n ...rest\n}: CardBaseProps<T, E>): ReactElement<T> {\n const _id = useId();\n const globalComponentStyle = useStore((state) => state.globalComponentStyles);\n const [openModal, setOpenModal] = useState(false);\n const domain = _entity ? computeDomain(_entity) : null;\n const entity = useEntity(_entity ?? \"unknown\", {\n returnNullIfNotFound: true,\n });\n const internalRef = useRef<HTMLDivElement | null>(null);\n // useful so users can subscribe to the resize event\n const { width = 0 } = useResizeDetector({\n refreshMode: \"debounce\",\n refreshRate: 50,\n handleHeight: false,\n skipOnMount: false,\n targetRef: internalRef,\n ...(resizeDetectorProps ?? {}),\n });\n const graphEntity = useEntity(graph?.entity ?? \"unknown\", {\n returnNullIfNotFound: true,\n historyOptions: {\n disable: false,\n ...graph?.historyOptions,\n },\n });\n const isUnavailable = useMemo(() => (typeof entity?.state === \"string\" ? isUnavailableState(entity.state) : false), [entity?.state]);\n const _borderRadius = borderRadius;\n const StyledElement = useMemo(() => getBaseElement(as as \"div\", onlyFunctionality), [as, onlyFunctionality]);\n const bind = useLongPress<HTMLDivElement>(\n (e) => {\n if (typeof longPressCallback === \"function\") {\n if (entity !== null) {\n // we don't know the types at this level, but they will be correct at the parent level\n longPressCallback(entity as never, e);\n } else {\n longPressCallback(null as never, e);\n }\n }\n if (typeof _entity === \"string\" && !openModal && !disableModal) {\n setOpenModal(true);\n }\n internalRef.current?.classList.remove(\"card-base-active\");\n },\n {\n threshold: 300,\n cancelOnMovement: true,\n cancelOutsideElement: true,\n filterEvents(e) {\n return !(\"button\" in e && e.button === 2);\n },\n },\n );\n const onClickHandler = useCallback(\n (event: React.MouseEvent<HTMLDivElement>) => {\n if (disabled) return;\n // so we can expect it to throw errors however the parent level ts validation will catch invalid params.\n if (typeof service === \"string\" && entity && !isUnavailable) {\n // @ts-expect-error - we don't actually know the service at this level\n const caller = entity.service[service];\n caller(serviceData);\n }\n if (typeof onClick === \"function\") {\n if (entity !== null) {\n // we don't know the types at this level, but they will be correct at the parent level\n onClick(entity as never, event);\n } else {\n onClick(null as never, event);\n }\n }\n },\n [service, disabled, entity, serviceData, onClick, isUnavailable],\n );\n // use the input title if provided, else use the domain if available, else null\n const title = useMemo(\n () => _title || entity?.attributes?.friendly_name || (domain !== null ? startCase(lowerCase(domain)) : null),\n [_title, entity, domain],\n );\n\n const columnClassNames = useMemo(() => {\n const mergedGrids = Object.entries(DEFAULT_SIZES).reduce<Required<AvailableQueries>>((acc, [key, value]) => {\n const inputValue = rest[key as BreakPoint];\n return {\n ...acc,\n [key]: inputValue ?? value,\n };\n }, DEFAULT_SIZES);\n return Object.entries(mergedGrids)\n .map(([key, value]) => `${key}-${value}`)\n .join(\" \");\n // this is okay, we only want this effect to re-run when the breakpoints change not the entire prop object\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rest.xxs, rest.xs, rest.sm, rest.md, rest.lg, rest.xlg]);\n\n const filteredRelatedEntities = Children.toArray(relatedEntities).filter((child): child is ReactElement<typeof ButtonBarButton> =>\n isValidElement(child),\n );\n const filteredFeaturedEntities = Children.toArray(features).filter((child): child is ReactElement<typeof ButtonBarButton> =>\n isValidElement(child),\n );\n\n const featuredElements = Children.map(filteredFeaturedEntities, (child, index) => {\n if (isValidElement<FeatureEntityProps<EntityName>>(child)) {\n return cloneElement(child, {\n key: child.key || `${_id}${index}`,\n ...child.props,\n });\n }\n return child;\n });\n const hasFeatures = filteredFeaturedEntities.length > 0;\n const featureBar = hasFeatures && (\n <ButtonBar layoutType=\"bubble\" className=\"features\" fullWidth gap=\"0.5rem\" {...featureBarProps}>\n {featuredElements}\n </ButtonBar>\n );\n\n function calculateSvgHeight(parentWidth: number): CSSProperties[\"paddingBottom\"] {\n const aspectRatio = SVG_WIDTH / SVG_HEIGHT;\n return `calc(${parentWidth / aspectRatio}px - ${graph?.adjustGraphSpaceBy ?? \"0px\"});`;\n }\n\n const svgHeight = calculateSvgHeight(width);\n\n const _classes = useMemo(() => {\n return [\n \"card-base\",\n className ?? \"\",\n disableColumns ? \"\" : columnClassNames,\n disableScale ? \"disable-scale-effect\" : \"\",\n active ? \"active\" : \"\",\n isUnavailable ? \"unavailable\" : \"\",\n disabled || isUnavailable ? \"disabled\" : \"\",\n hasFeatures ? \"has-features\" : \"\",\n graphEntity ? \"has-graph\" : \"\",\n ]\n .filter((x) => !!x)\n .join(\" \");\n }, [active, className, columnClassNames, disableScale, disableColumns, disabled, graphEntity, hasFeatures, isUnavailable]);\n\n const handlePointerDown = () => {\n internalRef.current?.classList.add(\"card-base-active\");\n };\n\n const handlePointerUp = () => {\n internalRef.current?.classList.remove(\"card-base-active\");\n };\n\n return (\n <>\n <StyledElement\n key={key}\n ref={(ref) => {\n internalRef.current = ref;\n if (refCallback) {\n refCallback(internalRef);\n }\n }}\n id={id ?? \"\"}\n className={_classes}\n css={css`\n padding-bottom: ${graphEntity ? svgHeight : \"inherit\"};\n ${globalComponentStyle.cardBase ?? \"\"}\n ${cssStyles ?? \"\"}\n `}\n style={{\n ...(style ?? {}),\n borderRadius: _borderRadius,\n }}\n disableActiveState={disableActiveState}\n disabled={isUnavailable || disabled}\n {...bind()}\n {...(rest as unknown as React.HTMLAttributes<HTMLDivElement>)}\n >\n {graphEntity && (\n <div className={\"graph-element history\"}>\n {graphEntity.history.loading ? (\n <Alert className={\"loading\"} description={localize(\"loading\")} />\n ) : graphEntity.history.coordinates.length > 0 ? (\n <SvgGraph coordinates={graphEntity.history.coordinates} {...graph?.props} />\n ) : (\n <Alert className={\"no-state-history\"} description={localize(\"no_state_history_found\")} />\n )}\n </div>\n )}\n {disableRipples ? (\n <Trigger\n className={`contents trigger-element ${triggerClass}`}\n onClick={onClickHandler}\n onPointerDown={handlePointerDown}\n onPointerUp={handlePointerUp}\n >\n {children}\n {featureBar}\n </Trigger>\n ) : (\n <StyledRipples {...rippleProps} key={rippleProps?.key} borderRadius={_borderRadius} disabled={disabled || isUnavailable}>\n <Trigger\n className={`contents trigger-element ${triggerClass}`}\n onClick={onClickHandler}\n onPointerDown={handlePointerDown}\n onPointerUp={handlePointerUp}\n >\n {children}\n {featureBar}\n </Trigger>\n </StyledRipples>\n )}\n {Children.map(filteredRelatedEntities, (child, index) => {\n if (isValidElement<RelatedEntityProps<EntityName>>(child)) {\n return cloneElement(child, {\n key: child.key || `${_id}${index}`,\n ...child.props,\n });\n }\n return child;\n })}\n </StyledElement>\n {typeof _entity === \"string\" && (\n <ModalByEntityDomain\n {...modalProps}\n entity={_entity as EntityName}\n title={modalProps?.title ?? title ?? localize(\"unknown\")}\n onClose={() => {\n setOpenModal(false);\n if (modalProps?.onClose) {\n modalProps.onClose();\n }\n }}\n open={modalProps?.open || openModal}\n id={_id}\n />\n )}\n </>\n );\n};\n\n/**\n * This is the base card component that every other card component should extend, it comes with everything we need to be able to replicate functionality\n * like the modal popup, ripples and more.\n *\n * You can use this if you want an empty shell of a component that you can build on top of.\n * */\nexport const CardBase = memo(function CardBase<T extends keyof React.JSX.IntrinsicElements = \"div\", E extends EntityName = EntityName>(\n props: CardBaseProps<T, E>,\n) {\n return <ErrorBoundary {...fallback({ prefix: \"CardBase\" })}>{CardBaseInternal<T, E>(props)}</ErrorBoundary>;\n});\n","import styled from \"@emotion/styled\";\nimport { memo, useCallback, useMemo, type ReactNode, type CSSProperties } from \"react\";\nimport {\n type EntityName,\n type ExtractDomain,\n type HassEntityWithService,\n type DomainService,\n type ServiceData,\n isUnavailableState,\n computeDomain,\n useEntity,\n useIconByDomain,\n useIconByEntity,\n useIcon,\n} from \"@hakit/core\";\nimport { fallback } from \"@components\";\nimport { ErrorBoundary } from \"react-error-boundary\";\nimport { type IconProps } from \"@iconify/react\";\n\ntype Position =\n | \"left top\"\n | \"left center\"\n | \"left bottom\"\n | \"center top\"\n | \"center center\"\n | \"center bottom\"\n | \"right top\"\n | \"right center\"\n | \"right bottom\";\n\nexport interface RelatedEntityProps<E extends EntityName = EntityName> extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"onClick\"> {\n /** The name of the entity */\n entity: E;\n /** The service name to call */\n service?: DomainService<ExtractDomain<E>>;\n /** The data to pass to the service */\n serviceData?: ServiceData<ExtractDomain<E>, DomainService<ExtractDomain<E>>>;\n /** overwrite the default for the entity */\n icon?: string;\n /** properties for the icon */\n iconProps?: Omit<IconProps, \"icon\">;\n /** the position of the entity element, @default \"top left\" */\n position?: Position;\n /** should the element be disabled or not which will block the click events @default false */\n disabled?: boolean;\n /** custom render method for the element, this will replace any default children of this component */\n render?: (entity: HassEntityWithService<ExtractDomain<E>>, icon: ReactNode | null) => ReactNode;\n /** margin for the custom element @default 1rem */\n margin?: CSSProperties[\"margin\"];\n /** padding for the custom element @default 0 */\n padding?: CSSProperties[\"padding\"];\n /** The onClick handler is called when the button is pressed, the first argument will be entity object with api methods if entity is provided */\n onClick?: (entity: HassEntityWithService<ExtractDomain<E>>, event: React.MouseEvent<HTMLElement>) => void;\n}\n\ntype PartialStyleProps = Pick<RelatedEntityProps<EntityName>, \"position\" | \"padding\" | \"margin\">;\n\nconst RelatedEntityEl = styled.div<PartialStyleProps>`\n position: absolute;\n margin: ${(props) => props.margin || \"1rem\"};\n padding: ${(props) => props.padding || \"0\"};\n cursor: ${(props) => (props.onClick ? \"pointer\" : \"default\")};\n ${(props) => {\n switch (props.position) {\n case \"left top\":\n return `top: 0; left: 0;`;\n case \"left center\":\n return `top: 50%; left: 0; transform: translateY(-50%);`;\n case \"left bottom\":\n return `bottom: 0; left: 0;`;\n case \"center top\":\n return `top: 0; left: 50%; transform: translateX(-50%);`;\n case \"center center\":\n return `top: 50%; left: 50%; transform: translate(-50%, -50%);`;\n case \"center bottom\":\n return `bottom: 0; left: 50%; transform: translateX(-50%);`;\n case \"right top\":\n return `top: 0; right: 0;`;\n case \"right center\":\n return `top: 50%; right: 0; transform: translateY(-50%);`;\n case \"right bottom\":\n return `bottom: 0; right: 0;`;\n default:\n return `top: 0; right: 0;`;\n }\n }}\n`;\n\nfunction InternalRelatedEntity<E extends EntityName>({\n entity: _entity,\n icon: _icon,\n iconProps,\n render,\n position,\n onClick,\n disabled,\n service,\n serviceData,\n ...rest\n}: RelatedEntityProps<E>) {\n const entity = useEntity(_entity);\n const iconElement = useIcon(_icon ?? null, iconProps);\n const domain = computeDomain(_entity);\n const domainIcon = useIconByDomain(domain === null ? \"unknown\" : domain, iconProps);\n const entityIcon = useIconByEntity(_entity || \"unknown\", iconProps);\n const icon = iconElement ?? entityIcon ?? domainIcon;\n const isUnavailable = useMemo(() => (typeof entity?.state === \"string\" ? isUnavailableState(entity.state) : false), [entity?.state]);\n const onClickHandler = useCallback(\n (event: React.MouseEvent<HTMLElement>) => {\n if (disabled) return;\n // so we can expect it to throw errors however the parent level ts validation will catch invalid params.\n if (typeof service === \"string\" && entity && !isUnavailable) {\n // @ts-expect-error - we don't actually know the service at this level\n const caller = entity.service[service];\n caller(serviceData);\n }\n if (typeof onClick === \"function\") {\n if (entity !== null) {\n // we don't know the types at this level, but they will be correct at the parent level\n onClick(entity as never, event);\n } else {\n onClick(null as never, event);\n }\n }\n },\n [service, disabled, entity, serviceData, onClick, isUnavailable],\n );\n return (\n <RelatedEntityEl position={position} onClick={onClickHandler} {...rest}>\n {render ? render(entity, icon) : <>{icon}</>}\n </RelatedEntityEl>\n );\n}\n\n/**\n * This can be used within the `relatedEntities` prop for any card that extends CardBase where you can place icons/elements in predefined positions across the card with full control over style/positions/rendering capabilities, click actions and more.\n * Each individual related entity can have clickable actions, stylable and more.\n * This would be useful to show an icon for an entity to indicate it's battery level or state.\n * */\nexport const RelatedEntity = memo(function RelatedEntity<E extends EntityName>(props: RelatedEntityProps<E>) {\n return (\n <ErrorBoundary {...fallback({ prefix: \"RelatedEntity\" })}>\n <InternalRelatedEntity {...props} />\n </ErrorBoundary>\n );\n});\n","import { type EntityName } from \"@hakit/core\";\nimport { fallback, ButtonBarButton, type ButtonBarButtonProps } from \"@components\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nexport type FeatureEntityProps<E extends EntityName = EntityName> = ButtonBarButtonProps<E>;\n\nfunction _FeatureEntity<E extends EntityName>({ children, active, ...rest }: ButtonBarButtonProps<E>) {\n return (\n <ButtonBarButton\n // @ts-expect-error - will need to fix this typescript problem later\n as=\"div\"\n rippleProps={{\n preventPropagation: true,\n }}\n active={active}\n cssStyles={`\n &.button-bar-button {\n .contents {\n > div {\n padding: 0.6rem;\n }\n }\n }\n `}\n {...rest}\n >\n {children}\n </ButtonBarButton>\n );\n}\n\n/**\n * This can be used within the `features` prop for any card that extends CardBase where you can place actions at the bottom of every card allowing you to replace similar functionality of the \"features\" option within home assistant.\n * */\nexport const FeatureEntity = function FeatureEntity<E extends EntityName>(props: ButtonBarButtonProps<E>) {\n return (\n <ErrorBoundary {...fallback({ prefix: \"FeatureEntity\" })}>\n <_FeatureEntity {...props} />\n </ErrorBoundary>\n );\n};\n","import { useRef, useCallback, Children, isValidElement, cloneElement, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport styled from \"@emotion/styled\";\nimport { fallback } from \"@components\";\nimport { ErrorBoundary } from \"react-error-boundary\";\nimport { useStore } from \"@hakit/core\";\n\nconst TooltipSpan = styled.span<Pick<TooltipProps, \"placement\">>`\n position: fixed;\n top: 0;\n left: 0;\n background-color: var(--ha-S300);\n color: var(--ha-S100-contrast);\n padding: 8px;\n border-radius: 4px;\n box-shadow: 0px 2px 4px var(--ha-S100);\n font-size: 0.9rem;\n z-index: 1000;\n visibility: hidden;\n opacity: 0;\n transition: var(--ha-transition-duration) var(--ha-easing);\n transition-property: opacity, visibility;\n pointer-events: none;\n transform: ${(props) => {\n switch (props.placement) {\n default:\n case \"top\":\n return \"translateY(calc(-100% - 10px)) translateX(-50%)\";\n case \"right\":\n return \"translateX(10px) translateY(-50%)\";\n case \"bottom\":\n return \"translateY(10px) translateX(-50%)\";\n case \"left\":\n return \"translateX(calc(-100% - 10px)) translateY(-50%)\";\n }\n }};\n &::before {\n content: \"\";\n position: absolute;\n width: 0;\n height: 0;\n border-style: solid;\n display: block;\n ${(props) => {\n switch (props.placement) {\n default:\n case \"top\":\n return `\n border-width: 6px 6px 0 6px;\n border-color: var(--ha-S300) transparent transparent transparent;\n top: 100%;\n left: 50%;\n transform: translateX(-50%);\n `;\n case \"right\":\n return `\n border-width: 6px 6px 6px 0;\n border-color: transparent var(--ha-S300) transparent transparent;\n left: 0;\n top: 50%;\n transform: translate(-100%, -50%);\n `;\n case \"bottom\":\n return `\n border-width: 0 6px 6px 6px;\n border-color: transparent transparent var(--ha-S300) transparent;\n bottom: 100%;\n left: 50%;\n transform: translateX(-50%);\n `;\n case \"left\":\n return `\n border-width: 6px 0 6px 6px;\n border-color: transparent transparent transparent var(--ha-S300);\n right: 0;\n top: 50%;\n transform: translate(100%, -50%);\n `;\n }\n }};\n }\n`;\n\nexport interface TooltipProps extends Omit<React.ComponentPropsWithRef<\"div\">, \"title\"> {\n /** the placement of the tooltip @default 'top' */\n placement?: \"top\" | \"right\" | \"bottom\" | \"left\";\n /** the title of the tooltip */\n title?: React.ReactNode | null;\n /** the children of the tooltip */\n children: React.ReactNode;\n}\n\nfunction InternalTooltip({ placement = \"top\", title = null, children, ref, ...rest }: TooltipProps) {\n const tooltipRef = useRef<HTMLSpanElement | null>(null);\n const childRef = useRef<HTMLDivElement | null>(null);\n const portalRoot = useStore((store) => store.portalRoot);\n const windowContext = useStore((store) => store.windowContext);\n const win = windowContext ?? window;\n const [show, setShow] = useState(false);\n\n const calculatePosition = useCallback(\n (el: HTMLSpanElement) => {\n const childRect = childRef.current?.getBoundingClientRect();\n if (typeof childRect === \"undefined\") return;\n let top = 0;\n let left = 0;\n switch (placement) {\n case \"top\":\n top = childRect.top;\n left = childRect.left + childRect.width / 2;\n break;\n case \"right\":\n top = childRect.top + childRect.height / 2;\n left = childRect.right;\n break;\n case \"bottom\":\n top = childRect.bottom;\n left = childRect.left + childRect.width / 2;\n break;\n case \"left\":\n top = childRect.top + childRect.height / 2;\n left = childRect.left;\n break;\n }\n el.style.top = `${top}px`;\n el.style.left = `${left}px`;\n // to ensure animations play out, we need to update these values after the next tick\n setTimeout(() => {\n el.style.opacity = \"1\";\n el.style.visibility = \"visible\";\n }, 0);\n },\n [placement],\n );\n\n const handleMouseEnter = useCallback(() => {\n setShow(true);\n }, []);\n\n const handleHide = useCallback(() => {\n const tooltipEl = tooltipRef.current;\n if (!tooltipEl) return;\n tooltipEl.style.opacity = \"0\";\n tooltipEl.style.visibility = \"hidden\";\n tooltipEl.setAttribute(\"aria-hidden\", \"true\");\n setTimeout(() => {\n setShow(false);\n }, 250);\n }, []);\n\n if (title === null || title === \"\") {\n return children;\n }\n\n return (\n <div\n ref={childRef}\n onBlur={handleHide}\n onTouchEnd={handleHide}\n onTouchStart={handleMouseEnter}\n onMouseUp={handleHide}\n onMouseEnter={handleMouseEnter}\n onMouseLeave={handleHide}\n {...rest}\n >\n {Children.map(children, (child, index) => {\n if (\n isValidElement<\n Omit<React.ComponentPropsWithRef<\"div\">, \"onClick\"> & {\n onClick: (unknown: null, event: React.MouseEvent<HTMLDivElement>) => void;\n }\n >(child)\n ) {\n return cloneElement(child, {\n ...child.props,\n onClick(_unknown: null, event: React.MouseEvent<HTMLDivElement>) {\n child.props.onClick?.(_unknown, event);\n rest?.onClick?.(event);\n },\n ref,\n key: child.key ?? index,\n });\n }\n return child;\n })}\n {typeof document !== \"undefined\" &&\n createPortal(\n show && (\n <TooltipSpan\n className=\"tooltip-inner\"\n placement={placement}\n ref={(ref) => {\n if (ref) {\n tooltipRef.current = ref;\n calculatePosition(ref);\n }\n }}\n aria-hidden=\"false\"\n >\n {title}\n </TooltipSpan>\n ),\n portalRoot ?? win.document.body,\n )}\n </div>\n );\n}\n\n/** Tooltip is a simplified component similar to material ui's Tooltip component, simply wrap any element in the Tooltip and provide a title as either ReactNode or string, and it will render the tooltip on hover, if you want to conditionally render a tooltip, simply set the title to either null or an empty string */\nexport function Tooltip(props: TooltipProps) {\n return (\n <ErrorBoundary {...fallback({ prefix: \"Tooltip\" })}>\n <InternalTooltip {...props} />\n </ErrorBoundary>\n );\n}\n","import { type ErrorBoundaryProps } from \"react-error-boundary\";\nimport { Alert } from \"@components\";\nimport { localize } from \"@hakit/core\";\n\ninterface Fallback {\n prefix?: string;\n}\n\nexport const fallback = ({ prefix }: Fallback): ErrorBoundaryProps => ({\n fallbackRender({ error, resetErrorBoundary }) {\n return (\n <Alert\n className={`error-boundary-alert`}\n title={`${prefix ? `${prefix} - ` : \"\"}${localize(\"unknown_error\")}`}\n description={error.message}\n type=\"error\"\n onClick={() => resetErrorBoundary()}\n />\n );\n },\n});\n","import { useMemo, Children, isValidElement, ReactNode } from \"react\";\nimport styled from \"@emotion/styled\";\nimport {\n localize,\n useEntity,\n useStore,\n useIconByDomain,\n useIcon,\n useIconByEntity,\n isUnavailableState,\n ON,\n OFF,\n computeDomainTitle,\n computeDomain,\n type LocaleKeys,\n type HassEntityWithService,\n type ExtractDomain,\n type EntityName,\n} from \"@hakit/core\";\nimport { type IconProps } from \"@iconify/react\";\nimport { fallback, Column, CardBase, type CardBaseProps, type AvailableQueries } from \"@components\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nconst StyledButtonCard = styled(CardBase)`\n &.slim {\n justify-content: center;\n .fab-card-inner {\n width: 3rem;\n height: 3rem;\n }\n .button-card-trigger {\n align-items: center;\n > .contents {\n width: 100%;\n }\n }\n }\n .button-card-trigger > .features {\n width: 100%;\n }\n .button-card-trigger > .features > .fit-content {\n flex-basis: 100%;\n }\n .children {\n width: 100%;\n }\n &.slim-vertical {\n justify-content: center;\n .fab-card-inner {\n width: 3rem;\n height: 3rem;\n }\n .button-card-trigger {\n align-items: center;\n }\n }\n .footer > .title {\n text-align: left;\n }\n &:not(.disabled),\n &:not(:disabled) {\n &:not(:focus):hover {\n .fab-card-inner:not(.custom) {\n background-color: var(--ha-S500);\n color: var(--ha-S500-contrast);\n }\n }\n }\n`;\n\nconst Contents = styled.div`\n padding: 1rem;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: stretch;\n height: 100%;\n`;\n\ninterface ToggleProps {\n active: boolean;\n}\n\nconst ToggleState = styled.div<ToggleProps>`\n background-color: var(--ha-100);\n border-radius: 100%;\n width: 16px;\n height: 16px;\n position: absolute;\n top: 2px;\n left: 0;\n box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.5);\n transition: var(--ha-transition-duration) var(--ha-easing);\n transition-property: left, transform;\n left: ${(props) => (props.active ? \"100%\" : \"0px\")};\n transform: ${(props) => (props.active ? \"translate3d(calc(-100% - 2px), 0, 0)\" : \"translate3d(calc(0% + 2px), 0, 0)\")};\n`;\n\nconst Toggle = styled.div<ToggleProps>`\n position: relative;\n background-color: ${(props) => (props.active ? \"var(--ha-A400)\" : \"var(--ha-S100)\")};\n border-radius: 10px;\n width: 40px;\n height: 20px;\n flex-grow: 0;\n flex-shrink: 0;\n transition: background-color var(--ha-transition-duration) var(--ha-easing);\n margin-left: 20px;\n`;\n\nconst Fab = styled.div<\n React.ComponentProps<\"div\"> & {\n brightness: string;\n }\n>`\n border-radius: 100%;\n padding: 6px;\n width: 2rem;\n height: 2rem;\n display: flex;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.2);\n ${(props) =>\n props.brightness &&\n `\n filter: ${props.brightness};\n `}\n transition: var(--ha-transition-duration) var(--ha-easing);\n transition-property: background-color, color, filter;\n svg {\n transition: var(--ha-transition-duration) var(--ha-easing);\n transition-property: color;\n }\n`;\n\nconst LayoutBetween = styled.div`\n display: flex;\n align-items: center;\n justify-content: space-between;\n flex-direction: row;\n gap: 10px;\n width: 100%;\n &.vertical {\n flex-direction: column;\n height: 100%;\n }\n`;\n\nconst Footer = styled.div`\n display: flex;\n align-items: flex-start;\n justify-content: flex-start;\n flex-direction: column;\n margin-top: 20px;\n width: 100%;\n`;\n\nconst Title = styled.div`\n color: var(--ha-S100-contrast);\n font-size: 0.9rem;\n font-weight: bold;\n margin-bottom: 0.25rem;\n`;\n\nconst Description = styled.div`\n color: var(--ha-S300-contrast);\n font-size: 0.7rem;\n margin: 2px 0;\n text-align: left;\n width: 100%;\n &.center {\n text-align: center;\n }\n &.secondary {\n color: var(--ha-S500-contrast);\n }\n &.slim-vertical {\n text-align: center;\n }\n`;\n\ntype OmitProperties = \"as\" | \"children\" | \"ref\" | \"description\";\n\nexport interface ButtonCardProps<E extends EntityName> extends Omit<CardBaseProps<\"button\", E>, OmitProperties> {\n /** Optional icon param, this is automatically retrieved by the \"domain\" name if provided, or can be overwritten with a custom value, provide a string with the name of the icon, or a custom icon by providing a react node */\n icon?: ReactNode | null;\n /** the props for the icon, which includes styles for the icon */\n iconProps?: Omit<IconProps, \"icon\">;\n /** the props to provide to the Fab element within the card, useful if you want to re-style it */\n fabProps?: React.ComponentProps<\"div\">;\n /** By default, the title is retrieved from the friendly name of the entity, or you can specify a manual title */\n title?: ReactNode | null;\n /** The description will naturally fall under the title, by default it will show the information of the entity like the state */\n description?: ReactNode | null;\n /** override the unit displayed alongside the state if the entity has a unit of measurement */\n unitOfMeasurement?: ReactNode;\n /** The layout of the button card, mimics the style of HA mushroom cards in slim/slim-vertical @default \"default\" */\n layoutType?: \"default\" | \"slim\" | \"slim-vertical\";\n /** custom method to render the state however you choose, this will just change how the \"suffix\" of the title will appear */\n customRenderState?: (entity: HassEntityWithService<ExtractDomain<E>>) => ReactNode;\n /** hide the icon shown in the component @default false */\n hideIcon?: boolean;\n /** Hide the state value @default false */\n hideState?: boolean;\n /** Hide the last updated time @default false */\n hideLastUpdated?: boolean;\n /** This forces hideState, hideLastUpdated and will only show the entity name / description prop @default false */\n hideDetails?: boolean;\n /** Will hide the \"toggle\" element shown in the default layout @default false */\n hideToggle?: boolean;\n /** The children to render at the bottom of the card */\n children?: React.ReactNode;\n}\nfunction InternalButtonCard<E extends EntityName>({\n entity: _entity,\n service,\n serviceData,\n iconProps,\n icon: _icon,\n fabProps,\n active,\n onClick,\n description: _description,\n title: _title,\n layoutType,\n disabled = false,\n className,\n hideIcon = false,\n hideState = false,\n hideLastUpdated = false,\n children,\n hideDetails = false,\n cssStyles,\n key,\n hideToggle = false,\n unitOfMeasurement,\n customRenderState,\n ...rest\n}: ButtonCardProps<E>): React.ReactNode {\n const globalComponentStyle = useStore((state) => state.globalComponentStyles);\n const domain = _entity ? computeDomain(_entity) : null;\n const entity = useEntity(_entity || \"unknown\", {\n returnNullIfNotFound: true,\n });\n const iconNode = typeof _icon !== \"undefined\" && typeof _icon !== \"string\" ? _icon : null;\n const domainIcon = useIconByDomain(domain === null ? \"unknown\" : domain, {\n ...(iconProps ?? {}),\n });\n const entityIcon = useIconByEntity(_entity || \"unknown\", {\n ...(iconProps ?? {}),\n });\n const isDefaultLayout = layoutType === \"default\" || layoutType === undefined;\n const isSlimLayout = layoutType === \"slim\" || layoutType === \"slim-vertical\";\n const isUnavailable = typeof entity?.state === \"string\" ? isUnavailableState(entity.state) : false;\n const on = entity ? entity.state !== \"off\" && !isUnavailable && !disabled : active || false;\n const iconElement = useIcon(typeof _icon === \"string\" ? _icon : null, {\n ...(iconProps ?? {}),\n });\n // use the input description if provided, else use the friendly name if available, else entity name, else null\n const title = useMemo(() => {\n return _title === null ? null : _title || entity?.attributes.friendly_name || entity?.entity_id || null;\n }, [_title, entity]);\n // use the input title if provided, else use the domain if available, else null\n const description = useMemo(\n () => _description || (domain !== null && _entity ? computeDomainTitle(_entity, entity?.attributes?.device_class) : null),\n [_description, domain, entity, _entity],\n );\n\n function renderState() {\n if (hideState) return null;\n if (customRenderState && entity) {\n // @ts-expect-error - this is correct, no idea why it's complaining\n