@seamapi/react 2.18.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/elements.js +5498 -5316
- package/dist/elements.js.map +1 -1
- package/dist/index.css +117 -0
- package/dist/index.css.map +1 -1
- package/dist/index.min.css +1 -1
- package/dist/index.min.css.map +1 -1
- package/lib/icons/Clock.d.ts +2 -0
- package/lib/icons/Clock.js +7 -0
- package/lib/icons/Clock.js.map +1 -0
- package/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.js +12 -1
- package/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.js.map +1 -1
- package/lib/seam/events/use-events.d.ts +8 -0
- package/lib/seam/events/use-events.js +23 -0
- package/lib/seam/events/use-events.js.map +1 -0
- package/lib/ui/TabSet.d.ts +9 -0
- package/lib/ui/TabSet.js +37 -0
- package/lib/ui/TabSet.js.map +1 -0
- package/lib/ui/layout/ContentHeader.d.ts +1 -0
- package/lib/ui/layout/ContentHeader.js +3 -2
- package/lib/ui/layout/ContentHeader.js.map +1 -1
- package/lib/ui/noise-sensor/NoiseSensorActivityList.d.ts +7 -0
- package/lib/ui/noise-sensor/NoiseSensorActivityList.js +16 -0
- package/lib/ui/noise-sensor/NoiseSensorActivityList.js.map +1 -0
- package/lib/ui/noise-sensor/NoiseSensorEventItem.d.ts +7 -0
- package/lib/ui/noise-sensor/NoiseSensorEventItem.js +27 -0
- package/lib/ui/noise-sensor/NoiseSensorEventItem.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/lib/icons/Clock.tsx +36 -0
- package/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx +56 -24
- package/src/lib/seam/events/use-events.ts +44 -0
- package/src/lib/ui/TabSet.tsx +110 -0
- package/src/lib/ui/layout/ContentHeader.tsx +5 -2
- package/src/lib/ui/noise-sensor/NoiseSensorActivityList.tsx +31 -0
- package/src/lib/ui/noise-sensor/NoiseSensorEventItem.tsx +67 -0
- package/src/lib/version.ts +1 -1
- package/src/styles/_device-details.scss +12 -0
- package/src/styles/_layout.scss +8 -0
- package/src/styles/_main.scss +4 -0
- package/src/styles/_noise-sensor.scss +72 -0
- package/src/styles/_tab-set.scss +52 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import {
|
|
3
|
+
type MouseEventHandler,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react'
|
|
9
|
+
|
|
10
|
+
interface TabSetProps<TabType extends string> {
|
|
11
|
+
tabs: TabType[]
|
|
12
|
+
tabTitles: Record<TabType, string>
|
|
13
|
+
activeTab: TabType
|
|
14
|
+
onTabChange: (tab: TabType) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface HighlightStyle {
|
|
18
|
+
left: number
|
|
19
|
+
width: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function TabSet<TabType extends string>({
|
|
23
|
+
tabs,
|
|
24
|
+
tabTitles,
|
|
25
|
+
activeTab,
|
|
26
|
+
onTabChange,
|
|
27
|
+
}: TabSetProps<TabType>): JSX.Element {
|
|
28
|
+
const [highlightStyle, setHighlightStyle] = useState<HighlightStyle>({
|
|
29
|
+
left: 0,
|
|
30
|
+
width: 140,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const calculateHighlightStyle = useCallback(() => {
|
|
34
|
+
const tabButton: HTMLButtonElement | null =
|
|
35
|
+
globalThis.document?.querySelector(
|
|
36
|
+
`.seam-tab-button:nth-of-type(${tabs.indexOf(activeTab) + 1})`
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
setHighlightStyle({
|
|
40
|
+
left: tabButton?.offsetLeft ?? 0,
|
|
41
|
+
width: tabButton?.offsetWidth ?? 140,
|
|
42
|
+
})
|
|
43
|
+
}, [activeTab, tabs])
|
|
44
|
+
|
|
45
|
+
useLayoutEffect(() => {
|
|
46
|
+
calculateHighlightStyle()
|
|
47
|
+
}, [activeTab, calculateHighlightStyle])
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
globalThis.addEventListener?.('resize', calculateHighlightStyle)
|
|
51
|
+
return () => {
|
|
52
|
+
globalThis.removeEventListener?.('resize', calculateHighlightStyle)
|
|
53
|
+
}
|
|
54
|
+
}, [calculateHighlightStyle])
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className='seam-tab-set'>
|
|
58
|
+
<div className='seam-tab-set-buttons'>
|
|
59
|
+
<div className='seam-tab-set-highlight' style={highlightStyle} />
|
|
60
|
+
|
|
61
|
+
{tabs.map((tab) => (
|
|
62
|
+
<TabButton<TabType>
|
|
63
|
+
key={tab}
|
|
64
|
+
tab={tab}
|
|
65
|
+
title={tabTitles[tab]}
|
|
66
|
+
isActive={activeTab === tab}
|
|
67
|
+
onTabChange={onTabChange}
|
|
68
|
+
setHighlightStyle={setHighlightStyle}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface TabButtonProps<TabType> {
|
|
77
|
+
tab: TabType
|
|
78
|
+
title: string
|
|
79
|
+
isActive: boolean
|
|
80
|
+
onTabChange: (tab: TabType) => void
|
|
81
|
+
setHighlightStyle: (style: HighlightStyle) => void
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function TabButton<TabType extends string>({
|
|
85
|
+
tab,
|
|
86
|
+
title,
|
|
87
|
+
isActive,
|
|
88
|
+
onTabChange,
|
|
89
|
+
setHighlightStyle,
|
|
90
|
+
}: TabButtonProps<TabType>): JSX.Element {
|
|
91
|
+
const handleClick: MouseEventHandler<HTMLButtonElement> = (ev) => {
|
|
92
|
+
onTabChange(tab)
|
|
93
|
+
setHighlightStyle({
|
|
94
|
+
left: ev.currentTarget.offsetLeft,
|
|
95
|
+
width: ev.currentTarget.offsetWidth,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<button
|
|
101
|
+
className={classNames(
|
|
102
|
+
'seam-tab-button',
|
|
103
|
+
isActive && 'seam-tab-button-active'
|
|
104
|
+
)}
|
|
105
|
+
onClick={handleClick}
|
|
106
|
+
>
|
|
107
|
+
<p className='seam-tab-button-label'>{title}</p>
|
|
108
|
+
</button>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
|
|
1
3
|
import { ArrowBackIcon } from 'lib/icons/ArrowBack.js'
|
|
2
4
|
|
|
3
5
|
interface ContentHeaderProps {
|
|
4
6
|
onBack: (() => void) | undefined
|
|
5
7
|
title?: string
|
|
6
8
|
subheading?: string
|
|
9
|
+
className?: string
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export function ContentHeader(props: ContentHeaderProps): JSX.Element | null {
|
|
10
|
-
const { title, onBack, subheading } = props
|
|
13
|
+
const { title, onBack, subheading, className } = props
|
|
11
14
|
if (title == null && onBack == null) {
|
|
12
15
|
return null
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
return (
|
|
16
|
-
<div className='seam-content-header'>
|
|
19
|
+
<div className={classNames('seam-content-header', className)}>
|
|
17
20
|
<BackIcon onClick={onBack} />
|
|
18
21
|
<div>
|
|
19
22
|
<span className='seam-title'>{title}</span>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { NoiseSensorDevice } from 'seamapi'
|
|
3
|
+
|
|
4
|
+
import { useEvents } from 'lib/seam/events/use-events.js'
|
|
5
|
+
import { NoiseSensorEventItem } from 'lib/ui/noise-sensor/NoiseSensorEventItem.js'
|
|
6
|
+
import { useNow } from 'lib/ui/use-now.js'
|
|
7
|
+
|
|
8
|
+
interface NoiseSensorActivityListProps {
|
|
9
|
+
device: NoiseSensorDevice
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function NoiseSensorActivityList({
|
|
13
|
+
device,
|
|
14
|
+
}: NoiseSensorActivityListProps): JSX.Element {
|
|
15
|
+
const now = useNow()
|
|
16
|
+
const [mountedAt] = useState(now)
|
|
17
|
+
|
|
18
|
+
const { events } = useEvents({
|
|
19
|
+
device_id: device.device_id,
|
|
20
|
+
event_type: 'noise_sensor.noise_threshold_triggered',
|
|
21
|
+
since: mountedAt.minus({ months: 1 }).toString(),
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className='seam-noise-sensor-activity-list'>
|
|
26
|
+
{events?.map((event) => (
|
|
27
|
+
<NoiseSensorEventItem key={event.event_id} event={event} />
|
|
28
|
+
))}
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { DateTime } from 'luxon'
|
|
2
|
+
import type { Event } from 'seamapi'
|
|
3
|
+
|
|
4
|
+
import { ClockIcon } from 'lib/icons/Clock.js'
|
|
5
|
+
|
|
6
|
+
interface NoiseSensorEventItemProps {
|
|
7
|
+
event: Event
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function NoiseSensorEventItem({
|
|
11
|
+
event,
|
|
12
|
+
}: NoiseSensorEventItemProps): JSX.Element {
|
|
13
|
+
const date = formatDate(event.created_at)
|
|
14
|
+
const time = formatTime(event.created_at)
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className='seam-noise-sensor-event-item'>
|
|
18
|
+
<div className='seam-noise-sensor-event-item-column-wrap'>
|
|
19
|
+
<ClockIcon />
|
|
20
|
+
|
|
21
|
+
<div className='seam-noise-sensor-event-item-datetime-wrap'>
|
|
22
|
+
<p className='seam-noise-sensor-event-item-date'>{date}</p>
|
|
23
|
+
<p className='seam-noise-sensor-event-item-time'>{time}</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className='seam-noise-sensor-event-item-context-wrap'>
|
|
28
|
+
<p className='seam-noise-sensor-event-item-context-label'>
|
|
29
|
+
{t.noiseThresholdTriggered}
|
|
30
|
+
</p>
|
|
31
|
+
{getContextSublabel(event) != null && (
|
|
32
|
+
<p className='seam-noise-sensor-event-item-context-sublabel'>
|
|
33
|
+
{getContextSublabel(event)}
|
|
34
|
+
</p>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className='seam-noise-sensor-event-item-right-block' />
|
|
39
|
+
</div>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getContextSublabel(event: Event): string | null {
|
|
44
|
+
if ('noise_threshold_name' in event) {
|
|
45
|
+
// @ts-expect-error UPSTREAM: Shallow event type
|
|
46
|
+
// https://github.com/seamapi/react/issues/611
|
|
47
|
+
return event.noise_threshold_name
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if ('noise_level_decibels' in event) {
|
|
51
|
+
// UPSTREAM: Shallow event types.
|
|
52
|
+
return `${event.noise_level_decibels as string} ${t.decibel}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const formatDate = (dateTime: string): string =>
|
|
59
|
+
DateTime.fromISO(dateTime).toLocaleString(DateTime.DATE_FULL)
|
|
60
|
+
|
|
61
|
+
const formatTime = (dateTime: string): string =>
|
|
62
|
+
DateTime.fromISO(dateTime).toLocaleString(DateTime.TIME_SIMPLE)
|
|
63
|
+
|
|
64
|
+
const t = {
|
|
65
|
+
decibel: 'dB',
|
|
66
|
+
noiseThresholdTriggered: 'Noise threshold triggered',
|
|
67
|
+
}
|
package/src/lib/version.ts
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
gap: 16px;
|
|
9
9
|
margin: 0 24px 24px;
|
|
10
10
|
|
|
11
|
+
&.seam-body-no-margin {
|
|
12
|
+
margin: 0;
|
|
13
|
+
gap: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
.seam-box {
|
|
12
17
|
border: 1px solid colors.$text-gray-3;
|
|
13
18
|
border-radius: 8px;
|
|
@@ -56,6 +61,13 @@
|
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
|
|
64
|
+
.seam-contained-summary {
|
|
65
|
+
padding: 0 24px;
|
|
66
|
+
box-shadow:
|
|
67
|
+
0 2px 8px 0 rgb(0 0 0 / 8%),
|
|
68
|
+
0 1px 0 0 rgb(0 0 0 / 10%);
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
.seam-summary {
|
|
60
72
|
padding: 24px 16px;
|
|
61
73
|
border-radius: 16px;
|
package/src/styles/_layout.scss
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
justify-content: center;
|
|
10
10
|
margin: 0 16px;
|
|
11
11
|
|
|
12
|
+
&.seam-content-header-contained {
|
|
13
|
+
margin: 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
.seam-back-icon {
|
|
13
17
|
position: absolute;
|
|
14
18
|
top: 50%;
|
|
@@ -34,6 +38,10 @@
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
@mixin detail-section {
|
|
41
|
+
.seam-padded-container {
|
|
42
|
+
padding: 0 16px;
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
.seam-detail-sections {
|
|
38
46
|
display: flex;
|
|
39
47
|
flex-direction: column;
|
package/src/styles/_main.scss
CHANGED
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
@use './climate-setting-schedule-form';
|
|
30
30
|
@use './climate-setting-schedule-details';
|
|
31
31
|
@use './time-zone-picker';
|
|
32
|
+
@use './tab-set';
|
|
33
|
+
@use './noise-sensor';
|
|
32
34
|
|
|
33
35
|
.seam-components {
|
|
34
36
|
// Reset
|
|
@@ -53,6 +55,7 @@
|
|
|
53
55
|
@include spinner.all;
|
|
54
56
|
@include switch.all;
|
|
55
57
|
@include time-zone-picker.all;
|
|
58
|
+
@include tab-set.all;
|
|
56
59
|
|
|
57
60
|
// Components
|
|
58
61
|
@include device-details.all;
|
|
@@ -66,4 +69,5 @@
|
|
|
66
69
|
@include thermostat.all;
|
|
67
70
|
@include seam-table.all;
|
|
68
71
|
@include climate-setting-schedule-details.all;
|
|
72
|
+
@include noise-sensor.all;
|
|
69
73
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
@use './colors';
|
|
2
|
+
|
|
3
|
+
@mixin all {
|
|
4
|
+
.seam-noise-sensor-activity-list {
|
|
5
|
+
width: 100%;
|
|
6
|
+
display: flex;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
align-items: center;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.seam-noise-sensor-event-item {
|
|
13
|
+
width: 100%;
|
|
14
|
+
min-height: 64px;
|
|
15
|
+
padding: 12px 8px 12px 16px;
|
|
16
|
+
border-bottom: 1px solid colors.$divider-stroke-light;
|
|
17
|
+
display: flex;
|
|
18
|
+
gap: 40px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.seam-noise-sensor-event-item-column-wrap {
|
|
22
|
+
display: flex;
|
|
23
|
+
justify-content: flex-start;
|
|
24
|
+
align-items: center;
|
|
25
|
+
flex-direction: row;
|
|
26
|
+
gap: 18px;
|
|
27
|
+
flex: 0.5;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.seam-noise-sensor-event-item-datetime-wrap {
|
|
31
|
+
color: colors.$text-gray-1;
|
|
32
|
+
font-size: 14px;
|
|
33
|
+
line-height: 134%;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.seam-noise-sensor-event-item-date,
|
|
37
|
+
.seam-noise-sensor-event-item-time,
|
|
38
|
+
.seam-noise-sensor-event-item-context-label,
|
|
39
|
+
.seam-noise-sensor-event-item-context-sublabel {
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.seam-noise-sensor-event-item-context-wrap {
|
|
44
|
+
display: flex;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
align-items: flex-start;
|
|
47
|
+
flex-direction: column;
|
|
48
|
+
flex: 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.seam-noise-sensor-event-item-context-label {
|
|
52
|
+
color: colors.$text-default;
|
|
53
|
+
font-size: 14px;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
max-width: 100%;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
text-overflow: ellipsis;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.seam-noise-sensor-event-item-context-sublabel {
|
|
61
|
+
color: colors.$text-gray-2;
|
|
62
|
+
font-size: 14px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.seam-noise-sensor-event-item-right-block {
|
|
66
|
+
display: flex;
|
|
67
|
+
justify-content: flex-end;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 18px;
|
|
70
|
+
flex: 0.5;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
@use './colors';
|
|
2
|
+
|
|
3
|
+
@mixin all {
|
|
4
|
+
.seam-tab-set {
|
|
5
|
+
width: 100%;
|
|
6
|
+
display: flex;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
align-items: center;
|
|
9
|
+
flex-direction: row;
|
|
10
|
+
|
|
11
|
+
.seam-tab-set-buttons {
|
|
12
|
+
width: 100%;
|
|
13
|
+
display: flex;
|
|
14
|
+
justify-content: center;
|
|
15
|
+
align-items: center;
|
|
16
|
+
flex-direction: row;
|
|
17
|
+
position: relative;
|
|
18
|
+
padding-top: 8px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.seam-tab-set-highlight {
|
|
22
|
+
height: 4px;
|
|
23
|
+
border-radius: 6px 6px 0 0;
|
|
24
|
+
background-color: colors.$primary;
|
|
25
|
+
position: absolute;
|
|
26
|
+
bottom: 0;
|
|
27
|
+
transition: 240ms cubic-bezier(0.2, 0, 0.38, 0.9);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.seam-tab-button {
|
|
31
|
+
appearance: none;
|
|
32
|
+
width: 140px;
|
|
33
|
+
padding-top: 4px;
|
|
34
|
+
padding-bottom: 12px;
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
align-items: center;
|
|
38
|
+
font-size: 14px;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
background-color: transparent;
|
|
41
|
+
box-shadow: none;
|
|
42
|
+
border: none;
|
|
43
|
+
color: colors.$text-gray-2;
|
|
44
|
+
transition: 240ms ease-in-out;
|
|
45
|
+
|
|
46
|
+
&:hover,
|
|
47
|
+
&.seam-tab-button-active {
|
|
48
|
+
color: colors.$text-default;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|