@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.
Files changed (43) hide show
  1. package/README.md +2 -2
  2. package/dist/elements.js +5498 -5316
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +117 -0
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.min.css +1 -1
  7. package/dist/index.min.css.map +1 -1
  8. package/lib/icons/Clock.d.ts +2 -0
  9. package/lib/icons/Clock.js +7 -0
  10. package/lib/icons/Clock.js.map +1 -0
  11. package/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.js +12 -1
  12. package/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.js.map +1 -1
  13. package/lib/seam/events/use-events.d.ts +8 -0
  14. package/lib/seam/events/use-events.js +23 -0
  15. package/lib/seam/events/use-events.js.map +1 -0
  16. package/lib/ui/TabSet.d.ts +9 -0
  17. package/lib/ui/TabSet.js +37 -0
  18. package/lib/ui/TabSet.js.map +1 -0
  19. package/lib/ui/layout/ContentHeader.d.ts +1 -0
  20. package/lib/ui/layout/ContentHeader.js +3 -2
  21. package/lib/ui/layout/ContentHeader.js.map +1 -1
  22. package/lib/ui/noise-sensor/NoiseSensorActivityList.d.ts +7 -0
  23. package/lib/ui/noise-sensor/NoiseSensorActivityList.js +16 -0
  24. package/lib/ui/noise-sensor/NoiseSensorActivityList.js.map +1 -0
  25. package/lib/ui/noise-sensor/NoiseSensorEventItem.d.ts +7 -0
  26. package/lib/ui/noise-sensor/NoiseSensorEventItem.js +27 -0
  27. package/lib/ui/noise-sensor/NoiseSensorEventItem.js.map +1 -0
  28. package/lib/version.d.ts +1 -1
  29. package/lib/version.js +1 -1
  30. package/package.json +1 -1
  31. package/src/lib/icons/Clock.tsx +36 -0
  32. package/src/lib/seam/components/DeviceDetails/NoiseSensorDeviceDetails.tsx +56 -24
  33. package/src/lib/seam/events/use-events.ts +44 -0
  34. package/src/lib/ui/TabSet.tsx +110 -0
  35. package/src/lib/ui/layout/ContentHeader.tsx +5 -2
  36. package/src/lib/ui/noise-sensor/NoiseSensorActivityList.tsx +31 -0
  37. package/src/lib/ui/noise-sensor/NoiseSensorEventItem.tsx +67 -0
  38. package/src/lib/version.ts +1 -1
  39. package/src/styles/_device-details.scss +12 -0
  40. package/src/styles/_layout.scss +8 -0
  41. package/src/styles/_main.scss +4 -0
  42. package/src/styles/_noise-sensor.scss +72 -0
  43. 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
+ }
@@ -1,3 +1,3 @@
1
- const seamapiReactVersion = '2.18.0'
1
+ const seamapiReactVersion = '2.19.0'
2
2
 
3
3
  export default seamapiReactVersion
@@ -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;
@@ -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;
@@ -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
+ }