Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35649eaaa5 | ||
|
|
e31dffb42d | ||
|
|
1c089dd906 | ||
|
|
ad0d6e0498 | ||
|
|
5f572f1b92 | ||
|
|
a7aa89fafe | ||
|
|
e723cbe6dd |
No files matched your search
@@ -361,6 +361,7 @@ const LoadedBoardItemContent = ({
|
|||||||
widgetStateRef={widgetStateRef}
|
widgetStateRef={widgetStateRef}
|
||||||
widgetRuntimeRef={widgetRuntimeRef}
|
widgetRuntimeRef={widgetRuntimeRef}
|
||||||
sourceRef={sourceRef}
|
sourceRef={sourceRef}
|
||||||
|
disabled={isAdvanced}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
ref={sourceRef}
|
ref={sourceRef}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ interface WidgetContextMenuProps {
|
|||||||
widgetStateRef: MutableRefObject<Record<string, unknown> | null>;
|
widgetStateRef: MutableRefObject<Record<string, unknown> | null>;
|
||||||
widgetRuntimeRef: WidgetRuntimeRef;
|
widgetRuntimeRef: WidgetRuntimeRef;
|
||||||
sourceRef: RefObject<HTMLElement | null>;
|
sourceRef: RefObject<HTMLElement | null>;
|
||||||
|
disabled?: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ export const WidgetContextMenu = ({
|
|||||||
previewDimensions,
|
previewDimensions,
|
||||||
widgetRuntimeRef,
|
widgetRuntimeRef,
|
||||||
sourceRef,
|
sourceRef,
|
||||||
|
disabled = false,
|
||||||
children,
|
children,
|
||||||
}: WidgetContextMenuProps) => {
|
}: WidgetContextMenuProps) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@@ -162,6 +164,7 @@ export const WidgetContextMenu = ({
|
|||||||
}) ?? [];
|
}) ?? [];
|
||||||
|
|
||||||
const openEditModal = useCallback(() => {
|
const openEditModal = useCallback(() => {
|
||||||
|
setMenuOpened(false);
|
||||||
openModal(
|
openModal(
|
||||||
{
|
{
|
||||||
kind: item.kind,
|
kind: item.kind,
|
||||||
@@ -230,6 +233,7 @@ export const WidgetContextMenu = ({
|
|||||||
|
|
||||||
const handleOpenAdvancedFocus = useCallback(() => {
|
const handleOpenAdvancedFocus = useCallback(() => {
|
||||||
if (!sourceRef.current) return;
|
if (!sourceRef.current) return;
|
||||||
|
setMenuOpened(false);
|
||||||
openAdvancedFocus(item.id, sourceRef.current, {
|
openAdvancedFocus(item.id, sourceRef.current, {
|
||||||
restoreFocusTarget:
|
restoreFocusTarget:
|
||||||
sourceRef.current.querySelector<HTMLElement>("[data-advanced-focus-trigger]") ?? sourceRef.current,
|
sourceRef.current.querySelector<HTMLElement>("[data-advanced-focus-trigger]") ?? sourceRef.current,
|
||||||
@@ -256,7 +260,7 @@ export const WidgetContextMenu = ({
|
|||||||
opened={menuOpened}
|
opened={menuOpened}
|
||||||
onChange={setMenuOpened}
|
onChange={setMenuOpened}
|
||||||
>
|
>
|
||||||
<Menu.ContextMenu>{children}</Menu.ContextMenu>
|
<Menu.ContextMenu disabled={disabled}>{children}</Menu.ContextMenu>
|
||||||
<WidgetContextMenuDropdown opened={menuOpened} onClose={handleCloseMenu} title={getWidgetName(item.kind, t)}>
|
<WidgetContextMenuDropdown opened={menuOpened} onClose={handleCloseMenu} title={getWidgetName(item.kind, t)}>
|
||||||
{canOpenAdvancedFocus && (
|
{canOpenAdvancedFocus && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -45,4 +45,13 @@
|
|||||||
--mantine-h4-font-size: calc(1.125rem * var(--board-canvas-ui-scale, 1));
|
--mantine-h4-font-size: calc(1.125rem * var(--board-canvas-ui-scale, 1));
|
||||||
--mantine-h5-font-size: calc(1rem * var(--board-canvas-ui-scale, 1));
|
--mantine-h5-font-size: calc(1rem * var(--board-canvas-ui-scale, 1));
|
||||||
--mantine-h6-font-size: calc(0.875rem * var(--board-canvas-ui-scale, 1));
|
--mantine-h6-font-size: calc(0.875rem * var(--board-canvas-ui-scale, 1));
|
||||||
|
/* Without these, border-radius tokens (e.g. the board's "Item radius" setting) stay at their
|
||||||
|
* flat rem value while the canvas zoom shrinks the rendered card, so the chosen radius becomes
|
||||||
|
* imperceptibly small on busy/zoomed-out boards - same class of bug as the spacing/font tokens
|
||||||
|
* above, just never ported over to radius. */
|
||||||
|
--mantine-radius-xs: calc(0.25rem * var(--board-canvas-ui-scale, 1));
|
||||||
|
--mantine-radius-sm: calc(0.25rem * var(--board-canvas-ui-scale, 1));
|
||||||
|
--mantine-radius-md: calc(0.5rem * var(--board-canvas-ui-scale, 1));
|
||||||
|
--mantine-radius-lg: calc(0.75rem * var(--board-canvas-ui-scale, 1));
|
||||||
|
--mantine-radius-xl: calc(1rem * var(--board-canvas-ui-scale, 1));
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,22 @@
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --board-grid-drag-height inherits down the DOM tree like any custom property. A capped
|
||||||
|
section's own GridPreviewLayer effect correctly avoids setting it on itself (see
|
||||||
|
grid-preview-layer.tsx), but that does nothing to stop it inheriting the value an
|
||||||
|
*uncapped* ancestor section (e.g. the board's root section) legitimately sets on itself
|
||||||
|
while growing during a drag happening anywhere else on the board - any container (not just
|
||||||
|
a scrollable one - a non-scrollable, e.g. collapsible, container is just as capped) sits
|
||||||
|
inside that section's DOM tree, so the stray value cascades straight through and silently
|
||||||
|
defeats the `var(--board-grid-drag-height, ...px)` fallback in section-grid.tsx (including
|
||||||
|
its collapsibleHeaderInset adjustment), which only kicks in when the property is entirely
|
||||||
|
unset, not merely inherited. Reset it here so every container can never inherit a stray
|
||||||
|
value from anywhere above it, no matter what any ancestor's own drag state is doing.
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables#custom_properties_and_inheritance */
|
||||||
|
.viewport[data-section-kind="container"] {
|
||||||
|
--board-grid-drag-height: initial;
|
||||||
|
}
|
||||||
|
|
||||||
/* Chained with .viewport (rather than a standalone .scrollableViewport rule) so this always
|
/* Chained with .viewport (rather than a standalone .scrollableViewport rule) so this always
|
||||||
wins on specificity, not source order. GridEditor loads as a separate async chunk in edit
|
wins on specificity, not source order. GridEditor loads as a separate async chunk in edit
|
||||||
mode, and a board mutation can trigger its stylesheet to re-register - at equal specificity,
|
mode, and a board mutation can trigger its stylesheet to re-register - at equal specificity,
|
||||||
@@ -11,17 +27,14 @@
|
|||||||
.viewport.scrollableViewport {
|
.viewport.scrollableViewport {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
/* --board-grid-drag-height inherits down the DOM tree like any custom property. A capped
|
/* The grid content inside has a fixed pixel width matching this viewport's full
|
||||||
section's own GridPreviewLayer effect correctly avoids setting it on itself (see
|
outer width (needed for the drag/resize math), so there's no room to reserve
|
||||||
grid-preview-layer.tsx), but that does nothing to stop it inheriting the value an
|
for a scrollbar without it eating into - or overlapping - the content on
|
||||||
*uncapped* ancestor section (e.g. the board's root section) legitimately sets on itself
|
whichever side it renders. Hiding the native scrollbar track entirely (wheel/
|
||||||
while growing during a drag happening anywhere else on the board - a nested scrollable
|
touch/drag scrolling still works) avoids that instead of trying to reserve
|
||||||
container sits inside that section's DOM tree, so the stray value cascades straight
|
space for it. */
|
||||||
through and silently defeats the `var(--board-grid-drag-height, ...px)` fallback in
|
scrollbar-width: none;
|
||||||
section-grid.tsx, which only kicks in when the property is entirely unset, not merely
|
-ms-overflow-style: none;
|
||||||
inherited. Reset it here so this element can never inherit a stray value from anywhere
|
|
||||||
above it, no matter what any ancestor's own drag state is doing.
|
|
||||||
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables#custom_properties_and_inheritance */
|
|
||||||
--board-grid-drag-height: initial;
|
--board-grid-drag-height: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,8 +148,25 @@ export const SectionGrid = ({
|
|||||||
// card and visually spills past its right/bottom edges. 10 must match that CSS rule's inset.
|
// card and visually spills past its right/bottom edges. 10 must match that CSS rule's inset.
|
||||||
const effectiveCanvasScale = Number.isFinite(canvasScale) && canvasScale > 0 ? canvasScale : 1;
|
const effectiveCanvasScale = Number.isFinite(canvasScale) && canvasScale > 0 ? canvasScale : 1;
|
||||||
const outerCardInset = section.kind === "container" ? (2 * 10) / effectiveCanvasScale : 0;
|
const outerCardInset = section.kind === "container" ? (2 * 10) / effectiveCanvasScale : 0;
|
||||||
|
// A collapsible container's toggle bar (see the `containerToggle` Button in
|
||||||
|
// container-section.tsx) is an absolutely positioned overlay sitting on top of this grid.
|
||||||
|
// Its height comes from a Mantine size prop, which - like spacing/font-size - is compensated
|
||||||
|
// by --mantine-scale to render at a constant physical size regardless of board zoom (see
|
||||||
|
// scaled-board-canvas.module.css). That makes it behave like the outerCardInset gap above,
|
||||||
|
// not like the grid's own un-compensated logical-pixel math, so it needs the same
|
||||||
|
// effectiveCanvasScale conversion - a plain, unconverted subtraction would under-reserve on
|
||||||
|
// a zoomed-out board and leave the header overlapping the content. Without reserving it at
|
||||||
|
// all, the header bar covers the first row of the container's content instead of sitting
|
||||||
|
// above it. Only the vertical axis is affected - the toggle spans the full width already.
|
||||||
|
const collapsibleHeaderInset =
|
||||||
|
section.kind === "container" && section.options.collapsible
|
||||||
|
? CONTAINER_HEADER_HEIGHT / effectiveCanvasScale
|
||||||
|
: 0;
|
||||||
const logicalWidth = getLogicalGridSize(columnCount) - outerCardInset;
|
const logicalWidth = getLogicalGridSize(columnCount) - outerCardInset;
|
||||||
const viewportHeight = getLogicalGridSize(viewportRowCount) - outerCardInset;
|
const viewportHeight = Math.max(
|
||||||
|
1,
|
||||||
|
getLogicalGridSize(viewportRowCount) - outerCardInset - collapsibleHeaderInset,
|
||||||
|
);
|
||||||
// Items are positioned in a coordinate space sized to the *un-inset* column/row count
|
// Items are positioned in a coordinate space sized to the *un-inset* column/row count
|
||||||
// (fullGridWidth/Height below - see getLogicalItemStyle), but logicalWidth/Height above are
|
// (fullGridWidth/Height below - see getLogicalItemStyle), but logicalWidth/Height above are
|
||||||
// deliberately smaller by outerCardInset to match the container's actual visible card size.
|
// deliberately smaller by outerCardInset to match the container's actual visible card size.
|
||||||
@@ -283,6 +300,7 @@ export const SectionGrid = ({
|
|||||||
{
|
{
|
||||||
width: logicalWidth,
|
width: logicalWidth,
|
||||||
height: `var(--board-grid-drag-height, ${viewportHeight}px)`,
|
height: `var(--board-grid-drag-height, ${viewportHeight}px)`,
|
||||||
|
marginTop: collapsibleHeaderInset || undefined,
|
||||||
"--board-item-radius": `var(--mantine-radius-${board.itemRadius})`,
|
"--board-item-radius": `var(--mantine-radius-${board.itemRadius})`,
|
||||||
} as CSSProperties
|
} as CSSProperties
|
||||||
}
|
}
|
||||||
@@ -317,6 +335,9 @@ export const SectionGrid = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Matches the collapsible container toggle's h={24} in container-section.tsx.
|
||||||
|
const CONTAINER_HEADER_HEIGHT = 24;
|
||||||
|
|
||||||
const INTERACTIVE_GRID_SELECTOR =
|
const INTERACTIVE_GRID_SELECTOR =
|
||||||
'a,button,input,textarea,select,option,[contenteditable="true"],[role="button"],[data-grid-no-drag]';
|
'a,button,input,textarea,select,option,[contenteditable="true"],[role="button"],[data-grid-no-drag]';
|
||||||
const EDIT_ACTIVATION_KEYS = new Set(["Enter", " ", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]);
|
const EDIT_ACTIVATION_KEYS = new Set(["Enter", " ", "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header[data-desktop-visible="false"] {
|
.header[data-visible="false"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,39 +73,38 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.userMenu {
|
||||||
|
display: flex;
|
||||||
|
padding-inline: rem(6px);
|
||||||
|
}
|
||||||
|
|
||||||
.mobileContent {
|
.mobileContent {
|
||||||
display: none;
|
display: none;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--mantine-spacing-xs);
|
gap: var(--mantine-spacing-xs);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-inline: var(--mantine-spacing-xs);
|
padding-inline: var(--mantine-spacing-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileIdentity,
|
.mobileZone {
|
||||||
.mobileActions {
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileIdentity {
|
.mobileZone::-webkit-scrollbar {
|
||||||
flex: 1 1 auto;
|
display: none;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileIdentity > :last-child {
|
.mobileZone[data-zone="left"] {
|
||||||
flex: 1 1 auto;
|
justify-content: flex-start;
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileIdentity h2 {
|
.mobileZone[data-zone="right"] {
|
||||||
overflow: hidden;
|
justify-content: flex-end;
|
||||||
text-overflow: ellipsis;
|
max-width: 72vw;
|
||||||
}
|
|
||||||
|
|
||||||
.mobileActions {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.floatingControls {
|
.floatingControls {
|
||||||
@@ -155,21 +154,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $mantine-breakpoint-sm) {
|
@media (max-width: $mantine-breakpoint-sm) {
|
||||||
.header[data-desktop-visible="false"] {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.desktopContent {
|
.desktopContent {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobileContent {
|
.mobileContent {
|
||||||
display: flex;
|
display: grid;
|
||||||
}
|
|
||||||
|
|
||||||
.floatingControls,
|
|
||||||
.floatingControlsCorner {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,7 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { AppShellHeader, Avatar, Group, Indicator, Loader, Tooltip, useMantineColorScheme } from "@mantine/core";
|
import { AppShellHeader, Avatar, Group, Indicator, Loader, Tooltip, useMantineColorScheme } from "@mantine/core";
|
||||||
import {
|
import { IconBrandDocker, IconHome, IconReplace, IconRobot, IconSettings, IconSunMoon } from "@tabler/icons-react";
|
||||||
IconBrandDocker,
|
|
||||||
IconHome,
|
|
||||||
IconLayoutDashboard,
|
|
||||||
IconReplace,
|
|
||||||
IconRobot,
|
|
||||||
IconSettings,
|
|
||||||
IconSunMoon,
|
|
||||||
} from "@tabler/icons-react";
|
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -81,7 +73,10 @@ export const ConfigurableHeader = ({
|
|||||||
return (
|
return (
|
||||||
<BoardSwitcher>
|
<BoardSwitcher>
|
||||||
{(boardSwitcher) => {
|
{(boardSwitcher) => {
|
||||||
const renderItems = (items: HeaderItem[]) =>
|
const renderItems = (
|
||||||
|
items: HeaderItem[],
|
||||||
|
display: { search: "input" | "icon"; logo: "logo" | "logoAndText" },
|
||||||
|
) =>
|
||||||
items.map((item) => (
|
items.map((item) => (
|
||||||
<HeaderItem
|
<HeaderItem
|
||||||
key={getHeaderItemKey(item)}
|
key={getHeaderItemKey(item)}
|
||||||
@@ -95,8 +90,8 @@ export const ConfigurableHeader = ({
|
|||||||
isAdmin={isAdmin}
|
isAdmin={isAdmin}
|
||||||
isDockerEnabled={isDockerEnabled}
|
isDockerEnabled={isDockerEnabled}
|
||||||
board={item.type === "board" ? boardsById.get(item.boardId) : undefined}
|
board={item.type === "board" ? boardsById.get(item.boardId) : undefined}
|
||||||
searchDisplay={headerPreferences.searchDisplay}
|
searchDisplay={display.search}
|
||||||
logoDisplay={headerPreferences.logoDisplay}
|
logoDisplay={display.logo}
|
||||||
boardSwitcher={boardSwitcher}
|
boardSwitcher={boardSwitcher}
|
||||||
assistant={assistant}
|
assistant={assistant}
|
||||||
openDockerModal={openDockerModal}
|
openDockerModal={openDockerModal}
|
||||||
@@ -106,13 +101,19 @@ export const ConfigurableHeader = ({
|
|||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const desktopDisplay = {
|
||||||
|
search: headerPreferences.searchDisplay,
|
||||||
|
logo: headerPreferences.logoDisplay,
|
||||||
|
};
|
||||||
|
const mobileDisplay = { search: "icon", logo: "logo" } as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppShellHeader
|
<AppShellHeader
|
||||||
maw="100vw"
|
maw="100vw"
|
||||||
zIndex="var(--homarr-z-index-board-header)"
|
zIndex="var(--homarr-z-index-board-header)"
|
||||||
className={classes.header}
|
className={classes.header}
|
||||||
data-desktop-visible={headerPreferences.visible}
|
data-visible={headerPreferences.visible}
|
||||||
data-advanced-focus-background
|
data-advanced-focus-background
|
||||||
data-app-shell-header
|
data-app-shell-header
|
||||||
>
|
>
|
||||||
@@ -120,37 +121,26 @@ export const ConfigurableHeader = ({
|
|||||||
<div className={classes.content}>
|
<div className={classes.content}>
|
||||||
<div className={classes.zone} data-zone="left">
|
<div className={classes.zone} data-zone="left">
|
||||||
{hasNavigation ? <ClientBurger /> : null}
|
{hasNavigation ? <ClientBurger /> : null}
|
||||||
{renderItems(headerPreferences.zones.left)}
|
{renderItems(headerPreferences.zones.left, desktopDisplay)}
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.zone} data-zone="center">
|
<div className={classes.zone} data-zone="center">
|
||||||
{renderItems(headerPreferences.zones.center)}
|
{renderItems(headerPreferences.zones.center, desktopDisplay)}
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.zone} data-zone="right">
|
<div className={classes.zone} data-zone="right">
|
||||||
{renderItems(headerPreferences.zones.right)}
|
{renderItems(headerPreferences.zones.right, desktopDisplay)}
|
||||||
{actions ? <Group className={classes.contextActions}>{actions}</Group> : null}
|
{actions ? <Group className={classes.contextActions}>{actions}</Group> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.mobileContent}>
|
<div className={classes.mobileContent}>
|
||||||
<Group className={classes.mobileIdentity} gap="xs" wrap="nowrap">
|
<Group className={classes.mobileZone} data-zone="left" gap="xs" wrap="nowrap">
|
||||||
{hasNavigation ? <ClientBurger /> : null}
|
{hasNavigation ? <ClientBurger /> : null}
|
||||||
<HeaderLogo display="logo" logo={logo} logoWithTitle={logoWithTitle} label={t("items.logo")} />
|
{renderItems(headerPreferences.zones.left, mobileDisplay)}
|
||||||
</Group>
|
</Group>
|
||||||
<Group className={classes.mobileActions} gap="xs" wrap="nowrap">
|
<Group className={classes.mobileZone} data-zone="right" gap="xs" wrap="nowrap">
|
||||||
{actions}
|
{renderItems(headerPreferences.zones.center, mobileDisplay)}
|
||||||
{boardEditAction}
|
{renderItems(headerPreferences.zones.right, mobileDisplay)}
|
||||||
{boardSettingsAction}
|
{actions ? <Group className={classes.contextActions}>{actions}</Group> : null}
|
||||||
<TourTarget id="board-search">
|
|
||||||
<MobileSearchButton alwaysVisible />
|
|
||||||
</TourTarget>
|
|
||||||
<TourTarget id="board-user-menu">
|
|
||||||
<UserButtonClient
|
|
||||||
avatar={avatar}
|
|
||||||
isAdmin={isAdmin}
|
|
||||||
isDockerEnabled={isDockerEnabled}
|
|
||||||
boardSwitcher={boardSwitcher}
|
|
||||||
/>
|
|
||||||
</TourTarget>
|
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
</AppShellHeader>
|
</AppShellHeader>
|
||||||
@@ -298,7 +288,9 @@ const HeaderItem = ({
|
|||||||
return (
|
return (
|
||||||
<Tooltip label={boardUnavailableLabel}>
|
<Tooltip label={boardUnavailableLabel}>
|
||||||
<HeaderButton aria-label={boardUnavailableLabel} disabled>
|
<HeaderButton aria-label={boardUnavailableLabel} disabled>
|
||||||
<IconLayoutDashboard size={20} stroke={1.5} />
|
<Avatar size={22} radius="sm">
|
||||||
|
?
|
||||||
|
</Avatar>
|
||||||
</HeaderButton>
|
</HeaderButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@@ -416,14 +408,19 @@ const HeaderItem = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TourTarget id="board-user-menu">
|
<TourTarget id="board-user-menu">
|
||||||
<UserButtonClient
|
<div className={classes.userMenu}>
|
||||||
avatar={avatar}
|
<UserButtonClient
|
||||||
isAdmin={isAdmin}
|
avatar={avatar}
|
||||||
isDockerEnabled={isDockerEnabled}
|
isAdmin={isAdmin}
|
||||||
boardSwitcher={boardSwitcher}
|
isDockerEnabled={isDockerEnabled}
|
||||||
/>
|
boardSwitcher={boardSwitcher}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TourTarget>
|
</TourTarget>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBoardInitial = (name: string) => Array.from(name.trim())[0]?.toLocaleUpperCase() ?? "?";
|
const getBoardInitial = (name: string) => {
|
||||||
|
const firstCharacter = Array.from(name.trim())[0] ?? "?";
|
||||||
|
return Array.from(firstCharacter.toUpperCase())[0] ?? "?";
|
||||||
|
};
|
||||||
@@ -22,7 +22,7 @@ export const ClientShell = ({
|
|||||||
const collapsed = useAtomValue(navigationCollapsedAtom);
|
const collapsed = useAtomValue(navigationCollapsedAtom);
|
||||||
const backgroundProps = useOptionalBackgroundProps();
|
const backgroundProps = useOptionalBackgroundProps();
|
||||||
const { headerPreferences } = useSettings();
|
const { headerPreferences } = useSettings();
|
||||||
const headerHeight = headerPreferences.visible ? appShellHeaderHeight : { base: appShellHeaderHeight, sm: 0 };
|
const headerHeight = headerPreferences.visible ? appShellHeaderHeight : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<AppShell
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Badge, Indicator, Kbd, Loader, Menu, Text } from "@mantine/core";
|
|||||||
import {
|
import {
|
||||||
IconBrandDocker,
|
IconBrandDocker,
|
||||||
IconHome,
|
IconHome,
|
||||||
|
IconLayoutDashboard,
|
||||||
IconLogin,
|
IconLogin,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
IconReplace,
|
IconReplace,
|
||||||
@@ -16,6 +17,8 @@ import {
|
|||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { signOut, useSession } from "@homarr/auth/client";
|
import { signOut, useSession } from "@homarr/auth/client";
|
||||||
|
import { constructBoardPermissions } from "@homarr/auth/shared";
|
||||||
|
import { useOptionalBoard } from "@homarr/boards/context";
|
||||||
import { hotkeys, invariantTechnicalLabels } from "@homarr/definitions";
|
import { hotkeys, invariantTechnicalLabels } from "@homarr/definitions";
|
||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
@@ -46,6 +49,8 @@ export const UserAvatarMenu = ({ children, availableUpdates, isDockerEnabled, bo
|
|||||||
const t = useI18n("common.userAvatar.menu");
|
const t = useI18n("common.userAvatar.menu");
|
||||||
const tBoard = useI18n("board");
|
const tBoard = useI18n("board");
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
const board = useOptionalBoard();
|
||||||
|
const boardPermissions = board && constructBoardPermissions(board, session.data ?? null);
|
||||||
|
|
||||||
const { logoutUrl } = useAuthContext();
|
const { logoutUrl } = useAuthContext();
|
||||||
const { openModal: openDockerModal } = useModalAction(DockerQuickAccessModal);
|
const { openModal: openDockerModal } = useModalAction(DockerQuickAccessModal);
|
||||||
@@ -67,6 +72,15 @@ export const UserAvatarMenu = ({ children, availableUpdates, isDockerEnabled, bo
|
|||||||
<Menu.Item component={Link} href="/boards" leftSection={<IconHome size="1rem" />}>
|
<Menu.Item component={Link} href="/boards" leftSection={<IconHome size="1rem" />}>
|
||||||
{t("homeBoard")}
|
{t("homeBoard")}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
{board && boardPermissions?.hasChangeAccess && (
|
||||||
|
<Menu.Item
|
||||||
|
component={Link}
|
||||||
|
href={`/boards/${encodeURIComponent(board.name)}/settings`}
|
||||||
|
leftSection={<IconLayoutDashboard size="1rem" />}
|
||||||
|
>
|
||||||
|
{tBoard("action.settings")}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<IconReplace size="1rem" />}
|
leftSection={<IconReplace size="1rem" />}
|
||||||
rightSection={<Kbd size="xs">{formatHotkeyLabel(boardSwitcher.hotkey, t("modifier"))}</Kbd>}
|
rightSection={<Kbd size="xs">{formatHotkeyLabel(boardSwitcher.hotkey, t("modifier"))}</Kbd>}
|
||||||
|
|||||||
@@ -49,8 +49,7 @@ export const BoardCreateForm = ({ onCancel }: BoardCreateFormProps) => {
|
|||||||
message: tBoard("action.create.notification.success.message", { name: values.name }),
|
message: tBoard("action.create.notification.success.message", { name: values.name }),
|
||||||
});
|
});
|
||||||
const name = encodeURIComponent(result.name);
|
const name = encodeURIComponent(result.name);
|
||||||
const layoutId = encodeURIComponent(result.layoutId);
|
router.push(`/boards/${name}`);
|
||||||
router.push(`/boards/${name}?layout=${layoutId}&edit=true&add=true`);
|
|
||||||
},
|
},
|
||||||
onError() {
|
onError() {
|
||||||
showErrorNotification({
|
showErrorNotification({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
|
|
||||||
interface ModalFormFooterProps {
|
interface ModalFormFooterProps {
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
form?: string;
|
||||||
submitLabel?: string;
|
submitLabel?: string;
|
||||||
cancelLabel?: string;
|
cancelLabel?: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
@@ -18,24 +19,38 @@ const ActionButtons = ({
|
|||||||
submitLabel,
|
submitLabel,
|
||||||
cancelLabel,
|
cancelLabel,
|
||||||
loading,
|
loading,
|
||||||
}: Pick<ModalFormFooterProps, "onCancel" | "submitLabel" | "cancelLabel" | "loading">) => {
|
form,
|
||||||
const t = useI18n();
|
}: Pick<ModalFormFooterProps, "onCancel" | "submitLabel" | "cancelLabel" | "loading" | "form">) => {
|
||||||
|
const t = useI18n("common.action");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={onCancel} variant="subtle" color="gray">
|
<Button onClick={onCancel} variant="subtle" color="gray">
|
||||||
{cancelLabel ?? t("common.action.cancel")}
|
{cancelLabel ?? t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" loading={loading}>
|
<Button type="submit" form={form} loading={loading}>
|
||||||
{submitLabel ?? t("common.action.saveChanges")}
|
{submitLabel ?? t("saveChanges")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModalFormFooter = ({ onCancel, submitLabel, cancelLabel, loading, leftSection }: ModalFormFooterProps) => {
|
export const ModalFormFooter = ({
|
||||||
|
onCancel,
|
||||||
|
form,
|
||||||
|
submitLabel,
|
||||||
|
cancelLabel,
|
||||||
|
loading,
|
||||||
|
leftSection,
|
||||||
|
}: ModalFormFooterProps) => {
|
||||||
const actionButtons = (
|
const actionButtons = (
|
||||||
<ActionButtons onCancel={onCancel} submitLabel={submitLabel} cancelLabel={cancelLabel} loading={loading} />
|
<ActionButtons
|
||||||
|
onCancel={onCancel}
|
||||||
|
form={form}
|
||||||
|
submitLabel={submitLabel}
|
||||||
|
cancelLabel={cancelLabel}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (leftSection) {
|
if (leftSection) {
|
||||||
|
|||||||
@@ -2430,6 +2430,16 @@
|
|||||||
"label": "Animate weather icon",
|
"label": "Animate weather icon",
|
||||||
"description": "Animate the weather icon. Disabled by default and always disabled when reduced motion is requested."
|
"description": "Animate the weather icon. Disabled by default and always disabled when reduced motion is requested."
|
||||||
},
|
},
|
||||||
|
"colorWeatherByDayNight": {
|
||||||
|
"label": "Color weather by day/night",
|
||||||
|
"description": "Tint the weather icon and temperature with a day color or a night color, based on whether it's currently day or night at the chosen location."
|
||||||
|
},
|
||||||
|
"dayWeatherColor": {
|
||||||
|
"label": "Day color"
|
||||||
|
},
|
||||||
|
"nightWeatherColor": {
|
||||||
|
"label": "Night color"
|
||||||
|
},
|
||||||
"worldClockCities": {
|
"worldClockCities": {
|
||||||
"label": "World clock cities",
|
"label": "World clock cities",
|
||||||
"description": "Add and order up to six cities for advanced view."
|
"description": "Add and order up to six cities for advanced view."
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const headerBuiltinItemIds = [
|
|||||||
"user",
|
"user",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const requiredHeaderBuiltinItemIds = ["boardEdit", "boardSettings", "user"] as const;
|
export const requiredHeaderBuiltinItemIds = ["user"] as const;
|
||||||
|
|
||||||
export const headerPreferenceItemIds = headerBuiltinItemIds;
|
export const headerPreferenceItemIds = headerBuiltinItemIds;
|
||||||
export const headerItemIds = headerBuiltinItemIds;
|
export const headerItemIds = headerBuiltinItemIds;
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ const PrimaryClock = ({ now, options, primary }: Pick<ModeProps, "now" | "option
|
|||||||
locationName={options.weatherLocation.name}
|
locationName={options.weatherLocation.name}
|
||||||
isFahrenheit={options.isWeatherFormatFahrenheit}
|
isFahrenheit={options.isWeatherFormatFahrenheit}
|
||||||
animateIcon={options.animateWeatherIcon}
|
animateIcon={options.animateWeatherIcon}
|
||||||
|
colorByDayNight={options.colorWeatherByDayNight}
|
||||||
|
dayColor={options.dayWeatherColor}
|
||||||
|
nightColor={options.nightWeatherColor}
|
||||||
detailed
|
detailed
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ export default function ClockWidget({ options, width, height, displayMode }: Wid
|
|||||||
locationName={options.weatherLocation.name}
|
locationName={options.weatherLocation.name}
|
||||||
isFahrenheit={options.isWeatherFormatFahrenheit}
|
isFahrenheit={options.isWeatherFormatFahrenheit}
|
||||||
animateIcon={options.animateWeatherIcon}
|
animateIcon={options.animateWeatherIcon}
|
||||||
|
colorByDayNight={options.colorWeatherByDayNight}
|
||||||
|
dayColor={options.dayWeatherColor}
|
||||||
|
nightColor={options.nightWeatherColor}
|
||||||
detailed={false}
|
detailed={false}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -12,6 +12,23 @@ import {
|
|||||||
|
|
||||||
const timeZoneOptions = getTimeZoneOptions();
|
const timeZoneOptions = getTimeZoneOptions();
|
||||||
|
|
||||||
|
// Mantine theme color names, used to let users pick their own day/night weather colors.
|
||||||
|
const weatherColorOptions: string[] = [
|
||||||
|
"red",
|
||||||
|
"pink",
|
||||||
|
"grape",
|
||||||
|
"violet",
|
||||||
|
"indigo",
|
||||||
|
"blue",
|
||||||
|
"cyan",
|
||||||
|
"teal",
|
||||||
|
"green",
|
||||||
|
"lime",
|
||||||
|
"yellow",
|
||||||
|
"orange",
|
||||||
|
"gray",
|
||||||
|
];
|
||||||
|
|
||||||
export const { definition, componentLoader } = createWidgetDefinition("clock", {
|
export const { definition, componentLoader } = createWidgetDefinition("clock", {
|
||||||
icon: IconClock,
|
icon: IconClock,
|
||||||
supportsAdvancedFocus: true,
|
supportsAdvancedFocus: true,
|
||||||
@@ -101,6 +118,18 @@ export const { definition, componentLoader } = createWidgetDefinition("clock", {
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
withDescription: true,
|
withDescription: true,
|
||||||
}),
|
}),
|
||||||
|
colorWeatherByDayNight: factory.switch({
|
||||||
|
defaultValue: false,
|
||||||
|
withDescription: true,
|
||||||
|
}),
|
||||||
|
dayWeatherColor: factory.select({
|
||||||
|
options: weatherColorOptions,
|
||||||
|
defaultValue: "orange",
|
||||||
|
}),
|
||||||
|
nightWeatherColor: factory.select({
|
||||||
|
options: weatherColorOptions,
|
||||||
|
defaultValue: "blue",
|
||||||
|
}),
|
||||||
worldClockCities: factory.timezoneList({
|
worldClockCities: factory.timezoneList({
|
||||||
defaultValue: defaultWorldClockCities.map((city) => ({ ...city })),
|
defaultValue: defaultWorldClockCities.map((city) => ({ ...city })),
|
||||||
maxValues: maximumWorldClockCities,
|
maxValues: maximumWorldClockCities,
|
||||||
@@ -131,6 +160,15 @@ export const { definition, componentLoader } = createWidgetDefinition("clock", {
|
|||||||
animateWeatherIcon: {
|
animateWeatherIcon: {
|
||||||
shouldHide: (options) => !options.showWeather,
|
shouldHide: (options) => !options.showWeather,
|
||||||
},
|
},
|
||||||
|
colorWeatherByDayNight: {
|
||||||
|
shouldHide: (options) => !options.showWeather,
|
||||||
|
},
|
||||||
|
dayWeatherColor: {
|
||||||
|
shouldHide: (options) => !options.showWeather || !options.colorWeatherByDayNight,
|
||||||
|
},
|
||||||
|
nightWeatherColor: {
|
||||||
|
shouldHide: (options) => !options.showWeather || !options.colorWeatherByDayNight,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ interface ClockWeatherSummaryProps {
|
|||||||
locationName: string;
|
locationName: string;
|
||||||
isFahrenheit: boolean;
|
isFahrenheit: boolean;
|
||||||
animateIcon: boolean;
|
animateIcon: boolean;
|
||||||
|
colorByDayNight: boolean;
|
||||||
|
dayColor: string;
|
||||||
|
nightColor: string;
|
||||||
detailed: boolean;
|
detailed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +26,9 @@ export const ClockWeatherSummary = ({
|
|||||||
locationName,
|
locationName,
|
||||||
isFahrenheit,
|
isFahrenheit,
|
||||||
animateIcon,
|
animateIcon,
|
||||||
|
colorByDayNight,
|
||||||
|
dayColor,
|
||||||
|
nightColor,
|
||||||
detailed,
|
detailed,
|
||||||
}: ClockWeatherSummaryProps) => {
|
}: ClockWeatherSummaryProps) => {
|
||||||
const tCommon = useI18n("common");
|
const tCommon = useI18n("common");
|
||||||
@@ -46,6 +52,10 @@ export const ClockWeatherSummary = ({
|
|||||||
return `${Math.round(preferred)}${unit}`;
|
return `${Math.round(preferred)}${unit}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const weatherColor = colorByDayNight
|
||||||
|
? `var(--mantine-color-${weather.current.isDay ? dayColor : nightColor}-5)`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={detailed ? 3 : 2} align={detailed ? "flex-start" : "center"}>
|
<Stack gap={detailed ? 3 : 2} align={detailed ? "flex-start" : "center"}>
|
||||||
{detailed ? (
|
{detailed ? (
|
||||||
@@ -54,9 +64,9 @@ export const ClockWeatherSummary = ({
|
|||||||
animated={animateIcon}
|
animated={animateIcon}
|
||||||
code={weather.current.weatherCode}
|
code={weather.current.weatherCode}
|
||||||
isDay={weather.current.isDay}
|
isDay={weather.current.isDay}
|
||||||
style={zoomCompensatedSize(28)}
|
style={{ ...zoomCompensatedSize(28), ...(weatherColor ? { color: weatherColor } : undefined) }}
|
||||||
/>
|
/>
|
||||||
<Text size="sm" fw={600}>
|
<Text size="sm" fw={600} c={weatherColor}>
|
||||||
{Math.round(temperature)}
|
{Math.round(temperature)}
|
||||||
{unit}
|
{unit}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -68,9 +78,9 @@ export const ClockWeatherSummary = ({
|
|||||||
animated={animateIcon}
|
animated={animateIcon}
|
||||||
code={weather.current.weatherCode}
|
code={weather.current.weatherCode}
|
||||||
isDay={weather.current.isDay}
|
isDay={weather.current.isDay}
|
||||||
style={zoomCompensatedSize(26)}
|
style={{ ...zoomCompensatedSize(26), ...(weatherColor ? { color: weatherColor } : undefined) }}
|
||||||
/>
|
/>
|
||||||
<Text size="xs" c="dimmed">
|
<Text size="xs" c={weatherColor ?? "dimmed"}>
|
||||||
{Math.round(temperature)}
|
{Math.round(temperature)}
|
||||||
{unit}
|
{unit}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -143,6 +143,7 @@
|
|||||||
margin-inline: calc(var(--mantine-spacing-md) * -1);
|
margin-inline: calc(var(--mantine-spacing-md) * -1);
|
||||||
margin-bottom: calc(var(--mantine-spacing-md) * -1);
|
margin-bottom: calc(var(--mantine-spacing-md) * -1);
|
||||||
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
|
padding: var(--mantine-spacing-sm) var(--mantine-spacing-md);
|
||||||
|
padding-bottom: max(var(--mantine-spacing-sm), env(safe-area-inset-bottom));
|
||||||
border-top: 1px solid var(--mantine-color-default-border);
|
border-top: 1px solid var(--mantine-color-default-border);
|
||||||
background: color-mix(in srgb, var(--mantine-color-body) 94%, transparent);
|
background: color-mix(in srgb, var(--mantine-color-body) 94%, transparent);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { objectEntries } from "@homarr/common";
|
|||||||
import { IntegrationProvider, useSession } from "@homarr/auth/client";
|
import { IntegrationProvider, useSession } from "@homarr/auth/client";
|
||||||
import { useOptionalBoard } from "@homarr/boards/context";
|
import { useOptionalBoard } from "@homarr/boards/context";
|
||||||
import type { WidgetKind } from "@homarr/definitions";
|
import type { WidgetKind } from "@homarr/definitions";
|
||||||
import { createModal, modalSizeForm } from "@homarr/modals";
|
import { createModal, ModalFormFooter, modalSizeForm } from "@homarr/modals";
|
||||||
import type { SettingsContextProps } from "@homarr/settings/creator";
|
import type { SettingsContextProps } from "@homarr/settings/creator";
|
||||||
import { SpotlightProvider } from "@homarr/spotlight";
|
import { SpotlightProvider } from "@homarr/spotlight";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
@@ -674,16 +674,7 @@ export const WidgetEditModal = createModal<WidgetEditModalProps<WidgetKind>>(({
|
|||||||
|
|
||||||
const footer = (
|
const footer = (
|
||||||
<Box className={classes.footer}>
|
<Box className={classes.footer}>
|
||||||
<Group justify="end" wrap="nowrap">
|
<ModalFormFooter onCancel={actions.closeModal} form={widgetFormId} loading={isSubmitting} />
|
||||||
<Group gap="xs" wrap="nowrap">
|
|
||||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
|
||||||
{tCommon("action.cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" form={widgetFormId} loading={isSubmitting}>
|
|
||||||
{tCommon("action.saveChanges")}
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const AnimatedWeatherIcon = ({ animated = false, code, isDay, size = 26,
|
|||||||
let animationClass = "";
|
let animationClass = "";
|
||||||
if (animated && (code !== 0 || isDay !== false)) animationClass = getAnimationClass(code);
|
if (animated && (code !== 0 || isDay !== false)) animationClass = getAnimationClass(code);
|
||||||
return (
|
return (
|
||||||
<span className={`weather-anim-wrapper ${animationClass}`}>
|
<span className={`weather-anim-wrapper ${animationClass}`} data-is-day={isDay === false ? "false" : "true"}>
|
||||||
<WeatherIcon code={code} isDay={isDay} size={size} style={style} />
|
<WeatherIcon code={code} isDay={isDay} size={size} style={style} />
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user