From ff09d8cfd533dd7bdfcd703e0d353b6531300225 Mon Sep 17 00:00:00 2001
From: Innders <49156310+Innders@users.noreply.github.com>
Date: Wed, 2 Oct 2024 15:17:54 +0100
Subject: [PATCH 1/5] chore: export enum template
---
src/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/index.tsx b/src/index.tsx
index 0ad11d1..225e459 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -30,7 +30,7 @@ export type { TagsSelectProps } from './Dropdowns/TagsSelect'
export { IconSelect } from './Dropdowns/IconSelect'
export type { IconSelectProps } from './Dropdowns/IconSelect'
// enumDropdown
-export { EnumDropdown } from './Dropdowns/EnumDropdown'
+export { EnumDropdown, EnumTemplate } from './Dropdowns/EnumDropdown'
export type { EnumDropdownProps } from './Dropdowns/EnumDropdown'
// versionSelect
export { VersionSelect } from './Dropdowns/VersionSelect'
From 4ceb3ce7e32e011c81f8c994cf44711d54b2180b Mon Sep 17 00:00:00 2001
From: Innders <49156310+Innders@users.noreply.github.com>
Date: Wed, 2 Oct 2024 15:27:07 +0100
Subject: [PATCH 2/5] fix(EntityCard): always show reviewable icon
---
src/EntityCard/EntityCard.stories.tsx | 8 ++++----
src/EntityCard/EntityCard.styled.ts | 8 --------
2 files changed, 4 insertions(+), 12 deletions(-)
diff --git a/src/EntityCard/EntityCard.stories.tsx b/src/EntityCard/EntityCard.stories.tsx
index 0cea076..4eb6c7d 100644
--- a/src/EntityCard/EntityCard.stories.tsx
+++ b/src/EntityCard/EntityCard.stories.tsx
@@ -1,9 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react'
import { EntityCard, EntityCardProps, PriorityType } from '.'
-import { MouseEvent, useEffect, useState } from 'react'
-import { Toolbar } from '../Layout/Toolbar'
+import { MouseEvent, useState } from 'react'
import { Button } from '../Button'
-import { Panel } from '../Panels/Panel'
import DnDTemplate from './DnD/DnDTemplate'
import getRandomImage from '../helpers/getRandomImage'
import styled from 'styled-components'
@@ -38,7 +36,7 @@ const Template = ({ onActivate, ...props }: TemplateProps) => {
const [isActive, setIsActive] = useState(false)
return (
-
+
{
@@ -58,6 +56,7 @@ const StatusWrapper = styled.div`
`
const StyledCell = styled.div`
+ width: 250px;
padding: 8px;
background-color: var(--md-sys-color-surface-container-low);
border: 1px solid var(--md-sys-color-outline-variant);
@@ -204,6 +203,7 @@ export const ProgressView: Story = {
statusOptions: statuses,
statusMiddle: true,
statusNameOnly: true,
+ isPlayable: true,
},
render: (args) => ,
}
diff --git a/src/EntityCard/EntityCard.styled.ts b/src/EntityCard/EntityCard.styled.ts
index fefc657..f8b1e70 100644
--- a/src/EntityCard/EntityCard.styled.ts
+++ b/src/EntityCard/EntityCard.styled.ts
@@ -284,9 +284,6 @@ export const Card = styled.div`
container-type: inline-size;
/* use container query for when the card gets smaller */
@container card (inline-size < 150px) {
- .playable {
- display: none;
- }
.title {
.icon {
display: none;
@@ -296,11 +293,6 @@ export const Card = styled.div`
}
}
}
- @container card (inline-size < 100px) {
- .playable {
- display: none;
- }
- }
/* hide everything on bottom but the status icon */
@container card (inline-size < 85px) {
From e18cd7dde3f3cfa1c80042c79efac12fac81d02d Mon Sep 17 00:00:00 2001
From: Innders <49156310+Innders@users.noreply.github.com>
Date: Wed, 2 Oct 2024 15:31:16 +0100
Subject: [PATCH 3/5] fix(EntityCard): Use EnumDropdown for priority
---
src/EntityCard/EntityCard.stories.tsx | 9 +++++----
src/EntityCard/EntityCard.tsx | 20 +++++++-------------
src/EntityCard/priorities.json | 8 ++++----
3 files changed, 16 insertions(+), 21 deletions(-)
diff --git a/src/EntityCard/EntityCard.stories.tsx b/src/EntityCard/EntityCard.stories.tsx
index 4eb6c7d..10044eb 100644
--- a/src/EntityCard/EntityCard.stories.tsx
+++ b/src/EntityCard/EntityCard.stories.tsx
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react'
-import { EntityCard, EntityCardProps, PriorityType } from '.'
+import { EntityCard, EntityCardProps } from '.'
import { MouseEvent, useState } from 'react'
import { Button } from '../Button'
import DnDTemplate from './DnD/DnDTemplate'
@@ -9,6 +9,7 @@ import clsx from 'clsx'
import { allUsers } from '../Dropdowns/helpers'
import prioritiesData from './priorities.json'
import { randomStatus, statuses } from '../Dropdowns/StatusSelect'
+import { EnumDropdownOption } from '../Dropdowns/EnumDropdown'
const meta: Meta = {
component: EntityCard,
@@ -21,7 +22,7 @@ type Story = StoryObj
interface DataProps extends EntityCardProps {}
-const priorities = prioritiesData as PriorityType[]
+const priorities = prioritiesData as EnumDropdownOption[]
// pick 1 - 3 users randomly from the array
const randomUsers = allUsers
@@ -76,7 +77,7 @@ const StatusTemplate = (props: TemplateProps) => {
const [selectedUsers, setSelectedUsers] = useState(props.users?.map((u) => u.name) || [])
const [selectedStatus, setSelectedStatus] = useState(props.status?.name)
- const [selectedPriority, setSelectedPriority] = useState(props.priority?.name)
+ const [selectedPriority, setSelectedPriority] = useState(props.priority?.value)
const handleChange = (change: any, key: string) => {
console.log('changed:', change, key)
@@ -84,7 +85,7 @@ const StatusTemplate = (props: TemplateProps) => {
const users = props.assigneeOptions?.filter((u) => selectedUsers.includes(u.name)) || []
const status = statuses.find((s) => s.name === selectedStatus)
- const priority = priorities.find((p) => p.name === selectedPriority)
+ const priority = priorities.find((p) => p.value === selectedPriority)
const handleCellClick = (e: MouseEvent) => {
// check if the click is editable item
diff --git a/src/EntityCard/EntityCard.tsx b/src/EntityCard/EntityCard.tsx
index 35e5b86..d518263 100644
--- a/src/EntityCard/EntityCard.tsx
+++ b/src/EntityCard/EntityCard.tsx
@@ -13,7 +13,8 @@ import { User } from '../User/UserImagesStacked'
import clsx from 'clsx'
import useImageLoader from '../helpers/useImageLoader'
import useUserImagesLoader from './useUserImagesLoader'
-import { Dropdown, DropdownProps, DropdownRef } from '../Dropdowns/Dropdown'
+import { EnumDropdown, EnumDropdownOption, EnumDropdownProps } from '../Dropdowns/EnumDropdown'
+import { DropdownRef } from '../Dropdowns/Dropdown'
import { AssigneeSelect, AssigneeSelectProps } from '../Dropdowns/AssigneeSelect'
import { Status, StatusSelect, StatusSelectProps } from '../Dropdowns/StatusSelect'
import { UserImage } from '../User/UserImage'
@@ -40,13 +41,6 @@ const notifications: {
},
}
-export type PriorityType = {
- label?: string
- color?: string
- icon: IconType
- name: string
-}
-
type Section = 'title' | 'header' | 'users' | 'status' | 'priority'
export interface EntityCardProps extends React.HTMLAttributes {
@@ -61,7 +55,7 @@ export interface EntityCardProps extends React.HTMLAttributes {
status?: Status // bottom right
statusMiddle?: boolean // puts status in the center and priority in the bottom right
statusNameOnly?: boolean // only show the status name unless it's too small to show, then use icon
- priority?: PriorityType // bottom left after users
+ priority?: EnumDropdownOption // bottom left after users
hidePriority?: boolean
imageUrl?: string
imageAlt?: string
@@ -80,7 +74,7 @@ export interface EntityCardProps extends React.HTMLAttributes {
// editing options
assigneeOptions?: User[]
statusOptions?: Status[]
- priorityOptions?: PriorityType[]
+ priorityOptions?: EnumDropdownOption[]
editOnHover?: boolean
editAutoClose?: boolean
// editing callbacks
@@ -95,7 +89,7 @@ export interface EntityCardProps extends React.HTMLAttributes {
image?: HTMLAttributes
assigneeSelect?: Partial
statusSelect?: Partial
- prioritySelect?: Partial
+ prioritySelect?: Partial
title?: HTMLAttributes
topRow?: HTMLAttributes
playableTag?: HTMLAttributes
@@ -409,11 +403,11 @@ export const EntityCard = forwardRef(
{/* priority dropdown */}
{priorityEditable && (
- onPriorityChange(value as string[])}
- value={[priority.name]}
+ value={[priority.value]}
options={priorityOptions}
tabIndex={0}
{...pt.prioritySelect}
diff --git a/src/EntityCard/priorities.json b/src/EntityCard/priorities.json
index 4589f1b..7744363 100644
--- a/src/EntityCard/priorities.json
+++ b/src/EntityCard/priorities.json
@@ -3,25 +3,25 @@
"label": "Critical",
"color": "rgb(203, 26, 26)",
"icon": "keyboard_double_arrow_up",
- "name": "urgent"
+ "value": "urgent"
},
{
"label": "High",
"color": "rgb(0, 240, 180)",
"icon": "keyboard_arrow_up",
- "name": "high"
+ "value": "high"
},
{
"label": "Medium",
"color": "rgb(52, 152, 219)",
"icon": "check_indeterminate_small",
- "name": "medium"
+ "value": "medium"
},
{
"label": "Low",
"color": "rgb(186, 186, 186)",
"icon": "keyboard_double_arrow_down",
- "name": "low"
+ "value": "low"
}
]
From 2c4f85af0ba631b111656c3c648e2f9baf5e2bdf Mon Sep 17 00:00:00 2001
From: Innders <49156310+Innders@users.noreply.github.com>
Date: Thu, 3 Oct 2024 08:28:43 +0100
Subject: [PATCH 4/5] fix: priority status on entityCard
---
src/Dropdowns/EnumDropdown/EnumDropdown.tsx | 11 +++++++----
src/EntityCard/EntityCard.tsx | 18 +++++++++++++-----
2 files changed, 20 insertions(+), 9 deletions(-)
diff --git a/src/Dropdowns/EnumDropdown/EnumDropdown.tsx b/src/Dropdowns/EnumDropdown/EnumDropdown.tsx
index 70a40fb..490818b 100644
--- a/src/Dropdowns/EnumDropdown/EnumDropdown.tsx
+++ b/src/Dropdowns/EnumDropdown/EnumDropdown.tsx
@@ -15,7 +15,7 @@ export const EnumTemplate = ({ option, isSelected, isChanged, ...props }: EnumTe
return (
@@ -26,20 +26,21 @@ export const EnumTemplate = ({ option, isSelected, isChanged, ...props }: EnumTe
}
export type EnumDropdownOption = {
- value: string
+ value: string | number | boolean
label: string
icon?: IconType
color?: string
}
export interface EnumDropdownProps
- extends Omit {
+ extends Omit {
options: EnumDropdownOption[]
colorInverse?: boolean
+ value: (string | number | boolean)[]
}
export const EnumDropdown = forwardRef(
- ({ colorInverse, ...props }, ref) => {
+ ({ colorInverse, value, ...props }, ref) => {
return (
(
String(v))}
$color={props.isChanged ? undefined : option?.color} // use color (but not when in changed state - editor)
className={clsx({ inverse: colorInverse })}
>
@@ -59,6 +61,7 @@ export const EnumDropdown = forwardRef(
itemTemplate={(option, isSelected) => (
)}
+ value={value?.map((v) => String(v))}
{...props}
/>
)
diff --git a/src/EntityCard/EntityCard.tsx b/src/EntityCard/EntityCard.tsx
index d518263..f226f2a 100644
--- a/src/EntityCard/EntityCard.tsx
+++ b/src/EntityCard/EntityCard.tsx
@@ -404,7 +404,6 @@ export const EntityCard = forwardRef(
{/* priority dropdown */}
{priorityEditable && (
onPriorityChange(value as string[])}
value={[priority.value]}
@@ -448,7 +447,7 @@ export const EntityCard = forwardRef(
)}
- {/* bottom center - status */}
+ {/* bottom right - status */}
{shouldShowTag(status, 'status') && (
(
)}
- {/* bottom right - priority */}
+ {/* bottom left - priority */}
{shouldShowTag(priority && !hidePriority, 'priority') && (
editOnHover && handleEditableHover(e, 'priority')}
onClick={(e) => handleEditableHover(e, 'priority')}
{...pt.priorityTag}
+ className={clsx(
+ 'tag priority',
+ { editable: priorityEditable, isLoading },
+ pt.priorityTag?.className,
+ )}
>
- {priority?.icon && }
+ {priority?.icon && (
+
+ )}
)}
From dcc6b6fb2fae77ed6c4f46d720188ff55be3f526 Mon Sep 17 00:00:00 2001
From: Innders <49156310+Innders@users.noreply.github.com>
Date: Thu, 3 Oct 2024 09:23:40 +0100
Subject: [PATCH 5/5] fix(EntityCard): notification dot
---
src/EntityCard/EntityCard.stories.tsx | 4 +-
src/EntityCard/EntityCard.styled.ts | 4 +
src/EntityCard/EntityCard.tsx | 529 +++++++++---------
.../Notification/Notification.styled.ts | 23 +
src/EntityCard/Notification/Notification.tsx | 72 +++
src/EntityCard/index.ts | 1 +
src/index.tsx | 4 +-
7 files changed, 361 insertions(+), 276 deletions(-)
create mode 100644 src/EntityCard/Notification/Notification.styled.ts
create mode 100644 src/EntityCard/Notification/Notification.tsx
diff --git a/src/EntityCard/EntityCard.stories.tsx b/src/EntityCard/EntityCard.stories.tsx
index 10044eb..d48369f 100644
--- a/src/EntityCard/EntityCard.stories.tsx
+++ b/src/EntityCard/EntityCard.stories.tsx
@@ -37,7 +37,7 @@ const Template = ({ onActivate, ...props }: TemplateProps) => {
const [isActive, setIsActive] = useState(false)
return (
-
+
{
@@ -167,6 +167,7 @@ const initData: DataProps = {
export const Default: Story = {
args: {
...initData,
+ notification: { comment: true },
},
render: Template,
}
@@ -182,7 +183,6 @@ export const Loading: Story = {
export const TaskStatus: Story = {
args: {
variant: 'status',
- notification: undefined,
disabled: false,
...initData,
isPlayable: false,
diff --git a/src/EntityCard/EntityCard.styled.ts b/src/EntityCard/EntityCard.styled.ts
index f8b1e70..78524db 100644
--- a/src/EntityCard/EntityCard.styled.ts
+++ b/src/EntityCard/EntityCard.styled.ts
@@ -22,6 +22,10 @@ const cardHoverStyles = css`
}
`
+export const Wrapper = styled.div`
+ position: relative;
+`
+
type CardProps = {
$statusColor?: string
}
diff --git a/src/EntityCard/EntityCard.tsx b/src/EntityCard/EntityCard.tsx
index f226f2a..314e066 100644
--- a/src/EntityCard/EntityCard.tsx
+++ b/src/EntityCard/EntityCard.tsx
@@ -18,28 +18,7 @@ import { DropdownRef } from '../Dropdowns/Dropdown'
import { AssigneeSelect, AssigneeSelectProps } from '../Dropdowns/AssigneeSelect'
import { Status, StatusSelect, StatusSelectProps } from '../Dropdowns/StatusSelect'
import { UserImage } from '../User/UserImage'
-
-type NotificationType = 'comment' | 'due' | 'overdue'
-
-const notifications: {
- [key in NotificationType]: {
- color: string
- icon: IconType
- }
-} = {
- comment: {
- color: 'var(--md-sys-color-primary)',
- icon: 'mark_unread_chat_alt',
- },
- due: {
- color: 'var(--md-custom-color-warning)',
- icon: 'schedule',
- },
- overdue: {
- color: 'var(--md-sys-color-error-container)',
- icon: 'alarm',
- },
-}
+import { NotificationDot, NotificationProps } from './Notification/Notification'
type Section = 'title' | 'header' | 'users' | 'status' | 'priority'
@@ -60,7 +39,7 @@ export interface EntityCardProps extends React.HTMLAttributes {
imageUrl?: string
imageAlt?: string
imageIcon?: IconType
- notification?: NotificationType
+ notification?: NotificationProps['notification']
isActive?: boolean
isLoading?: boolean
loadingSections?: Section[]
@@ -97,6 +76,7 @@ export interface EntityCardProps extends React.HTMLAttributes {
usersTag?: HTMLAttributes
statusTag?: HTMLAttributes
priorityTag?: HTMLAttributes
+ notificationDot?: HTMLAttributes
}
}
@@ -245,272 +225,277 @@ export const EntityCard = forwardRef(
(!!value && !isLoading) || (isLoading && loadingSections.includes(name))
return (
- {
- if (!clickedEditableElement(e)) {
- onActivate && onActivate()
- }
- props.onClick && props.onClick(e)
- }}
- onKeyDown={(e) => {
- props.onKeyDown && props.onKeyDown(e)
- if (e.code === 'Enter' || e.code === ' ') {
- if (!clickedEditableElement(e)) onActivate && onActivate()
- }
- }}
- >
- {shouldShowTag(header, 'header') && (
-
- {path && (
-
-
- {project && (
- <>
- {project}
-
- /
-
- >
- )}
- {path && (
- <>
- ...
- /
- {path}
- /
- >
- )}
-
-
- )}
- {isLoading ? '' : header}
-
- )}
- {
- if (!isDraggable) return
- e.stopPropagation()
- pt.thumbnail?.onKeyDown && pt.thumbnail?.onKeyDown(e)
- if (e.code === 'Enter' || e.code === 'Space') {
+
+ {
+ if (!clickedEditableElement(e)) {
onActivate && onActivate()
}
+ props.onClick && props.onClick(e)
+ }}
+ onKeyDown={(e) => {
+ props.onKeyDown && props.onKeyDown(e)
+ if (e.code === 'Enter' || e.code === ' ') {
+ if (!clickedEditableElement(e)) onActivate && onActivate()
+ }
}}
>
- {/* middle Icon */}
-
-
- {imageUrl && (
-
+ {path && (
+
+
+ {project && (
+ <>
+ {project}
+
+ /
+
+ >
+ )}
+ {path && (
+ <>
+ ...
+ /
+ {path}
+ /
+ >
+ )}
+
+
)}
- />
+ {isLoading ? '' : header}
+
)}
- {/* TOP ROW */}
-
- {/* top left */}
- {(!isLoading || loadingSections.includes('title')) && (
-
- {isLoading ? (
- 'loading card...'
- ) : (
- <>
- {titleIcon && }
- {title && {title}}
- >
- )}
-
- )}
-
- {/* top right */}
- {isPlayable && (
-
-
-
- )}
-
- {/* BOTTOM ROW */}
- {
+ if (!isDraggable) return
+ e.stopPropagation()
+ pt.thumbnail?.onKeyDown && pt.thumbnail?.onKeyDown(e)
+ if (e.code === 'Enter' || e.code === 'Space') {
+ onActivate && onActivate()
+ }
+ }}
>
- {atLeastOneEditable && (
- <>
- {/* EDITORS */}
-
-
-
- {/* assignees dropdown */}
- {assigneesEditable && (
- user.name)}
- options={assigneeOptions}
- ref={assigneesDropdownRef}
- onChange={(added, removed) => onAssigneeChange(added, removed)}
- tabIndex={0}
- {...pt.assigneeSelect}
- />
- )}
+ {/* middle Icon */}
+
- {statusEditable && (
- onStatusChange([value])}
- tabIndex={0}
- {...pt.statusSelect}
- />
+ {imageUrl && (
+
+ )}
+ {/* TOP ROW */}
+
+ {/* top left */}
+ {(!isLoading || loadingSections.includes('title')) && (
+
+ {isLoading ? (
+ 'loading card...'
+ ) : (
+ <>
+ {titleIcon && }
+ {title && {title}}
+ >
)}
+
+ )}
- {/* priority dropdown */}
- {priorityEditable && (
- onPriorityChange(value as string[])}
- value={[priority.value]}
- options={priorityOptions}
- tabIndex={0}
- {...pt.prioritySelect}
- />
- )}
-
- >
- )}
+ {/* top right */}
+ {isPlayable && (
+
+
+
+ )}
+
+ {/* BOTTOM ROW */}
+
+ {atLeastOneEditable && (
+ <>
+ {/* EDITORS */}
+
- {/* bottom left - users */}
- {shouldShowTag(users, 'users') && (
- editOnHover && handleEditableHover(e, 'assignees')}
- onClick={(e) => handleEditableHover(e, 'assignees')}
- {...pt.usersTag}
- >
- {users?.length ? (
- 2 })}>
- {[...userWithValidatedImages].slice(0, 2).map((user, i) => (
-
+ {/* assignees dropdown */}
+ {assigneesEditable && (
+ user.name)}
+ options={assigneeOptions}
+ ref={assigneesDropdownRef}
+ onChange={(added, removed) => onAssigneeChange(added, removed)}
+ tabIndex={0}
+ {...pt.assigneeSelect}
/>
- ))}
-
- ) : (
-
- )}
-
- )}
+ )}
- {/* bottom right - status */}
- {shouldShowTag(status, 'status') && (
-
-
- editOnHover && handleEditableHover(e, 'status')}
- onClick={(e) => handleEditableHover(e, 'status')}
- {...pt.statusTag}
- >
- {status?.icon && (
- onStatusChange([value])}
+ tabIndex={0}
+ {...pt.statusSelect}
/>
)}
- {status?.name && (
-
- {status.name}
-
+
+ {/* priority dropdown */}
+ {priorityEditable && (
+ onPriorityChange(value as string[])}
+ value={[priority.value]}
+ options={priorityOptions}
+ tabIndex={0}
+ {...pt.prioritySelect}
+ />
)}
- {status?.shortName && {status.shortName}}
-
-
-
- )}
+
+ >
+ )}
- {/* bottom left - priority */}
- {shouldShowTag(priority && !hidePriority, 'priority') && (
- editOnHover && handleEditableHover(e, 'priority')}
- onClick={(e) => handleEditableHover(e, 'priority')}
- {...pt.priorityTag}
- className={clsx(
- 'tag priority',
- { editable: priorityEditable, isLoading },
- pt.priorityTag?.className,
- )}
- >
- {priority?.icon && (
-
- )}
-
- )}
-
-
-
+ {/* bottom left - users */}
+ {shouldShowTag(users, 'users') && (
+ editOnHover && handleEditableHover(e, 'assignees')}
+ onClick={(e) => handleEditableHover(e, 'assignees')}
+ {...pt.usersTag}
+ >
+ {users?.length ? (
+ 2 })}>
+ {[...userWithValidatedImages].slice(0, 2).map((user, i) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+ )}
+
+ {/* bottom right - status */}
+ {shouldShowTag(status, 'status') && (
+
+
+ editOnHover && handleEditableHover(e, 'status')}
+ onClick={(e) => handleEditableHover(e, 'status')}
+ {...pt.statusTag}
+ >
+ {status?.icon && (
+
+ )}
+ {status?.name && (
+
+ {status.name}
+
+ )}
+ {status?.shortName && (
+ {status.shortName}
+ )}
+
+
+
+ )}
+
+ {/* bottom left - priority */}
+ {shouldShowTag(priority && !hidePriority, 'priority') && (
+ editOnHover && handleEditableHover(e, 'priority')}
+ onClick={(e) => handleEditableHover(e, 'priority')}
+ {...pt.priorityTag}
+ className={clsx(
+ 'tag priority',
+ { editable: priorityEditable, isLoading },
+ pt.priorityTag?.className,
+ )}
+ >
+ {priority?.icon && (
+
+ )}
+
+ )}
+
+
+
+
+
)
},
)
diff --git a/src/EntityCard/Notification/Notification.styled.ts b/src/EntityCard/Notification/Notification.styled.ts
new file mode 100644
index 0000000..13312d6
--- /dev/null
+++ b/src/EntityCard/Notification/Notification.styled.ts
@@ -0,0 +1,23 @@
+import styled from 'styled-components'
+
+export const Notification = styled.div`
+ position: absolute;
+ top: 0;
+ right: 0;
+ translate: 40% -30%;
+ width: 20px;
+ height: 20px;
+ z-index: 100;
+
+ border-radius: 12px;
+ box-shadow: -1px 1px 4px 1px #00000040;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .icon {
+ font-size: 16px;
+ font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 200, 'opsz' 5;
+ }
+`
diff --git a/src/EntityCard/Notification/Notification.tsx b/src/EntityCard/Notification/Notification.tsx
new file mode 100644
index 0000000..cfe7d5e
--- /dev/null
+++ b/src/EntityCard/Notification/Notification.tsx
@@ -0,0 +1,72 @@
+import { Icon, IconType } from '../../Icon'
+import * as Styled from './Notification.styled'
+
+// type NotificationType = 'comment' | 'due' | 'overdue'
+export type Notification = {
+ comment?: boolean
+ due?: boolean
+ overdue?: boolean
+}
+
+type NotificationKey = keyof Notification
+
+type NotificationConfig = {
+ color: string
+ icon: IconType
+}
+
+const notifications: Record = {
+ comment: {
+ color: 'primary',
+ icon: 'forum',
+ },
+ due: {
+ color: 'warning',
+ icon: 'schedule',
+ },
+ overdue: {
+ color: 'error',
+ icon: 'alarm',
+ },
+}
+
+// if there are multiple notifications, the priority is based on the order of the keys, first has highest priority
+const notificationPriorities: NotificationKey[] = ['overdue', 'due', 'comment']
+
+const resolveNotification = (notification: Notification): NotificationConfig | undefined => {
+ const highest = notificationPriorities.find((key) => notification[key] === true)
+ return highest ? notifications[highest] : undefined
+}
+
+const resolveColors = (colorKey: string): { backgroundColor: string; color: string } => {
+ const color = `--md-sys-color-on-${colorKey}`
+ const backgroundColor = `--md-sys-color-${colorKey}`
+
+ return {
+ backgroundColor: `var(${backgroundColor})`,
+ color: `var(${color})`,
+ }
+}
+
+import { forwardRef } from 'react'
+
+export interface NotificationProps extends React.HTMLAttributes {
+ notification?: Notification
+}
+
+export const NotificationDot = forwardRef(
+ ({ notification, style = {}, ...props }, ref) => {
+ if (!notification) return null
+ // get the notification color and icon
+ const config = resolveNotification(notification)
+ if (!config) return null
+ const { icon, color: colorKey } = config
+ const { backgroundColor, color } = resolveColors(colorKey)
+
+ return (
+
+
+
+ )
+ },
+)
diff --git a/src/EntityCard/index.ts b/src/EntityCard/index.ts
index b9a5baa..9907d59 100644
--- a/src/EntityCard/index.ts
+++ b/src/EntityCard/index.ts
@@ -1,2 +1,3 @@
export * from './EntityCard'
export * from './EntityCard.styled'
+export * from './Notification/Notification'
diff --git a/src/index.tsx b/src/index.tsx
index 225e459..db307c6 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -130,8 +130,8 @@ export type { UserImagesStackedProps } from './User/UserImagesStacked'
// ENTITY
// entityCard
-export { EntityCard } from './EntityCard'
-export type { EntityCardProps } from './EntityCard'
+export { EntityCard, NotificationDot } from './EntityCard'
+export type { EntityCardProps, Notification } from './EntityCard'
// export getShimmerStyles
export { getShimmerStyles } from './helpers'