@uniai-fe/uds-templates 0.10.3 → 0.11.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/dist/styles.css +194 -21
- package/package.json +15 -15
- package/src/cctv/components/cam-list/Item.tsx +4 -3
- package/src/cctv/components/pagination/Provider.tsx +6 -2
- package/src/cctv/components/pagination/list/Item.tsx +1 -0
- package/src/cctv/components/video/Card.tsx +30 -0
- package/src/cctv/components/video/Container.tsx +3 -0
- package/src/cctv/components/video/Template.tsx +6 -1
- package/src/cctv/components/video/index.tsx +2 -0
- package/src/cctv/components/video/overlay/footer/Container.tsx +2 -0
- package/src/cctv/components/video/overlay/footer/Metric.tsx +22 -0
- package/src/cctv/components/video/overlay/footer/Metrics.tsx +13 -0
- package/src/cctv/components/video/overlay/header/LiveState.tsx +1 -1
- package/src/cctv/components/video/overlay/index.tsx +4 -0
- package/src/cctv/components/viewer/desktop/Container.tsx +35 -4
- package/src/cctv/components/viewer/desktop/Headline.tsx +28 -0
- package/src/cctv/components/viewer/desktop/Overlay.tsx +60 -0
- package/src/cctv/components/viewer/desktop/Video.tsx +17 -5
- package/src/cctv/components/viewer/index.tsx +4 -0
- package/src/cctv/hooks/streamRegistry.ts +69 -30
- package/src/cctv/img/time.svg +4 -0
- package/src/cctv/styles/cam-list.scss +5 -15
- package/src/cctv/styles/pagination.scss +2 -1
- package/src/cctv/styles/variables.scss +18 -1
- package/src/cctv/styles/video.scss +95 -5
- package/src/cctv/styles/viewer.scss +73 -2
- package/src/cctv/types/props.ts +83 -0
package/dist/styles.css
CHANGED
|
@@ -93,9 +93,16 @@
|
|
|
93
93
|
/* Card layout */
|
|
94
94
|
--cctv-video-radius: 12px;
|
|
95
95
|
--cctv-list-gap: var(--spacing-gap-5);
|
|
96
|
-
--cctv-list-item-min-width:
|
|
96
|
+
--cctv-list-item-min-width: 300px;
|
|
97
97
|
--cctv-overlay-padding-x: var(--spacing-padding-8);
|
|
98
98
|
--cctv-overlay-padding-y: var(--spacing-padding-6);
|
|
99
|
+
--cctv-viewer-video-radius: 16px;
|
|
100
|
+
--cctv-viewer-video-max-height: 618px;
|
|
101
|
+
--cctv-viewer-overlay-padding-x: var(--spacing-padding-11);
|
|
102
|
+
--cctv-viewer-overlay-padding-y: var(--spacing-padding-9);
|
|
103
|
+
--cctv-card-settings-bg: var(--color-common-99);
|
|
104
|
+
--cctv-card-settings-border: var(--color-border-assistive);
|
|
105
|
+
--cctv-card-height: 240px;
|
|
99
106
|
--cctv-video-bg: var(--color-surface-heavy);
|
|
100
107
|
--cctv-overlay-bg: linear-gradient(
|
|
101
108
|
180deg,
|
|
@@ -109,12 +116,20 @@
|
|
|
109
116
|
--cctv-live-state-bg-off: var(--color-cool-gray-55);
|
|
110
117
|
--cctv-live-state-dot-off: var(--color-border-strong);
|
|
111
118
|
--cctv-live-state-text-off: var(--color-label-disabled);
|
|
119
|
+
--cctv-state-dot-size: var(--spacing-gap-3);
|
|
120
|
+
/* Sensor metric */
|
|
121
|
+
--cctv-metric-bg: rgba(0, 0, 0, 0.55);
|
|
122
|
+
--cctv-metric-text: var(--color-common-99);
|
|
123
|
+
--cctv-metric-dot-normal: var(--color-feedback-success);
|
|
124
|
+
--cctv-metric-dot-attention: var(--color-feedback-new);
|
|
125
|
+
--cctv-metric-dot-unavailable: var(--color-border-strong);
|
|
112
126
|
/* Error */
|
|
113
127
|
--cctv-status-text-color: var(--color-common-99);
|
|
114
128
|
--cctv-error-text-color: var(--color-label-disabled);
|
|
115
129
|
--cctv-error-icon-color: var(--color-label-disabled);
|
|
116
130
|
/* Pagination */
|
|
117
131
|
--cctv-carousel-gap: var(--spacing-gap-5);
|
|
132
|
+
--cctv-pagination-item-width: 170px;
|
|
118
133
|
--cctv-pagination-move-button-size: 48px;
|
|
119
134
|
--cctv-pagination-move-button-active: var(--color-cool-gray-20);
|
|
120
135
|
--cctv-pagination-move-button-disabled: var(--color-cool-gray-85);
|
|
@@ -1743,6 +1758,59 @@
|
|
|
1743
1758
|
visibility: hidden;
|
|
1744
1759
|
}
|
|
1745
1760
|
|
|
1761
|
+
.cctv-video-container[data-layout="card"] {
|
|
1762
|
+
min-height: 0;
|
|
1763
|
+
max-height: 240px;
|
|
1764
|
+
aspect-ratio: 16/9;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
.cctv-video-container[data-layout="carousel"] {
|
|
1768
|
+
min-height: 0;
|
|
1769
|
+
aspect-ratio: 16/9;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
.cctv-video-card {
|
|
1773
|
+
overflow: hidden;
|
|
1774
|
+
border-radius: var(--cctv-video-radius);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.cctv-video-card[data-has-settings="true"] {
|
|
1778
|
+
height: var(--cctv-card-height);
|
|
1779
|
+
display: flex;
|
|
1780
|
+
flex-direction: column;
|
|
1781
|
+
}
|
|
1782
|
+
.cctv-video-card[data-has-settings="true"] .cctv-video-card-media {
|
|
1783
|
+
min-height: 0;
|
|
1784
|
+
flex: 1;
|
|
1785
|
+
}
|
|
1786
|
+
.cctv-video-card[data-has-settings="true"]
|
|
1787
|
+
.cctv-video-card-media
|
|
1788
|
+
.cctv-video-container {
|
|
1789
|
+
height: 100%;
|
|
1790
|
+
min-height: 0;
|
|
1791
|
+
aspect-ratio: auto;
|
|
1792
|
+
}
|
|
1793
|
+
.cctv-video-card[data-has-settings="true"]
|
|
1794
|
+
.cctv-video-card-media
|
|
1795
|
+
.cctv-video-container,
|
|
1796
|
+
.cctv-video-card[data-has-settings="true"]
|
|
1797
|
+
.cctv-video-card-media
|
|
1798
|
+
.cctv-video-box
|
|
1799
|
+
video,
|
|
1800
|
+
.cctv-video-card[data-has-settings="true"]
|
|
1801
|
+
.cctv-video-card-media
|
|
1802
|
+
.cctv-video-overlay-container {
|
|
1803
|
+
border-bottom-right-radius: 0;
|
|
1804
|
+
border-bottom-left-radius: 0;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
.cctv-video-card-settings {
|
|
1808
|
+
flex: 0 0 auto;
|
|
1809
|
+
padding: var(--spacing-padding-6) var(--spacing-padding-8);
|
|
1810
|
+
background: var(--cctv-card-settings-bg);
|
|
1811
|
+
box-shadow: inset 0 0 0 1px var(--cctv-card-settings-border);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1746
1814
|
.cctv-video-box {
|
|
1747
1815
|
width: 100%;
|
|
1748
1816
|
height: 100%;
|
|
@@ -1771,6 +1839,9 @@
|
|
|
1771
1839
|
.cctv-video-overlay-header {
|
|
1772
1840
|
width: 100%;
|
|
1773
1841
|
height: fit-content;
|
|
1842
|
+
display: flex;
|
|
1843
|
+
flex-direction: column;
|
|
1844
|
+
gap: var(--spacing-gap-5);
|
|
1774
1845
|
}
|
|
1775
1846
|
.cctv-video-overlay-header.empty {
|
|
1776
1847
|
height: calc(var(--font-heading-xsmall-size) * 1.5);
|
|
@@ -1799,10 +1870,10 @@
|
|
|
1799
1870
|
}
|
|
1800
1871
|
|
|
1801
1872
|
.cctv-video-live-state-dot {
|
|
1802
|
-
width:
|
|
1803
|
-
height:
|
|
1804
|
-
margin-right:
|
|
1805
|
-
border-radius:
|
|
1873
|
+
width: var(--cctv-state-dot-size);
|
|
1874
|
+
height: var(--cctv-state-dot-size);
|
|
1875
|
+
margin-right: var(--spacing-gap-3);
|
|
1876
|
+
border-radius: var(--cctv-state-dot-size);
|
|
1806
1877
|
background: var(--cctv-live-state-dot-off);
|
|
1807
1878
|
}
|
|
1808
1879
|
|
|
@@ -1836,7 +1907,7 @@
|
|
|
1836
1907
|
}
|
|
1837
1908
|
|
|
1838
1909
|
.cctv-video-header-title {
|
|
1839
|
-
font-size: var(--font-heading-
|
|
1910
|
+
font-size: var(--font-heading-medium-size);
|
|
1840
1911
|
}
|
|
1841
1912
|
|
|
1842
1913
|
.cctv-video-footer-title {
|
|
@@ -1917,12 +1988,52 @@
|
|
|
1917
1988
|
margin-top: auto;
|
|
1918
1989
|
}
|
|
1919
1990
|
|
|
1991
|
+
.cctv-video-metrics {
|
|
1992
|
+
display: flex;
|
|
1993
|
+
gap: var(--spacing-gap-4);
|
|
1994
|
+
margin-left: auto;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
.cctv-video-metric {
|
|
1998
|
+
display: inline-flex;
|
|
1999
|
+
align-items: center;
|
|
2000
|
+
gap: var(--spacing-gap-3);
|
|
2001
|
+
padding: var(--spacing-padding-2) var(--spacing-padding-5);
|
|
2002
|
+
border-radius: 999px;
|
|
2003
|
+
color: var(--cctv-metric-text);
|
|
2004
|
+
background: var(--cctv-metric-bg);
|
|
2005
|
+
font-size: var(--font-label-small-size);
|
|
2006
|
+
line-height: var(--font-label-small-line-height);
|
|
2007
|
+
font-weight: var(--font-label-small-weight);
|
|
2008
|
+
white-space: nowrap;
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
.cctv-video-metric-dot {
|
|
2012
|
+
width: var(--cctv-state-dot-size);
|
|
2013
|
+
height: var(--cctv-state-dot-size);
|
|
2014
|
+
flex: 0 0 var(--cctv-state-dot-size);
|
|
2015
|
+
border-radius: var(--cctv-state-dot-size);
|
|
2016
|
+
background: var(--cctv-metric-dot-normal);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
.cctv-video-metric[data-state="attention"] .cctv-video-metric-dot {
|
|
2020
|
+
background: var(--cctv-metric-dot-attention);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
.cctv-video-metric[data-state="unavailable"] .cctv-video-metric-dot {
|
|
2024
|
+
background: var(--cctv-metric-dot-unavailable);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1920
2027
|
.cctv-video-open-button {
|
|
1921
2028
|
flex: 0 0 auto;
|
|
1922
2029
|
display: flex;
|
|
1923
2030
|
align-items: center;
|
|
1924
2031
|
justify-content: center;
|
|
1925
2032
|
cursor: auto;
|
|
2033
|
+
position: absolute;
|
|
2034
|
+
top: var(--cctv-overlay-padding-y);
|
|
2035
|
+
right: var(--cctv-overlay-padding-x);
|
|
2036
|
+
z-index: 2;
|
|
1926
2037
|
}
|
|
1927
2038
|
.cctv-video-open-button svg path {
|
|
1928
2039
|
stroke: var(--cctv-open-button-color-off);
|
|
@@ -1965,29 +2076,21 @@
|
|
|
1965
2076
|
}
|
|
1966
2077
|
|
|
1967
2078
|
.cctv-cam-list-item .cctv-video-container {
|
|
2079
|
+
min-height: 0;
|
|
1968
2080
|
max-height: 240px;
|
|
2081
|
+
aspect-ratio: 16/9;
|
|
1969
2082
|
}
|
|
1970
2083
|
|
|
1971
|
-
@container cctv-cam-list (min-width:
|
|
2084
|
+
@container cctv-cam-list (min-width: 612px) {
|
|
1972
2085
|
.cctv-cam-list-track {
|
|
1973
2086
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1974
2087
|
}
|
|
1975
2088
|
}
|
|
1976
|
-
@container cctv-cam-list (min-width:
|
|
2089
|
+
@container cctv-cam-list (min-width: 924px) {
|
|
1977
2090
|
.cctv-cam-list-track {
|
|
1978
2091
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1979
2092
|
}
|
|
1980
2093
|
}
|
|
1981
|
-
@container cctv-cam-list (min-width: 1316px) {
|
|
1982
|
-
.cctv-cam-list-track {
|
|
1983
|
-
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
@container cctv-cam-list (min-width: 1648px) {
|
|
1987
|
-
.cctv-cam-list-track {
|
|
1988
|
-
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
2094
|
.cctv-pagination-container {
|
|
1992
2095
|
width: 100%;
|
|
1993
2096
|
display: flex;
|
|
@@ -2027,7 +2130,7 @@
|
|
|
2027
2130
|
}
|
|
2028
2131
|
|
|
2029
2132
|
.cctv-pagination-track > li {
|
|
2030
|
-
flex: 0 0
|
|
2133
|
+
flex: 0 0 var(--cctv-pagination-item-width);
|
|
2031
2134
|
}
|
|
2032
2135
|
|
|
2033
2136
|
.cctv-pagination-list-item {
|
|
@@ -2035,6 +2138,7 @@
|
|
|
2035
2138
|
}
|
|
2036
2139
|
|
|
2037
2140
|
.cctv-pagination-list-item .cctv-video-container {
|
|
2141
|
+
min-height: 0;
|
|
2038
2142
|
max-height: 152px;
|
|
2039
2143
|
}
|
|
2040
2144
|
|
|
@@ -2060,12 +2164,81 @@
|
|
|
2060
2164
|
background: var(--cctv-pagination-move-button-disabled);
|
|
2061
2165
|
}
|
|
2062
2166
|
|
|
2167
|
+
.cctv-viewer-desktop-container {
|
|
2168
|
+
display: grid;
|
|
2169
|
+
gap: var(--spacing-gap-6);
|
|
2170
|
+
width: min(100%, 1216px);
|
|
2171
|
+
margin: 0 auto;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
.cctv-viewer-desktop-headline {
|
|
2175
|
+
min-height: 34px;
|
|
2176
|
+
display: flex;
|
|
2177
|
+
align-items: center;
|
|
2178
|
+
justify-content: space-between;
|
|
2179
|
+
gap: var(--spacing-gap-6);
|
|
2180
|
+
}
|
|
2181
|
+
.cctv-viewer-desktop-headline h3 {
|
|
2182
|
+
min-width: 0;
|
|
2183
|
+
margin: 0;
|
|
2184
|
+
overflow: hidden;
|
|
2185
|
+
color: var(--color-label-strong);
|
|
2186
|
+
font-size: var(--font-heading-medium-size);
|
|
2187
|
+
line-height: 1.4;
|
|
2188
|
+
font-weight: 700;
|
|
2189
|
+
text-overflow: ellipsis;
|
|
2190
|
+
white-space: nowrap;
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
.cctv-viewer-desktop-time {
|
|
2194
|
+
display: inline-flex;
|
|
2195
|
+
align-items: center;
|
|
2196
|
+
gap: var(--spacing-gap-3);
|
|
2197
|
+
color: var(--color-label-neutral);
|
|
2198
|
+
font-size: var(--font-label-small-size);
|
|
2199
|
+
line-height: var(--font-label-small-line-height);
|
|
2200
|
+
font-weight: var(--font-label-small-weight);
|
|
2201
|
+
white-space: nowrap;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2063
2204
|
.cctv-viewer-desktop-video-container {
|
|
2064
|
-
|
|
2205
|
+
--cctv-video-radius: var(--cctv-viewer-video-radius);
|
|
2206
|
+
--cctv-overlay-padding-x: var(--cctv-viewer-overlay-padding-x);
|
|
2207
|
+
--cctv-overlay-padding-y: var(--cctv-viewer-overlay-padding-y);
|
|
2208
|
+
max-height: var(--cctv-viewer-video-max-height);
|
|
2209
|
+
min-height: 0;
|
|
2210
|
+
aspect-ratio: 608/309;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
.cctv-viewer-desktop-overlay-body {
|
|
2214
|
+
flex-direction: row;
|
|
2215
|
+
justify-content: space-between;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
.cctv-viewer-desktop-move-button.cctv-pagination-move-button {
|
|
2219
|
+
width: 42px;
|
|
2220
|
+
height: 42px;
|
|
2221
|
+
background: var(--cctv-pagination-move-button-active);
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
.cctv-viewer-desktop-video-container .cctv-video-overlay-footer {
|
|
2225
|
+
justify-content: flex-end;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
.cctv-visually-hidden {
|
|
2229
|
+
position: absolute;
|
|
2230
|
+
width: 1px;
|
|
2231
|
+
height: 1px;
|
|
2232
|
+
padding: 0;
|
|
2233
|
+
margin: -1px;
|
|
2234
|
+
overflow: hidden;
|
|
2235
|
+
clip: rect(0, 0, 0, 0);
|
|
2236
|
+
white-space: nowrap;
|
|
2237
|
+
border: 0;
|
|
2065
2238
|
}
|
|
2066
2239
|
|
|
2067
2240
|
.cctv-viewer-desktop-pagination-container {
|
|
2068
|
-
margin-top: var(--spacing-gap-
|
|
2241
|
+
margin-top: var(--spacing-gap-2);
|
|
2069
2242
|
}
|
|
2070
2243
|
|
|
2071
2244
|
.service-inquiry-form {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniai-fe/uds-templates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "UNIAI Design System; UI Templates Package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -56,28 +56,28 @@
|
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@svgr/webpack": "^8.1.0",
|
|
59
|
-
"@tanstack/react-query": "^5.101.
|
|
60
|
-
"@types/node": "^24.13.
|
|
59
|
+
"@tanstack/react-query": "^5.101.4",
|
|
60
|
+
"@types/node": "^24.13.3",
|
|
61
61
|
"@types/react": "^19.2.17",
|
|
62
62
|
"@types/react-dom": "^19.2.3",
|
|
63
|
-
"eslint": "^9.39.
|
|
63
|
+
"eslint": "^9.39.5",
|
|
64
64
|
"jotai": "^2.20.2",
|
|
65
65
|
"next": "16.2.10",
|
|
66
|
-
"prettier": "^3.
|
|
67
|
-
"react-hook-form": "^7.
|
|
68
|
-
"sass": "^1.101.
|
|
66
|
+
"prettier": "^3.9.6",
|
|
67
|
+
"react-hook-form": "^7.82.0",
|
|
68
|
+
"sass": "^1.101.3",
|
|
69
69
|
"typescript": "6.0.3",
|
|
70
|
-
"@uniai-fe/eslint-config": "0.4.1",
|
|
71
70
|
"@uniai-fe/next-devkit": "0.4.0",
|
|
72
|
-
"@uniai-fe/
|
|
73
|
-
"@uniai-fe/react-hooks": "0.3.0",
|
|
71
|
+
"@uniai-fe/uds-primitives": "0.10.2",
|
|
74
72
|
"@uniai-fe/uds-foundation": "0.5.0",
|
|
75
|
-
"@uniai-fe/
|
|
76
|
-
"@uniai-fe/
|
|
73
|
+
"@uniai-fe/eslint-config": "0.4.2",
|
|
74
|
+
"@uniai-fe/react-hooks": "0.3.0",
|
|
75
|
+
"@uniai-fe/tsconfig": "0.2.0",
|
|
77
76
|
"@uniai-fe/util-api": "0.2.1",
|
|
78
|
-
"@uniai-fe/util-
|
|
79
|
-
"@uniai-fe/util-
|
|
80
|
-
"@uniai-fe/util-
|
|
77
|
+
"@uniai-fe/util-functions": "0.4.3",
|
|
78
|
+
"@uniai-fe/util-jotai": "0.3.0",
|
|
79
|
+
"@uniai-fe/util-rtc": "0.2.1",
|
|
80
|
+
"@uniai-fe/util-next": "0.5.0"
|
|
81
81
|
},
|
|
82
82
|
"scripts": {
|
|
83
83
|
"check:pre-commit": "pnpm --dir ../../.. run check:pre-commit",
|
|
@@ -50,15 +50,16 @@ export default function CCTVCamListItem({
|
|
|
50
50
|
ref={videoRef}
|
|
51
51
|
cam={cam}
|
|
52
52
|
className="cctv-cam-list-video-container"
|
|
53
|
-
|
|
53
|
+
layout="card"
|
|
54
|
+
// list card에서는 live/share 상태와 제목을 header에 함께 노출한다.
|
|
54
55
|
headerOptions={{
|
|
55
56
|
activeLiveState: true,
|
|
56
|
-
activeTitle:
|
|
57
|
+
activeTitle: true,
|
|
57
58
|
isLive,
|
|
58
59
|
isShared: cam.is_public === true,
|
|
59
60
|
title: cam.cam_name,
|
|
60
61
|
}}
|
|
61
|
-
footerOptions={{ activeTitle:
|
|
62
|
+
footerOptions={{ activeTitle: false, activeOpenButton: true, cam }}
|
|
62
63
|
// 변경 설명: list item custom overlay는 template seam 하나만 따라간다.
|
|
63
64
|
renderOverlay={renderOverlay}
|
|
64
65
|
{...{
|
|
@@ -84,8 +84,9 @@ export function CctvPaginationCarouselProvider({
|
|
|
84
84
|
const scrollToIndex = useCallback((index: number) => {
|
|
85
85
|
if (!viewportRef.current || !trackRef.current) return;
|
|
86
86
|
const panel = trackRef.current.children[index] as HTMLElement | undefined;
|
|
87
|
+
const firstPanel = trackRef.current.children[0] as HTMLElement | undefined;
|
|
87
88
|
if (!panel) return;
|
|
88
|
-
const targetLeft = panel.offsetLeft;
|
|
89
|
+
const targetLeft = panel.offsetLeft - (firstPanel?.offsetLeft ?? 0);
|
|
89
90
|
const currentLeft = viewportRef.current.scrollLeft;
|
|
90
91
|
if (Math.abs(currentLeft - targetLeft) < 1) {
|
|
91
92
|
setCurrentIndex(index);
|
|
@@ -136,11 +137,14 @@ export function CctvPaginationCarouselProvider({
|
|
|
136
137
|
const { scrollLeft } = viewport;
|
|
137
138
|
const panels = trackRef.current?.children ?? [];
|
|
138
139
|
if (!panels.length) return;
|
|
140
|
+
const firstOffset = (panels[0] as HTMLElement).offsetLeft;
|
|
139
141
|
let closestIndex = 0;
|
|
140
142
|
let smallestDistance = Number.POSITIVE_INFINITY;
|
|
141
143
|
for (let i = 0; i < panels.length; i += 1) {
|
|
142
144
|
const panel = panels[i] as HTMLElement;
|
|
143
|
-
const distance = Math.abs(
|
|
145
|
+
const distance = Math.abs(
|
|
146
|
+
panel.offsetLeft - firstOffset - scrollLeft,
|
|
147
|
+
);
|
|
144
148
|
if (distance < smallestDistance) {
|
|
145
149
|
smallestDistance = distance;
|
|
146
150
|
closestIndex = i;
|
|
@@ -67,6 +67,7 @@ export default function CCTVPaginationListItem({
|
|
|
67
67
|
ref={videoRef}
|
|
68
68
|
cam={cam}
|
|
69
69
|
className="cctv-pagination-list-video-container"
|
|
70
|
+
layout="carousel"
|
|
70
71
|
// thumbnail에서는 header live/share 상태만 보이고 footer action은 최소화한다.
|
|
71
72
|
headerOptions={{
|
|
72
73
|
activeLiveState: true,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import type { CctvVideoCardProps } from "../../types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CCTV; service settings를 결합하는 video card
|
|
6
|
+
* @component
|
|
7
|
+
* @param {CctvVideoCardProps} props
|
|
8
|
+
* @param {React.ReactNode} props.children video/media 영역
|
|
9
|
+
* @param {React.ReactNode} [props.settings] media 하단 서비스 설정 영역
|
|
10
|
+
* @param {string} [props.className] 카드 루트 클래스
|
|
11
|
+
* @example
|
|
12
|
+
* <CCTV.Video.Card settings={<Controls />}><CCTV.Video.Template /></CCTV.Video.Card>
|
|
13
|
+
*/
|
|
14
|
+
export default function CCTVVideoCard({
|
|
15
|
+
children,
|
|
16
|
+
className,
|
|
17
|
+
settings,
|
|
18
|
+
}: CctvVideoCardProps) {
|
|
19
|
+
return (
|
|
20
|
+
<section
|
|
21
|
+
className={clsx("cctv-video-card", className)}
|
|
22
|
+
data-has-settings={typeof settings !== "undefined" ? "true" : undefined}
|
|
23
|
+
>
|
|
24
|
+
<div className="cctv-video-card-media">{children}</div>
|
|
25
|
+
{typeof settings !== "undefined" ? (
|
|
26
|
+
<div className="cctv-video-card-settings">{settings}</div>
|
|
27
|
+
) : null}
|
|
28
|
+
</section>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -4,15 +4,18 @@ export default function CCTVVideoContainer({
|
|
|
4
4
|
className,
|
|
5
5
|
children,
|
|
6
6
|
isError,
|
|
7
|
+
layout,
|
|
7
8
|
}: {
|
|
8
9
|
className?: string;
|
|
9
10
|
children: React.ReactNode;
|
|
10
11
|
isError?: boolean;
|
|
12
|
+
layout?: "card" | "carousel" | "viewer";
|
|
11
13
|
}) {
|
|
12
14
|
return (
|
|
13
15
|
<div
|
|
14
16
|
className={clsx("cctv-video-container", className)}
|
|
15
17
|
data-error={isError ? "true" : undefined}
|
|
18
|
+
data-layout={layout}
|
|
16
19
|
>
|
|
17
20
|
{children}
|
|
18
21
|
</div>
|
|
@@ -33,6 +33,7 @@ const CCTVVideoTemplate = forwardRef<HTMLVideoElement, CctvVideoTemplateProps>(
|
|
|
33
33
|
footerOptions,
|
|
34
34
|
isError,
|
|
35
35
|
isLive,
|
|
36
|
+
layout,
|
|
36
37
|
overlayMessage,
|
|
37
38
|
streamStage,
|
|
38
39
|
canReconnect,
|
|
@@ -69,7 +70,11 @@ const CCTVVideoTemplate = forwardRef<HTMLVideoElement, CctvVideoTemplateProps>(
|
|
|
69
70
|
: defaultOverlay;
|
|
70
71
|
|
|
71
72
|
return (
|
|
72
|
-
<CCTVVideoContainer
|
|
73
|
+
<CCTVVideoContainer
|
|
74
|
+
className={className}
|
|
75
|
+
isError={isError}
|
|
76
|
+
layout={layout}
|
|
77
|
+
>
|
|
73
78
|
<CCTVVideoContents ref={ref} muted />
|
|
74
79
|
{overlayNode}
|
|
75
80
|
</CCTVVideoContainer>
|
|
@@ -2,10 +2,12 @@ import CCTVVideoContainer from "./Container";
|
|
|
2
2
|
import { CCTVVideoOverlay } from "./overlay";
|
|
3
3
|
import CCTVVideoTemplate from "./Template";
|
|
4
4
|
import CCTVVideoContents from "./Video";
|
|
5
|
+
import CCTVVideoCard from "./Card";
|
|
5
6
|
|
|
6
7
|
export const CCTVVideo = {
|
|
7
8
|
Container: CCTVVideoContainer,
|
|
8
9
|
Contents: CCTVVideoContents,
|
|
10
|
+
Card: CCTVVideoCard,
|
|
9
11
|
Template: CCTVVideoTemplate,
|
|
10
12
|
Overlay: CCTVVideoOverlay,
|
|
11
13
|
};
|
|
@@ -5,6 +5,7 @@ import type { CctvVideoOverlayFooterProps } from "../../../../types";
|
|
|
5
5
|
|
|
6
6
|
export default function CCTVVideoOverlayFooter({
|
|
7
7
|
className,
|
|
8
|
+
children,
|
|
8
9
|
activeTitle = true,
|
|
9
10
|
activeOpenButton,
|
|
10
11
|
cam,
|
|
@@ -23,6 +24,7 @@ export default function CCTVVideoOverlayFooter({
|
|
|
23
24
|
{activeOpenButton && cam && (
|
|
24
25
|
<CCTVVideoOpenButton {...cam} isLive={isLive} />
|
|
25
26
|
)}
|
|
27
|
+
{children}
|
|
26
28
|
</footer>
|
|
27
29
|
);
|
|
28
30
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CctvVideoMetricProps } from "../../../../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CCTV; state dot을 포함한 sensor metric
|
|
5
|
+
* @component
|
|
6
|
+
* @param {CctvVideoMetricProps} props
|
|
7
|
+
* @param {React.ReactNode} props.children 표시할 센서 값
|
|
8
|
+
* @param {"normal" | "attention" | "unavailable"} [props.state="normal"] state dot 표시 상태
|
|
9
|
+
* @example
|
|
10
|
+
* <CCTV.Video.Overlay.Footer.Metric state="attention">온도 38℃</CCTV.Video.Overlay.Footer.Metric>
|
|
11
|
+
*/
|
|
12
|
+
export default function CCTVVideoMetric({
|
|
13
|
+
children,
|
|
14
|
+
state = "normal",
|
|
15
|
+
}: CctvVideoMetricProps) {
|
|
16
|
+
return (
|
|
17
|
+
<span className="cctv-video-metric" data-state={state}>
|
|
18
|
+
<span className="cctv-video-metric-dot" aria-hidden="true" />
|
|
19
|
+
<span>{children}</span>
|
|
20
|
+
</span>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CCTV; sensor metric group
|
|
3
|
+
* @component
|
|
4
|
+
* @param {object} props
|
|
5
|
+
* @param {React.ReactNode} props.children sensor metric 목록
|
|
6
|
+
*/
|
|
7
|
+
export default function CCTVVideoMetrics({
|
|
8
|
+
children,
|
|
9
|
+
}: {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}) {
|
|
12
|
+
return <div className="cctv-video-metrics">{children}</div>;
|
|
13
|
+
}
|
|
@@ -3,6 +3,8 @@ import CCTVVideoError from "./body/Error";
|
|
|
3
3
|
import CCTVVideoOverlayContainer from "./Container";
|
|
4
4
|
import CCTVVideoOverlayFooter from "./footer/Container";
|
|
5
5
|
import CCTVVideoOpenButton from "./footer/OpenButton";
|
|
6
|
+
import CCTVVideoMetric from "./footer/Metric";
|
|
7
|
+
import CCTVVideoMetrics from "./footer/Metrics";
|
|
6
8
|
import CCTVVideoCloseButton from "./header/CloseButton";
|
|
7
9
|
import CCTVVideoOverlayHeader from "./header/Container";
|
|
8
10
|
import CCTVVideoLiveState from "./header/LiveState";
|
|
@@ -20,5 +22,7 @@ export const CCTVVideoOverlay = {
|
|
|
20
22
|
Footer: {
|
|
21
23
|
Container: CCTVVideoOverlayFooter,
|
|
22
24
|
OpenButton: CCTVVideoOpenButton,
|
|
25
|
+
Metric: CCTVVideoMetric,
|
|
26
|
+
Metrics: CCTVVideoMetrics,
|
|
23
27
|
},
|
|
24
28
|
};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import clsx from "clsx";
|
|
2
4
|
import CCTVViewerContainer from "../Container";
|
|
3
5
|
import CCTVViewerDesktopPagination from "./Pagination";
|
|
4
6
|
import CCTVViewerDesktopVideo from "./Video";
|
|
7
|
+
import CCTVViewerDesktopHeadline from "./Headline";
|
|
5
8
|
import type { CctvViewerDesktopContainerProps } from "../../../types";
|
|
9
|
+
import { useCctvContext } from "../../../hooks";
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* CCTV; viewer desktop container
|
|
@@ -21,8 +25,27 @@ export default function CCTVViewerDesktopContainer({
|
|
|
21
25
|
children,
|
|
22
26
|
selectedCam,
|
|
23
27
|
camList,
|
|
28
|
+
metrics,
|
|
29
|
+
onNext,
|
|
30
|
+
onPrev,
|
|
24
31
|
renderOverlay,
|
|
32
|
+
time,
|
|
25
33
|
}: CctvViewerDesktopContainerProps) {
|
|
34
|
+
const context = useCctvContext();
|
|
35
|
+
const resolvedCam = selectedCam ?? context.selectedCam;
|
|
36
|
+
const resolvedList = camList ?? context.cams;
|
|
37
|
+
const currentIndex = resolvedList.findIndex(
|
|
38
|
+
cam => cam.cam_id === resolvedCam?.cam_id,
|
|
39
|
+
);
|
|
40
|
+
const resolvedPrev =
|
|
41
|
+
onPrev ??
|
|
42
|
+
(currentIndex > 0 ? resolvedList[currentIndex - 1]?.onSelect : undefined);
|
|
43
|
+
const resolvedNext =
|
|
44
|
+
onNext ??
|
|
45
|
+
(currentIndex >= 0 && currentIndex < resolvedList.length - 1
|
|
46
|
+
? resolvedList[currentIndex + 1]?.onSelect
|
|
47
|
+
: undefined);
|
|
48
|
+
|
|
26
49
|
return (
|
|
27
50
|
<CCTVViewerContainer
|
|
28
51
|
className={clsx("cctv-viewer-desktop-container", className)}
|
|
@@ -31,15 +54,23 @@ export default function CCTVViewerDesktopContainer({
|
|
|
31
54
|
children
|
|
32
55
|
) : (
|
|
33
56
|
<>
|
|
57
|
+
<CCTVViewerDesktopHeadline
|
|
58
|
+
title={resolvedCam?.cam_name ?? "-"}
|
|
59
|
+
time={time}
|
|
60
|
+
/>
|
|
34
61
|
<CCTVViewerDesktopVideo
|
|
35
|
-
{...(typeof
|
|
36
|
-
? { cam:
|
|
62
|
+
{...(typeof resolvedCam !== "undefined"
|
|
63
|
+
? { cam: resolvedCam }
|
|
37
64
|
: {})}
|
|
38
|
-
|
|
65
|
+
metrics={metrics}
|
|
66
|
+
onNext={resolvedNext}
|
|
67
|
+
onPrev={resolvedPrev}
|
|
39
68
|
renderOverlay={renderOverlay}
|
|
40
69
|
/>
|
|
41
70
|
<CCTVViewerDesktopPagination
|
|
42
|
-
{...(typeof
|
|
71
|
+
{...(typeof resolvedList !== "undefined"
|
|
72
|
+
? { list: resolvedList }
|
|
73
|
+
: {})}
|
|
43
74
|
/>
|
|
44
75
|
</>
|
|
45
76
|
)}
|