);
};
diff --git a/packages/webapp/src/components/HoverPill/styles.module.scss b/packages/webapp/src/components/HoverPillOverflowList/styles.module.scss
similarity index 90%
rename from packages/webapp/src/components/HoverPill/styles.module.scss
rename to packages/webapp/src/components/HoverPillOverflowList/styles.module.scss
index 6e461b7cff..f0db9c8859 100644
--- a/packages/webapp/src/components/HoverPill/styles.module.scss
+++ b/packages/webapp/src/components/HoverPillOverflowList/styles.module.scss
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 LiteFarm.org
+ * Copyright 2024, 2025 LiteFarm.org
* This file is part of LiteFarm.
*
* LiteFarm is free software: you can redistribute it and/or modify
@@ -13,6 +13,17 @@
* GNU General Public License for more details, see .
*/
+.container {
+ display: flex;
+ gap: 8px;
+}
+
+.itemText {
+ color: var(--Colors-Neutral-Neutral-700);
+ font-size: 16px;
+ font-weight: 400;
+}
+
.pillText,
.detailText {
user-select: none;
diff --git a/packages/webapp/src/components/StatusIndicatorPill/index.tsx b/packages/webapp/src/components/StatusIndicatorPill/index.tsx
new file mode 100644
index 0000000000..a0a2a4f3a0
--- /dev/null
+++ b/packages/webapp/src/components/StatusIndicatorPill/index.tsx
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2025 LiteFarm.org
+ * This file is part of LiteFarm.
+ *
+ * LiteFarm is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LiteFarm is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details, see .
+ */
+
+import clsx from 'clsx';
+import { useTranslation } from 'react-i18next';
+import { Tooltip } from '@mui/material';
+import { ReactComponent as StatusIndicatorDot } from '../../assets/images/status-indicator-dot.svg';
+import styles from './styles.module.scss';
+import { Semibold, Main } from '../Typography';
+
+export enum Status {
+ ONLINE = 'ONLINE',
+ OFFLINE = 'OFFLINE',
+}
+
+export interface StatusIndicatorPillProps {
+ status: Status;
+ pillText: string;
+ tooltipText?: string;
+ showHoverTooltip?: boolean;
+}
+
+export const StatusIndicatorPill = ({
+ status,
+ pillText,
+ tooltipText = '',
+ showHoverTooltip = true,
+}: StatusIndicatorPillProps) => {
+ const { t } = useTranslation();
+
+ const isOnline = status === Status.ONLINE;
+
+ const hoverContent = {t(tooltipText)};
+
+ // https://mui.com/material-ui/react-tooltip/#distance-from-anchor
+ const PopperProps = {
+ modifiers: [
+ {
+ name: 'offset',
+ options: {
+ offset: [0, 4],
+ },
+ },
+ ],
+ };
+
+ const statusPill = (
+
+
+ {t(pillText)}
+
+ );
+
+ return showHoverTooltip ? (
+
+ {statusPill}
+
+ ) : (
+ statusPill
+ );
+};
diff --git a/packages/webapp/src/components/StatusIndicatorPill/styles.module.scss b/packages/webapp/src/components/StatusIndicatorPill/styles.module.scss
new file mode 100644
index 0000000000..52a330ada3
--- /dev/null
+++ b/packages/webapp/src/components/StatusIndicatorPill/styles.module.scss
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2025 LiteFarm.org
+ * This file is part of LiteFarm.
+ *
+ * LiteFarm is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LiteFarm is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details, see .
+ */
+
+.pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+
+ padding: 2px 4px;
+ border-radius: 40px;
+
+ &.hover {
+ cursor: default;
+ }
+}
+
+.pill.online {
+ background: var(--Colors-Secondary-Secondary-green-100);
+
+ .pillText {
+ color: var(--Colors-Secondary-Secondary-green-700);
+ }
+
+ &.hover:hover {
+ background: var(--Colors-Secondary-Secondary-green-700);
+
+ .pillText {
+ color: var(--Colors-Secondary-Secondary-green-50);
+ }
+
+ svg circle {
+ fill: var(--Colors-Secondary-Secondary-green-50);
+ }
+ }
+}
+
+.pill.offline {
+ background: var(--Colors-Accent---singles-Red-light);
+
+ svg circle {
+ fill: var(--Colors-Accent---singles-Red-full);
+ }
+
+ .pillText {
+ color: var(--Colors-Accent---singles-Red-full);
+ }
+
+ &.hover:hover {
+ background: var(--Colors-Accent---singles-Red-full);
+
+ .pillText {
+ color: var(--Colors-Accent---singles-Red-light);
+ }
+
+ svg circle {
+ fill: var(--Colors-Accent---singles-Red-light);
+ }
+ }
+}
+
+.pillText,
+.hoverText {
+ font-family: 'Open Sans';
+ user-select: none;
+ font-weight: 400;
+}
+
+.pillText {
+ font-size: 16px;
+ line-height: 20px;
+}
+
+.hoverText {
+ font-size: 14px;
+ line-height: normal;
+ color: #000;
+}
+
+.tooltipContainer {
+ margin: 0 !important; // override MUI's default margin
+ padding: 8px 16px;
+ border-radius: 8px;
+ background: var(--white, #fff);
+ box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.25);
+}
diff --git a/packages/webapp/src/components/Table/Cell/CellTypes/HoverPillOverflow.tsx b/packages/webapp/src/components/Table/Cell/CellTypes/HoverPillOverflow.tsx
deleted file mode 100644
index b956844ceb..0000000000
--- a/packages/webapp/src/components/Table/Cell/CellTypes/HoverPillOverflow.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2024 LiteFarm.org
- * This file is part of LiteFarm.
- *
- * LiteFarm is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LiteFarm is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details, see .
- */
-import clsx from 'clsx';
-import { HoverPill, HoverPillProps } from '../../../HoverPill';
-import styles from '../styles.module.scss';
-
-export type HoverPillOverflowProps = HoverPillProps & { noneText?: string };
-
-const HoverPillOverFlow = ({ items, noneText = '' }: HoverPillOverflowProps) => {
- return (
-
- );
-};
-
-export default HoverPillOverFlow;
diff --git a/packages/webapp/src/components/Table/Cell/index.tsx b/packages/webapp/src/components/Table/Cell/index.tsx
index 03b4be04d0..818cbe51e2 100644
--- a/packages/webapp/src/components/Table/Cell/index.tsx
+++ b/packages/webapp/src/components/Table/Cell/index.tsx
@@ -13,27 +13,31 @@
* GNU General Public License for more details, see .
*/
import Plain from './CellTypes/Plain';
-import HoverPillOverflow from './CellTypes/HoverPillOverflow';
import RightChevronLink from './CellTypes/RightChevronLink';
import IconText from './CellTypes/IconText';
+import { StatusIndicatorPill, StatusIndicatorPillProps } from '../../StatusIndicatorPill';
+import { HoverPillOverflowList, HoverPillOverflowListProps } from '../../HoverPillOverflowList';
import { CellKind } from '../types';
-import type { HoverPillOverflowProps } from './CellTypes/HoverPillOverflow';
import type { IconTextProps } from './CellTypes/IconText';
import type { PlainCellProps } from './CellTypes/Plain';
import type { RightChevronLinkProps } from './CellTypes/RightChevronLink';
-type HoverPillOverflowPropsStrategy = HoverPillOverflowProps & {
+type HoverPillOverflowPropsStrategy = HoverPillOverflowListProps & {
kind: CellKind.HOVER_PILL_OVERFLOW;
};
type IconTextPropsStrategy = IconTextProps & { kind: CellKind.ICON_TEXT };
type PlainCellPropsStrategy = PlainCellProps & { kind: CellKind.PLAIN };
type RightChevronLinkPropsStrategy = RightChevronLinkProps & { kind: CellKind.RIGHT_CHEVRON_LINK };
+type StatusIndicatorPillPropsStrategy = StatusIndicatorPillProps & {
+ kind: CellKind.STATUS_INDICATOR_PILL;
+};
type CellStrategyProps =
| HoverPillOverflowPropsStrategy
| IconTextPropsStrategy
| PlainCellPropsStrategy
- | RightChevronLinkPropsStrategy;
+ | RightChevronLinkPropsStrategy
+ | StatusIndicatorPillPropsStrategy;
/**
* A component that selects between available Cell styles.
@@ -42,13 +46,15 @@ type CellStrategyProps =
const Cell = ({ kind, ...props }: CellStrategyProps) => {
switch (kind) {
case CellKind.HOVER_PILL_OVERFLOW:
- return ;
+ return ;
case CellKind.ICON_TEXT:
return ;
case CellKind.PLAIN:
return ;
case CellKind.RIGHT_CHEVRON_LINK:
return ;
+ case CellKind.STATUS_INDICATOR_PILL:
+ return ;
default:
const _exhaustiveCheck: never = kind;
return null;
diff --git a/packages/webapp/src/components/Table/Cell/styles.module.scss b/packages/webapp/src/components/Table/Cell/styles.module.scss
index 65c9c88a24..2dadeb6d41 100644
--- a/packages/webapp/src/components/Table/Cell/styles.module.scss
+++ b/packages/webapp/src/components/Table/Cell/styles.module.scss
@@ -73,10 +73,6 @@
font-size: 12px;
}
-.marginRight8px {
- margin-right: 8px;
-}
-
.highlightedText {
display: flex;
padding: 4px;
diff --git a/packages/webapp/src/components/Table/types.ts b/packages/webapp/src/components/Table/types.ts
index e2a5a3d5c6..618e899882 100644
--- a/packages/webapp/src/components/Table/types.ts
+++ b/packages/webapp/src/components/Table/types.ts
@@ -29,6 +29,7 @@ export enum CellKind {
ICON_TEXT = 'iconText',
PLAIN = 'plain',
RIGHT_CHEVRON_LINK = 'rightChevronLink',
+ STATUS_INDICATOR_PILL = 'StatusIndicatorPill',
}
export enum Alignment {
diff --git a/packages/webapp/src/stories/HoverPill/HoverPill.stories.tsx b/packages/webapp/src/stories/HoverPillOverflowList/HoverPillOverflowList.stories.tsx
similarity index 75%
rename from packages/webapp/src/stories/HoverPill/HoverPill.stories.tsx
rename to packages/webapp/src/stories/HoverPillOverflowList/HoverPillOverflowList.stories.tsx
index a81bbf2153..b290225089 100644
--- a/packages/webapp/src/stories/HoverPill/HoverPill.stories.tsx
+++ b/packages/webapp/src/stories/HoverPillOverflowList/HoverPillOverflowList.stories.tsx
@@ -17,17 +17,20 @@ import { ReactNode } from 'react';
import { Meta, StoryObj } from '@storybook/react';
import clsx from 'clsx';
import { componentDecoratorsFullHeight } from '../Pages/config/Decorators';
-import { HoverPill, HoverPillProps } from '../../components/HoverPill';
+import {
+ HoverPillOverflowList,
+ HoverPillOverflowListProps,
+} from '../../components/HoverPillOverflowList';
import styles from './styles.module.scss';
-type HoverPillStoryProps = HoverPillProps & {
+type HoverPillStoryProps = HoverPillOverflowListProps & {
position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
};
// https://storybook.js.org/docs/writing-stories/typescript
const meta: Meta = {
- title: 'Components/HoverPill',
- component: HoverPill,
+ title: 'Components/HoverPillOverflowList',
+ component: HoverPillOverflowList,
argTypes: {
position: {
control: 'select',
@@ -66,16 +69,35 @@ const Wrapper = ({ children, position = 'center' }: WrapperProps) => {
return
{children}
;
};
-type Story = StoryObj;
+type Story = StoryObj;
-export const Plural: Story = {
+export const NoItems: Story = {
args: {
- items: ['Heifers', 'Foot Rot treatment', 'Feeding change'],
+ items: [],
+ },
+};
+
+export const NoItemsWithNoneText: Story = {
+ args: {
+ items: [],
+ noneText: 'none',
+ },
+};
+
+export const OneItem: Story = {
+ args: {
+ items: ['Heifers'],
},
};
-export const Singular: Story = {
+export const TwoItems: Story = {
args: {
- items: ['Feeding change'],
+ items: ['Heifers', 'Foot Rot treatment'],
+ },
+};
+
+export const ThreeItems: Story = {
+ args: {
+ items: ['Heifers', 'Foot Rot treatment', 'Feeding change'],
},
};
diff --git a/packages/webapp/src/stories/HoverPill/styles.module.scss b/packages/webapp/src/stories/HoverPillOverflowList/styles.module.scss
similarity index 100%
rename from packages/webapp/src/stories/HoverPill/styles.module.scss
rename to packages/webapp/src/stories/HoverPillOverflowList/styles.module.scss
diff --git a/packages/webapp/src/stories/StatusIndicatorPill/StatusIndicatorPill.stories.tsx b/packages/webapp/src/stories/StatusIndicatorPill/StatusIndicatorPill.stories.tsx
new file mode 100644
index 0000000000..1784cbd47a
--- /dev/null
+++ b/packages/webapp/src/stories/StatusIndicatorPill/StatusIndicatorPill.stories.tsx
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2025 LiteFarm.org
+ * This file is part of LiteFarm.
+ *
+ * LiteFarm is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LiteFarm is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details, see .
+ */
+
+import { ReactNode } from 'react';
+import { Meta, StoryObj } from '@storybook/react';
+import clsx from 'clsx';
+import { componentDecoratorsFullHeight } from '../Pages/config/Decorators';
+import {
+ StatusIndicatorPill,
+ StatusIndicatorPillProps,
+ Status,
+} from '../../components/StatusIndicatorPill';
+import styles from './styles.module.scss';
+
+type StatusIndicatorPillStoryProps = StatusIndicatorPillProps & {
+ position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center';
+};
+
+// https://storybook.js.org/docs/writing-stories/typescript
+const meta: Meta = {
+ title: 'Components/StatusIndicatorPill',
+ component: StatusIndicatorPill,
+ argTypes: {
+ position: {
+ control: 'select',
+ options: ['top-left', 'top-right', 'bottom-left', 'bottom-right', 'center'],
+ description: 'Position of the status indicator pill within the Storybook frame',
+ },
+ },
+ decorators: [
+ (Story, context) => (
+
+
+
+ ),
+ ...componentDecoratorsFullHeight,
+ ],
+
+ // To reduce the height of the docs page canvas when using 100vh decorator
+ // See https://github.com/storybookjs/storybook/issues/13765
+ parameters: {
+ docs: {
+ story: {
+ inline: false,
+ iframeHeight: 300,
+ },
+ },
+ },
+};
+export default meta;
+
+interface WrapperProps {
+ children: ReactNode;
+ position?: string;
+}
+
+const Wrapper = ({ children, position = 'center' }: WrapperProps) => {
+ return
{children}
;
+};
+
+type Story = StoryObj;
+
+export const Online: Story = {
+ args: {
+ status: Status.ONLINE,
+ pillText: 'Online',
+ tooltipText: 'Device has sent data in the last 12 hours',
+ },
+};
+
+export const Offline: Story = {
+ args: {
+ status: Status.OFFLINE,
+ pillText: 'Offline',
+ tooltipText: 'Device has not sent data in the last 12 hours',
+ },
+};
+
+export const HoverTooltipDisabled: Story = {
+ args: {
+ status: Status.OFFLINE,
+ pillText: 'Offline',
+ tooltipText: 'Device has not sent data in the last 12 hours',
+ showHoverTooltip: false,
+ },
+};
diff --git a/packages/webapp/src/stories/StatusIndicatorPill/styles.module.scss b/packages/webapp/src/stories/StatusIndicatorPill/styles.module.scss
new file mode 100644
index 0000000000..fe1034487a
--- /dev/null
+++ b/packages/webapp/src/stories/StatusIndicatorPill/styles.module.scss
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2025 LiteFarm.org
+ * This file is part of LiteFarm.
+ *
+ * LiteFarm is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LiteFarm is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details, see .
+ */
+
+.wrapper {
+ position: relative;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ padding: 24px;
+}
+
+.center {
+ align-items: center;
+ justify-content: center;
+}
+
+.top-left {
+ align-items: flex-start;
+ justify-content: flex-start;
+}
+
+.top-right {
+ align-items: flex-start;
+ justify-content: flex-end;
+}
+
+.bottom-left {
+ align-items: flex-end;
+ justify-content: flex-start;
+}
+
+.bottom-right {
+ align-items: flex-end;
+ justify-content: flex-end;
+}
diff --git a/packages/webapp/src/stories/Table/TableCells.stories.jsx b/packages/webapp/src/stories/Table/TableCells.stories.jsx
index 07f965d6e1..d07b0f1493 100644
--- a/packages/webapp/src/stories/Table/TableCells.stories.jsx
+++ b/packages/webapp/src/stories/Table/TableCells.stories.jsx
@@ -17,6 +17,7 @@ import { v2TableDecorator } from '../Pages/config/Decorators';
import Table from '../../components/Table';
import Cell from '../../components/Table/Cell';
import { TableKind, CellKind } from '../../components/Table/types';
+import { Status } from '../../components/StatusIndicatorPill';
export default {
title: 'Components/Tables/Cells',
@@ -42,6 +43,22 @@ const getFakeColumns = () => {
label: 'Revenue',
format: (d) => ,
},
+ {
+ id: 'StatusIndicatorPill',
+ label: 'Availability',
+ format: (d) => {
+ const isAvailable = Math.random() < 3 / 4;
+ return (
+
+ );
+ },
+ sortable: false,
+ },
{
id: 'rightChevronLink',
label: '',